committing commit

This commit is contained in:
2021-10-13 19:03:45 +03:00
parent 5cae7858ab
commit 4c7a43e378
40 changed files with 8376 additions and 5238 deletions

5
.gitignore vendored
View File

@@ -1,4 +1,5 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.idea/
/public/*.svg
# dependencies
/node_modules
@@ -21,3 +22,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/public/icons/
/.trash/

2
.idea/hometask2.iml generated
View File

@@ -5,6 +5,8 @@
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/.trash" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

12332
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,10 @@
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"faker": "^5.5.3",
"node-sass": "^6.0.1",
"react": "^17.0.2",
"react-context-devtool": "^2.0.3",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"web-vitals": "^1.1.2"
@@ -15,7 +18,8 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"postbuild": "purgecss --safelist icon-archive icon-unarchive icon-quote icon-task icon-random_thought icon-idea --css build/static/css/*.css --content build/index.html build/static/js/*.js --output build/static/css"
},
"eslintConfig": {
"extends": [
@@ -25,14 +29,15 @@
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
"since 2010"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@fullhuman/postcss-purgecss": "^4.0.3"
}
}

6
public/.htaccess Normal file
View File

@@ -0,0 +1,6 @@
Options +FollowSymLinks
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [L]

BIN
public/192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -9,35 +9,13 @@
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>NOTES LIST :: React / Redux / Hooks using functional components</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<div id="app" class="uk-container"></div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -8,12 +8,12 @@
"type": "image/x-icon"
},
{
"src": "logo192.png",
"src": "192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"src": "512x512.png",
"type": "image/png",
"sizes": "512x512"
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#2980B9"><path d="M21 11.01L3 11v2h18zM3 16h12v2H3zM21 6H3v2.01L21 8z"/></svg>

After

Width:  |  Height:  |  Size: 171 B

View File

@@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -1,25 +1,40 @@
import logo from './logo.svg';
import './App.css';
import Context from './redux/context'
import { useReducer, useState } from 'react'
import Header from './components/Header'
import Footer from './components/Footer'
import Grid from './components/Grid'
import reducer from './redux/reducer'
import Database from './controllers/Database'
import nav from './config/navigation'
import env from './env'
import grid from './config/grid'
import { ___fetchFilter, ___fetchLimit, ___route, ___set404 } from './helpers'
function App () {
const initialPath = window.location.pathname.slice(1).split('/')[0]
/* Display Error404 Page if condition is TRUE, the initialPath is not listed in routes config */
___set404(nav.routes.filter(r => initialPath === r.slug).length === 0)
/* Managing states of Modal Edit Form */
const [show, setShow] = useState(false)
/* The Hooks*/
const [route, dispatchRoute] = useReducer(reducer, ___route(initialPath))
const [storage, dispatchStorage] = useReducer(
reducer,
route !== 'summary' ? Database.getNotesSanitized( ___fetchFilter(route), ___fetchLimit(route) ) : Database.getAnalytics()
)
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
<Context.Provider value={{ env, nav, grid, route, storage, dispatchStorage, dispatchRoute, show, setShow }}>
<Header/>
<Grid/>
<Footer/>
</Context.Provider>
)
}
export default App;
export default App

View File

