diff options
Diffstat (limited to 'apps/Notes')
-rw-r--r-- | apps/Notes/components/List.js | 129 | ||||
-rw-r--r-- | apps/Notes/components/ListItem.js | 119 | ||||
-rw-r--r-- | apps/Notes/components/Note.js | 140 | ||||
-rw-r--r-- | apps/Notes/components/NoteView.js | 63 | ||||
-rw-r--r-- | apps/Notes/helpers/noteActions.js | 104 | ||||
-rw-r--r-- | apps/Notes/helpers/sortNotes.js | 0 | ||||
-rw-r--r-- | apps/Notes/hooks/useNotes.js | 8 | ||||
-rw-r--r-- | apps/Notes/hooks/useSort.js | 22 | ||||
-rw-r--r-- | apps/Notes/index.js | 3 |
9 files changed, 588 insertions, 0 deletions
diff --git a/apps/Notes/components/List.js b/apps/Notes/components/List.js new file mode 100644 index 0000000..8561d4f --- /dev/null +++ b/apps/Notes/components/List.js @@ -0,0 +1,129 @@ +import React, {useState, useEffect, useRef} from 'react' +import useUser from 'lib/useUser' +import useNotes from '../hooks/useNotes' +import useSort from '../hooks/useSort' +import fetchJson from 'lib/fetchJson' +import {Layout} from 'components' +import ListItem from './ListItem' +import NoteView from './NoteView' +import Note from './Note' + +const List = () => { + const [fetchedNote, setFetchedNote] = useState() + const [action, setAction] = useState('') + const {notes, error} = useNotes() + const [sortedBy, sortBy, sortFn] = useSort(2) + const {user, mutateUser} = useUser({ + redirectToLogin: true, + redirectToVerify: true, + }) + + if (error) return <p>Failed to fetch notes</p> + + if (!user || !user.isLoggedIn || !user.isVerified || !notes || !sortFn) { + return <p>Loading...</p> + } + + + return ( + <> + { + action === '' && ( + <> + <div className='window__submenu'> + <div onClick={() => setAction('addNote')}>New note</div> + </div> + <table className="list"> + <thead> + <tr> + <th className='list__title' onClick={() => sortBy(1)}>Title {sortedBy(1)}</th> + <th className='list__date' onClick={() => sortBy(2)}>Created {sortedBy(2)}</th> + <th className='list__date' onClick={() => sortBy(3)}>Modified {sortedBy(3)}</th> + </tr> + </thead> + <tbody> + { + notes.length > 0 + ? (notes.sort(sortFn).map(note => ( + <ListItem + key={note._id} + note={note} + setAction={setAction} + setFetchedNote={setFetchedNote} + /> + ))) : ( + <tr> + <td>Your notes list is empty.</td> + </tr> + )} + </tbody> + </table> + </> + ) + } + { + action === 'addNote' && ( + <Note + action={action} + setAction={setAction} + /> + ) + } + { + action === 'showNote' && ( + <NoteView + fetchedNote={fetchedNote} + setFetchedNote={setFetchedNote} + setAction={setAction} + /> + ) + } + { + action === 'editNote' && ( + <Note + action={action} + setAction={setAction} + fetchedNote={fetchedNote} + /> + ) + } + <style jsx>{` + table { + display: block; + overflow: auto; + width: 100%; + table-layout: fixed; + word-wrap: break-word; + height: 100%; + margin-top: -1em; + padding-top: 1em; + } + + tbody, thead { + display: block; + } + + tr { + display: flex; + padding: 0 .5em; + } + + th { + font-weight: 600; + text-align: left; + min-width: 10em; + white-space: nowrap; + padding-bottom: .5em; + cursor: pointer; + line-height: 1.1em; + } + + th:first-of-type { + width: 99%; + } + `}</style> + </> + ) +} + +export default List diff --git a/apps/Notes/components/ListItem.js b/apps/Notes/components/ListItem.js new file mode 100644 index 0000000..42d67c0 --- /dev/null +++ b/apps/Notes/components/ListItem.js @@ -0,0 +1,119 @@ +import React, { useContext } from 'react' +import fetchJson from 'lib/fetchJson' +import {getNote, removeNote} from '../helpers/noteActions.js' +import useNotes from '../hooks/useNotes' +import Context from 'context'; +import { faEdit, faTrash } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +const datestring = date => { + const d = new Date(date); + return ("0" + d.getDate()).slice(-2) + "-" + ("0"+(d.getMonth()+1)).slice(-2) + "-" + + d.getFullYear() + " " + ("0" + d.getHours()).slice(-2) + ":" + ("0" + d.getMinutes()).slice(-2) +}; + +const ListItem = ({note, setAction, setFetchedNote}) => { + const {mutateNotes} = useNotes() + const {setPopup} = useContext(Context) + + const handleNoteAction = async (a, note, e) => { + if (e) e.stopPropagation() + await getNote(note, setFetchedNote, setPopup, () => setAction(a)) + } + + return ( + <> + <tr key={note._id}> + <td + onClick={() => handleNoteAction('showNote', note)} + > + <span>{`${note.title}`}</span> + <span + onClick={e => handleNoteAction('editNote', note, e)} + > + <FontAwesomeIcon icon={faEdit} /> + </span> + <span + onClick={(e) => removeNote(e, note._id, mutateNotes, setPopup, setAction)} + > + <FontAwesomeIcon icon={faTrash} /> + </span> + </td> + <td + onClick={() => handleNoteAction('showNote', note)} + > + {datestring(note.created_at)} + </td> + <td + onClick={() => handleNoteAction('showNote', note)} + > + {datestring(note.updated_at)} + </td> + </tr> + <style jsx>{` + tr { + display: flex; + padding: .5em; + } + + td { + min-width: 10em; + white-space: nowrap; + } + + td:first-of-type { + width: 99%; + display: flex; + padding-right: 1em; + } + + td:first-of-type > span:nth-child(2), + td:first-of-type > span:nth-child(3) { + margin-left: .5em; + padding: .15em .5em; + line-height: 1em; + border-radius: 50%; + visibility: hidden; + opacity: 0; + font-size: 80%; + transition: .3s opacity linear .3s; + } + + td:first-of-type > span:nth-child(1) { + text-overflow: ellipsis; + flex-grow: 1; + } + + // td:first-of-type > span:nth-child(2) { + // margin-left: 1.25em; + // } + + tr:hover { + background: #eee; + border-radius: .5em; + cursor: pointer; + } + + tr:hover > td:first-of-type > span:nth-child(2), + tr:hover > td:first-of-type > span:nth-child(3) { + color: #666; + visibility: visible; + opacity: 1; + } + + tr > td:first-of-type > span:nth-child(2):hover { + color: #333; + background-color: #deffde; + } + + tr > td:first-of-type > span:nth-child(3):hover { + background-color: #ffdede; + color: #333; + } + + `}</style> + </> + ) +} + +export default ListItem diff --git a/apps/Notes/components/Note.js b/apps/Notes/components/Note.js new file mode 100644 index 0000000..d23806a --- /dev/null +++ b/apps/Notes/components/Note.js @@ -0,0 +1,140 @@ +import React, {useState, useContext} from 'react' +import Context from 'context'; +import fetchJson from 'lib/fetchJson' +import useNotes from '../hooks/useNotes' +import {addNote, updateNote} from '../helpers/noteActions.js' + +const Note = ({action, setAction, fetchedNote}) => { + const [text, setText] = useState('') + const {mutateNotes} = useNotes() + const [errorMsg, setErrorMsg] = useState('') + const {setPopup} = useContext(Context) + + const handleSubmit = e => { + e.preventDefault() + fetchedNote + ? updateNote(e, fetchedNote, mutateNotes, setAction, setPopup) + : addNote(e, mutateNotes, setAction, setPopup) + } + + // if (!fetchedNote) return <p>Loading...</p> + // if (fetchedNote.error) { + // setFetchedNote() + // setAction('') + // } + + return ( + <div className='note'> + <h2>{fetchedNote ? 'Edit note:' : 'Add new note:'}</h2> + <form onSubmit={handleSubmit} className='note__form'> + <input + className='note__title' + name='title' + type='text' + placeholder='Title' + defaultValue={fetchedNote ? fetchedNote.title : ''} + /> + <textarea + required + className='note__text' + placeholder='Note' + name='content' + defaultValue={fetchedNote ? fetchedNote.content : ''} + /> + <div className='note__buttons'> + <span + className='window__button' + onClick={() => {setAction(fetchedNote ? 'showNote' : '')}} + > + Cancel + </span> + <input + className='window__button' + type="submit" + value={fetchedNote ? 'Save note' : 'Add note'} + /> + </div> + </form> + <style jsx>{` + .note { + display: flex; + flex-direction: column; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: white; + padding: 1em 1em 2em; + animation: fade-in .3s; + } + + @keyframes fade-in { + from {opacity: 0;} + to {opacity: 1;} + } + + h2 { + font-size: 1.2em; + margin-bottom: .5em; + } + + .note__form { + display: flex; + flex-direction: column; + width: 100%; + flex-grow: 1; + } + + .note__title { + margin-bottom: .5rem; + height: 3rem; + border: none; + padding: 0.5rem; + font-size: 1rem; + // font-weight: 600; + border: 1px dashed #666; + } + + .note__title:placeholder { + font: inherit; + } + + .note__text { + font-size: 1rem; + flex-grow: 1; + resize: none; + height: 100%; + width: 100%; + border: none; + border: 1px dashed #666; + padding: 0.5rem; + } + + .note__text:placeholder { + font: inherit; + } + + .note__close { + position: absolute; + top: 8px; + right: 15px; + transform: rotate(45deg); + font-size: 26px; + cursor: pointer; + transition: .3s transform; + } + + .note__close:hover { + transform: rotate(135deg); + } + + .note__buttons { + text-align: right; + } + `}</style> + </div> + ) +} + +export default Note diff --git a/apps/Notes/components/NoteView.js b/apps/Notes/components/NoteView.js new file mode 100644 index 0000000..15d4944 --- /dev/null +++ b/apps/Notes/components/NoteView.js @@ -0,0 +1,63 @@ +import React, {useContext} from 'react'; +import Context from 'context'; +import useNotes from '../hooks/useNotes' +import {removeNote} from '../helpers/noteActions.js' + +const NoteView = ({fetchedNote, setFetchedNote, setAction}) => { + const {mutateNotes} = useNotes() + const {setPopup} = useContext(Context) + + if (!fetchedNote) return <p>Loading...</p> + if (fetchedNote.error) { + setFetchedNote() + setAction('') + } + + const {_id, content, title} = fetchedNote + + return ( + <section> + <div className='window__submenu'> + <div onClick={() => { setFetchedNote(); setAction('') }}>Back</div> + <div>Copy</div> + <div onClick={() => { setAction('editNote')}}>Edit</div> + <div onClick={e => { removeNote(e, _id, mutateNotes, setPopup, setAction) }}>Remove</div> + </div> + + <div className='window__scroll'> + <h2 className='selectable'>{title}</h2> + <p className='selectable'>{content}</p> + </div> + <style jsx>{` + section { + background: white; + padding: 1rem; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + animation: fade-in .3s; + } + + @keyframes fade-in { + from {opacity: 0;} + to {opacity: 1;} + } + + h2 { + font-size: 1.25em; + font-weight: 600; + padding: 1rem; + } + + p { + padding: 0 1rem 1rem; + white-space: pre-line; + } + `}</style> + </section> + ) +} + +export default NoteView diff --git a/apps/Notes/helpers/noteActions.js b/apps/Notes/helpers/noteActions.js new file mode 100644 index 0000000..c296c97 --- /dev/null +++ b/apps/Notes/helpers/noteActions.js @@ -0,0 +1,104 @@ +import fetchJson from 'lib/fetchJson' + +export const getNote = async (note, setFetchedNote, setPopup, callback) => { + try { + const {content} = await fetchJson(`/api/note/${note.noteId}`) + setFetchedNote({ ...note, content}) + callback() + } catch (err) { + setFetchedNote() + setPopup({ + content: 'Could not open note', + time: 2000, + error: true, + }) + } +} + +export const addNote = async (e, mutateNotes, setAction, setPopup) => { + const content = e.currentTarget.content.value + const title = e.currentTarget.title.value + + try { + mutateNotes( + await fetchJson('/api/notes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({title, content}), + }) + ) + setPopup({ + content: 'New note added', + time: 2000, + }) + setAction('') + } catch (e) { + setPopup({ + content: 'Could not save note', + time: 2000, + error: true, + }) + } +} + +export const updateNote = async (e, note, mutateNotes, setAction, setPopup) => { + const content = e.currentTarget.content.value + const title = e.currentTarget.title.value + const {_id, noteId} = note + + try { + mutateNotes( + await fetchJson('/api/notes', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({_id, title, noteId, content}), + }) + ) + setPopup({ + content: 'Note updated', + time: 2000, + }) + setAction('') + } catch (e) { + setPopup({ + content: 'Could not update note', + time: 2000, + error: true, + }) + } +} + +export const removeNote = (e, _id, mutateNotes, setPopup, setAction) => { + e.stopPropagation() + + const remove = async () => { + try { + await mutateNotes( + await fetchJson('/api/notes', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({_id}), + }) + ) + setPopup({ + content: 'Note was removed', + time: 2000, + }) + setAction('') + } catch (err) { + setPopup({ + content: 'Could not remove note', + time: 2000, + error: true, + }) + } + } + + setPopup({ + content: 'Do you want to remove note?', + yes: { label: 'Remove', action: remove }, + no: { label: 'Cancel', action: () => {} }, + error: true, + }) +} + diff --git a/apps/Notes/helpers/sortNotes.js b/apps/Notes/helpers/sortNotes.js new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/apps/Notes/helpers/sortNotes.js diff --git a/apps/Notes/hooks/useNotes.js b/apps/Notes/hooks/useNotes.js new file mode 100644 index 0000000..9d79034 --- /dev/null +++ b/apps/Notes/hooks/useNotes.js @@ -0,0 +1,8 @@ +import useSWR from 'swr' +import fetchJson from 'lib/fetchJson' + +export default function useNotes(){ + const { data: notes, error, mutate: mutateNotes } = useSWR('/api/notes') + + return {notes, mutateNotes, error} +} diff --git a/apps/Notes/hooks/useSort.js b/apps/Notes/hooks/useSort.js new file mode 100644 index 0000000..78c01b4 --- /dev/null +++ b/apps/Notes/hooks/useSort.js @@ -0,0 +1,22 @@ +import {useState} from 'react' +import {faSortAmountDown, faSortAmountUp} from '@fortawesome/free-solid-svg-icons' +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' + +const useSort = (d) => { + const [sort, setSort] = useState(d) + + const sortedBy = s => Math.abs(sort) === s && <FontAwesomeIcon icon={sort>0 ? faSortAmountDown : faSortAmountUp} /> + const sortBy = s => sort === s ? setSort(-1*s) : setSort(s) + const sortFn = (a, b) => { + const d = sort > 0 ? 1 : -1 + switch (Math.abs(sort)) { + case 1: return d * a.title.localeCompare(b.title) + case 2: return d * (new Date(b.created_at) - new Date(a.created_at)) + case 3: return d * (new Date(b.updated_at) - new Date(a.updated_at)) + } + } + + return [sortedBy, sortBy, sortFn] +} + +export default useSort diff --git a/apps/Notes/index.js b/apps/Notes/index.js new file mode 100644 index 0000000..6fe1f2e --- /dev/null +++ b/apps/Notes/index.js @@ -0,0 +1,3 @@ +import Notes from './components/List' + +export default Notes |