committing commit
5
.gitignore
vendored
@@ -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
@@ -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
13
package.json
@@ -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
@@ -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
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/512x512.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 15 KiB |
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -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"
|
||||
}
|
||||
|
||||
1
public/notes_black_48dp.svg
Normal 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 |
38
src/App.css
@@ -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);
|
||||
}
|
||||
}
|
||||
57
src/App.js
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
98
src/components/EditForm.js
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,12 @@
|
||||
import HeaderNavigation from './partials/HeaderNavigation'
|
||||
|
||||
const Header = () => {
|
||||
|
||||
return (
|
||||
<header className="uk-background-secondary">
|
||||
<HeaderNavigation/>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
74
src/components/partials/GridViewBody.js
Normal 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
|
||||
24
src/components/partials/GridViewHeader.js
Normal 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
|
||||
45
src/components/partials/HeaderNavigation.js
Normal 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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
98
src/controllers/ValidationController.js
Normal 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')
|
||||
}*/
|
||||
|
||||
}
|
||||
41
src/controllers/db/faker.js
Normal 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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
24
src/index.js
@@ -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" });
|
||||
|
||||
@@ -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
@@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
const Context = createContext()
|
||||
|
||||
export default Context
|
||||
27
src/redux/reducer.js
Normal 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
|
||||
3
src/redux/reducersTypes.js
Normal 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'
|
||||
40
src/styles/components/editForm.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
169
src/styles/components/grid.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||