aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/Notes/components/List.js129
-rw-r--r--apps/Notes/components/ListItem.js119
-rw-r--r--apps/Notes/components/Note.js140
-rw-r--r--apps/Notes/components/NoteView.js63
-rw-r--r--apps/Notes/helpers/noteActions.js104
-rw-r--r--apps/Notes/helpers/sortNotes.js0
-rw-r--r--apps/Notes/hooks/useNotes.js8
-rw-r--r--apps/Notes/hooks/useSort.js22
-rw-r--r--apps/Notes/index.js3
-rw-r--r--apps/index.js1
10 files changed, 589 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
diff --git a/apps/index.js b/apps/index.js
new file mode 100644
index 0000000..fa08e0e
--- /dev/null
+++ b/apps/index.js
@@ -0,0 +1 @@
+export {default as Notes} from './Notes'