@@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,98 @@
import '../styles/components/editForm.scss'
import { ___ucWords } from '../helpers'
import { FILTER_GRID_DATA_BY_ROUTE } from '../redux/reducersTypes'
import Database from '../controllers/Database'
const EditForm = ({ label = 'Note', show, setShow, dispatch, route, alert, setAlert }) => {
if (!show) return null
const categories = window.env.schemas.note.category[1].slice(2, -2).split('|')
const submitNote = e => {
e.preventDefault()
const formData = new FormData(e.target.closest('form'))
const data = {}
for (let key of formData.keys()) {
data[key] = formData.get(key)
}
const isSavedNote = Database.saveNote(data)
if (isSavedNote === true) {
dispatch({ type: FILTER_GRID_DATA_BY_ROUTE, payload: route })
setShow(false)
} else {
setAlert({ hiddenClass: '', value: isSavedNote })
}
}
return (
<div className="modal">
<form id="editNoteForm" className="uk-form-horizontal uk-margin-large modal-content">
<fieldset className="uk-fieldset">
<legend className="uk-legend">
<span className="uk-float-right uk-text-small">
created: <input 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>{label} #<input className="uk-text-large" type="text" name="id" readOnly="readonly" value=""
placeholder="New"/></span>
</legend>
<pre id="modalMsgBox" className={'uk-text-bold uk-alert-danger uk-alert-danger uk-padding-top ' +
alert.hiddenClass}>{alert.value}</pre>
<div className="uk-margin">
<label className="uk-form-label uk-text-uppercase" htmlFor="formTitle">Title <span/></label>
<div className="uk-form-controls">
<input id="formTitle" name="title" className="uk-input" type="text" maxLength={window.env.schemas.note.title[1]}
placeholder="Add a title..." required/>
</div>
</div>
<div className="uk-margin">
<label className="uk-form-label uk-float-right uk-text-uppercase" htmlFor="formCategory">Category</label>
<div className="uk-form-controls">
<select id="formCategory" name="category" className="uk-select" defaultValue={'Select a category...'} required>
<option disabled>Select a category...</option>
<option disabled/>
{categories.map((value, key) => <option key={key} value={value}>{___ucWords(value)}</option>)}
</select>
</div>
</div>
<div className="uk-margin">
<label className="uk-form-label uk-text-uppercase" htmlFor="noteContentInput">Description <span/></label>
<textarea id="noteContentInput" name="content" className="uk-textarea uk-margin-top" rows="6"
maxLength={window.env.schemas.note.content[1]} placeholder="Description..."/>
</div>
<div className="uk-margin">
<label className="uk-form-label uk-float-right" htmlFor="formArchived">Archived</label>
<div className="uk-form-controls">
<input id="formArchived" name="archive" className="uk-checkbox" type="checkbox"/>
</div>
</div>
<hr/>
<div className="uk-clearfix">
<div className="uk-float-right">
<button type="submit" className="uk-button uk-button-default" onClick={submitNote}>Submit</button>
</div>
<div className="uk-float-left">
<button type="button" id="btnDestroyModal" className="uk-button uk-button-danger"
onClick={() => setShow(false)}>Close
</button>
</div>
</div>
</fieldset>
</form>
</div>
)
}
export default EditForm

36
src/components/Footer.js Normal file
View File

@@ -0,0 +1,36 @@
import { useContext, useState } from 'react'
import Context from '../redux/context'
import { PRE_POPULATE_STORAGE } from '../redux/reducersTypes'
import EditForm from'./EditForm'
function Footer () {
const [data] = useState([1,2,4,8,16,32,64])
const [selected, setSelected] = useState(16)
const [alert, setAlert] = useState({ hiddenClass: 'uk-hidden', value: '' } )
const {dispatchStorage, route, show, setShow} = useContext(Context)
const prePopulate = e => {
const select = document.querySelector('footer select')
dispatchStorage({ type: PRE_POPULATE_STORAGE, payload: select.value, route: route })
setSelected(select.value = 16)
}
return (
<footer className="uk-margin">
<div className="uk-margin-large-top">
<button className="uk-button uk-button-primary uk-text-bold uk-border-rounded uk-float-right" onClick={() => setShow(true)}>Create Note</button>
<label htmlFor="pp" className="uk-text-bold">Use drop-list to select amount...</label>
<select id="pp" className="uk-input uk-form-width-medium uk-margin-left uk-margin-right" defaultValue={selected} >
{ data.map((value, key) => <option key={key}>{value}</option>) }
</select>
<button className="uk-button uk-button-danger uk-text-bold uk-border-rounded" onClick={prePopulate}>Pre-populate</button>
</div>
<EditForm show={show} setShow={setShow} dispatch={dispatchStorage} route={route} alert={alert} setAlert={setAlert} />
</footer>
)
}
export default Footer

19
src/components/Grid.js Normal file
View File

