Compare commits

...

8 Commits

35 changed files with 1761 additions and 0 deletions

3
.browserslistrc Normal file
View File

@@ -0,0 +1,3 @@
last 2 versions
> 1%
IE 9

239
.gitignore vendored Normal file
View File

@@ -0,0 +1,239 @@
.idea/
package-lock.json
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
/.trash/
/dist/*.LICENSE.txt

3
babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
presets: ["@babel/preset-env"]
}

1
dist/app.532fa67fd2123b5dfcf1.css vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/app.532fa67fd2123b5dfcf1.js vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/faker.532fa67fd2123b5dfcf1.js vendored Normal file

File diff suppressed because one or more lines are too long

15
dist/index.html vendored Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Notes in JS (Home task 1)</title>
<link href="app.532fa67fd2123b5dfcf1.css" rel="stylesheet"></head>
<body>
<div id="appConsole" class="uk-container uk-margin-remove uk-background-muted uk-text-small"></div>
<div id="app" class="uk-container"></div>
<script defer src="vendors-node_modules_faker_index_js.532fa67fd2123b5dfcf1.js"></script><script defer src="faker.532fa67fd2123b5dfcf1.js"></script><script defer src="app.532fa67fd2123b5dfcf1.js"></script></body>
</html>

File diff suppressed because one or more lines are too long

43
package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "radency_hometask_1_js",
"version": "1.0.0",
"description": "",
"main": "index.js",
"private": true,
"scripts": {
"start": "cross-env SOURCE_MAP_ENV=true webpack serve --open",
"srv": "cross-env SOURCE_MAP_ENV=true webpack serve",
"srv-prod": "cross-env SOURCE_MAP_ENV=true NODE_ENV=production webpack serve",
"watch": "webpack --watch",
"build": "cross-env NODE_ENV=production webpack",
"build-dev": "cross-env SOURCE_MAP_ENV=true NODE_ENV=production webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/preset-env": "^7.15.6",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0",
"cross-env": "^7.0.3",
"css-loader": "^6.3.0",
"css-minimizer-webpack-plugin": "^3.1.1",
"css-mqpacker-webpack-plugin": "^0.12.1",
"glob": "^7.2.0",
"html-webpack-plugin": "^5.3.2",
"mini-css-extract-plugin": "^2.3.0",
"mini-svg-data-uri": "^1.3.3",
"postcss-loader": "^6.1.1",
"postcss-preset-env": "^6.7.0",
"purgecss-webpack-plugin": "^4.0.3",
"style-loader": "^3.3.0",
"webpack": "^5.56.1",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.3.1"
},
"dependencies": {
"faker": "^5.5.3"
}
}

58
src/env.json Normal file
View File

@@ -0,0 +1,58 @@
{
"routes": {
"/all": "All Notes",
"/": "Recent Notes",
"/archive": "Archived Notes",
"/analytics": "Analytics"
},
"localStorageKey": "notes-home-task-1",
"prePopulateAmount": 7,
"fakerAmount": 44,
"lists": {
"monthsFull": ["January","February","March","April","May","June","July", "August","September","October","November","December"]
},
"alertTypes": [
"primary", "success", "warning", "danger"
],
"querySelectors": {
"root": "#app",
"console": "#appConsole",
"navTabs": "#asSwitcher",
"modal": ".modal",
"analyticsGrid": "#analyticsGrid",
"notesGrid": "#notesGrid",
"noteEditForm": "#noteEditForm",
"btnPrepopulate": "#btnPrepopulate",
"selectPrepopulate": "#selectPrepopulate",
"btnCreateNote": "#btnCreateNote",
"btnDestroyModal": "#btnDestroyModal",
"saveNote": "#saveNote",
"templates": {
"gridRow": "#noteRowTemplate",
"editForm": "#noteEditFormTemplate"
}
},
"gridOrder": {
"notes": [
"createdAt",
"title",
"category",
"content",
"dates",
"edit",
"archive",
"delete"
]
},
"schemas": {
"note": {
"id": [ false, "^[0-9]{6}$" ],
"title": [ true, 128 ],
"category": [ true, "^(task|random_thought|idea|quote)$" ],
"content": [ false, 1024 ],
"archive": [ false, "^(on|true|)$" ],
"createdAt": [ false, "^202[1-9]-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$" ],
"updatedAt": [ false, "^202[1-9]-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$" ]
}
}
}

45
src/faker.js Normal file
View File

@@ -0,0 +1,45 @@
import { env, randomID6n } from './helpers'
const faker = require('faker')
export const fakerDB = length => {
const dates = [
'1/1/11', '1.1.11', '1-1-11', '01/01/11', '01.01.11', '01-01-11',
'01/01/2011', '01.01.2011', '01-01-2011', '01/1/2011', '01.1.2011', '01-1-2011',
'1/11/2011', '1.11.2011', '1-11-2011', '1/11/11', '1.11.11', '1-11-11',
'11/1/11', '11.1.11', '11-1-11', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
]
const data = []
const cat = env.schemas.note.category[1].slice(2, -2).split('|')
//
for (let i = 0; i < length; i++) {
const up = faker.date.between('2021-07-01', '2021-10-07')
let pickedDates = []
for( let i = 0; i< 4; i++ ) {
pickedDates.push( dates[(Math.random() * dates.length) | 0] )
}
pickedDates = pickedDates.filter( el => el !== '' )
const rec = {
id: randomID6n(),
title: faker.animal.snake(),
category: cat[(Math.random() * cat.length) | 0],
archive: ['', 'on'][(Math.random() * 2) | 0],
content: faker.hacker.phrase() + (pickedDates.length > 0 ? "<br/><strong style='color: green'>Dates:</strong> [ " + pickedDates.join(', ') : '') + " ]",
createdAt: faker.date.between('2021-07-06', '2021-10-01'),
updatedAt: ['', up][(Math.random() * 2) | 0],
}
data.push(rec)
}
return data
}
//console.log( JSON.stringify( data, null, 2 ))

86
src/helpers.js Normal file
View File

@@ -0,0 +1,86 @@
export const env = require('./env.json')
export const msgBox = document.getElementById(env.querySelectors.console.slice(1))
/**
*
* @type {{}}
*/
const monthsNamesFull = env.lists.monthsFull
export const randomID6n = () => Math.floor(100000 + Math.random() * 900000)
export const parseURL = url => new URL(url)
/**
*
* @param string
* @returns {string}
*/
export const ucWords = string => {
return string.split('_').map(word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase()).join(' ')
}
/**
*
* @param date
* @returns {string}
*/
export const formatDate = date => {
const dt = new Date(date)
return [monthsNamesFull[dt.getMonth()], dt.getDate(), `, ${dt.getFullYear()}`].join(' ')
}
/**
*
* @param str
* @returns {*[]}
*/
export const findDates = str => {
const dates = []
const regex = new RegExp('[0-3]?[0-9].[0-3]?[0-9].(?:[0-9]{2})?[0-9]{2}', 'mg')
for (const match of (str || '').matchAll(regex)) {
dates.push(match[0])
}
return dates
}
/**
*
* @param args
* @returns {*}
*/
export const createNode = (...args) => {
const [tag, attrs, content, callback] = args
const node = document.createElement(tag)
for (const [key, value] of Object.entries(attrs || [])) {
node.setAttribute(key, value)
}
node.innerHTML = content || ''
return node
}
/**
*
* @param node
*/
export const destroyNode = (node) => {
if (!(node instanceof Node) && typeof node === 'string') {
node = document.querySelector(node)
}
try {
node.remove()
} catch (e) {}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#fcc343"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27zm-8.89 11.92L6.5 12H10v-2h4v2h3.5l-5.15 5.15c-.19.19-.51.19-.7 0zM5.12 5l.81-1h12l.94 1H5.12z"/></svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#d6676d"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2H8c-1.1 0-2 .9-2 2v10zM18 4h-2.5l-.71-.71c-.18-.18-.44-.29-.7-.29H9.91c-.26 0-.52.11-.7.29L8.5 4H6c-.55 0-1 .45-1 1s.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1z"/></svg>

After

Width:  |  Height:  |  Size: 368 B

1
src/images/edit_48dp.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#abcdef"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM21.41 6.34l-3.75-3.75-2.53 2.54 3.75 3.75 2.53-2.54z"/></svg>

After

Width:  |  Height:  |  Size: 259 B

1
src/images/idea_48dp.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#f8f8ff"><path d="M0 0h24v24H0z" fill="none"/><path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"/></svg>

After

Width:  |  Height:  |  Size: 447 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#f8f8ff"><path d="M5 17h3l2-4V7H4v6h3l-2 4zm10 0h3l2-4V7h-6v6h3l-2 4z"/></svg>

After

Width:  |  Height:  |  Size: 171 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="48px" viewBox="0 0 20 20" width="48px" fill="#f8f8ff"><g><rect fill="none" height="20" width="20"/></g><g><g><path d="M10.5,4C8.22,4,6.35,5.71,6.06,7.91l-1.54,2.31C4.3,10.55,4.53,11,4.93,11H6v2c0,0.55,0.45,1,1,1h1v2h5v-3.76 c1.21-0.81,2-2.18,2-3.74C15,6.01,12.99,4,10.5,4z M12.33,8.67c0,0.09-0.01,0.18-0.02,0.26l0.56,0.44 c0.05,0.04,0.07,0.11,0.03,0.17l-0.53,0.92c-0.03,0.06-0.1,0.08-0.16,0.06l-0.66-0.27c-0.14,0.1-0.29,0.19-0.45,0.26L11,11.22 c-0.01,0.07-0.06,0.11-0.13,0.11H9.8c-0.07,0-0.12-0.05-0.13-0.11l-0.1-0.71c-0.16-0.07-0.31-0.15-0.45-0.26l-0.66,0.27 c-0.06,0.02-0.13,0-0.16-0.06L7.76,9.54C7.73,9.48,7.74,9.41,7.79,9.37l0.56-0.44C8.34,8.84,8.33,8.75,8.33,8.67 s0.01-0.18,0.02-0.26L7.79,7.97C7.74,7.93,7.73,7.86,7.76,7.8l0.53-0.92c0.03-0.06,0.1-0.08,0.16-0.06l0.66,0.27 c0.14-0.1,0.29-0.19,0.45-0.26l0.1-0.71C9.68,6.05,9.73,6,9.8,6h1.07c0.07,0,0.12,0.05,0.13,0.11l0.1,0.71 c0.16,0.07,0.31,0.15,0.45,0.26l0.66-0.27c0.06-0.02,0.13,0,0.16,0.06l0.53,0.92c0.03,0.06,0.02,0.13-0.03,0.17l-0.56,0.44 C12.33,8.49,12.33,8.58,12.33,8.67z"/><path d="M10.33,7.71c-0.52,0-0.95,0.43-0.95,0.95c0,0.52,0.43,0.95,0.95,0.95s0.95-0.43,0.95-0.95 C11.29,8.14,10.86,7.71,10.33,7.71z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

1
src/images/task_48dp.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#f8f8ff"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17 18c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2zM7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zm0-3l1.1-2h7.45c.75 0 1.41-.41 1.75-1.03L21.7 4H5.21l-.94-2H1v2h2l3.6 7.59L3.62 17H19v-2H7z"/></svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#bada55"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M18.71 3H5.29L3 5.79V21h18V5.79L18.71 3zM14 15v2h-4v-2H6.5L12 9.5l5.5 5.5H14zM5.12 5l.81-1h12l.94 1H5.12z"/></svg>

After

Width:  |  Height:  |  Size: 264 B

15
src/index.html Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Notes in JS (Home task 1)</title>
</head>
<body>
<div id="appConsole" class="uk-container uk-margin-remove uk-background-muted uk-text-small"></div>
<div id="app" class="uk-container"></div>
</body>
</html>

11
src/index.js Normal file
View File

@@ -0,0 +1,11 @@
import { env } from './helpers'
import './styles/uikit.min.css'
import './styles/main.css'
import DOMController from './modules/DOMController'
import RouteController from './modules/RouteController'
window.env = env
const app = document.getElementById(env.querySelectors.root.slice(1))
new DOMController(app, new RouteController())

View File

@@ -0,0 +1,28 @@
import { createNode, msgBox } from '../helpers'
export default class AlertController {
static showError () {
this.displayDialog('Error occurred :(', 3000, 3)
}
/**
*
* @param msg
* @param fadeOut
* @param type
* @param parent
*/
static displayDialog (msg = 'Alert without message', fadeOut = 0, type = 0, parent = null) {
const alertTypes = window.env.alertTypes
const dialog = createNode('div', { class: `uk-alert-${alertTypes[type]} uk-padding-small` }, msg)
parent instanceof Element ? parent.appendChild(alert) : msgBox.appendChild(dialog)
if (fadeOut > 0) {
setTimeout(() => {
dialog.remove()
}, fadeOut)
}
}
}

View File

@@ -0,0 +1,362 @@
import Templates from '../templates/Views'
import { createNode, destroyNode, findDates, formatDate, parseURL, replaceURLPathname, ucWords } from '../helpers'
import NotesAPI from './NotesAPI'
import ValidationController from './ValidationController'
import Alert from './AlertController'
export default class DOMController {
constructor (root, routes) {
this.routes = routes
this.root = root
try {
this.root.innerHTML = Templates.grid
this.renderRoutes()
} catch (e) {
Alert.displayDialog(e.toString(), 0, 3)
}
this.btnCreateNote = this.root.querySelector(window.env.querySelectors.btnCreateNote)
this.btnPrepopulate = this.root.querySelector(window.env.querySelectors.btnPrepopulate)
this.createNotesGrid(window.env.prePopulateAmount || 0)
this.listener([
[this.btnCreateNote, 'click', (this.btnCreateNote.dataset.action || 'showError'), { legend: 'Create Note' }],
[this.btnPrepopulate, 'click', (this.btnPrepopulate.dataset.action || 'showError')],
])
}
/**
*
*/
renderRoutes () {
for (const [route, name] of Object.entries(this.routes.routes)) {
const tab = createNode('li')
tab.classList.add('nav-tab')
if (route === '/') {
tab.classList.add('initial')
tab.classList.add('uk-active')
}
tab.innerHTML = `<a href="${route}">${name}</a>`
const anchor = tab.querySelector('a')
this.routes.navbar.appendChild(tab)
this.listener([anchor, 'click', 'createNotesGridByRoute', anchor])
}
}
createNotesGridByRoute (navTab) {
const pathname = parseURL(navTab.href).pathname
const route = pathname.slice(1).split('\/')[0]
window.history.pushState('', navTab.innerText, pathname) //change url without reload
//history.pushState({}, null, pathname) //change url
this.routes.navbar.querySelector('.uk-active').classList.remove('uk-active')
navTab.closest('.nav-tab').classList.add('uk-active')
document.getElementById(window.env.querySelectors.analyticsGrid.slice(1)).classList.add('uk-hidden')
document.getElementById(window.env.querySelectors.notesGrid.slice(1)).classList.remove('uk-hidden')
switch (route) {
case 'all':
this.createNotesGrid(0, null)
break
case '':
this.createNotesGrid(window.env.prePopulateAmount || 0)
break
case 'archive':
this.createNotesGrid(window.env.prePopulateAmount || 0, true)
break
case 'analytics':
document.getElementById(window.env.querySelectors.notesGrid.slice(1)).classList.add('uk-hidden')
const analyticsGrid = document.getElementById(window.env.querySelectors.analyticsGrid.slice(1))
this.createAnalyticsGrid(analyticsGrid)
break
}
}
prePopulate () {
const fakerAmount = document.querySelector(env.querySelectors.selectPrepopulate)
if (+fakerAmount.value <= 0) {
return
}
env.fakerAmount = (+fakerAmount.value > 44) ? 44 : +fakerAmount.value
localStorage.removeItem(window.env.localStorageKey)
fakerAmount.selectedIndex = 0
document.querySelector('.nav-tab.uk-active > a').click()
}
/**
* The method is being called for typical DOM-event task inside the table row
* to manipulate with records: deleting, editing, archiving.
*
* @param node
* @param method
*/
noteToAction (node, method) {
const row = node.closest('tr')
NotesAPI[method](parseInt(row.dataset.id, 10))
row.classList.add('as-removing')
setTimeout(() => {
row.remove()
document.querySelector('.nav-tab.uk-active > a').click()
}, 1050)
}
/**
*
* @param node
*/
noteToArchive (node) {
this.noteToAction(node, 'archiveNote')
}
/**
*
* @param node
*/
noteFromArchive (node) {
this.noteToAction(node, 'unArchiveNote')
}
/**
*
* @param node
*/
noteToTrash (node) {
this.noteToAction(node, 'deleteNote')
}
createAnalyticsGrid (grid) {
grid.classList.remove('uk-hidden')
const aGrid = grid.querySelector(' tbody')
aGrid.innerHTML = ''
for (const [key, value] of Object.entries(NotesAPI.getAnalytics())) {
const row = createNode('tr')
row.classList.add('category-row')
const td = createNode('td', {}, ucWords(key))
const i = createNode('i', { class: `icon-${key}` }, )
td.appendChild(i)
row.appendChild(td)
for (const [key, val] of Object.entries( value ) ) {
const td = createNode('td', {}, val.toString())
row.appendChild(td)
}
aGrid.appendChild(row)
}
}
/**
* TODO: need to rewrite simplified
* @param len
* @param archived
*/
createNotesGrid (len = window.env.prePopulateAmount || 0, archived = false) {
const notesGrid = document.querySelector(window.env.querySelectors.notesGrid + ' tbody')
notesGrid.innerHTML = ''
const notes = NotesAPI.getNotesSanitized(len, archived)
if (notes.length === 0) { return }
notes.forEach(el => {
const row = createNode('tr')
row.dataset.id = el.id
row.classList.add('category-row')
let i = createNode('i', { class: `icon-${el.category}` }, )
window.env.gridOrder.notes.forEach(key => {
let attrs = {}
let value = el[key] || ''
switch (key) {
case 'createdAt':
value = formatDate(value)
break
case 'category':
value = ucWords(value)
break
case 'title':
attrs.class = 'uk-text-bold'
break
case 'dates':
attrs.style = 'font-size: 12px'
value = findDates(el.content).join(', ')
break
case 'edit' :
attrs.class = 'grid-control icon icon-edit'
attrs['data-action'] = 'getEditForm'
break
case 'archive' :
attrs.class = 'grid-control icon ' + (value === 'on' ? 'icon-unarchive' : 'icon-archive')
attrs['data-action'] = value === 'on' ? 'noteFromArchive' : 'noteToArchive'
value = ''
break
case 'delete' :
attrs.class = 'icon icon-delete grid-control'
attrs['data-action'] = 'noteToTrash'
break
}
const td = createNode('td', attrs, value)
if( key === 'createdAt'){
td.appendChild(i)
}
if (key.match(/^(edit|archive|delete)$/)) {
this.listener([td, 'click', td.dataset.action, td])
}
row.appendChild(td)
})
notesGrid.appendChild(row)
})
}
/**
*
* @param selector
* @returns {{}}
*/
static getFormData (selector = window.env.querySelectors.noteEditForm) {
const formData = new FormData(document.querySelector(selector))
const data = {}
for (let key of formData.keys()) {
data[key] = formData.get(key)
}
return data
}
/**
*
* @param args
*/
getEditForm (args = {}) {
const node = createNode('div', { class: window.env.querySelectors.modal.slice(1) },
Templates.templates.editForm)
if (args.legend) {
node.querySelector('legend').innerHTML = args.legend
} else {
DOMController.fillTheForm(node, args)
}
this.root.appendChild(node)
node.style.display = 'block'
const contentInput = node.querySelector('textarea[maxlength]')
const titleInput = node.querySelector('input[maxlength]')
this.listener([
[node.querySelector(window.env.querySelectors.btnDestroyModal), 'click', destroyNode, node], //Remove modal dialog
[node.querySelector('form'), 'submit', NotesAPI.saveNote],
[contentInput, 'keydown', ValidationController.lengthOnKeyUp, contentInput],
[titleInput, 'keydown', ValidationController.lengthOnKeyUp, titleInput],
])
}
/**
*
* @param $form
* @param $el
*/
static fillTheForm ($form, $el) {
if ($el instanceof Element) {
const note = NotesAPI.getNoteByID(+$el.closest('tr').dataset.id)
if (note.length === 1) {
for (const [key, value] of Object.entries(note[0])) {
const $input = $form.querySelector(`[name='${key}']`)
if ($input instanceof Element) {
if ($input.type === 'checkbox') {
if (value !== '') {
$input.click()
}
} else {
$input.value = value
}
}
}
}
}
}
/**
* The function takes single DOM element or list of them
*
* @param els
* @returns {void|*}
*/
listener (els) { return els[0] instanceof Element ? this.on(els) : els.forEach(el => this.on(el)) }
/**
* TODO: guessed this should be rethought
* @param args
*/
on (args) {
const [selector, event, method] = args
if (!(selector instanceof Element)) {
Alert.displayDialog(`Internal Error: Try reload a page`, 3000, 3)
return false
}
try {
selector.addEventListener(event, evt => {
const params = args[3] || {}
if (selector.tagName === 'A') {
evt.preventDefault()
}
if (evt.type === 'submit') {
evt.preventDefault()
setTimeout(() => {
document.querySelector('.nav-tab.uk-active > a').click()
}, 300)
}
if (typeof this[method] === 'function') {
this[method](params)
} else if (typeof method === 'function') {
method(params)
}
})
return true
} catch (err) {
Alert.displayDialog(`Error: ${err.toString()}`, 3000, 3)
return false
}
}
}

183
src/modules/NotesAPI.js Normal file
View File

@@ -0,0 +1,183 @@
import DOMController from './DOMController'
import Validation from './ValidationController'
import { fakerDB } from '../faker'
import Alert from './AlertController'
export default class NotesAPI {
/**
*
* @returns {any}
* @param save
*/
static getStorage (save = true) {
const key = window.env.localStorageKey
let storage = localStorage.getItem(key)
if (storage === null) {
storage = JSON.stringify(fakerDB(env.fakerAmount)) || '[]'
if (save) localStorage.setItem(key, storage)
}
return JSON.parse(storage)
}
/**
*
* @param n
* @param archive
* @returns {*}
*/
static getNotes (n = 0, archive = false) {
let notes = NotesAPI.getStorage()
if (typeof archive === 'boolean') {
notes = notes.filter(el => (archive ? el.archive === 'on' : el.archive !== 'on'))
}
notes.sort((a, b) => {
return new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1
})
return n > 0 ? notes.slice(0, n) : notes
}
/**
*
* @param id
* @returns {*}
*/
static getNoteByID (id) {
return NotesAPI.getStorage().filter(el => el.id === id)
}
/**
*
* @param n
* @param archive
* @returns {*[]|[]}
*/
static getNotesSanitized (n = 0, archive = false) {
let notes = NotesAPI.getNotes(0, archive)
notes = Validation.allAgainstSchema(notes, window.env.schemas.note)
return n > 0 ? notes.slice(0, n) : notes
}
/**
*
* @param noteToStore
* @param action
*/
static saveNote (noteToStore = null, action = 'modal') {
if (Object.keys(noteToStore).length === 0) {
noteToStore = DOMController.getFormData()
}
const isValid = Validation.againstSchema(noteToStore, window.env.schemas.note)
if (isValid.status === true) {
noteToStore = isValid.data
const notes = NotesAPI.getNotes(0, null)
const noteExists = notes.find(note => note.id === +noteToStore.id)
if (noteExists) {
noteExists.title = noteToStore.title
noteExists.content = noteToStore.content
noteExists.category = noteToStore.category
noteExists.archive = noteToStore.archive || ''
noteExists.updatedAt = new Date().toISOString()
} else {
noteToStore.id = Math.floor(100000 + Math.random() * 900000)
noteToStore.archive = noteToStore.archive || ''
noteToStore.createdAt = new Date().toISOString()
notes.push(noteToStore)
}
NotesAPI.saveNotesToStorage(notes)
if (action === 'modal') {
//TODO: Need to implement in different way
document.querySelector(window.env.querySelectors.noteEditForm).closest('.modal').remove()
}
} else {
Alert.displayDialog('Could save the note! ' + isValid.errors.join(', '), 6000, 2) //, document.getElementById('modalMsgBox')
}
}
/**
*
* @param id
*/
static deleteNote (id) {
const notes = NotesAPI.getStorage()
const filteredNotesByID = notes.filter(note => note.id !== id)
NotesAPI.saveNotesToStorage(filteredNotesByID)
}
static unArchiveNote (id) {
this.archiveNote(id, '')
}
/**
*
* @param id
* @param value
*/
static archiveNote (id, value = 'on') {
if (value !== '') value = 'on'
let note = NotesAPI.getNoteByID(id)
if (note.length > 0 && note[0].archive !== value) {
note[0].archive = value
NotesAPI.saveNote(note[0], 'archive')
return true
}
return false
}
/**
*
* @param notes
* @param key
*/
static saveNotesToStorage (notes, key = window.env.localStorageKey) {
localStorage.setItem(key, JSON.stringify(notes, null, 2))
}
/**
*
* @returns {{}}
*/
static getAnalytics () {
const analytics = {}
const data = NotesAPI.getStorage()
const cats = [...new Set(data.map(item => item.category))]
if (typeof cats[0] !== 'undefined') {
const isArchive = data.map(item => {
const container = {}
container[item.category] = item.archive
return container
})
cats.forEach((cat, i) => {
analytics[cat] = {
Archived: isArchive.filter(el => el[cat] === 'on').length,
Active: isArchive.filter(el => el[cat] == '').length,
}
})
}
return analytics
}
}

View File

@@ -0,0 +1,55 @@
import Views from '../templates/Views'
export default class RouteController {
constructor () {
document.addEventListener('DOMContentLoaded', e => {
this._navigation()
this._history()
})
}
get routes () {
return window.env.routes
}
/**
*
* @returns {*}
*/
get navbar () {
return document.getElementById(window.env.querySelectors.navTabs.slice(1))
}
get page404(){
document.body.innerHTML = Views.templates.notFound
setTimeout( () => {
window.location.assign('/')
}, 5000)
}
/**
*
* @private
*/
_navigation(){
if( Object.keys(this.routes).filter( path => window.location.pathname === path ).length === 0 ){
this.page404
} else{
document.querySelector( `a[href='${window.location.pathname}']` ).click()
}
}
_history(){
window.addEventListener('popstate', event => {
try {
document.querySelector('.uk-active').classList.remove('uk-active')
document.querySelector(window.env.querySelectors.navTabs + ` a[href='${event.target.location.pathname}']`).closest('.nav-tab').classList.add('uk-active')
} catch (e) {}
})
}
}

View File

@@ -0,0 +1,98 @@
import { env } from '../helpers'
export default class ValidationController {
/**
*
* @param array
* @param schema
* @returns {*[]}
*/
static allAgainstSchema (array, schema) {
const objs = []
array.forEach(obj => {
const res = this.againstSchema(obj, schema)
if (res.status === true) {
objs.push(res.data)
}
})
return objs
}
/**
* @param obj
* @param obj.key => schema.key
* @param obj.values => [ <required>: boolean, <regexp_pattern>: string ]
* @param schema
* @returns {{error_text: *[], data: {}, errors: *[], status: boolean}}
*/
static againstSchema (obj, schema) {
const out = {
status: false,
error_text: [],
data: {},
errors: [],
}
try {
for (const [key, value] of Object.entries(schema)) {
const field = (obj[key] || '').toString().trim()
if (field !== '') {
if (typeof value[1] !== 'number') {
const regex = new RegExp(value[1].toString(), 'i')
if (regex.exec(field)) {
out.data[key] = field
} else {
if (value[0]) {
out.errors.push(key)
out.error_text.push(`${key} is required`)
}
}
} else if (field.length > 0) {
out.data[key] = field.slice(0, value[1])
}
} else {
if (value[0]) {
out.errors.push(key)
out.error_text.push(`${key} is empty`)
}
}
}
} catch (e) {
out.error_text.push(`Critical error! ` + e.toString())
}
out.status = !(out.error_text.length > 0 && out.errors.length > 0)
return out
}
/**
*
* @param node
*/
static lengthOnKeyUp (node) {
const logger = document.querySelector(`[for='${node.id}'] > span`)
logger.innerHTML = `${node.value.length}/${env.schemas.note[node.getAttribute('name')][1]}`
node.value.length > env.schemas.note[node.getAttribute('name')][1]
? logger.classList.add('uk-text-danger')
: logger.classList.remove(
'uk-text-danger')
}
}

177
src/styles/main.css Normal file
View File

@@ -0,0 +1,177 @@
body{
margin-top: 100px;
}
tbody td{
font-size: 14px;
}
#as-navbar{
overflow: hidden; position: fixed; left: 0; top: 0; width: 100%
}
.uk-text-truncate{
cursor: text;
}
#appConsole{
display: flex;
z-index: 2;
position: fixed;
bottom: 0;
right: 0;
white-space: pre-wrap;
font-family: Consolas, monospace;
box-sizing: content-box;
overflow: hidden;
}
.icon{
background-repeat: no-repeat;
background-position: center;
background-size: 24px;
}
.icon.grid-control{
cursor: pointer;
filter: opacity(.5);
transition: transform 250ms;
}
.icon.grid-control:hover{
filter: opacity(1);
}
.icon.grid-control:not(.icon-unarchive):hover{
transform: translateY(4px);
}
.icon.grid-control.icon-unarchive:hover{
transform: translateY(-4px);
}
.icon.as-gray{
filter: grayscale(1);
}
@keyframes fadeInAnimation {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.as-removing{
animation: fadeInAnimation ease 1s;
animation-iteration-count: 1;
animation-fill-mode: forwards;
}
.icon.as-transition:hover{
transition: ease-in-out 100ms;
background-size: 16px;
}
tr.category-row td:first-child {
position: relative;
padding-left: 60px;
}
tr.category-row td:first-child > i{
position: absolute;
display: block;
width: 32px;
height: 32px;
top: 4px;
left: 8px;
background-repeat: no-repeat;
background-position: center;
background-size: 24px;
border-radius: 50%;
background-color: #888;
}
.icon-task{
background-image: url(../images/task_48dp.svg);
background-size: 20px !important;
}
.icon-quote{
background-image: url(../images/quote_48dp.svg)
}
.icon-random_thought{
background-image: url(../images/random_thought_48dp.svg)
}
.icon-idea{
background-image: url(../images/idea_48dp.svg)
}
.icon-edit{
background-image: url(../images/edit_48dp.svg)
}
.icon-archive{
background-image: url(../images/archive_48dp.svg)
}
.icon-unarchive{
background-image: url(../images/unarchive_48dp.svg)
}
.icon-delete{
background-image: url(../images/delete_48dp.svg)
}
/* The Modal (background) */
#noteEditFormModal, .modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.6); /* Black w/ opacity */
}
#alertMsg{
display: none;
}
/* Modal Content/Box */
.modal-content {
background-color: #fefefe;
margin: 15% auto; /* 15% from the top and centered */
padding: 20px;
border: 1px solid #888;
width: 80%; /* Could be more or less, depending on screen size */
}
/* The Close Button */
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
input[readonly]{
outline: none;
border: none;
background: transparent;
color: #666;
}

