Split page into multiple components. Organize URLs as resource-oriented. Also make layout mobile-responsive.feature/transactions
@@ -7,7 +7,7 @@ module.exports = { | |||||
return [ | return [ | ||||
{ | { | ||||
source: '/', | source: '/', | ||||
destination: '/notes', | |||||
destination: '/notes?parentFolderId=00000000-0000-0000-000000000000', | |||||
permanent: true, | permanent: true, | ||||
}, | }, | ||||
] | ] | ||||
@@ -19,12 +19,14 @@ | |||||
"sequelize": "5.22.0", | "sequelize": "5.22.0", | ||||
"sequelize-cli": "^6.2.0", | "sequelize-cli": "^6.2.0", | ||||
"sequelize-typescript": "^1.1.0", | "sequelize-typescript": "^1.1.0", | ||||
"sqlite3": "^5.0.0", | |||||
"styled-components": "^5.2.0", | "styled-components": "^5.2.0", | ||||
"uuid": "^8.3.1" | "uuid": "^8.3.1" | ||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@types/node": "^14.14.2", | "@types/node": "^14.14.2", | ||||
"@types/react": "^16.9.53", | "@types/react": "^16.9.53", | ||||
"@types/styled-components": "^5.1.4", | |||||
"@types/uuid": "^8.3.0", | "@types/uuid": "^8.3.0", | ||||
"typescript": "^4.0.3" | "typescript": "^4.0.3" | ||||
}, | }, | ||||
@@ -4,8 +4,6 @@ import { | |||||
Container, | Container, | ||||
MarkupButton, | MarkupButton, | ||||
LinkButton, | LinkButton, | ||||
SectionSelect, | |||||
AttributeSelect, | |||||
SectionButton, | SectionButton, | ||||
Editor as MobileDocEditor, | Editor as MobileDocEditor, | ||||
} from 'react-mobiledoc-editor' | } 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' | import * as Storage from '../services/Storage' | ||||
export const loadFolders = async ({ setFolders, }) => { | |||||
export const load = async ({ setFolders, }) => { | |||||
const theFolders = await Storage.loadFolders() | const theFolders = await Storage.loadFolders() | ||||
setFolders(theFolders) | setFolders(theFolders) | ||||
} | } |
@@ -1,67 +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 { useRouter } from 'next/router' | ||||
import Editor from '../components/Editor/Editor' | import Editor from '../components/Editor/Editor' | ||||
import Navbar from '../components/Navbar/Navbar' | |||||
import generateId from '../utilities/Id' | import generateId from '../utilities/Id' | ||||
import Link from 'next/link' | |||||
import { formatDate } from '../utilities/Date' | import { formatDate } from '../utilities/Date' | ||||
import * as Note from '../controllers/Note' | import * as Note from '../controllers/Note' | ||||
import * as Folder from '../controllers/Folder' | 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')({ | const Main = styled('main')({ | ||||
margin: '2rem 0', | margin: '2rem 0', | ||||
'@media (min-width: 1080px)': { | '@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')({ | const TitleInput = styled('input')({ | ||||
border: 0, | border: 0, | ||||
background: 'transparent', | background: 'transparent', | ||||
@@ -108,98 +40,6 @@ const TitleInput = styled('input')({ | |||||
outline: 0, | 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')({ | const PostMeta = styled('small')({ | ||||
opacity: 0.5, | opacity: 0.5, | ||||
height: '1.25rem', | height: '1.25rem', | ||||
@@ -228,7 +68,7 @@ const Notes = ({ id: idProp }) => { | |||||
}, []) | }, []) | ||||
React.useEffect(() => { | React.useEffect(() => { | ||||
Folder.loadFolders({ setFolders }) | |||||
Folder.load({ setFolders }) | |||||
}, []) | }, []) | ||||
React.useEffect(() => { | 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> | <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> | |||||
<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> | <Main> | ||||
<Container> | <Container> | ||||
{ | { | ||||
@@ -466,7 +275,7 @@ const Notes = ({ id: idProp }) => { | |||||
id, | id, | ||||
setNotes, | setNotes, | ||||
})} | })} | ||||
placeholder="Start typing here." | |||||
placeholder="Start typing here..." | |||||
/> | /> | ||||
</React.Fragment> | </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, | |||||
} |