@@ -0,0 +1,19 @@
import '../styles/components/grid.scss'
import Context from '../redux/context'
import { useContext } from 'react'
import GridViewHeader from './partials/GridViewHeader'
import GridViewBody from './partials/GridViewBody'
import { ___template } from '../helpers'
const Grid = () => {
const { route, grid, storage } = useContext(Context)
return (
<table className={grid.parentClassList}>
<GridViewHeader template={grid[___template(route)]}/>
<GridViewBody data={storage} route={route}/>
</table>
)
}
export default Grid

12
src/components/Header.js Normal file
View File

@@ -0,0 +1,12 @@
import HeaderNavigation from './partials/HeaderNavigation'
const Header = () => {
return (
<header className="uk-background-secondary">
<HeaderNavigation/>
</header>
)
}
export default Header

View File

@@ -0,0 +1,74 @@
import { ___fillTheForm, ___findDates, ___formatDate, ___ucWords } from '../../helpers'
import Database from '../../controllers/Database'
import { FILTER_GRID_DATA_BY_ROUTE } from '../../redux/reducersTypes'
import Context from '../../redux/context'
import { useContext } from 'react'
const getRaw = e => e.target.closest('.category-row')
const NotesRows = note => {
const { route, dispatchStorage, setShow } = useContext(Context)
const toAction = e => {
const row = getRaw(e)
const noteID = +(row.dataset.id || -1)
const noteAction = e.target.dataset.action
if (noteAction === 'editNote') {
setShow(true)
setTimeout(() => {
___fillTheForm(document.getElementById('editNoteForm'), Database.getNoteByID(noteID))
}, 224)
} else {
if (typeof Database[noteAction] === 'function') {
row.classList.add(noteAction)
setTimeout(() => {
Database[noteAction](noteID)
dispatchStorage({ type: FILTER_GRID_DATA_BY_ROUTE, payload: route })
row.classList.remove(noteAction)
}, 999)
}
}
}
return (
<tr className="category-row" data-id={note.id}>
<td>{___formatDate(note.createdAt)}<i className={`icon icon-${note.category}`}/></td>
<td className="uk-text-bold">{note.title}</td>
<td>{___ucWords(note.category)}</td>
<td dangerouslySetInnerHTML={{ __html: note.content }}/>
<td>{___findDates(note.content)}</td>
<td className="icon icon-edit grid-control" onClick={toAction} data-action="editNote"/>
<td className={`icon icon-${note.archive === 'on' ? 'un' : ''}archive grid-control`}
onClick={toAction}
data-action={(note.archive === 'on' ? 'unA' : 'a') + 'rchiveNote'}/>
<td className="icon icon-delete grid-control" onClick={toAction} data-action="deleteNote"/>
</tr>
)
}
const SummaryRows = (sum) => {
return (
<tr className="category-row">
<td>{___ucWords(sum.title)}<i className={`icon icon-${sum.title}`}/></td>
<td>{sum.active}</td>
<td>{sum.archived}</td>
</tr>
)
}
const GridViewBody = ({ data, route }) => {
return (
<tbody>
{(() => (route !== 'summary')
? data.map((note, key) => <NotesRows key={key + '-' + note.id} {...note} />)
: data.map((note, key) => <SummaryRows key={key} {...note} />)
)()}
</tbody>
)
}
export default GridViewBody

View File

@@ -0,0 +1,24 @@
const Cols = ({ width = null }) => {
return (
<col width={width}/>
)
}
const Headers = ({ title = null, icon = null }) => {
return (
<th className={icon}>{title}</th>
)
}
const GridViewHeader = ({ template }) => {
return (
<>
<colgroup>{template.cols.map((item, key) => <Cols key={key} {...item} />)}</colgroup>
<thead>
<tr>{template.cols.map((item, key) => <Headers key={key} {...item} />)}</tr>
</thead>
</>
)
}
export default GridViewHeader

View File