1
src/styles/uikit.min.css vendored Normal file

File diff suppressed because one or more lines are too long

15
src/templates/404.js Normal file
View File

@@ -0,0 +1,15 @@
export default `<div class="uk-container uk-text-center">
<div class="uk-container">
<ul id="asSwitcher"
class="uk-subnav uk-subnav-divider uk-margin-large-bottom uk-flex-center">
<li class="nav-tab"><a href="/all">All Notes</a></li>
<li class="nav-tab initial uk-active"><a href="/">Recent Notes</a></li>
<li class="nav-tab"><a href="/archive">Archived Notes</a></li>
<li class="nav-tab"><a href="/analytics">Analytics</a></li>
</ul>
</div>
<h1 class=" uk-text-danger">404 - NOT FOUND</h1>
<div class="uk-text-large uk-text-muted uk-text-bold">U will be redirected soon</div>
</div>`

11
src/templates/Views.js Normal file
View File

@@ -0,0 +1,11 @@
import grid from './grid'
import editForm from './editForm'
import _404 from './404'
export default {
templates: {
notFound: _404,
editForm: editForm,
},
grid: grid
}

62
src/templates/editForm.js Normal file
View File

@@ -0,0 +1,62 @@
import { env } from '../helpers'
export default `<form id="noteEditForm" class="uk-form-horizontal uk-margin-large modal-content" style="background: ghostwhite">
<fieldset class="uk-fieldset">
<legend class="uk-legend">
<span class="uk-float-right uk-text-small">
created: <input class="" type="text" name="createdAt" readonly="readonly" value="" placeholder="0000-00-00">
updated: <input type="text" name="updatedAt" readonly="readonly" value="" placeholder="0000-00-00">
</span>
<span>Edit Note #<input class="uk-text-large" type="text" name="id" readonly="readonly" value="" placeholder="0"></span>
</legend>
<div id="modalMsgBox"></div>
<div class="uk-margin">
<label class="uk-form-label" for="formTitle">Title <span></span></label>
<div class="uk-form-controls">
<input id="formTitle" name="title" class="uk-input" type="text" maxlength="${env.schemas.note.title[1]}" placeholder="Add a title...">
</div>
</div>
<div class="uk-margin">
<label class="uk-form-label uk-float-right" for="formCategory">Category</label>
<div class="uk-form-controls">
<select id="formCategory" name="category" class="uk-select">
<option disabled value="" selected>Select a category...</option>
<option disabled value=""></option>
<option value="task">Task</option>
<option value="random_thought">Random Thought</option>
<option value="idea">Idea</option>
<option value="quote">Quote</option>
</select>
</div>
</div>
<div class="uk-margin">
<label class="uk-form-label" for="noteContentInput">Description <span></span></label>
<textarea id="noteContentInput" name="content" class="uk-textarea" rows="6" maxlength="${env.schemas.note.content[1]}" placeholder="Description..."></textarea>
</div>
<div class="uk-margin">
<label class="uk-form-label uk-float-right" for="formArchived">Archived</label>
<div class="uk-form-controls">
<input id="formArchived" name="archive" class="uk-checkbox" type="checkbox">
</div>
</div>
<hr>
<div class="uk-clearfix">
<div class="uk-float-right">
<button type="submit" class="uk-button uk-button-default">Sumbit</button>
</div>
<div class="uk-float-left">
<button type="button" id="btnDestroyModal" class="uk-button uk-button-danger">Close</button>
</div>
</div>
</fieldset>
</form>`

