Browse Source

Refactor components, set URLs

Split page into multiple components. Organize URLs as resource-oriented.

Also make layout mobile-responsive.
feature/transactions
TheoryOfNekomata 3 years ago
parent
commit
2dbae87db3
11 changed files with 6044 additions and 5674 deletions
  1. +1
    -1
      next.config.js
  2. +2
    -0
      package.json
  3. +0
    -2
      src/components/Editor/Editor.tsx
  4. +36
    -0
      src/components/Icon/Icon.tsx
  5. +195
    -0
      src/components/Navbar/Navbar.tsx
  6. +57
    -0
      src/components/PrimaryNavItem/PrimaryNavItem.tsx
  7. +181
    -0
      src/components/SecondaryNavItem/SecondaryNavItem.tsx
  8. +1
    -1
      src/controllers/Folder.ts
  9. +141
    -332
      src/pages/notes.tsx
  10. +17
    -0
      src/services/storages/Storage.ts
  11. +5413
    -5338
      yarn.lock

+ 1
- 1
next.config.js View File

@@ -7,7 +7,7 @@ module.exports = {
return [
{
source: '/',
destination: '/notes',
destination: '/notes?parentFolderId=00000000-0000-0000-000000000000',
permanent: true,
},
]


+ 2
- 0
package.json View File

@@ -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"
},


+ 0
- 2
src/components/Editor/Editor.tsx View File

@@ -4,8 +4,6 @@ import {
Container,
MarkupButton,
LinkButton,
SectionSelect,
AttributeSelect,
SectionButton,
Editor as MobileDocEditor,
} from 'react-mobiledoc-editor'


+ 36
- 0
src/components/Icon/Icon.tsx View File

@@ -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

+ 195
- 0
src/components/Navbar/Navbar.tsx View File

@@ -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

+ 57
- 0
src/components/PrimaryNavItem/PrimaryNavItem.tsx View File

@@ -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

+ 181
- 0
src/components/SecondaryNavItem/SecondaryNavItem.tsx View File

@@ -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
- 1
src/controllers/Folder.ts View File

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

+ 141
- 332
src/pages/notes.tsx View File

@@ -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>
)


+ 17
- 0
src/services/storages/Storage.ts View File

@@ -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,
}

+ 5413
- 5338
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save