Extract data persistence and updates from views. Also use custom CSS files for theming.feature/transactions
@@ -0,0 +1,9 @@ | |||||
:root { | |||||
background-color: var(--color-bg); | |||||
color: var(--color-fg); | |||||
font-family: var(--font-family-base), sans-serif; | |||||
} | |||||
body { | |||||
margin: 0; | |||||
} |
@@ -0,0 +1,155 @@ | |||||
/** | |||||
* Editor | |||||
*/ | |||||
.__mobiledoc-editor { | |||||
font-family: var(--font-family-body), serif; | |||||
margin: 1em 0; | |||||
color: #454545; | |||||
font-size: 1.2em; | |||||
line-height: 1.6em; | |||||
position: relative; | |||||
min-height: 1em; | |||||
} | |||||
.__mobiledoc-editor:focus { | |||||
outline: none; | |||||
} | |||||
.__mobiledoc-editor > * { | |||||
position: relative; | |||||
} | |||||
.__mobiledoc-editor.__has-no-content:after { | |||||
content: attr(data-placeholder); | |||||
color: #bbb; | |||||
cursor: text; | |||||
position: absolute; | |||||
top: 0; | |||||
} | |||||
.__mobiledoc-editor a { | |||||
color: var(--color-primary); | |||||
white-space: nowrap; | |||||
} | |||||
.__mobiledoc-editor h1, | |||||
.__mobiledoc-editor h2, | |||||
.__mobiledoc-editor h3, | |||||
.__mobiledoc-editor h4, | |||||
.__mobiledoc-editor h5, | |||||
.__mobiledoc-editor h6 { | |||||
font-family: var(--font-family-base), sans-serif; | |||||
letter-spacing: -0.02em; | |||||
} | |||||
.__mobiledoc-editor blockquote { | |||||
border-left: 4px solid var(--color-primary); | |||||
margin: 1em 0 1em -1.2em; | |||||
padding-left: 1.05em; | |||||
color: #a0a0a0; | |||||
} | |||||
.__mobiledoc-editor img { | |||||
display: block; | |||||
max-width: 100%; | |||||
margin: 0 auto; | |||||
} | |||||
.__mobiledoc-editor div, | |||||
.__mobiledoc-editor iframe { | |||||
max-width: 100%; | |||||
} | |||||
.__mobiledoc-editor [data-md-text-align='left'] { | |||||
text-align: left; | |||||
} | |||||
.__mobiledoc-editor [data-md-text-align='center'] { | |||||
text-align: center; | |||||
} | |||||
.__mobiledoc-editor [data-md-text-align='right'] { | |||||
text-align: right; | |||||
} | |||||
.__mobiledoc-editor [data-md-text-align='justify'] { | |||||
text-align: justify; | |||||
} | |||||
.__mobiledoc-editor ol, | |||||
.__mobiledoc-editor ul { | |||||
list-style-position: inside; | |||||
} | |||||
/** | |||||
* Cards | |||||
*/ | |||||
.__mobiledoc-card { | |||||
display: inline-block; | |||||
} | |||||
/** | |||||
* Tooltips | |||||
*/ | |||||
@-webkit-keyframes fade-in { | |||||
0% { opacity: 0; } | |||||
100% { opacity: 1; } | |||||
} | |||||
@keyframes fade-in { | |||||
0% { opacity: 0; } | |||||
100% { opacity: 1; } | |||||
} | |||||
.__mobiledoc-tooltip { | |||||
font-family: var(--font-family-base), sans-serif; | |||||
font-size: 0.7em; | |||||
white-space: nowrap; | |||||
position: absolute; | |||||
background-color: rgba(43,43,43,0.9); | |||||
border-radius: 3px; | |||||
line-height: 1em; | |||||
padding: 0.7em 0.9em; | |||||
color: #FFF; | |||||
-webkit-animation: fade-in 0.2s; | |||||
animation: fade-in 0.2s; | |||||
} | |||||
.__mobiledoc-tooltip:before { | |||||
content: ''; | |||||
position: absolute; | |||||
left: 50%; | |||||
width: 0; | |||||
height: 0; | |||||
border-left: 5px solid transparent; | |||||
border-right: 5px solid transparent; | |||||
border-bottom: 5px solid rgba(43,43,43,0.9); | |||||
top: -5px; | |||||
margin-left: -5px; | |||||
} | |||||
/* help keeps mouseover state when moving from link to tooltip */ | |||||
.__mobiledoc-tooltip:after { | |||||
content: ''; | |||||
position: absolute; | |||||
left: 0; | |||||
right: 0; | |||||
top: -5px; | |||||
height: 5px; | |||||
} | |||||
.__mobiledoc-tooltip a { | |||||
color: #FFF; | |||||
text-decoration: none; | |||||
} | |||||
.__mobiledoc-tooltip a:hover { | |||||
text-decoration: underline; | |||||
} | |||||
.__mobiledoc-tooltip__edit-link { | |||||
margin-left: 5px; | |||||
cursor: pointer; | |||||
} |
@@ -0,0 +1,10 @@ | |||||
:root { | |||||
--color-positive: #222; | |||||
--color-negative: #fff; | |||||
--color-accent: #7c47d2; | |||||
--color-bg: var(--color-negative); | |||||
--color-fg: var(--color-positive); | |||||
--color-primary: var(--color-accent); | |||||
--font-family-base: system-ui; | |||||
--font-family-body: Georgia; | |||||
} |
@@ -134,7 +134,11 @@ const Editor: React.FC<Props> = ({ | |||||
</ToolbarItem> | </ToolbarItem> | ||||
<ToolbarItem><StyledSectionButton tag="ol">1.</StyledSectionButton></ToolbarItem> | <ToolbarItem><StyledSectionButton tag="ol">1.</StyledSectionButton></ToolbarItem> | ||||
</ToolbarWrapper> | </ToolbarWrapper> | ||||
<MobileDocEditor /> | |||||
<MobileDocEditor | |||||
style={{ | |||||
color: 'inherit', | |||||
}} | |||||
/> | |||||
</Container> | </Container> | ||||
) | ) | ||||
} | } | ||||
@@ -0,0 +1,6 @@ | |||||
import * as Storage from '../services/Storage' | |||||
export const loadFolders = async ({ setFolders, }) => { | |||||
const theFolders = await Storage.loadFolders() | |||||
setFolders(theFolders) | |||||
} |
@@ -0,0 +1,104 @@ | |||||
import * as Storage from '../services/Storage' | |||||
const save = async ({ stateRef, router, id, setNotes, }) => { | |||||
stateRef.current.updatedAt = new Date().toISOString() | |||||
const newNote = await Storage.saveNote(stateRef.current) | |||||
if (router.query.id !== id) { | |||||
await router.replace( | |||||
{ | |||||
pathname: '/notes/[id]', | |||||
query: { id, }, | |||||
}, | |||||
undefined, | |||||
{ | |||||
shallow: true, | |||||
} | |||||
) | |||||
} | |||||
setNotes(oldNotes => { | |||||
let notes | |||||
if (oldNotes.some((a) => a.id === id)) { | |||||
notes = oldNotes.map(n => n.id === id ? newNote : n) | |||||
} else { | |||||
notes = [newNote, ...oldNotes] | |||||
} | |||||
return notes.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) | |||||
}) | |||||
} | |||||
const triggerAutoSave = ({ | |||||
stateRef, | |||||
timeoutRef, | |||||
router, | |||||
id, | |||||
setNotes, | |||||
}) => { | |||||
if (timeoutRef.current !== null) { | |||||
clearTimeout(timeoutRef.current) | |||||
} | |||||
timeoutRef.current = setTimeout(async () => { | |||||
await save({ stateRef, router, id, setNotes, }) | |||||
timeoutRef.current = null | |||||
}, 3000) | |||||
} | |||||
export const updateContent = ({ | |||||
stateRef, | |||||
timeoutRef, | |||||
router, | |||||
id, | |||||
setNotes, | |||||
}) => e => { | |||||
stateRef.current.content = e | |||||
triggerAutoSave({ | |||||
stateRef, | |||||
timeoutRef, | |||||
router, | |||||
id, | |||||
setNotes, | |||||
}) | |||||
} | |||||
export const updateTitle = ({ | |||||
stateRef, | |||||
timeoutRef, | |||||
router, | |||||
id, | |||||
setNotes, | |||||
setTitle, | |||||
}) => e => { | |||||
setTitle(stateRef.current.title = e.target.value) | |||||
triggerAutoSave({ | |||||
stateRef, | |||||
timeoutRef, | |||||
router, | |||||
id, | |||||
setNotes, | |||||
}) | |||||
} | |||||
export const remove = ({ | |||||
setNotes, | |||||
notes, | |||||
router, | |||||
}) => note => async () => { | |||||
setNotes(notes.filter(n => n.id !== note.id)) | |||||
await router.replace( | |||||
{ | |||||
pathname: '/notes', | |||||
}, | |||||
undefined, | |||||
{ | |||||
shallow: true, | |||||
} | |||||
) | |||||
const result = await Storage.deleteNote(note) | |||||
if (!result) { | |||||
setNotes(notes) | |||||
} | |||||
} | |||||
export const load = async ({ setNotes, }) => { | |||||
const theNotes = await Storage.loadNotes() | |||||
setNotes(theNotes) | |||||
} |
@@ -1,5 +1,7 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import 'mobiledoc-kit/dist/mobiledoc.css' | |||||
import '../assets/theme.css' | |||||
import '../assets/global.css' | |||||
import '../assets/mobiledoc.css' | |||||
const App = ({ Component, pageProps }) => ( | const App = ({ Component, pageProps }) => ( | ||||
<Component {...pageProps} /> | <Component {...pageProps} /> | ||||
@@ -19,7 +19,6 @@ export default class MyDocument extends Document { | |||||
...initialProps, | ...initialProps, | ||||
styles: ( | styles: ( | ||||
<React.Fragment> | <React.Fragment> | ||||
<style>{`:root { font-family: system-ui, sans-serif } body { margin: 0 }`}</style> | |||||
{initialProps.styles} | {initialProps.styles} | ||||
{sheet.getStyleElement()} | {sheet.getStyleElement()} | ||||
</React.Fragment> | </React.Fragment> | ||||
@@ -1,13 +1,14 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import Head from 'next/head' | import Head from 'next/head' | ||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
import { Trash2, FilePlus, FolderPlus, FileText, GitBranch, User } from 'react-feather' | |||||
import { useRouter } from 'next/router' | |||||
import Editor from '../components/Editor/Editor' | import Editor from '../components/Editor/Editor' | ||||
import * as Storage from '../services/Storage' | |||||
import generateId from '../utilities/Id' | import generateId from '../utilities/Id' | ||||
import Link from 'next/link' | import Link from 'next/link' | ||||
import { formatDate } from '../utilities/Date' | import { formatDate } from '../utilities/Date' | ||||
import { useRouter } from 'next/router' | |||||
import { Trash2, FilePlus, FolderPlus, FileText, GitBranch, User } from 'react-feather' | |||||
import * as Note from '../controllers/Note' | |||||
import * as Folder from '../controllers/Folder' | |||||
const Navbar = styled('aside')({ | const Navbar = styled('aside')({ | ||||
width: 360, | width: 360, | ||||
@@ -15,7 +16,8 @@ const Navbar = styled('aside')({ | |||||
position: 'fixed', | position: 'fixed', | ||||
top: 0, | top: 0, | ||||
left: -360, | left: -360, | ||||
backgroundColor: 'yellow', | |||||
backgroundColor: 'var(--color-fg)', | |||||
color: 'var(--color-bg)', | |||||
'@media (min-width: 1080px)': { | '@media (min-width: 1080px)': { | ||||
width: `${100 / 3}%`, | width: `${100 / 3}%`, | ||||
left: 0, | left: 0, | ||||
@@ -36,6 +38,7 @@ const PrimaryNavItems = styled('nav')({ | |||||
const SecondaryNavItems = styled('nav')({ | const SecondaryNavItems = styled('nav')({ | ||||
height: '100%', | height: '100%', | ||||
position: 'relative', | position: 'relative', | ||||
backgroundColor: 'var(--color-bg)', | |||||
'::before': { | '::before': { | ||||
content: "''", | content: "''", | ||||
display: 'block', | display: 'block', | ||||
@@ -44,9 +47,8 @@ const SecondaryNavItems = styled('nav')({ | |||||
width: '100%', | width: '100%', | ||||
height: '100%', | height: '100%', | ||||
position: 'absolute', | position: 'absolute', | ||||
zIndex: -1, | |||||
backgroundColor: 'white', | |||||
opacity: 0.5, | |||||
backgroundColor: 'black', | |||||
opacity: 0.03125, | |||||
}, | }, | ||||
'@media (min-width: 1080px)': { | '@media (min-width: 1080px)': { | ||||
flex: 'auto', | flex: 'auto', | ||||
@@ -57,6 +59,7 @@ const SecondaryNavItemsOverflow = styled('div')({ | |||||
overflow: 'auto', | overflow: 'auto', | ||||
width: '100%', | width: '100%', | ||||
height: '100%', | height: '100%', | ||||
position: 'relative', | |||||
}) | }) | ||||
const Main = styled('main')({ | const Main = styled('main')({ | ||||
@@ -101,6 +104,7 @@ const TitleInput = styled('input')({ | |||||
font: 'inherit', | font: 'inherit', | ||||
fontSize: '3rem', | fontSize: '3rem', | ||||
fontWeight: 'bold', | fontWeight: 'bold', | ||||
color: 'inherit', | |||||
outline: 0, | outline: 0, | ||||
}) | }) | ||||
@@ -123,6 +127,7 @@ const NoteLinkTitle = styled('strong')({ | |||||
const LinkContainer = styled('div')({ | const LinkContainer = styled('div')({ | ||||
position: 'relative', | position: 'relative', | ||||
color: 'var(--color-primary, blue)', | |||||
}) | }) | ||||
const NoteActions = styled('div')({ | const NoteActions = styled('div')({ | ||||
@@ -145,13 +150,26 @@ const NoteAction = styled('button')({ | |||||
}) | }) | ||||
const NoteLinkBackground = styled('span')({ | const NoteLinkBackground = styled('span')({ | ||||
opacity: 0.125, | |||||
backgroundColor: 'currentColor', | |||||
top: 0, | |||||
left: 0, | |||||
width: '100%', | |||||
height: '100%', | |||||
position: 'absolute', | |||||
'::before': { | |||||
content: "''", | |||||
position: 'absolute', | |||||
top: 0, | |||||
left: 0, | |||||
width: '0.25rem', | |||||
height: '100%', | |||||
display: 'block', | |||||
backgroundColor: 'currentColor', | |||||
}, | |||||
'::after': { | |||||
content: "''", | |||||
opacity: 0.125, | |||||
backgroundColor: 'currentColor', | |||||
top: 0, | |||||
left: 0, | |||||
width: '100%', | |||||
height: '100%', | |||||
position: 'absolute', | |||||
}, | |||||
}) | }) | ||||
const NewIcon = styled(FilePlus)({ | const NewIcon = styled(FilePlus)({ | ||||
@@ -196,6 +214,7 @@ const PostPrimary = styled('div')({ | |||||
type NoteInstance = { id: string, title: string, content?: object, updatedAt: string, } | type NoteInstance = { id: string, title: string, content?: object, updatedAt: string, } | ||||
const Notes = ({ id: idProp }) => { | const Notes = ({ id: idProp }) => { | ||||
// TODO remove extra state for ID | |||||
const [id, setId, ] = React.useState(idProp) | const [id, setId, ] = React.useState(idProp) | ||||
const [title, setTitle, ] = React.useState('') | const [title, setTitle, ] = React.useState('') | ||||
const [notes, setNotes, ] = React.useState(null) | const [notes, setNotes, ] = React.useState(null) | ||||
@@ -204,64 +223,12 @@ const Notes = ({ id: idProp }) => { | |||||
const timeoutRef = React.useRef<number>(null) | const timeoutRef = React.useRef<number>(null) | ||||
const router = useRouter() | const router = useRouter() | ||||
const autoSave = () => { | |||||
if (timeoutRef.current !== null) { | |||||
window.clearTimeout(timeoutRef.current) | |||||
} | |||||
timeoutRef.current = window.setTimeout(async () => { | |||||
const newNote = await Storage.saveNote({ | |||||
...stateRef.current, | |||||
updatedAt: (stateRef.current.updatedAt = new Date().toISOString()), | |||||
}) | |||||
if (router.query.id !== id) { | |||||
await router.push(`/notes/${id}`, undefined, { shallow: true }) | |||||
} | |||||
setNotes(oldNotes => { | |||||
let notes | |||||
if (oldNotes.some((a) => a.id === id)) { | |||||
notes = oldNotes.map(n => n.id === id ? newNote : n) | |||||
} else { | |||||
notes = [newNote, ...oldNotes] | |||||
} | |||||
return notes.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) | |||||
}) | |||||
timeoutRef.current = null | |||||
}, 3000) | |||||
} | |||||
const handleEditorChange = e => { | |||||
stateRef.current.content = e | |||||
autoSave() | |||||
} | |||||
const handleTitleChange = e => { | |||||
stateRef.current.title = e.target.value | |||||
setTitle(e.target.value) | |||||
autoSave() | |||||
} | |||||
const deleteNote = note => async () => { | |||||
setNotes(notes.filter(n => n.id !== note.id)) | |||||
const result = await Storage.deleteNote(note) | |||||
if (!result) { | |||||
setNotes(notes) | |||||
} | |||||
} | |||||
React.useEffect(() => { | React.useEffect(() => { | ||||
const loadNotes = async () => { | |||||
const theNotes = await Storage.loadNotes() | |||||
setNotes(theNotes) | |||||
} | |||||
loadNotes() | |||||
Note.load({ setNotes }) | |||||
}, []) | }, []) | ||||
React.useEffect(() => { | React.useEffect(() => { | ||||
const loadFolders = async () => { | |||||
const theFolders = await Storage.loadFolders() | |||||
setFolders(theFolders) | |||||
} | |||||
loadFolders() | |||||
Folder.loadFolders({ setFolders }) | |||||
}, []) | }, []) | ||||
React.useEffect(() => { | React.useEffect(() => { | ||||
@@ -280,7 +247,7 @@ const Notes = ({ id: idProp }) => { | |||||
return ( | return ( | ||||
<React.Fragment> | <React.Fragment> | ||||
<Head> | <Head> | ||||
<title>{ idProp === undefined ? 'Notes | New Note' : `Notes | ${title.length > 0 ? title : '(untitled)'}`}</title> | |||||
<title>{ idProp === undefined ? 'Notes | New Note' : `Notes | Edit Note - ${title.length > 0 ? title : '(untitled)'}`}</title> | |||||
<link rel="icon" href="/favicon.ico" /> | <link rel="icon" href="/favicon.ico" /> | ||||
</Head> | </Head> | ||||
<Navbar> | <Navbar> | ||||
@@ -324,57 +291,63 @@ const Notes = ({ id: idProp }) => { | |||||
</PrimaryNavItems> | </PrimaryNavItems> | ||||
<SecondaryNavItems> | <SecondaryNavItems> | ||||
<SecondaryNavItemsOverflow> | <SecondaryNavItemsOverflow> | ||||
<Link | |||||
href={{ | |||||
pathname: '/profile', | |||||
}} | |||||
passHref | |||||
> | |||||
<NoteLink> | |||||
<NavbarItemContent> | |||||
<NoteLinkPrimary> | |||||
<NewFolderIcon /> | |||||
<NoteLinkTitle> | |||||
Personal | |||||
</NoteLinkTitle> | |||||
</NoteLinkPrimary> | |||||
</NavbarItemContent> | |||||
</NoteLink> | |||||
</Link> | |||||
<Link | |||||
href={{ | |||||
pathname: '/folders/new', | |||||
}} | |||||
passHref | |||||
> | |||||
<NoteLink> | |||||
<NavbarItemContent> | |||||
<NoteLinkPrimary> | |||||
<NewFolderIcon /> | |||||
<NoteLinkTitle> | |||||
Create Folder | |||||
</NoteLinkTitle> | |||||
</NoteLinkPrimary> | |||||
</NavbarItemContent> | |||||
</NoteLink> | |||||
</Link> | |||||
<Link | |||||
href={{ | |||||
pathname: '/notes', | |||||
}} | |||||
passHref | |||||
> | |||||
<NoteLink> | |||||
<NavbarItemContent> | |||||
<NoteLinkPrimary> | |||||
<NewIcon /> | |||||
<NoteLinkTitle> | |||||
Create Note | |||||
</NoteLinkTitle> | |||||
</NoteLinkPrimary> | |||||
</NavbarItemContent> | |||||
</NoteLink> | |||||
</Link> | |||||
<LinkContainer> | |||||
<Link | |||||
href={{ | |||||
pathname: '/profile', | |||||
}} | |||||
passHref | |||||
> | |||||
<NoteLink> | |||||
<NavbarItemContent> | |||||
<NoteLinkPrimary> | |||||
<NewFolderIcon /> | |||||
<NoteLinkTitle> | |||||
Personal | |||||
</NoteLinkTitle> | |||||
</NoteLinkPrimary> | |||||
</NavbarItemContent> | |||||
</NoteLink> | |||||
</Link> | |||||
</LinkContainer> | |||||
<LinkContainer> | |||||
<Link | |||||
href={{ | |||||
pathname: '/folders/new', | |||||
}} | |||||
passHref | |||||
> | |||||
<NoteLink> | |||||
<NavbarItemContent> | |||||
<NoteLinkPrimary> | |||||
<NewFolderIcon /> | |||||
<NoteLinkTitle> | |||||
Create Folder | |||||
</NoteLinkTitle> | |||||
</NoteLinkPrimary> | |||||
</NavbarItemContent> | |||||
</NoteLink> | |||||
</Link> | |||||
</LinkContainer> | |||||
<LinkContainer> | |||||
<Link | |||||
href={{ | |||||
pathname: '/notes', | |||||
}} | |||||
passHref | |||||
> | |||||
<NoteLink> | |||||
<NavbarItemContent> | |||||
<NoteLinkPrimary> | |||||
<NewIcon /> | |||||
<NoteLinkTitle> | |||||
Create Note | |||||
</NoteLinkTitle> | |||||
</NoteLinkPrimary> | |||||
</NavbarItemContent> | |||||
</NoteLink> | |||||
</Link> | |||||
</LinkContainer> | |||||
{ | { | ||||
Array.isArray(notes!) | Array.isArray(notes!) | ||||
&& notes.map(n => ( | && notes.map(n => ( | ||||
@@ -417,7 +390,7 @@ const Notes = ({ id: idProp }) => { | |||||
</Link> | </Link> | ||||
<NoteActions> | <NoteActions> | ||||
<NoteAction | <NoteAction | ||||
onClick={deleteNote(n)} | |||||
onClick={Note.remove({ setNotes, notes, router, })(n)} | |||||
> | > | ||||
<Trash2 /> | <Trash2 /> | ||||
</NoteAction> | </NoteAction> | ||||
@@ -425,23 +398,25 @@ const Notes = ({ id: idProp }) => { | |||||
</LinkContainer> | </LinkContainer> | ||||
)) | )) | ||||
} | } | ||||
<Link | |||||
href={{ | |||||
pathname: '/bin', | |||||
}} | |||||
passHref | |||||
> | |||||
<NoteLink> | |||||
<NavbarItemContent> | |||||
<NoteLinkPrimary> | |||||
<BinIcon /> | |||||
<NoteLinkTitle> | |||||
View Binned Notes | |||||
</NoteLinkTitle> | |||||
</NoteLinkPrimary> | |||||
</NavbarItemContent> | |||||
</NoteLink> | |||||
</Link> | |||||
<LinkContainer> | |||||
<Link | |||||
href={{ | |||||
pathname: '/bin', | |||||
}} | |||||
passHref | |||||
> | |||||
<NoteLink> | |||||
<NavbarItemContent> | |||||
<NoteLinkPrimary> | |||||
<BinIcon /> | |||||
<NoteLinkTitle> | |||||
View Binned Notes | |||||
</NoteLinkTitle> | |||||
</NoteLinkPrimary> | |||||
</NavbarItemContent> | |||||
</NoteLink> | |||||
</Link> | |||||
</LinkContainer> | |||||
</SecondaryNavItemsOverflow> | </SecondaryNavItemsOverflow> | ||||
</SecondaryNavItems> | </SecondaryNavItems> | ||||
</NavbarItems> | </NavbarItems> | ||||
@@ -457,7 +432,14 @@ const Notes = ({ id: idProp }) => { | |||||
<TitleInput | <TitleInput | ||||
placeholder="Title" | placeholder="Title" | ||||
value={title} | value={title} | ||||
onChange={handleTitleChange} | |||||
onChange={Note.updateTitle({ | |||||
stateRef, | |||||
timeoutRef, | |||||
router, | |||||
id, | |||||
setNotes, | |||||
setTitle, | |||||
})} | |||||
/> | /> | ||||
<PostMeta> | <PostMeta> | ||||
{ | { | ||||
@@ -477,7 +459,13 @@ const Notes = ({ id: idProp }) => { | |||||
autoFocus={false} | autoFocus={false} | ||||
key={id} | key={id} | ||||
content={stateRef.current ? stateRef.current.content : undefined} | content={stateRef.current ? stateRef.current.content : undefined} | ||||
onChange={handleEditorChange} | |||||
onChange={Note.updateContent({ | |||||
stateRef, | |||||
timeoutRef, | |||||
router, | |||||
id, | |||||
setNotes, | |||||
})} | |||||
placeholder="Start typing here." | placeholder="Start typing here." | ||||
/> | /> | ||||
</React.Fragment> | </React.Fragment> | ||||
@@ -22,9 +22,14 @@ export const collection = (Model, Service) => async (req, res) => { | |||||
res.statusCode = status | res.statusCode = status | ||||
res.json(data) | res.json(data) | ||||
} catch (err) { | } catch (err) { | ||||
console.error(err) | |||||
const { status, data, } = err | const { status, data, } = err | ||||
res.statusCode = status | res.statusCode = status | ||||
res.json(data) | |||||
if (data && status !== 204) { | |||||
res.json(data) | |||||
return | |||||
} | |||||
res.end() | |||||
} | } | ||||
} | } | ||||
@@ -57,9 +62,13 @@ export const item = (Model, Service) => async (req, res) => { | |||||
} | } | ||||
res.end() | res.end() | ||||
} catch (err) { | } catch (err) { | ||||
console.log('ERROR', err) | |||||
console.error(err) | |||||
const { status, data, } = err | const { status, data, } = err | ||||
res.statusCode = status | |||||
res.json(data) | |||||
res.statusCode = status || 500 | |||||
if (data && status !== 204) { | |||||
res.json(data) | |||||
return | |||||
} | |||||
res.end() | |||||
} | } | ||||
} | } |
@@ -1,5 +1,7 @@ | |||||
import { addTime, TimeDivision } from '../utilities/Date' | import { addTime, TimeDivision } from '../utilities/Date' | ||||
import * as Serialization from '../utilities/Serialization' | import * as Serialization from '../utilities/Serialization' | ||||
import NoteModel from '../../models/Note' | |||||
import InferModel from '../utilities/Instance' | |||||
import LocalStorage from './LocalStorage' | import LocalStorage from './LocalStorage' | ||||
type StorageParams = { | type StorageParams = { | ||||
@@ -7,6 +9,8 @@ type StorageParams = { | |||||
url: string, | url: string, | ||||
} | } | ||||
type Note = InferModel<typeof NoteModel> | |||||
type LoadItems = <T extends Record<string, unknown>>(params: StorageParams) => () => Promise<T[]> | type LoadItems = <T extends Record<string, unknown>>(params: StorageParams) => () => Promise<T[]> | ||||
const loadItems: LoadItems = <T extends Record<string, unknown>>(params) => async (): Promise<T[]> => { | const loadItems: LoadItems = <T extends Record<string, unknown>>(params) => async (): Promise<T[]> => { | ||||
@@ -99,8 +103,8 @@ const deleteItem: DeleteItem = <T extends Record<string, unknown>>(params) => as | |||||
return response.status === 204 | return response.status === 204 | ||||
} | } | ||||
export const loadNotes = loadItems({ id: 'notes', url: '/api/notes' }) | |||||
export const loadNotes = loadItems<Note>({ id: 'notes', url: '/api/notes' }) | |||||
export const loadFolders = loadItems({ id: 'folders', url: '/api/folders' }) | export const loadFolders = loadItems({ id: 'folders', url: '/api/folders' }) | ||||
export const saveNote = saveItem({ id: 'notes', url: '/api/notes' }) | |||||
export const saveNote = saveItem<Note>({ id: 'notes', url: '/api/notes' }) | |||||
export const saveFolder = saveItem({ id: 'folders', url: '/api/folders' }) | export const saveFolder = saveItem({ id: 'folders', url: '/api/folders' }) | ||||
export const deleteNote = deleteItem({ id: 'notes', url: '/api/notes' }) | |||||
export const deleteNote = deleteItem<Note>({ id: 'notes', url: '/api/notes' }) |
@@ -22,6 +22,7 @@ type Model = { | |||||
type InferType<V extends Sequelize.DataType> = ( | type InferType<V extends Sequelize.DataType> = ( | ||||
V extends typeof Sequelize.STRING ? string : | V extends typeof Sequelize.STRING ? string : | ||||
V extends typeof Sequelize.TEXT ? string : | V extends typeof Sequelize.TEXT ? string : | ||||
V extends ReturnType<typeof Sequelize.TEXT> ? string : | |||||
V extends typeof Sequelize.DATE ? Date : | V extends typeof Sequelize.DATE ? Date : | ||||
V extends typeof Sequelize.DATEONLY ? Date : | V extends typeof Sequelize.DATEONLY ? Date : | ||||
V extends typeof Sequelize.UUIDV4 ? string : | V extends typeof Sequelize.UUIDV4 ? string : | ||||