Browse Source

Improve load/save code

Make sure backup makes saving to local storage a priority.
feature/transactions
TheoryOfNekomata 3 years ago
parent
commit
051e018a76
6 changed files with 263 additions and 63 deletions
  1. +87
    -16
      src/pages/notes.tsx
  2. +23
    -0
      src/services/LocalStorage.ts
  3. +0
    -1
      src/services/Note.ts
  4. +68
    -46
      src/services/Storage.ts
  5. +82
    -0
      src/utilities/Date.ts
  6. +3
    -0
      src/utilities/Serialization.ts

+ 87
- 16
src/pages/notes.tsx View File

@@ -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 (
<React.Fragment>
<Head>
@@ -195,6 +210,40 @@ const Notes = ({ id: idProp }) => {
<link rel="icon" href="/favicon.ico" />
</Head>
<Navbar>
<Link
href={{
pathname: '/profile',
}}
passHref
>
<NoteLink>
<NavbarContainer>
<NoteLinkPrimary>
<NewFolderIcon />
<NoteLinkTitle>
Personal
</NoteLinkTitle>
</NoteLinkPrimary>
</NavbarContainer>
</NoteLink>
</Link>
<Link
href={{
pathname: '/folders/new',
}}
passHref
>
<NoteLink>
<NavbarContainer>
<NoteLinkPrimary>
<NewFolderIcon />
<NoteLinkTitle>
Create Folder
</NoteLinkTitle>
</NoteLinkPrimary>
</NavbarContainer>
</NoteLink>
</Link>
<Link
href={{
pathname: '/notes',
@@ -203,9 +252,12 @@ const Notes = ({ id: idProp }) => {
>
<NoteLink>
<NavbarContainer>
<NoteLinkTitle>
New Note
</NoteLinkTitle>
<NoteLinkPrimary>
<NewIcon />
<NoteLinkTitle>
Create Note
</NoteLinkTitle>
</NoteLinkPrimary>
</NavbarContainer>
</NoteLink>
</Link>
@@ -230,11 +282,13 @@ const Notes = ({ id: idProp }) => {
>
<NoteLink>
<NavbarContainer>
<NoteLinkTitle
style={{ opacity: n.title.length > 0 ? 1 : 0.5, }}
>
{n.title.length > 0 ? n.title : '(untitled)'}
</NoteLinkTitle>
<NoteLinkPrimary>
<NoteLinkTitle
style={{ opacity: n.title.length > 0 ? 1 : 0.5, }}
>
{n.title.length > 0 ? n.title : '(untitled)'}
</NoteLinkTitle>
</NoteLinkPrimary>
{' '}
<small>
<time
@@ -248,12 +302,29 @@ const Notes = ({ id: idProp }) => {
</Link>
<NoteActions>
<NoteAction>
<XCircle />
<Trash2 />
</NoteAction>
</NoteActions>
</LinkContainer>
))
}
<Link
href={{
pathname: '/bin',
}}
passHref
>
<NoteLink>
<NavbarContainer>
<NoteLinkPrimary>
<BinIcon />
<NoteLinkTitle>
View Binned Notes
</NoteLinkTitle>
</NoteLinkPrimary>
</NavbarContainer>
</NoteLink>
</Link>
</Navbar>
<Main>
<Container>


+ 23
- 0
src/services/LocalStorage.ts View File

@@ -0,0 +1,23 @@
export default class LocalStorage<T> {
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)
}
}

+ 0
- 1
src/services/Note.ts View File

@@ -33,7 +33,6 @@ export const getMultiple = repository => async (query: Record<string, unknown>)
export const save = repository => (body: Partial<ModelInstance>) => 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: {


+ 68
- 46
src/services/Storage.ts View File

@@ -1,60 +1,82 @@
import Note from '../../models/Note'
import Instance from '../utilities/Instance'
type NoteInstance = Instance<typeof Note.rawAttributes>

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 = <T extends Record<string, unknown>>(params: LoadItemParams) => () => Promise<T[]>

const loadItems: LoadItems = <T extends Record<string, unknown>>(params) => async (): Promise<T[]> => {
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<T[]> = 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 = <T extends Record<string, unknown>>(p: LoadItemParams) => (item: T) => Promise<T>

const saveItem: SaveItem = <T extends Record<string, unknown>>(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' })

+ 82
- 0
src/utilities/Date.ts View File

@@ -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.')
}
}

+ 3
- 0
src/utilities/Serialization.ts View File

@@ -0,0 +1,3 @@
export const serialize = JSON.stringify

export const deserialize = JSON.parse

Loading…
Cancel
Save