Split page into multiple components. Organize URLs as resource-oriented. Also make layout mobile-responsive.feature/transactions
@@ -7,7 +7,7 @@ module.exports = { | |||
return [ | |||
{ | |||
source: '/', | |||
destination: '/notes', | |||
destination: '/notes?parentFolderId=00000000-0000-0000-000000000000', | |||
permanent: true, | |||
}, | |||
] | |||
@@ -19,12 +19,14 @@ | |||
"sequelize": "5.22.0", | |||
"sequelize-cli": "^6.2.0", | |||
"sequelize-typescript": "^1.1.0", | |||
"sqlite3": "^5.0.0", | |||
"styled-components": "^5.2.0", | |||
"uuid": "^8.3.1" | |||
}, | |||
"devDependencies": { | |||
"@types/node": "^14.14.2", | |||
"@types/react": "^16.9.53", | |||
"@types/styled-components": "^5.1.4", | |||
"@types/uuid": "^8.3.0", | |||
"typescript": "^4.0.3" | |||
}, | |||
@@ -4,8 +4,6 @@ import { | |||
Container, | |||
MarkupButton, | |||
LinkButton, | |||
SectionSelect, | |||
AttributeSelect, | |||
SectionButton, | |||
Editor as MobileDocEditor, | |||
} from 'react-mobiledoc-editor' | |||
@@ -0,0 +1,36 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import { ArrowLeft, Search, FilePlus, FileText, FolderPlus, GitBranch, Trash2, User, Menu } from 'react-feather' | |||
const DEFINED_ICONS = { | |||
'note': FileText, | |||
'mind-map': GitBranch, | |||
'user': User, | |||
'new-folder': FolderPlus, | |||
'new-note': FilePlus, | |||
'bin': Trash2, | |||
'back': ArrowLeft, | |||
'search': Search, | |||
'menu': Menu, | |||
} | |||
const propTypes = { | |||
name: PropTypes.oneOf(Object.keys(DEFINED_ICONS)).isRequired, | |||
} | |||
type Props = PropTypes.InferProps<typeof propTypes> | |||
const Icon: React.FC<Props> = ({ | |||
name | |||
}) => { | |||
const { [name as keyof typeof DEFINED_ICONS]: Component = null } = DEFINED_ICONS | |||
if (Component !== null) { | |||
return <Component /> | |||
} | |||
return null | |||
} | |||
Icon.propTypes = propTypes | |||
export default Icon |
@@ -0,0 +1,195 @@ | |||
import * as React from 'react' | |||
import styled from 'styled-components' | |||
import SecondaryNavItem from '../SecondaryNavItem/SecondaryNavItem' | |||
import PrimaryNavItem from '../PrimaryNavItem/PrimaryNavItem' | |||
const Base = styled('aside')({ | |||
width: 360, | |||
height: '100%', | |||
position: 'fixed', | |||
top: 0, | |||
left: -360, | |||
backgroundColor: 'var(--color-fg)', | |||
color: 'var(--color-bg)', | |||
zIndex: 1, | |||
'@media (min-width: 1080px)': { | |||
width: `${100 / 3}%`, | |||
left: 0, | |||
}, | |||
}) | |||
const PrimaryNavItems = styled('nav')({ | |||
width: '100%', | |||
height: '4rem', | |||
display: 'flex', | |||
position: 'fixed', | |||
bottom: 0, | |||
left: 0, | |||
zIndex: 1, | |||
backgroundColor: 'var(--color-fg)', | |||
color: 'var(--color-bg)', | |||
justifyContent: 'center', | |||
'@media (min-width: 1080px)': { | |||
position: 'static', | |||
bottom: 'auto', | |||
left: 'auto', | |||
width: '4rem', | |||
height: '100%', | |||
flexDirection: 'column', | |||
justifyContent: 'space-between', | |||
alignItems: 'stretch', | |||
}, | |||
}) | |||
const PrimaryNavItemGroup = styled('div')({ | |||
display: 'flex', | |||
alignItems: 'center', | |||
'@media (min-width: 1080px)': { | |||
display: 'block', | |||
}, | |||
}) | |||
const SecondaryNavItems = styled('nav')({ | |||
height: '100%', | |||
position: 'relative', | |||
backgroundColor: 'var(--color-bg)', | |||
'::before': { | |||
content: "''", | |||
display: 'block', | |||
top: 0, | |||
left: 0, | |||
width: '100%', | |||
height: '100%', | |||
position: 'absolute', | |||
backgroundColor: 'black', | |||
opacity: 0.03125, | |||
}, | |||
'@media (min-width: 1080px)': { | |||
flex: 'auto', | |||
}, | |||
}) | |||
const VisibleSecondaryNavItems = styled(SecondaryNavItems)({ | |||
position: 'fixed', | |||
top: 0, | |||
left: 0, | |||
width: '100%', | |||
paddingBottom: '4rem', | |||
boxSizing: 'border-box', | |||
'@media (min-width: 1080px)': { | |||
position: 'relative', | |||
top: 'auto', | |||
left: 'auto', | |||
width: 'auto', | |||
paddingBottom: 0, | |||
}, | |||
}) | |||
const SecondaryNavItemsOverflow = styled('div')({ | |||
overflow: 'auto', | |||
width: '100%', | |||
height: '100%', | |||
position: 'relative', | |||
}) | |||
const NavbarItems = styled('div')({ | |||
display: 'flex', | |||
width: '100%', | |||
height: '100%', | |||
}) | |||
const NavbarContainer = styled('div')({ | |||
display: 'block', | |||
width: '100%', | |||
height: '100%', | |||
margin: '0 0 0 auto', | |||
boxSizing: 'border-box', | |||
maxWidth: 360, | |||
}) | |||
const Navbar = ({ | |||
secondaryVisible, | |||
primaryItemsStart = [], | |||
primaryItemsEnd = [], | |||
secondaryItemsHeader = [], | |||
secondaryItems = [], | |||
}) => { | |||
const SecondaryNavItemsComponent = secondaryVisible ? VisibleSecondaryNavItems : SecondaryNavItems | |||
return ( | |||
<Base> | |||
<NavbarContainer> | |||
<NavbarItems> | |||
<PrimaryNavItems> | |||
<PrimaryNavItemGroup> | |||
{ | |||
Array.isArray(primaryItemsStart!) | |||
&& primaryItemsStart.map(i => ( | |||
<PrimaryNavItem | |||
key={i.id} | |||
mobileOnly={i.mobileOnly} | |||
href={i.href} | |||
iconName={i.iconName} | |||
title={i.title} | |||
active={i.active} | |||
/> | |||
)) | |||
} | |||
</PrimaryNavItemGroup> | |||
{ | |||
Array.isArray(primaryItemsEnd!) | |||
&& ( | |||
<PrimaryNavItemGroup> | |||
{ | |||
primaryItemsEnd.map(i => ( | |||
<PrimaryNavItem | |||
key={i.id} | |||
href={i.href} | |||
iconName={i.iconName} | |||
title={i.title} | |||
active={i.active} | |||
/> | |||
)) | |||
} | |||
</PrimaryNavItemGroup> | |||
) | |||
} | |||
</PrimaryNavItems> | |||
<SecondaryNavItemsComponent> | |||
<SecondaryNavItemsOverflow> | |||
{ | |||
secondaryItemsHeader.map(i => ( | |||
<SecondaryNavItem | |||
key={i.id} | |||
active={i.active} | |||
href={i.href} | |||
replace={i.replace} | |||
iconName={i.iconName} | |||
title={i.title} | |||
subtitle={i.subtitle} | |||
actions={i.actions} | |||
/> | |||
)) | |||
} | |||
{ | |||
secondaryItems.map(i => ( | |||
<SecondaryNavItem | |||
key={i.id} | |||
active={i.active} | |||
href={i.href} | |||
replace={i.replace} | |||
iconName={i.iconName} | |||
title={i.title} | |||
subtitle={i.subtitle} | |||
actions={i.actions} | |||
/> | |||
)) | |||
} | |||
</SecondaryNavItemsOverflow> | |||
</SecondaryNavItemsComponent> | |||
</NavbarItems> | |||
</NavbarContainer> | |||
</Base> | |||
) | |||
} | |||
export default Navbar |
@@ -0,0 +1,57 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import Link from 'next/link' | |||
import styled from 'styled-components' | |||
import Icon from '../Icon/Icon' | |||
const Base = styled('a')({ | |||
width: '4rem', | |||
height: '4rem', | |||
display: 'grid', | |||
placeContent: 'center', | |||
color: 'inherit', | |||
}) | |||
const MobileBase = styled(Base)({ | |||
'@media (min-width: 1080px)': { | |||
display: 'none', | |||
}, | |||
}) | |||
const propTypes = { | |||
href: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired, | |||
iconName: PropTypes.string.isRequired, | |||
title: PropTypes.string, | |||
mobileOnly: PropTypes.bool, | |||
active: PropTypes.bool, | |||
} | |||
type Props = PropTypes.InferProps<typeof propTypes> | |||
const PrimaryNavItem: React.FC<Props> = ({ | |||
href, | |||
iconName, | |||
title, | |||
mobileOnly = false, | |||
active = false, | |||
}) => { | |||
const BaseComponent = mobileOnly ? MobileBase : Base | |||
return ( | |||
<Link | |||
href={href} | |||
passHref | |||
> | |||
<BaseComponent | |||
title={title} | |||
> | |||
<Icon | |||
name={iconName} | |||
/> | |||
</BaseComponent> | |||
</Link> | |||
) | |||
} | |||
PrimaryNavItem.propTypes = propTypes | |||
export default PrimaryNavItem |
@@ -0,0 +1,181 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import Link from 'next/link' | |||
import styled from 'styled-components' | |||
import Icon from '../Icon/Icon' | |||
const NoteLink = styled('a')({ | |||
display: 'flex', | |||
textDecoration: 'none', | |||
color: 'inherit', | |||
height: '4rem', | |||
alignItems: 'center', | |||
position: 'relative', | |||
}) | |||
const NoteLinkPrimary = styled('div')({ | |||
display: 'block', | |||
}) | |||
const NoteLinkTitle = styled('strong')({ | |||
verticalAlign: 'middle', | |||
}) | |||
const LinkContainer = styled('div')({ | |||
position: 'relative', | |||
color: 'var(--color-primary, blue)', | |||
}) | |||
const NoteActions = styled('div')({ | |||
display: 'flex', | |||
position: 'absolute', | |||
alignItems: 'stretch', | |||
top: 0, | |||
right: 0, | |||
height: '100%', | |||
}) | |||
const NoteAction = styled('button')({ | |||
height: '100%', | |||
width: '4rem', | |||
background: 'transparent', | |||
border: 0, | |||
color: 'inherit', | |||
cursor: 'pointer', | |||
outline: 0, | |||
}) | |||
const NoteLinkBackground = styled('span')({ | |||
'::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 IconContainer = styled('span')({ | |||
display: 'inline-block', | |||
verticalAlign: 'middle', | |||
marginRight: '0.5rem', | |||
}) | |||
const NavbarItemContent = styled('span')({ | |||
padding: '0 1rem', | |||
boxSizing: 'border-box', | |||
}) | |||
const PostMeta = styled('small')({ | |||
opacity: 0.5, | |||
height: '1.25rem', | |||
display: 'block', | |||
lineHeight: 1.25, | |||
}) | |||
const propTypes = { | |||
active: PropTypes.bool, | |||
href: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired, | |||
replace: PropTypes.bool, | |||
iconName: PropTypes.string, | |||
title: PropTypes.string, | |||
subtitle: PropTypes.node, | |||
actions: PropTypes.arrayOf(PropTypes.shape({ | |||
id: PropTypes.string.isRequired, | |||
onClick: PropTypes.func, | |||
iconName: PropTypes.string, | |||
})), | |||
} | |||
type Props = PropTypes.InferProps<typeof propTypes> | |||
const SecondaryNavItem: React.FC<Props> = ({ | |||
active = false, | |||
href, | |||
replace = false, | |||
iconName, | |||
title, | |||
subtitle, | |||
actions, | |||
}) => ( | |||
<LinkContainer> | |||
{ | |||
active | |||
&& ( | |||
<NoteLinkBackground /> | |||
) | |||
} | |||
<Link | |||
href={href} | |||
replace={replace} | |||
passHref | |||
> | |||
<NoteLink> | |||
<NavbarItemContent> | |||
<NoteLinkPrimary> | |||
<IconContainer> | |||
{ | |||
iconName | |||
&& ( | |||
<Icon | |||
name={iconName} | |||
/> | |||
) | |||
} | |||
</IconContainer> | |||
<NoteLinkTitle | |||
style={{ opacity: title.length > 0 ? 1 : 0.5, }} | |||
> | |||
{title.length > 0 ? title : '(untitled)'} | |||
</NoteLinkTitle> | |||
</NoteLinkPrimary> | |||
{ | |||
subtitle | |||
&& ( | |||
<React.Fragment> | |||
{' '} | |||
<PostMeta> | |||
{subtitle} | |||
</PostMeta> | |||
</React.Fragment> | |||
) | |||
} | |||
</NavbarItemContent> | |||
</NoteLink> | |||
</Link> | |||
{ | |||
Array.isArray(actions) | |||
&& ( | |||
<NoteActions> | |||
{actions.map(a => ( | |||
<NoteAction | |||
key={a.id} | |||
onClick={a.onClick} | |||
> | |||
<Icon | |||
name={a.iconName} | |||
/> | |||
</NoteAction> | |||
))} | |||
</NoteActions> | |||
) | |||
} | |||
</LinkContainer> | |||
) | |||
SecondaryNavItem.propTypes = propTypes | |||
export default SecondaryNavItem |
@@ -1,6 +1,6 @@ | |||
import * as Storage from '../services/Storage' | |||
export const loadFolders = async ({ setFolders, }) => { | |||
export const load = async ({ setFolders, }) => { | |||
const theFolders = await Storage.loadFolders() | |||
setFolders(theFolders) | |||
} |
@@ -1,67 +1,14 @@ | |||
import * as React from 'react' | |||
import Head from 'next/head' | |||
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 Navbar from '../components/Navbar/Navbar' | |||
import generateId from '../utilities/Id' | |||
import Link from 'next/link' | |||
import { formatDate } from '../utilities/Date' | |||
import * as Note from '../controllers/Note' | |||
import * as Folder from '../controllers/Folder' | |||
const Navbar = styled('aside')({ | |||
width: 360, | |||
height: '100%', | |||
position: 'fixed', | |||
top: 0, | |||
left: -360, | |||
backgroundColor: 'var(--color-fg)', | |||
color: 'var(--color-bg)', | |||
'@media (min-width: 1080px)': { | |||
width: `${100 / 3}%`, | |||
left: 0, | |||
}, | |||
}) | |||
const PrimaryNavItems = styled('nav')({ | |||
height: '100%', | |||
display: 'flex', | |||
flexDirection: 'column', | |||
justifyContent: 'space-between', | |||
alignItems: 'stretch', | |||
'@media (min-width: 1080px)': { | |||
width: '4rem', | |||
}, | |||
}) | |||
const SecondaryNavItems = styled('nav')({ | |||
height: '100%', | |||
position: 'relative', | |||
backgroundColor: 'var(--color-bg)', | |||
'::before': { | |||
content: "''", | |||
display: 'block', | |||
top: 0, | |||
left: 0, | |||
width: '100%', | |||
height: '100%', | |||
position: 'absolute', | |||
backgroundColor: 'black', | |||
opacity: 0.03125, | |||
}, | |||
'@media (min-width: 1080px)': { | |||
flex: 'auto', | |||
}, | |||
}) | |||
const SecondaryNavItemsOverflow = styled('div')({ | |||
overflow: 'auto', | |||
width: '100%', | |||
height: '100%', | |||
position: 'relative', | |||
}) | |||
const Main = styled('main')({ | |||
margin: '2rem 0', | |||
'@media (min-width: 1080px)': { | |||
@@ -80,21 +27,6 @@ const Container = styled('div')({ | |||
}, | |||
}) | |||
const NavbarItems = styled('div')({ | |||
display: 'flex', | |||
width: '100%', | |||
height: '100%', | |||
}) | |||
const NavbarContainer = styled('div')({ | |||
display: 'block', | |||
width: '100%', | |||
height: '100%', | |||
margin: '0 0 0 auto', | |||
boxSizing: 'border-box', | |||
maxWidth: 360, | |||
}) | |||
const TitleInput = styled('input')({ | |||
border: 0, | |||
background: 'transparent', | |||
@@ -108,98 +40,6 @@ const TitleInput = styled('input')({ | |||
outline: 0, | |||
}) | |||
const NoteLink = styled('a')({ | |||
display: 'flex', | |||
textDecoration: 'none', | |||
color: 'inherit', | |||
height: '4rem', | |||
alignItems: 'center', | |||
position: 'relative', | |||
}) | |||
const NoteLinkPrimary = styled('div')({ | |||
display: 'block', | |||
}) | |||
const NoteLinkTitle = styled('strong')({ | |||
verticalAlign: 'middle', | |||
}) | |||
const LinkContainer = styled('div')({ | |||
position: 'relative', | |||
color: 'var(--color-primary, blue)', | |||
}) | |||
const NoteActions = styled('div')({ | |||
display: 'flex', | |||
position: 'absolute', | |||
alignItems: 'stretch', | |||
top: 0, | |||
right: 0, | |||
height: '100%', | |||
}) | |||
const NoteAction = styled('button')({ | |||
height: '100%', | |||
width: '4rem', | |||
background: 'transparent', | |||
border: 0, | |||
color: 'inherit', | |||
cursor: 'pointer', | |||
outline: 0, | |||
}) | |||
const NoteLinkBackground = styled('span')({ | |||
'::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)({ | |||
verticalAlign: 'middle', | |||
marginRight: '0.5rem', | |||
}) | |||
const NewFolderIcon = styled(FolderPlus)({ | |||
verticalAlign: 'middle', | |||
marginRight: '0.5rem', | |||
}) | |||
const BinIcon = styled(Trash2)({ | |||
verticalAlign: 'middle', | |||
marginRight: '0.5rem', | |||
}) | |||
const NavbarItemContent = styled('span')({ | |||
padding: '0 1rem', | |||
boxSizing: 'border-box', | |||
}) | |||
const PrimaryNavItem = styled('a')({ | |||
width: '4rem', | |||
height: '4rem', | |||
display: 'grid', | |||
placeContent: 'center', | |||
color: 'inherit', | |||
}) | |||
const PostMeta = styled('small')({ | |||
opacity: 0.5, | |||
height: '1.25rem', | |||
@@ -228,7 +68,7 @@ const Notes = ({ id: idProp }) => { | |||
}, []) | |||
React.useEffect(() => { | |||
Folder.loadFolders({ setFolders }) | |||
Folder.load({ setFolders }) | |||
}, []) | |||
React.useEffect(() => { | |||
@@ -250,178 +90,147 @@ const Notes = ({ id: idProp }) => { | |||
<title>{ idProp === undefined ? 'Notes | New Note' : `Notes | Edit Note - ${title.length > 0 ? title : '(untitled)'}`}</title> | |||
<link rel="icon" href="/favicon.ico" /> | |||
</Head> | |||
<Navbar> | |||
<NavbarContainer> | |||
<NavbarItems> | |||
<PrimaryNavItems> | |||
<div> | |||
<Link | |||
href={{ | |||
pathname: '/notes', | |||
}} | |||
passHref | |||
> | |||
<PrimaryNavItem> | |||
<FileText /> | |||
</PrimaryNavItem> | |||
</Link> | |||
<Link | |||
href={{ | |||
pathname: '/graph', | |||
}} | |||
passHref | |||
> | |||
<PrimaryNavItem> | |||
<GitBranch /> | |||
</PrimaryNavItem> | |||
</Link> | |||
</div> | |||
<div> | |||
<Link | |||
href={{ | |||
pathname: '/me', | |||
}} | |||
passHref | |||
> | |||
<PrimaryNavItem> | |||
<User /> | |||
</PrimaryNavItem> | |||
</Link> | |||
</div> | |||
</PrimaryNavItems> | |||
<SecondaryNavItems> | |||
<SecondaryNavItemsOverflow> | |||
<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 | |||
<Navbar | |||
secondaryVisible={Boolean(router.query.navbar)} | |||
primaryItemsStart={[ | |||
{ | |||
id: 'sidebar', | |||
mobileOnly: true, | |||
href: { | |||
pathname: router.pathname, | |||
query: { | |||
...router.query, | |||
navbar: 'true', | |||
}, | |||
}, | |||
iconName: 'menu', | |||
title: 'Notes', | |||
}, | |||
{ | |||
id: 'folders', | |||
active: router.pathname.startsWith('/folders'), | |||
href: { | |||
pathname: '/folders', | |||
}, | |||
iconName: 'note', | |||
title: 'Notes', | |||
}, | |||
{ | |||
id: 'search', | |||
href: { | |||
pathname: '/notes', | |||
query: { | |||
action: 'search', | |||
}, | |||
}, | |||
iconName: 'search', | |||
title: 'Search', | |||
}, | |||
{ | |||
id: 'binned', | |||
href: { | |||
pathname: '/notes', | |||
query: { | |||
status: 'binned', | |||
}, | |||
}, | |||
iconName: 'bin', | |||
title: 'View Binned Notes', | |||
}, | |||
]} | |||
primaryItemsEnd={[ | |||
{ | |||
id: 'user', | |||
href: { | |||
pathname: '/me', | |||
}, | |||
iconName: 'user', | |||
title: 'User', | |||
}, | |||
]} | |||
secondaryItemsHeader={[ | |||
{ | |||
id: 'parent', | |||
href: { | |||
pathname: '/notes', | |||
query: { | |||
folder: '00000000-0000-0000-000000000000', | |||
}, | |||
}, | |||
iconName: 'back', | |||
title: 'Folder Name', | |||
// todo use history back | |||
}, | |||
{ | |||
id: 'note', | |||
href: { | |||
pathname: '/notes', | |||
query: { | |||
action: 'new', | |||
parentFolderId: '00000000-0000-0000-000000000000', | |||
}, | |||
}, | |||
iconName: 'new-note', | |||
title: 'Create Note', | |||
}, | |||
{ | |||
id: 'folder', | |||
href: { | |||
pathname: '/folders', | |||
query: { | |||
action: 'new', | |||
parentFolderId: '00000000-0000-0000-000000000000', | |||
}, | |||
}, | |||
iconName: 'new-folder', | |||
title: 'Create Child Folder', | |||
}, | |||
{ | |||
id: 'map', | |||
href: { | |||
pathname: '/notes', | |||
query: { | |||
action: 'view-map', | |||
parentFolderId: '00000000-0000-0000-000000000000', | |||
}, | |||
}, | |||
iconName: 'mind-map', | |||
title: 'View Folder Mind Map', | |||
}, | |||
]} | |||
secondaryItems={ | |||
Array.isArray(notes!) | |||
? notes.map(n => ({ | |||
id: n.id, | |||
active: n.id === id, | |||
href: { | |||
pathname: '/notes/[id]', | |||
query: { id: n.id }, | |||
}, | |||
iconName: 'note', | |||
replace: true, | |||
title: n.title.trim(), | |||
subtitle: ( | |||
<React.Fragment> | |||
{'Last updated '} | |||
<time | |||
dateTime={new Date(n.updatedAt).toISOString()} | |||
> | |||
<NoteLink> | |||
<NavbarItemContent> | |||
<NoteLinkPrimary> | |||
<NewIcon /> | |||
<NoteLinkTitle> | |||
Create Note | |||
</NoteLinkTitle> | |||
</NoteLinkPrimary> | |||
</NavbarItemContent> | |||
</NoteLink> | |||
</Link> | |||
</LinkContainer> | |||
{formatDate(new Date(n.updatedAt))} | |||
</time> | |||
</React.Fragment> | |||
), | |||
actions: [ | |||
{ | |||
Array.isArray(notes!) | |||
&& notes.map(n => ( | |||
<LinkContainer | |||
key={n.id} | |||
> | |||
{ | |||
n.id === id | |||
&& ( | |||
<NoteLinkBackground /> | |||
) | |||
} | |||
<Link | |||
href={{ | |||
pathname: '/notes/[id]', | |||
query: { id: n.id }, | |||
}} | |||
replace | |||
passHref | |||
> | |||
<NoteLink> | |||
<NavbarItemContent> | |||
<NoteLinkPrimary> | |||
<NoteLinkTitle | |||
style={{ opacity: n.title.length > 0 ? 1 : 0.5, }} | |||
> | |||
{n.title.length > 0 ? n.title : '(untitled)'} | |||
</NoteLinkTitle> | |||
</NoteLinkPrimary> | |||
{' '} | |||
<PostMeta> | |||
<time | |||
dateTime={new Date(n.updatedAt).toISOString()} | |||
> | |||
Last updated {formatDate(new Date(n.updatedAt))} | |||
</time> | |||
</PostMeta> | |||
</NavbarItemContent> | |||
</NoteLink> | |||
</Link> | |||
<NoteActions> | |||
<NoteAction | |||
onClick={Note.remove({ setNotes, notes, router, })(n)} | |||
> | |||
<Trash2 /> | |||
</NoteAction> | |||
</NoteActions> | |||
</LinkContainer> | |||
)) | |||
id: 'bin', | |||
iconName: 'bin', | |||
onClick: Note.remove({ setNotes, notes, router, })(n), | |||
} | |||
<LinkContainer> | |||
<Link | |||
href={{ | |||
pathname: '/bin', | |||
}} | |||
passHref | |||
> | |||
<NoteLink> | |||
<NavbarItemContent> | |||
<NoteLinkPrimary> | |||
<BinIcon /> | |||
<NoteLinkTitle> | |||
View Binned Notes | |||
</NoteLinkTitle> | |||
</NoteLinkPrimary> | |||
</NavbarItemContent> | |||
</NoteLink> | |||
</Link> | |||
</LinkContainer> | |||
</SecondaryNavItemsOverflow> | |||
</SecondaryNavItems> | |||
</NavbarItems> | |||
</NavbarContainer> | |||
</Navbar> | |||
], | |||
})) | |||
: [] | |||
} | |||
/> | |||
<Main> | |||
<Container> | |||
{ | |||
@@ -466,7 +275,7 @@ const Notes = ({ id: idProp }) => { | |||
id, | |||
setNotes, | |||
})} | |||
placeholder="Start typing here." | |||
placeholder="Start typing here..." | |||
/> | |||
</React.Fragment> | |||
) | |||
@@ -0,0 +1,17 @@ | |||
type Collection<T> = { | |||
items: T[], | |||
total: number, | |||
skip: number, | |||
take: number, | |||
} | |||
type Query = { | |||
q: string, | |||
} | |||
export default interface Storage<T> { | |||
loadSingle(id: string): T, | |||
loadMultiple(query: Query): Collection<T>, | |||
saveSingle(data: T): (id: string) => boolean, | |||
deleteSingle(id: string): boolean, | |||
} |