@@ -0,0 +1,45 @@
import { useContext } from 'react'
import Context from '../../redux/context'
import { FILTER_GRID_DATA_BY_ROUTE, SET_ROUTE } from '../../redux/reducersTypes'
const TabList = ({ slug = '', label = 'Untitled' }) => {
const { nav, route, dispatchRoute, dispatchStorage } = useContext(Context)
const cls = nav.tabClassList.split(' ')
if( route === slug ){
cls.push(nav.activeClass)
}
const showGrid = e => {
e.preventDefault()
const pathname = new URL(e.target.href).pathname
const newRoute = pathname.slice(1)
window.history.pushState('', e.target.innerText, pathname)
dispatchRoute({ type: SET_ROUTE, payload: newRoute })
dispatchStorage({ type: FILTER_GRID_DATA_BY_ROUTE, payload: newRoute })
}
return (
<li className={cls.join(' ')}>
<a href={'/' + slug} onClick={e => showGrid(e)}>{label}</a>
</li>
)
}
const HeaderNavigation = () => {
const { nav } = useContext(Context)
if ((nav.routes || []).length <= 0) {
return <></>
}
return (
<nav className="uk-container">
<ul id={nav.parentID} className={nav.parentClassList}>
{nav.routes.map((item, key) => <TabList key={key} {...item} />)}
</ul>
</nav>
)
}
export default HeaderNavigation

24
src/config/grid.js Normal file
View File

@@ -0,0 +1,24 @@
const grid = {
parentClassList: 'uk-table uk-table-small uk-table-striped uk-table-hover uk-table-divider',
notes: {
cols: [
{ title: 'Created', width: 222 },
{ title: 'Title', width: 250 },
{ title: 'Category', width: 150 },
{ title: 'Content' },
{ title: 'Dates', width: 180 },
{ width: 32, icon: 'icon icon-edit' },
{ width: 32, icon: 'icon icon-archive' },
{ width: 32, icon: 'icon icon-delete' },
],
},
summary: {
cols: [
{ title: 'Note Category', width: 222 },
{ title: 'Active', width: 250 },
{ title: 'Archived' },
],
},
}
export default grid

34
src/config/navigation.js Normal file
View File

@@ -0,0 +1,34 @@
const navigation = {
parentID: 'navSwitcher',
parentClassList: 'uk-subnav uk-subnav-pill uk-margin-small-top uk-margin-small-bottom uk-padding-small',
tabClassList: 'nav-tab',
activeClass: 'uk-active',
initialTab: '',
template: 'notes',
routes: [
{
slug: 'all',
label: 'All Notes',
template: 'notes',
fetchLimit: 0
},
{
slug: '',
label: 'Active Notes',
template: 'notes'
},
{
slug: 'archive',
label: 'Archived Notes',
template: 'notes'
},
{
slug: 'summary',
label: 'Summary',
template: 'summary',
fetchLimit: 0
},
]
}
export default navigation

189
src/controllers/Database.js Normal file
View File