63
src/templates/grid.js Normal file
View File

@@ -0,0 +1,63 @@
export default `
<nav id="as-navbar" class="uk-background-secondary">
<div class="uk-container">
<ul id="asSwitcher" class="uk-subnav uk-subnav-pill uk-margin-small-top uk-margin-small-bottom uk-padding-small"></ul>
</div>
</nav>
<table id="notesGrid" class="uk-table uk-table-small uk-table-striped uk-table-hover uk-table-divider">
<col width="222">
<col width="250">
<col width="150">
<col>
<col width="180">
<col width="32">
<col width="32">
<col width="32">
<thead>
<tr>
<th>Created</th>
<th>Title</th>
<th>Category</th>
<th>Content</th>
<th>Dates</th>
<th></th>
<th class="icon icon-archive as-gray"></th>
<th class="icon icon-delete as-gray"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<table id="analyticsGrid" class="uk-table uk-table-small uk-table-striped uk-table-hover uk-table-divider uk-hidden">
<col width="222">
<col width="250">
<col>
<thead>
<tr>
<th>Note Category</th>
<th>Active</th>
<th>Archived</th>
</tr>
</thead>
<tbody>
<tr class="category-row">
<td><i class="icon-task icon-idea icon-quote icon-random_thought"></i></td>
</tr>
</tbody>
</table>
<div class="uk-margin" uk-margin>
<div class="uk-margin-large-top">
<button id="btnCreateNote" class="uk-button uk-button-primary uk-text-bold uk-border-rounded uk-float-right" data-action="getEditForm">Create Note</button>
<select id="selectPrepopulate" class="uk-input uk-form-width-medium">
<option value="" selected disabled>Select amount to...</option>
<option>7</option>
<option>15</option>
<option>25</option>
<option>44</option>
</select>
<button id="btnPrepopulate" class="uk-button uk-button-danger uk-text-bold uk-border-rounded" data-action="prePopulate">Pre-populate</button>
</div>
</div>
`

