diff --git a/src/pages/notes.tsx b/src/pages/notes.tsx index 42721b3..30262ff 100644 --- a/src/pages/notes.tsx +++ b/src/pages/notes.tsx @@ -7,7 +7,7 @@ import generateId from '../utilities/Id' import Link from 'next/link' import { formatDate } from '../utilities/Date' import { useRouter } from 'next/router' -import { XCircle } from 'react-feather' +import { Trash2, FilePlus, FolderPlus } from 'react-feather' const Navbar = styled('aside')({ width: 360, @@ -71,10 +71,14 @@ const NoteLink = styled('a')({ position: 'relative', }) -const NoteLinkTitle = styled('strong')({ +const NoteLinkPrimary = styled('div')({ display: 'block', }) +const NoteLinkTitle = styled('strong')({ + verticalAlign: 'middle', +}) + const LinkContainer = styled('div')({ position: 'relative', }) @@ -108,6 +112,21 @@ const NoteLinkBackground = styled('span')({ position: 'absolute', }) +const NewIcon = styled(FilePlus)({ + verticalAlign: 'middle', + marginRight: '0.5rem', +}) + +const NewFolderIcon = styled(FolderPlus)({ + verticalAlign: 'middle', + marginRight: '0.5rem', +}) + +const BinIcon = styled(Trash2)({ + verticalAlign: 'middle', + marginRight: '0.5rem', +}) + type NoteInstance = { id: string, title: string, content?: object, updatedAt: string, } const Notes = ({ id: idProp }) => { @@ -126,7 +145,6 @@ const Notes = ({ id: idProp }) => { timeoutRef.current = window.setTimeout(async () => { const newNote = await Storage.saveNote({ ...stateRef.current, - title, updatedAt: new Date().toISOString(), }) if (router.query.id !== id) { @@ -151,6 +169,7 @@ const Notes = ({ id: idProp }) => { } const handleTitleChange = e => { + stateRef.current.title = e.target.value setTitle(e.target.value) autoSave() } @@ -184,10 +203,6 @@ const Notes = ({ id: idProp }) => { setId(idProp || generateId()) }, [idProp]) - React.useEffect(() => { - autoSave() - }, [title]) - return ( @@ -195,6 +210,40 @@ const Notes = ({ id: idProp }) => { + + + + + + + Personal + + + + + + + + + + + + Create Folder + + + + + { > - - New Note - + + + + Create Note + + @@ -230,11 +282,13 @@ const Notes = ({ id: idProp }) => { > - 0 ? 1 : 0.5, }} - > - {n.title.length > 0 ? n.title : '(untitled)'} - + + 0 ? 1 : 0.5, }} + > + {n.title.length > 0 ? n.title : '(untitled)'} + + {' '}
diff --git a/src/services/LocalStorage.ts b/src/services/LocalStorage.ts new file mode 100644 index 0000000..4d46841 --- /dev/null +++ b/src/services/LocalStorage.ts @@ -0,0 +1,23 @@ +export default class LocalStorage { + constructor( + private readonly source: Storage, + private readonly serializer: (t: T) => string, + private readonly deserializer: (s: string) => T + ) {} + + getItem(id: string, fallback: T = null) { + const raw = this.source.getItem(id) + if (raw === null) { + return fallback + } + return this.deserializer(raw) + } + + setItem(id: string, item: T) { + this.source.setItem(id, this.serializer(item)) + } + + removeItem(id: string) { + this.source.removeItem(id) + } +} diff --git a/src/services/Note.ts b/src/services/Note.ts index 0a211d9..b5d95c0 100644 --- a/src/services/Note.ts +++ b/src/services/Note.ts @@ -33,7 +33,6 @@ export const getMultiple = repository => async (query: Record) export const save = repository => (body: Partial) => async (id: string, idColumnName = 'id') => { const { content: contentRaw, ...etcBody } = body const content = contentRaw! ? JSON.stringify(contentRaw) : null - console.log('REPOSITORY', repository) const [dao, created] = await repository.findOrCreate({ where: { [idColumnName]: id }, defaults: { diff --git a/src/services/Storage.ts b/src/services/Storage.ts index 7560256..d64b535 100644 --- a/src/services/Storage.ts +++ b/src/services/Storage.ts @@ -1,60 +1,82 @@ -import Note from '../../models/Note' -import Instance from '../utilities/Instance' -type NoteInstance = Instance - -export const loadNotes = async () => { - // const localNotes = window.localStorage.getItem('notes') - const localNotes = null - let theNotes: NoteInstance[] - if (localNotes === null) { - const notes = await window.fetch('/api/notes') - theNotes = await notes.json() - window.localStorage.setItem('notes', JSON.stringify(theNotes)) - } else { - theNotes = JSON.parse(localNotes) - } - return theNotes - .map(a => ({ - ...a, - updatedAt: new Date(a.updatedAt as string), - })) - .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()) +import {addTime, TimeDivision} from '../utilities/Date' +import * as Serialization from '../utilities/Serialization' +import LocalStorage from './LocalStorage' + +type LoadItemParams = { + id: string, + url: string, } -export const loadFolders = async () => { - // const localFolders = window.localStorage.getItem('folders') - const localFolders = null - let theFolders: unknown[] - if (localFolders === null) { - const folders = await window.fetch('/api/folders') - theFolders = await folders.json() - window.localStorage.setItem('folders', JSON.stringify(theFolders)) - } else { - theFolders = JSON.parse(localFolders) +type LoadItems = >(params: LoadItemParams) => () => Promise + +const loadItems: LoadItems = >(params) => async (): Promise => { + const { id, url, } = params + const storage = new LocalStorage( + window.localStorage, + Serialization.serialize, + Serialization.deserialize + ) + const localData = storage.getItem(id) + if (localData === null) { + const remoteItems = await window.fetch(url) + // TODO add custom serialization method + const theItems: T[] = await remoteItems.json() + storage.setItem(id, { + // TODO backend should set expiry + expiry: addTime(new Date(), 30)(TimeDivision.DAYS).getTime(), + items: theItems, + }) + return theItems + } + + const dataExpiry = new Date(Number(localData.expiry)) + const now = new Date() + if (now.getTime() > dataExpiry.getTime()) { + storage.removeItem(id) + const loader: () => Promise = loadItems(params) + return loader() } - return theFolders + return localData.items } -export const saveNote = async params => { - const { - id, - title, - content, - } = params - const response = await window.fetch(`/api/notes/${id}`, { +type SaveItem = >(p: LoadItemParams) => (item: T) => Promise + +const saveItem: SaveItem = >(params) => async (item) => { + const { id: storageId, url } = params + const storage = new LocalStorage( + window.localStorage, + Serialization.serialize, + Serialization.deserialize + ) + const localData = storage.getItem(storageId, { + expiry: addTime(new Date(), 30)(TimeDivision.DAYS).getTime(), + items: [] + }) + console.log(localData) + const localItems = localData.items + const { id: itemId, ...theBody } = item + const theItems: T[] = ( + localItems.some(i => i.id === itemId) + ? localItems.map(i => i.id === itemId ? item : i) + : [...localItems, item] + ) + storage.setItem(storageId, { ...localData, items: theItems }) + const response = await window.fetch(`${url}/${itemId}`, { method: 'put', - body: JSON.stringify({ - title, - content, - }), + body: JSON.stringify(theBody), headers: { 'Content-Type': 'application/json', }, }) - const body = await response.json() + const responseBody = await response.json() if (response.status !== 201 && response.status !== 200) { - throw body + throw responseBody } - return body + return responseBody as T } + +export const loadNotes = loadItems({ id: 'notes', url: '/api/notes' }) +export const loadFolders = loadItems({ id: 'folders', url: '/api/folders' }) +export const saveNote = saveItem({ id: 'notes', url: '/api/notes' }) +export const saveFolder = saveItem({ id: 'folders', url: '/api/folders' }) diff --git a/src/utilities/Date.ts b/src/utilities/Date.ts index 92ea206..676cb48 100644 --- a/src/utilities/Date.ts +++ b/src/utilities/Date.ts @@ -14,3 +14,85 @@ export const formatDate: FormatDate = d => { return `${year}-${month}-${date} ${hour}:${minute}` } + +export enum TimeDivision { + MILLISECONDS, + SECONDS, + MINUTES, + HOURS, + DAYS +} + +type GetTimeDifference = (a: Date, b: Date) => (c: TimeDivision) => number + +export const getTimeDifference: GetTimeDifference = (a, b) => { + const ms = b.getTime() - a.getTime() + const absoluteMs = ms < 0 ? -ms : ms + + return c => { + let divisionDifference = absoluteMs + if (c === TimeDivision.MILLISECONDS) { + return divisionDifference + } + + divisionDifference /= 1000 + if (c === TimeDivision.SECONDS) { + return divisionDifference + } + + divisionDifference /= 60 + if (c === TimeDivision.MINUTES) { + return divisionDifference + } + + divisionDifference /= 60 + if (c === TimeDivision.HOURS) { + return divisionDifference + } + + divisionDifference /= 24 + if (c === TimeDivision.DAYS) { + return divisionDifference + } + + throw new Error('Unknown time division.') + } +} + +type AddTime = (refDate: Date, increment: number) => (c: TimeDivision) => Date + +export const addTime: AddTime = (refDate, increment) => { + const futureDate = new Date(refDate.getTime()) + return c => { + let msIncrement = increment + if (c === TimeDivision.MILLISECONDS) { + futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement) + return futureDate + } + + msIncrement *= 1000 + if (c === TimeDivision.SECONDS) { + futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement) + return futureDate + } + + msIncrement *= 60 + if (c === TimeDivision.MINUTES) { + futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement) + return futureDate + } + + msIncrement *= 60 + if (c === TimeDivision.HOURS) { + futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement) + return futureDate + } + + msIncrement *= 24 + if (c === TimeDivision.DAYS) { + futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement) + return futureDate + } + throw new Error('Unknown time division.') + } +} diff --git a/src/utilities/Serialization.ts b/src/utilities/Serialization.ts new file mode 100644 index 0000000..3b8c73f --- /dev/null +++ b/src/utilities/Serialization.ts @@ -0,0 +1,3 @@ +export const serialize = JSON.stringify + +export const deserialize = JSON.parse