@@ -0,0 +1,189 @@
//import DOMController from './DOMController'
import Validation from './ValidationController'
import { fakerDB } from './db/faker'
//import Alert from './AlertController'
export default class Database {
static purgeStorage () {
localStorage.removeItem(window.env.localStorageKey)
}
/**
*
* @param save
* @param fakerAmount
* @returns {any}
*/
static getStorage (save = true, fakerAmount = window.env.fakerAmount) {
let storage = localStorage.getItem(window.env.localStorageKey)
if (storage === null || storage === '[]') {
storage = JSON.stringify(fakerDB(fakerAmount) || '[]')
if (save) localStorage.setItem(window.env.localStorageKey, storage)
}
return JSON.parse(storage || '[]')
}
/**
*
* @param notes
* @param key
*/
static saveNotesToStorage (notes, key = window.env.localStorageKey) {
return localStorage.setItem(key, JSON.stringify(notes, null, 2))
}
/**
*
* @param archive
* @param n
* @param fakerAmount
* @returns {*}
*/
static getNotes (archive = false, n = window.env.prePopulateAmount, fakerAmount = window.env.fakerAmount) {
let notes = Database.getStorage(true, fakerAmount)
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 archive
* @param n
* @param fakerAmount
* @returns {*[]|[]}
*/
static getNotesSanitized (archive = false, n = window.env.prePopulateAmount, fakerAmount = window.env.fakerAmount) {
let notes = Database.getNotes(archive, n, fakerAmount)
notes = Validation.allAgainstSchema(notes, window.env.schemas.note)
return n > 0 ? notes.slice(0, n) : notes
}
/**
*
* @param id
*/
static deleteNote (id) {
const notes = Database.getStorage()
const filteredNotesByID = notes.filter(note => note.id !== id)
Database.saveNotesToStorage(filteredNotesByID)
}
/**
*
* @param id
* @param value
*/
static archiveNote (id, value = 'on') {
if (value !== '') value = 'on'
let note = Database.getNoteByID(id)
if (note.length > 0 && note[0].archive !== value) {
note[0].archive = value
Database.saveNote(note[0], 'archive')
return true
}
return false
}
/**
*
* @param id
*/
static unArchiveNote (id) {
this.archiveNote(id, '')
}
/**
*
* @param fakerAmount
* @returns {*[]}
*/
static getAnalytics (fakerAmount = window.env.fakerAmount) {
const analytics = []
const data = Database.getStorage(true, fakerAmount)
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) => {
// Important! using 'Double Equality operator (==)'
// noinspection EqualityComparisonWithCoercionJS
analytics.push({
title: cat,
archived: isArchive.filter(el => el[cat] === 'on').length,
// eslint-disable-next-line
active: isArchive.filter(el => el[cat] == '').length,
})
})
}
return analytics
}
/**
*
* @param id
* @returns {*}
*/
static getNoteByID (id) {
return Database.getStorage().filter(el => el.id === id)
}
/**
*
* @param noteToStore
*/
static saveNote (noteToStore = null) {
if (Object.keys(noteToStore).length === 0) {
return 'Passed data is empty'
}
const isValid = Validation.againstSchema(noteToStore, window.env.schemas.note)
if (isValid.status === true) {
noteToStore = isValid.data
const notes = Database.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)
}
Database.saveNotesToStorage(notes)
} else {
return 'Passed data is not valid:\r\n - ' + isValid.error_text.join('\r\n - ')
}
return true
}
}

View File

@@ -0,0 +1,98 @@
//import env from '../env'
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')
}*/
}

View File

@@ -0,0 +1,41 @@
import { randomID6n } from '../../helpers'
const faker = require('faker')
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',
].concat(Array.from({ length: 42 }, () => '') )
export const fakerDB = length => {
const data = []
const cat = window.env.schemas.note.category[1].slice(2, -2).split('|')
length = length > window.env.fakerAmount*2 ? window.env.fakerAmount : length
for (let i = 0; i < length; i++) {
const updated = 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 !== '')
data.push({
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: ['', updated][(Math.random() * 2) | 0],
})
}
return data
}
//console.log( JSON.stringify( data, null, 2 ))

27
src/env.js Normal file
View File

@@ -0,0 +1,27 @@
const env = {
"localStorageKey": "notes-home-task-2",
"prePopulateAmount": 7,
"fakerAmount": 32,
"lists": {
"monthsFull": ["January","February","March","April","May","June","July", "August","September","October","November","December"]
},
"alertTypes": [
"primary", "success", "warning", "danger"
],
"querySelectors": {
"root": ["#", "app"]
},
"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$" ]
}
}
}
export default env

116
src/helpers.js Normal file
View File

@@ -0,0 +1,116 @@
import nav from './config/navigation'
export const randomID6n = () => Math.floor(100000 + Math.random() * 900000)
const getNavProperty = (route, prop, placeholder) =>{
try{
return nav.routes.filter(r => r.slug === route)[0][prop]
}catch (e) {
return placeholder
}
}
export const ___fetchFilter = route => {
return route === 'all' ? null : route === 'archive'
}
export const ___fetchLimit = route => {
return getNavProperty(route, 'fetchLimit', window.env.prePopulateAmount)
}
export const ___template = route => {
return getNavProperty(route, 'template', nav.template)
}
export const ___route = route => {
return getNavProperty(route, 'slug', nav.initialTab)
}
/**
*
* @param date
* @returns {string}
*/
export const ___formatDate = date => {
const dt = new Date(date)
return [ window.env.lists.monthsFull[dt.getMonth()], dt.getDate(), `, ${dt.getFullYear()}`].join(' ')
}
/**
*
* @param string
* @returns {string}
*/
export const ___ucWords = string => {
return string.split('_').map(word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase()).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
}
export const ___set404 = (set = false, sleep = 5000) => {
if( set === false ) return
document.body.innerHTML = `<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>`
setTimeout( () => {
window.location.assign('/')
}, sleep)
}
/**
*
* @param $form
* @param data
*/
export const ___fillTheForm = ( $form, data ) => {
if ($form instanceof Node) {
if (data.length === 1) {
for (const [key, value] of Object.entries(data[0])) {
const $input = $form.querySelector(`[name='${key}']`)
if ($input instanceof Element) {
if ($input.type === 'checkbox') {
if (value !== '') {
$input.click()
}
} else {
$input.value = value
}
}
}
}
}
}