20
styles.css Normal file
View File

@@ -0,0 +1,20 @@
.icon{
display: table-cell;
min-height: 24px;
max-width: 24px;
background-repeat: no-repeat;
background-position: center;
background-size: 24px;
color: green;
}
.icon:hover{
transition: ease-in-out 100ms;
background-size: 16px;
}
.icon-archive{
/*background-image: url(https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/Ic_archive_48px.svg/48px-Ic_archive_48px.svg.png)*/
background-image: url(https://upload.wikimedia.org/wikipedia/commons/f/fb/Ic_archive_48px.svg)
}

153
webpack.config.js Normal file
View File

@@ -0,0 +1,153 @@
const os = require('os')
const path = require('path')
const glob = require('glob')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const svgToMiniDataURI = require('mini-svg-data-uri')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const CSSMQPackerPlugin = require('css-mqpacker-webpack-plugin')
let mode = 'development', target = 'web', isProd = false
const babelExclude = /(node_modules|bower_components)/
if (process.env.NODE_ENV === 'production') {
mode = 'development'
target = 'browserslist'
isProd = true
}
const isDev = process.env.NODE_ENV !== 'production'
const plugins = [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name].[hash].css'
}),
new PurgeCSSPlugin({
paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, { nodir: true }),
//only: ['bundle', 'vendor']
}),
new HtmlWebpackPlugin({
template: './src/index.html',
inject: 'body',
}),
]
const stylesLoaders = loader => {
const loaders = [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
ident: 'postcss',
plugins: [
require('postcss-preset-env'),
],
},
},
},
]
console.log(loaders)
if (loader) {
loaders.push(loader)
}
return loaders
}
module.exports = {
mode: mode,
target: target,
module: {
rules: [
{
test: /\.css$/i,
use: stylesLoaders(),
},
{
test: /\.s[ac]ss$/i,
use: stylesLoaders('sass-loader'),
},
{
test: /\.m?js$/i,
exclude: babelExclude,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
],
plugins: [
'@babel/plugin-proposal-class-properties',
],
},
},
},
{
test: /\.(jpe?g|webp|png|gif|svg)$/i,
type: 'asset/resource',
},
{
test: /\.(svg)$/i,
type: 'asset/inline',
generator: {
dataUrl: content => {
content = content.toString()
return svgToMiniDataURI(content)
},
},
},
],
},
//
entry: { faker: './src/faker', app: './src/index' },
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[hash].js',
},
plugins: plugins,
devtool: mode !== 'production' ? (process.env.SOURCE_MAP_ENV ? 'eval-cheap-module-source-map' : false) : false,
// devtool: 'source-map',
optimization: {
minimize: isProd,
splitChunks: {
chunks: 'all',
},
minimizer: [
`...`,
new CssMinimizerPlugin({
parallel: os.cpus().length,
minimizerOptions: {
preset: [
'default', // for advanced need to run `npm i -D cssnano-preset-advanced`
{
discardComments: { removeAll: true },
},
],
},
}),
new CSSMQPackerPlugin({
test: /\.css$/i,
sort: true,
}),
],
},
devServer: {
static: path.resolve(__dirname, 'dist'),
compress: true,
port: 1976,
//disableHostCheck: true,
allowedHosts: [
'dev.amok.space',
],
liveReload: true,
historyApiFallback: true,
},
}