View File

@@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -1,17 +1,31 @@
import './styles/uikit.min.css';
import env from './env.js';
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import { render } from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
//import { debugContextDevtool } from 'react-context-devtool'
//import { Provider } from 'react-redux'
//import { store } from './reducers/root'
ReactDOM.render(
window.env = env
const root = document.getElementById(env.querySelectors.root[1])
render(
<React.StrictMode>
<App />
<App/>
</React.StrictMode>,
document.getElementById('root')
root
);
//<Provider store={store}>
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
//debugContextDevtool(root, { disable: process.env.NODE_ENV === "production" });

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

5
src/redux/context.js Normal file
View File

@@ -0,0 +1,5 @@
import { createContext } from 'react'
const Context = createContext()
export default Context

27
src/redux/reducer.js Normal file
View File

@@ -0,0 +1,27 @@
import { FILTER_GRID_DATA_BY_ROUTE, SET_ROUTE, PRE_POPULATE_STORAGE } from './reducersTypes'
import Database from '../controllers/Database'
import { ___fetchFilter, ___fetchLimit } from '../helpers'
const reducer = (state, action) => {
switch (action.type) {
case SET_ROUTE:
return action.payload
case PRE_POPULATE_STORAGE:
Database.purgeStorage()
if( action.route=== 'summary' ){
return Database.getAnalytics(action.payload)
}
return Database.getNotesSanitized(___fetchFilter(action.route), ___fetchLimit(action.route), action.payload)
case FILTER_GRID_DATA_BY_ROUTE:
if( action.payload === 'summary' ){
return Database.getAnalytics()
}
return Database.getNotesSanitized(___fetchFilter(action.payload), ___fetchLimit(action.payload))
default:
return state
}
}
export default reducer

View File

@@ -0,0 +1,3 @@
export const PRE_POPULATE_STORAGE = 'PRE_POPULATE_STORAGE'
export const SET_ROUTE = 'SET_ROUTE'
export const FILTER_GRID_DATA_BY_ROUTE = 'FILTER_GRID_DATA_BY_ROUTE'

View File

@@ -0,0 +1,40 @@
footer .modal {
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 */
.modal-content {
background: ghostwhite;
margin: 10% auto; /* 15% from the top and centered */
padding: 20px;
border: 1px solid #888;
width: 80%; /* Could be more or less, depending on screen size */
}
input[readonly] {
outline: none;
border: none;
background: transparent;
color: #666;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
}

View File

@@ -0,0 +1,169 @@
body {
margin-top: 100px;
}
tbody td {
font-size: 14px;
}
header {
overflow: hidden;
position: fixed;
left: 0;
top: 0;
width: 100%;
z-index: 999;
}
td.icon, th.icon {
background-repeat: no-repeat;
background-position: center;
background-size: 24px;
&.grid-control {
cursor: pointer;
filter: opacity(.5);
transition: transform 250ms;
&:hover {
filter: opacity(1);
}
&:not(.icon-unarchive):hover {
transform: translateY(4px);
}
&.icon-unarchive:hover {
transform: translateY(-4px);
}
}
&.icon-edit {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='48' viewBox='0 0 24 24' width='48' fill='%23abcdef'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath 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'/%3E%3C/svg%3E");
}
&.icon-archive {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='48' viewBox='0 0 24 24' width='48' fill='%23fcc343'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='m20.54 5.23-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'/%3E%3C/svg%3E");
}
&.icon-unarchive {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='48' viewBox='0 0 24 24' width='48' fill='%23bada55'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath 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'/%3E%3C/svg%3E");
}
&.icon-delete {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='48' viewBox='0 0 24 24' width='48' fill='%23d6676d'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath 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'/%3E%3C/svg%3E");
}
}
@keyframes fadeInAnimation {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes splashInAnimation {
0% {
opacity: 1;
}
75% {
opacity: 0;
}
100% {
opacity: .66;
}
}
.category-row {
animation-iteration-count: 1;
animation-fill-mode: forwards;
&.deleteNote {
animation: fadeInAnimation ease 1s;
td {
color: rgba(214, 103, 109, .8);
text-decoration: line-through;
}
&:hover {
background-color: rgba(214, 103, 109, .3);
}
}
&.archiveNote {
animation: splashInAnimation ease 1s;
td {
color: rgb(252, 195, 67);
}
&:hover {
background-color: rgba(252, 195, 67, .3);
}
}
&.unArchiveNote {
animation: splashInAnimation ease 1s;
td {
color: rgb(186, 218, 85);
}
&:hover {
background-color: rgba(186, 218, 85, .3);
}
}
td:first-child {
position: relative;
padding-left: 60px;
> i {
content: '';
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-idea {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='48' viewBox='0 0 24 24' width='48' fill='%23f8f8ff'%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3Cpath 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.1-.85.6V16h-4v-2.3l-.85-.6A4.997 4.997 0 0 1 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z'/%3E%3C/svg%3E")
}
&.icon-random_thought {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='48' viewBox='0 0 20 20' width='48' fill='%23f8f8ff'%3E%3Cpath fill='none' d='M0 0h20v20H0z'/%3E%3Cpath d='M10.5 4C8.22 4 6.35 5.71 6.06 7.91l-1.54 2.31a.5.5 0 0 0 .41.78H6v2c0 .55.45 1 1 1h1v2h5v-3.76c1.21-.81 2-2.18 2-3.74C15 6.01 12.99 4 10.5 4zm1.83 4.67c0 .09-.01.18-.02.26l.56.44c.05.04.07.11.03.17l-.53.92c-.03.06-.1.08-.16.06l-.66-.27c-.14.1-.29.19-.45.26l-.1.71c-.01.07-.06.11-.13.11H9.8a.13.13 0 0 1-.13-.11l-.1-.71c-.16-.07-.31-.15-.45-.26l-.66.27c-.06.02-.13 0-.16-.06l-.54-.92c-.03-.06-.02-.13.03-.17l.56-.44c-.01-.09-.02-.18-.02-.26s.01-.18.02-.26l-.56-.44c-.05-.04-.06-.11-.03-.17l.53-.92c.03-.06.1-.08.16-.06l.66.27c.14-.1.29-.19.45-.26l.1-.71c.02-.07.07-.12.14-.12h1.07c.07 0 .12.05.13.11l.1.71c.16.07.31.15.45.26l.66-.27c.06-.02.13 0 .16.06l.53.92c.03.06.02.13-.03.17l-.56.44c.02.09.02.18.02.27z'/%3E%3Cpath d='M10.33 7.71c-.52 0-.95.43-.95.95s.43.95.95.95.95-.43.95-.95a.943.943 0 0 0-.95-.95z'/%3E%3C/svg%3E");
}
&.icon-task {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='48' viewBox='0 0 24 24' width='48' fill='%23f8f8ff'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath 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-3 1.1-2h7.45c.75 0 1.41-.41 1.75-1.03L21.7 4H5.21l-.94-2H1v2h2l3.6 7.59L3.62 17H19v-2H7z'/%3E%3C/svg%3E");
background-size: 20px !important;
}
&.icon-quote {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='48' viewBox='0 0 24 24' width='48' fill='%23f8f8ff'%3E%3Cpath d='M5 17h3l2-4V7H4v6h3l-2 4zm10 0h3l2-4V7h-6v6h3l-2 4z'/%3E%3C/svg%3E");
}
}
}
}
thead {
.icon-edit {
opacity: 0;
}
.icon {
filter: grayscale(1);
}
}

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

File diff suppressed because one or more lines are too long