Browse Source

Extract submodules

Move each project to each repository.
master
TheoryOfNekomata 3 years ago
parent
commit
2a9db1a9e7
74 changed files with 16 additions and 9003 deletions
  1. +12
    -0
      .gitmodules
  2. +1
    -0
      packages/app
  3. +0
    -9
      packages/app/.babelrc
  4. +0
    -7
      packages/app/.sequelizerc
  5. +0
    -30
      packages/app/README.md
  6. +0
    -18
      packages/app/config/config.js
  7. +0
    -25
      packages/app/migrate.ts
  8. +0
    -4
      packages/app/next-env.d.ts
  9. +0
    -15
      packages/app/next.config.js
  10. +0
    -37
      packages/app/package.json
  11. BIN
      packages/app/public/favicon.ico
  12. +0
    -4
      packages/app/public/vercel.svg
  13. +0
    -9
      packages/app/src/assets/global.css
  14. +0
    -155
      packages/app/src/assets/mobiledoc.css
  15. +0
    -10
      packages/app/src/assets/theme.css
  16. +0
    -149
      packages/app/src/components/Editor/Editor.tsx
  17. +0
    -36
      packages/app/src/components/Icon/Icon.tsx
  18. +0
    -282
      packages/app/src/components/Navbar/Navbar.tsx
  19. +0
    -112
      packages/app/src/components/PrimaryNavItem/PrimaryNavItem.tsx
  20. +0
    -201
      packages/app/src/components/SecondaryNavItem/SecondaryNavItem.tsx
  21. +0
    -6
      packages/app/src/controllers/Folder.ts
  22. +0
    -104
      packages/app/src/controllers/Note.ts
  23. +0
    -13
      packages/app/src/models.ts
  24. +0
    -42
      packages/app/src/models/Folder.ts
  25. +0
    -46
      packages/app/src/models/Note.ts
  26. +0
    -22
      packages/app/src/models/Operation.ts
  27. +0
    -22
      packages/app/src/models/Tag.ts
  28. +0
    -34
      packages/app/src/models/Transaction.ts
  29. +0
    -10
      packages/app/src/pages/_app.tsx
  30. +0
    -31
      packages/app/src/pages/_document.tsx
  31. +0
    -36
      packages/app/src/pages/api/folders.ts
  32. +0
    -43
      packages/app/src/pages/api/folders/[id].ts
  33. +0
    -36
      packages/app/src/pages/api/notes.ts
  34. +0
    -43
      packages/app/src/pages/api/notes/[id].ts
  35. +0
    -315
      packages/app/src/pages/notes.tsx
  36. +0
    -4
      packages/app/src/pages/notes/[id].tsx
  37. +0
    -13
      packages/app/src/seeds.ts
  38. +0
    -23
      packages/app/src/services/LocalStorage.ts
  39. +0
    -15
      packages/app/src/services/Operation.ts
  40. +0
    -110
      packages/app/src/services/Storage.ts
  41. +0
    -64
      packages/app/src/services/entities/Folder.ts
  42. +0
    -78
      packages/app/src/services/entities/Note.ts
  43. +0
    -60
      packages/app/src/utilities/ColumnTypes.ts
  44. +0
    -98
      packages/app/src/utilities/Date.ts
  45. +0
    -5
      packages/app/src/utilities/Id.ts
  46. +0
    -51
      packages/app/src/utilities/ORM.ts
  47. +0
    -49
      packages/app/src/utilities/Response.ts
  48. +0
    -3
      packages/app/src/utilities/Serialization.ts
  49. +0
    -35
      packages/app/tsconfig.json
  50. +0
    -6112
      packages/app/yarn.lock
  51. +0
    -9
      packages/app/zeichen.config.ts
  52. +1
    -0
      packages/core
  53. +0
    -2
      packages/core/.npmignore
  54. +0
    -3
      packages/core/README.md
  55. +0
    -11
      packages/core/package.json
  56. +0
    -5
      packages/core/src/plugin.ts
  57. +0
    -16
      packages/core/src/storage.ts
  58. +0
    -28
      packages/core/tsconfig.json
  59. +1
    -0
      packages/plugin-local-storage
  60. +0
    -2
      packages/plugin-local-storage/.npmignore
  61. +0
    -3
      packages/plugin-local-storage/README.md
  62. +0
    -18
      packages/plugin-local-storage/package.json
  63. +0
    -23
      packages/plugin-local-storage/src/Engine.ts
  64. +0
    -55
      packages/plugin-local-storage/src/Storage.ts
  65. +0
    -13
      packages/plugin-local-storage/src/index.ts
  66. +0
    -27
      packages/plugin-local-storage/tsconfig.json
  67. +1
    -0
      packages/plugin-remote-storage
  68. +0
    -2
      packages/plugin-remote-storage/.npmignore
  69. +0
    -3
      packages/plugin-remote-storage/README.md
  70. +0
    -18
      packages/plugin-remote-storage/package.json
  71. +0
    -70
      packages/plugin-remote-storage/src/Engine.ts
  72. +0
    -27
      packages/plugin-remote-storage/src/Storage.ts
  73. +0
    -15
      packages/plugin-remote-storage/src/index.ts
  74. +0
    -27
      packages/plugin-remote-storage/tsconfig.json

+ 12
- 0
.gitmodules View File

@@ -0,0 +1,12 @@
[submodule "packages/app"]
path = packages/app
url = git@code.modal.sh:zeichen/app.git
[submodule "packages/core"]
path = packages/core
url = git@code.modal.sh:zeichen/core.git
[submodule "packages/plugin-local-storage"]
path = packages/plugin-local-storage
url = git@code.modal.sh:zeichen/plugin-local-storage.git
[submodule "packages/plugin-remote-storage"]
path = packages/plugin-remote-storage
url = git@code.modal.sh:zeichen/plugin-remote-storage.git

+ 1
- 0
packages/app

@@ -0,0 +1 @@
Subproject commit 8ccaaef742ccdf75adafb31fdcc99a408cf5cd89

+ 0
- 9
packages/app/.babelrc View File

@@ -1,9 +0,0 @@
{
"presets": ["next/babel"],
"plugins": [
"babel-plugin-transform-typescript-metadata",
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
"babel-plugin-parameter-decorator"
]
}

+ 0
- 7
packages/app/.sequelizerc View File

@@ -1,7 +0,0 @@
const path = require('path');

module.exports = {
'config-path': path.resolve('database', 'config', 'config.js'),
'seeders-path': path.resolve('database', 'seeders'),
'migrations-path': path.resolve('database', 'migrations')
};

+ 0
- 30
packages/app/README.md View File

@@ -1,30 +0,0 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

+ 0
- 18
packages/app/config/config.js View File

@@ -1,18 +0,0 @@
const dotenv = require('dotenv')

dotenv.config()

module.exports = {
"development": {
"host": process.env.DATABASE_URL,
"dialect": process.env.DATABASE_DIALECT
},
"test": {
"host": process.env.DATABASE_URL,
"dialect": process.env.DATABASE_DIALECT
},
"production": {
"host": process.env.DATABASE_URL,
"dialect": process.env.DATABASE_DIALECT
}
}

+ 0
- 25
packages/app/migrate.ts View File

@@ -1,25 +0,0 @@
import models from './src/models'
import seeds from './src/seeds'

// TODO support NoSQL

export const up = async queryInterface => {
const createTablePromises = models.map(m => queryInterface.createTable(m.tableName, m.attributes))
await Promise.all(createTablePromises)

const seedTablePromise = models
.filter(m => Boolean(seeds[m.modelName]))
.map(m => {
console.log(JSON.stringify(seeds[m.modelName]))
return queryInterface.bulkInsert(m.tableName, seeds[m.tableName])
})
return Promise.all(seedTablePromise)
}

export const down = async queryInterface => {
const dropTablePromises = models
.reduce((reverse, m) => [m, ...reverse], [])
.map(m => queryInterface.dropTable(m.tableName))

return Promise.all(dropTablePromises)
}

+ 0
- 4
packages/app/next-env.d.ts View File

@@ -1,4 +0,0 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

declare module 'react-mobiledoc-editor'

+ 0
- 15
packages/app/next.config.js View File

@@ -1,15 +0,0 @@
module.exports = {
poweredByHeader: false,
devIndicators: {
autoPrerender: false,
},
async redirects() {
return [
{
source: '/',
destination: '/notes?parentFolderId=00000000-0000-0000-000000000000',
permanent: true,
},
]
},
}

+ 0
- 37
packages/app/package.json View File

@@ -1,37 +0,0 @@
{
"name": "zeichen-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"migrate": "tsc migrate.ts --module commonjs --esModuleInterop --outDir database/migrations && sequelize-cli db:migrate",
"migrate:run": "sequelize-cli db:migrate"
},
"dependencies": {
"@fingerprintjs/fingerprintjs": "^3.0.3",
"dotenv": "^8.2.0",
"mobiledoc-kit": "^0.13.1",
"next": "9.5.5",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-feather": "^2.0.8",
"react-mobiledoc-editor": "^0.10.0",
"sequelize": "5.22.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",
"sequelize-cli": "^6.2.0",
"sqlite3": "^5.0.0"
},
"optionalDependencies": {
"sqlite3": "^5.0.0"
}
}

BIN
packages/app/public/favicon.ico View File

Before After

+ 0
- 4
packages/app/public/vercel.svg View File

@@ -1,4 +0,0 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

+ 0
- 9
packages/app/src/assets/global.css View File

@@ -1,9 +0,0 @@
:root {
background-color: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-family-base), sans-serif;
}

body {
margin: 0;
}

+ 0
- 155
packages/app/src/assets/mobiledoc.css View File

@@ -1,155 +0,0 @@
/**
* 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
- 10
packages/app/src/assets/theme.css View File

@@ -1,10 +0,0 @@
: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;
}

+ 0
- 149
packages/app/src/components/Editor/Editor.tsx View File

@@ -1,149 +0,0 @@
import * as React from 'react'
import * as PropTypes from 'prop-types'
import {
Container,
MarkupButton,
LinkButton,
SectionButton,
Editor as MobileDocEditor,
} from 'react-mobiledoc-editor'
import { Bold, Italic, Code, List, Link, MessageCircle, } from 'react-feather'
import styled from 'styled-components'

const ToolbarWrapper = styled('div')({
display: 'flex',
gap: '1rem',
})

const ToolbarItem = styled('div')({
display: 'block',
})

const StyledMarkupButton = styled(MarkupButton)({
border: 0,
background: 'transparent',
width: '2rem',
height: '2rem',
font: 'inherit',
padding: 0,
outline: 0,
})

const StyledLinkButton = styled(LinkButton)({
border: 0,
background: 'transparent',
width: '2rem',
height: '2rem',
font: 'inherit',
padding: 0,
outline: 0,
})

const StyledSectionButton = styled(SectionButton)({
border: 0,
background: 'transparent',
width: '2rem',
height: '2rem',
font: 'inherit',
fontFamily: 'monospace',
fontSize: '1.75rem',
lineHeight: 0,
padding: 0,
outline: 0,
})

const propTypes = {
onChange: PropTypes.func,
cards: PropTypes.array,
atoms: PropTypes.array,
content: PropTypes.object,
placeholder: PropTypes.string,
autoFocus: PropTypes.bool,
}

type Props = PropTypes.InferProps<typeof propTypes>

const Editor: React.FC<Props> = ({
onChange,
cards = [],
atoms = [],
content,
placeholder,
autoFocus,
}) => {
const [isClient, setIsClient, ] = React.useState(false)
const editorRef = React.useRef(null)

const handleInitialize = e => {
editorRef.current = e
}

React.useEffect(() => {
setIsClient(true)
}, [])

if (isClient) {
return (
<Container
autofocus={autoFocus}
onChange={onChange}
cards={[
...cards,
]}
atoms={[
...atoms,
]}
mobiledoc={content}
placeholder={placeholder}
didCreateEditor={handleInitialize}
>
<ToolbarWrapper>
<ToolbarItem>
<StyledMarkupButton tag="strong">
<Bold />
</StyledMarkupButton>
</ToolbarItem>
<ToolbarItem>
<StyledMarkupButton tag="em">
<Italic />
</StyledMarkupButton>
</ToolbarItem>
<ToolbarItem>
<StyledMarkupButton tag="code">
<Code />
</StyledMarkupButton>
</ToolbarItem>
<ToolbarItem>
<StyledLinkButton>
<Link />
</StyledLinkButton>
</ToolbarItem>
{/*<ToolbarItem><SectionSelect tags={["h1", "h2", "h3"]} /></ToolbarItem>*/}
{/*<ToolbarItem><AttributeSelect attribute="text-align" values={["left", "center", "right"]} /></ToolbarItem>*/}
<ToolbarItem>
<StyledSectionButton tag="blockquote">
<MessageCircle />
</StyledSectionButton>
</ToolbarItem>
<ToolbarItem>
<StyledSectionButton tag="ul">
<List />
</StyledSectionButton>
</ToolbarItem>
<ToolbarItem><StyledSectionButton tag="ol">1.</StyledSectionButton></ToolbarItem>
</ToolbarWrapper>
<MobileDocEditor
style={{
color: 'inherit',
}}
/>
</Container>
)
}

return null
}

Editor.propTypes = propTypes

export default Editor

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

@@ -1,36 +0,0 @@
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
- 282
packages/app/src/components/Navbar/Navbar.tsx View File

@@ -1,282 +0,0 @@
import * as React from 'react'
import styled, { createGlobalStyle } from 'styled-components'
import SecondaryNavItem from '../SecondaryNavItem/SecondaryNavItem'
import PrimaryNavItem from '../PrimaryNavItem/PrimaryNavItem'
import Link from 'next/link'

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',
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%',
},
})

const PrimaryNavItemsContainer = styled('div')({
width: '100%',
height: '100%',
maxWidth: 720,
display: 'flex',
margin: '0 auto',
'@media (min-width: 1080px)': {
width: '100%',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'stretch',
},
})

const PrimaryNavItemGroup = styled('div')({
display: 'contents',
'@media (min-width: 1080px)': {
display: 'block',
},
})

const SecondaryNavItems = styled('nav')({
width: '100%',
height: '100%',
position: 'fixed',
top: 0,
left: 0,
paddingBottom: '4rem',
boxSizing: 'border-box',
pointerEvents: 'none',
'@media (min-width: 1080px)': {
position: 'relative',
top: 'auto',
left: 'auto',
flex: 'auto',
width: 'auto',
paddingBottom: 0,
pointerEvents: 'initial',
},
})

const SecondaryNavItemsFg = styled('div')({
height: '100%',
backgroundColor: 'var(--color-bg)',
width: 360,
position: 'absolute',
top: 0,
left: -360,
paddingBottom: '4rem',
boxSizing: 'border-box',
transitionProperty: 'left',
transitionDuration: '350ms',
transitionTimingFunction: 'ease-out',
'::before': {
content: "''",
display: 'block',
top: 0,
left: 0,
width: '100%',
height: '100%',
position: 'absolute',
backgroundColor: 'black',
opacity: 0.03125,
},
'@media (min-width: 1080px)': {
position: 'relative',
top: 'auto',
left: 'auto',
flex: 'auto',
width: 'auto',
paddingBottom: 0,
},
})

const SecondaryNavItemsBg = styled('a')({
opacity: 0,
transitionProperty: 'opacity',
transitionDuration: '350ms',
transitionTimingFunction: 'ease-out',
'::before': {
content: "''",
display: 'block',
top: 0,
left: 0,
width: '100%',
height: '100%',
position: 'absolute',
backgroundColor: 'black',
opacity: 0.75,
},
})

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 SecondaryVisibleDummy = styled('div')({
display: 'none',
})

const Visible = createGlobalStyle({
[`div + ${SecondaryNavItems}`]: {
pointerEvents: 'initial',
},
[`div + ${SecondaryNavItems} ${SecondaryNavItemsFg}`]: {
left: 0,
},
[`div + ${SecondaryNavItems} ${SecondaryNavItemsBg}`]: {
opacity: 1,
},
'@media (min-width: 1080px)': {
[`div + ${SecondaryNavItems} ${SecondaryNavItemsFg}`]: {
left: 'auto',
},
[`div + ${SecondaryNavItems} ${SecondaryNavItemsBg}`]: {
opacity: 0,
},
}
})

const Navbar = ({
closeHref,
secondaryVisible,
primaryItemsStart = [],
primaryItemsEnd = [],
secondaryItemsHeader = [],
secondaryItems = [],
}) => (
<React.Fragment>
<Visible />
<Base>
<NavbarContainer>
<NavbarItems>
<PrimaryNavItems>
<PrimaryNavItemsContainer>
<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>
)
}
</PrimaryNavItemsContainer>
</PrimaryNavItems>
{
secondaryVisible
&& (
<SecondaryVisibleDummy />
)
}
<SecondaryNavItems>
<Link
href={closeHref}
passHref
shallow
>
<SecondaryNavItemsBg />
</Link>
<SecondaryNavItemsFg>
<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>
</SecondaryNavItemsFg>
</SecondaryNavItems>
</NavbarItems>
</NavbarContainer>
</Base>
</React.Fragment>
)

export default Navbar

+ 0
- 112
packages/app/src/components/PrimaryNavItem/PrimaryNavItem.tsx View File

@@ -1,112 +0,0 @@
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('div')({
width: 0,
height: '4rem',
flex: 'auto',
padding: '0.25rem',
boxSizing: 'border-box',
position: 'relative',
'@media (min-width: 1080px)': {
width: '4rem',
height: '4rem',
},
})

const ClickArea = styled('a')({
display: 'grid',
color: 'inherit',
placeContent: 'center',
width: '100%',
height: '100%',
position: 'absolute',
top: 0,
left: 0,
textDecoration: 'none',
})

const ClickAreaContent = styled('span')({
display: 'block',
textAlign: 'center',
lineHeight: 1,
})

const Highlight = styled('div')({
width: '100%',
height: '100%',
backgroundColor: 'var(--color-primary)',
opacity: 0.5,
borderRadius: '0.25rem',
[`+ ${ClickArea}`]: {
opacity: 0.75,
},
})

const Title = styled('small')({
display: 'block',
fontWeight: 'bolder',
position: 'absolute',
left: -999999,
})

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 (
<BaseComponent>
{
active
&& (
<Highlight />
)
}
<Link
href={href}
passHref
shallow
>
<ClickArea
title={title}
>
<ClickAreaContent>
<Icon
name={iconName}
/>
<Title>
{title}
</Title>
</ClickAreaContent>
</ClickArea>
</Link>
</BaseComponent>
)
}

PrimaryNavItem.propTypes = propTypes

export default PrimaryNavItem

+ 0
- 201
packages/app/src/components/SecondaryNavItem/SecondaryNavItem.tsx View File

@@ -1,201 +0,0 @@
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',
alignItems: 'center',
position: 'relative',
flex: 'auto',
padding: '0 1rem',
boxSizing: 'border-box',
})

const NoteLinkPrimary = styled('span')({
display: 'block',
flex: 'auto',
})

const NoteLinkTitle = styled('strong')({
display: 'block',
height: '1.25em',
position: 'relative',
})

const NoteLinkTitleOverflow = styled('span')({
whiteSpace: 'nowrap',
overflow: 'hidden',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
textOverflow: 'ellipsis',
})

const LinkContainer = styled('div')({
position: 'relative',
color: 'var(--color-primary, blue)',
display: 'flex',
alignItems: 'stretch',
height: '4rem',
})

const NoteActions = styled('div')({
display: 'flex',
alignItems: 'stretch',
height: '100%',
position: 'relative',
'@media (min-width: 1080px)': {
opacity: 0,
[`${LinkContainer}:hover &`]: {
opacity: 1,
},
},
})

const NoteAction = styled('button')({
height: '100%',
width: '4rem',
background: 'transparent',
border: 0,
color: 'inherit',
cursor: 'pointer',
outline: 0,
padding: 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 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
shallow
>
<NoteLink>
<IconContainer>
{
iconName
&& (
<Icon
name={iconName}
/>
)
}
</IconContainer>
<NoteLinkPrimary>
<NoteLinkTitle
style={{ opacity: title.length > 0 ? 1 : 0.5, }}
>
<NoteLinkTitleOverflow>
{title.length > 0 ? title : '(untitled)'}
</NoteLinkTitleOverflow>
</NoteLinkTitle>
{
subtitle
&& (
<React.Fragment>
{' '}
<PostMeta>
{subtitle}
</PostMeta>
</React.Fragment>
)
}
</NoteLinkPrimary>
</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

+ 0
- 6
packages/app/src/controllers/Folder.ts View File

@@ -1,6 +0,0 @@
import * as Storage from '../services/Storage'

export const load = async ({ setFolders, }) => {
const theFolders = await Storage.loadFolders()
setFolders(theFolders)
}

+ 0
- 104
packages/app/src/controllers/Note.ts View File

@@ -1,104 +0,0 @@
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)
}

+ 0
- 13
packages/app/src/models.ts View File

@@ -1,13 +0,0 @@
import Folder from './models/Folder'
import Note from './models/Note'
import Tag from './models/Tag'
import Operation from './models/Operation'
import Transaction from './models/Transaction'

export default [
Folder,
Note,
Tag,
Operation,
Transaction,
]

+ 0
- 42
packages/app/src/models/Folder.ts View File

@@ -1,42 +0,0 @@
import * as ColumnTypes from '../utilities/ColumnTypes'

const Folder: ColumnTypes.Model = {
modelName: 'Folder',
tableName: 'folders',
options: {
timestamps: true,
paranoid: true,
createdAt: 'createdAt',
updatedAt: 'updatedAt',
deletedAt: 'deletedAt',
},
attributes: {
id: {
allowNull: true,
primaryKey: true,
type: ColumnTypes.UUIDV4,
},
name: {
allowNull: false,
type: ColumnTypes.STRING,
},
parentId: {
allowNull: true,
type: ColumnTypes.UUIDV4,
},
createdAt: {
allowNull: false,
type: ColumnTypes.DATE,
},
updatedAt: {
allowNull: false,
type: ColumnTypes.DATE,
},
deletedAt: {
allowNull: true,
type: ColumnTypes.DATE,
},
}
}

export default Folder

+ 0
- 46
packages/app/src/models/Note.ts View File

@@ -1,46 +0,0 @@
import * as ColumnTypes from '../utilities/ColumnTypes'

const Note: ColumnTypes.Model = {
modelName: 'Note',
tableName: 'notes',
options: {
timestamps: true,
paranoid: true,
createdAt: 'createdAt',
updatedAt: 'updatedAt',
deletedAt: 'deletedAt',
},
attributes: {
id: {
allowNull: true,
primaryKey: true,
type: ColumnTypes.UUIDV4,
},
title: {
allowNull: false,
type: ColumnTypes.STRING,
},
content: {
allowNull: true,
type: ColumnTypes.TEXT({ length: 'long', }),
},
folderId: {
allowNull: true,
type: ColumnTypes.UUIDV4,
},
createdAt: {
allowNull: false,
type: ColumnTypes.DATE,
},
updatedAt: {
allowNull: false,
type: ColumnTypes.DATE,
},
deletedAt: {
allowNull: true,
type: ColumnTypes.DATE,
},
},
}

export default Note

+ 0
- 22
packages/app/src/models/Operation.ts View File

@@ -1,22 +0,0 @@
import * as ColumnTypes from '../utilities/ColumnTypes'

const Operation: ColumnTypes.Model = {
modelName: 'Operation',
tableName: 'operations',
options: {
timestamps: false,
},
attributes: {
id: {
allowNull: true,
primaryKey: true,
type: ColumnTypes.INTEGER,
},
name: {
allowNull: false,
type: ColumnTypes.STRING,
},
}
}

export default Operation

+ 0
- 22
packages/app/src/models/Tag.ts View File

@@ -1,22 +0,0 @@
import * as ColumnTypes from '../utilities/ColumnTypes'

const Tag: ColumnTypes.Model = {
modelName: 'Tag',
tableName: 'tags',
options: {
timestamps: false,
},
attributes: {
id: {
allowNull: true,
primaryKey: true,
type: ColumnTypes.UUIDV4,
},
name: {
allowNull: false,
type: ColumnTypes.STRING,
},
}
}

export default Tag

+ 0
- 34
packages/app/src/models/Transaction.ts View File

@@ -1,34 +0,0 @@
import * as ColumnTypes from '../utilities/ColumnTypes'

const Transaction: ColumnTypes.Model = {
modelName: 'Transaction',
tableName: 'transactions',
options: {
timestamps: false,
},
attributes: {
id: {
allowNull: true,
primaryKey: true,
type: ColumnTypes.UUIDV4,
},
deviceId: {
allowNull: false,
type: ColumnTypes.STRING,
},
operation: {
allowNull: false,
type: ColumnTypes.INTEGER,
},
objectId: {
allowNull: false,
type: ColumnTypes.STRING,
},
performedAt: {
allowNull: false,
type: ColumnTypes.DATE,
},
},
}

export default Transaction

+ 0
- 10
packages/app/src/pages/_app.tsx View File

@@ -1,10 +0,0 @@
import * as React from 'react'
import '../assets/theme.css'
import '../assets/global.css'
import '../assets/mobiledoc.css'

const App = ({ Component, pageProps }) => (
<Component {...pageProps} />
)

export default App

+ 0
- 31
packages/app/src/pages/_document.tsx View File

@@ -1,31 +0,0 @@
import * as React from 'react'
import Document from 'next/document'
import { ServerStyleSheet } from 'styled-components'

export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage

try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
})

const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<React.Fragment>
{initialProps.styles}
{sheet.getStyleElement()}
</React.Fragment>
),
}
} finally {
sheet.seal()
}
}
}

+ 0
- 36
packages/app/src/pages/api/folders.ts View File

@@ -1,36 +0,0 @@
import ORM, { DatabaseKind } from '../../utilities/ORM'
import * as Service from '../../services/entities/Folder'
import Model from '../../models/Folder'

export default async (req, res) => {
const orm = new ORM({
kind: process.env.DATABASE_DIALECT as DatabaseKind,
url: process.env.DATABASE_URL,
})
const repository = orm.getRepository(Model)
const methodHandlers = {
'GET': Service.getMultiple(repository),
}

const { [req.method as keyof typeof methodHandlers]: handler = null } = methodHandlers
if (handler === null) {
res.statusCode = 415
res.json({ message: 'Method not allowed.' })
return
}

try {
const { status, data, } = await handler(req.query)
res.statusCode = status
res.json(data)
} catch (err) {
console.error(err)
const { status, data, } = err
res.statusCode = status
if (data && status !== 204) {
res.json(data)
return
}
res.end()
}
}

+ 0
- 43
packages/app/src/pages/api/folders/[id].ts View File

@@ -1,43 +0,0 @@
import ORM, { DatabaseKind } from '../../../utilities/ORM'
import * as Service from '../../../services/entities/Folder'
import Model from '../../../models/Folder'

export default async (req, res) => {
const orm = new ORM({
kind: process.env.DATABASE_DIALECT as DatabaseKind,
url: process.env.DATABASE_URL,
})
const repository = orm.getRepository(Model)
const methodHandlers = {
'GET': Service.getSingle(repository),
'PUT': Service.save(repository)(req.body),
'DELETE': Service.remove(repository)
}

const { [req.method as keyof typeof methodHandlers]: handler = null } = methodHandlers
if (handler === null) {
res.statusCode = 415
res.json({ message: 'Method not allowed.' })
return
}

const { id } = req.query
try {
const { status, ...etcReturn } = await handler(id)
res.statusCode = status
if (etcReturn['data']) {
res.json(etcReturn['data'])
return
}
res.end()
} catch (err) {
console.error(err)
const { status, data, } = err
res.statusCode = status || 500
if (data && status !== 204) {
res.json(data)
return
}
res.end()
}
}

+ 0
- 36
packages/app/src/pages/api/notes.ts View File

@@ -1,36 +0,0 @@
import ORM, { DatabaseKind } from '../../utilities/ORM'
import * as Service from '../../services/entities/Note'
import Model from '../../models/Note'

export default async (req, res) => {
const orm = new ORM({
kind: process.env.DATABASE_DIALECT as DatabaseKind,
url: process.env.DATABASE_URL,
})
const repository = orm.getRepository(Model)
const methodHandlers = {
'GET': Service.getMultiple(repository),
}

const { [req.method as keyof typeof methodHandlers]: handler = null } = methodHandlers
if (handler === null) {
res.statusCode = 415
res.json({ message: 'Method not allowed.' })
return
}

try {
const { status, data, } = await handler(req.query)
res.statusCode = status
res.json(data)
} catch (err) {
console.error(err)
const { status, data, } = err
res.statusCode = status
if (data && status !== 204) {
res.json(data)
return
}
res.end()
}
}

+ 0
- 43
packages/app/src/pages/api/notes/[id].ts View File

@@ -1,43 +0,0 @@
import ORM, { DatabaseKind } from '../../../utilities/ORM'
import * as Service from '../../../services/entities/Note'
import Model from '../../../models/Note'

export default async (req, res) => {
const orm = new ORM({
kind: process.env.DATABASE_DIALECT as DatabaseKind,
url: process.env.DATABASE_URL,
})
const repository = orm.getRepository(Model)
const methodHandlers = {
'GET': Service.getSingle(repository),
'PUT': Service.save(repository)(req.body),
'DELETE': Service.remove(repository)
}

const { [req.method as keyof typeof methodHandlers]: handler = null } = methodHandlers
if (handler === null) {
res.statusCode = 415
res.json({ message: 'Method not allowed.' })
return
}

const { id } = req.query
try {
const { status, ...etcReturn } = await handler(id)
res.statusCode = status
if (etcReturn['data']) {
res.json(etcReturn['data'])
return
}
res.end()
} catch (err) {
console.error(err)
const { status, data, } = err
res.statusCode = status || 500
if (data && status !== 204) {
res.json(data)
return
}
res.end()
}
}

+ 0
- 315
packages/app/src/pages/notes.tsx View File

@@ -1,315 +0,0 @@
import * as React from 'react'
import Head from 'next/head'
import styled from 'styled-components'
import { useRouter } from 'next/router'
import Editor from '../components/Editor/Editor'
import Navbar from '../components/Navbar/Navbar'
import generateId from '../utilities/Id'
import { formatDate } from '../utilities/Date'
import * as Note from '../controllers/Note'
import * as Folder from '../controllers/Folder'

const Main = styled('main')({
margin: '2rem 0',
'@media (min-width: 1080px)': {
paddingLeft: `${100 / 3}%`,
boxSizing: 'border-box',
},
})

const Container = styled('div')({
width: '100%',
margin: '0 auto',
padding: '0 1rem',
boxSizing: 'border-box',
'@media (min-width: 720px)': {
maxWidth: 720,
},
})

const TitleInput = styled('input')({
border: 0,
background: 'transparent',
padding: 0,
display: 'block',
width: '100%',
font: 'inherit',
fontSize: '3rem',
fontWeight: 'bold',
color: 'inherit',
outline: 0,
})

const PostMeta = styled('small')({
opacity: 0.5,
height: '1.25rem',
display: 'block',
lineHeight: 1.25,
})

const PostPrimary = styled('div')({
marginBottom: '2rem',
})

type NoteInstance = { id: string, title: string, content?: object, updatedAt: string, }

const Notes = ({ id: idProp }) => {
// TODO remove extra state for ID
const [id, setId, ] = React.useState(idProp)
const [title, setTitle, ] = React.useState('')
const [notes, setNotes, ] = React.useState(null)
const [folders, setFolders, ] = React.useState(null)
const stateRef = React.useRef<NoteInstance>({ id, title: '', updatedAt: new Date().toISOString(), })
const timeoutRef = React.useRef<number>(null)
const router = useRouter()

React.useEffect(() => {
Note.load({ setNotes })
}, [])

React.useEffect(() => {
Folder.load({ setFolders })
}, [])

React.useEffect(() => {
if (!Array.isArray(notes!)) {
return
}
const theNote = notes.find(n => n.id === id)
stateRef.current = theNote ? theNote : { id, title: '', updatedAt: new Date().toISOString(), }
setTitle(stateRef.current.title)
}, [id, notes])

React.useEffect(() => {
setId(idProp || generateId())
}, [idProp])

return (
<React.Fragment>
<Head>
<title>{ idProp === undefined ? 'Notes | New Note' : `Notes | Edit Note - ${title.length > 0 ? title : '(untitled)'}`}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Navbar
closeHref={{
pathname: router.pathname,
query: Object
.entries(router.query)
.filter(([key]) => key !== 'navbar')
.reduce(
(theQuery, [key, value]) => ({
...theQuery,
[key]: value,
}),
{}
),
}}
secondaryVisible={Boolean(router.query.navbar)}
primaryItemsStart={[
{
id: 'sidebar',
mobileOnly: true,
active: Boolean(router.query.navbar),
href: {
pathname: router.pathname,
query: {
...router.query,
navbar: 'true',
},
},
iconName: 'menu',
title: 'Menu',
},
{
id: 'folders',
active: router.pathname.startsWith('/notes') && !Boolean(router.query.navbar),
href: {
pathname: '/notes',
},
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: 'Bin',
},
]}
primaryItemsEnd={[
{
id: 'user',
href: {
pathname: '/me',
},
iconName: 'user',
title: 'User',
},
]}
secondaryItemsHeader={[
{
id: 'parent',
href: {
pathname: '/notes',
query: {
folder: '00000000-0000-0000-000000000000',
navbar: router.query.navbar,
},
},
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: (
<time
dateTime={new Date(n.updatedAt).toISOString()}
>
{formatDate(new Date(n.updatedAt))}
</time>
),
actions: [
{
id: 'bin',
iconName: 'bin',
onClick: Note.remove({ setNotes, notes, router, })(n),
}
],
}))
: []
}
/>
<Main>
<Container>
{
Array.isArray(notes!)
&& (
<React.Fragment>
<PostPrimary>
<TitleInput
placeholder="Title"
value={title}
onChange={Note.updateTitle({
stateRef,
timeoutRef,
router,
id,
setNotes,
setTitle,
})}
/>
<PostMeta>
{
stateRef.current.updatedAt
&& router.query.id
&& (
<time
dateTime={new Date(stateRef.current.updatedAt).toISOString()}
>
Last updated {formatDate(new Date(stateRef.current.updatedAt))}
</time>
)
}
</PostMeta>
</PostPrimary>
<Editor
autoFocus={false}
key={id}
content={stateRef.current ? stateRef.current.content : undefined}
onChange={Note.updateContent({
stateRef,
timeoutRef,
router,
id,
setNotes,
})}
placeholder="Start typing here..."
/>
</React.Fragment>
)
}
</Container>
</Main>
</React.Fragment>
)
}

export const getServerSideProps = async ctx => {
if (ctx.params) {
return {
props: {
id: ctx.params?.id
}
}
}

return {
props: {}
}
}

export default Notes

+ 0
- 4
packages/app/src/pages/notes/[id].tsx View File

@@ -1,4 +0,0 @@
import Notes, { getServerSideProps } from '../notes'

export default Notes
export { getServerSideProps }

+ 0
- 13
packages/app/src/seeds.ts View File

@@ -1,13 +0,0 @@
import Operation from './services/Operation'

export default {
'operations': (
Object
.entries(Operation)
.filter(([, value]) => !isNaN(Number(value)))
.map(([name, id]) => ({
id: Number(id),
name,
}))
),
}

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

@@ -1,23 +0,0 @@
export default class LocalStorage<T> {
constructor(
private readonly source: Storage,
private readonly serializer: (t: T) => string,
private readonly deserializer: (s: string) => T
) {}

getItem(id: string, fallback: T = null) {
const raw = this.source.getItem(id)
if (raw === null) {
return fallback
}
return this.deserializer(raw)
}

setItem(id: string, item: T) {
this.source.setItem(id, this.serializer(item))
}

removeItem(id: string) {
this.source.removeItem(id)
}
}

+ 0
- 15
packages/app/src/services/Operation.ts View File

@@ -1,15 +0,0 @@
enum Operation {
'note:read' = 1,
'note:write' = 2,
'note:delete' = 3,
'note:import' = 4,
'note:export' = 5,
'folder:read' = 7,
'folder:write' = 8,
'social:publish' = 9,
'social:unpublish' = 10,
'social:share' = 11,
'userinfo:manage' = 12,
}

export default Operation

+ 0
- 110
packages/app/src/services/Storage.ts View File

@@ -1,110 +0,0 @@
import { addTime, TimeDivision } from '../utilities/Date'
import * as Serialization from '../utilities/Serialization'
import NoteModel from '../models/Note'
import * as ColumnTypes from '../utilities/ColumnTypes'
import LocalStorage from './LocalStorage'

type StorageParams = {
id: string,
url: string,
}

type Note = ColumnTypes.InferModel<typeof NoteModel>

type LoadItems = <T extends Record<string, unknown>>(params: StorageParams) => () => Promise<T[]>

const loadItems: LoadItems = <T extends Record<string, unknown>>(params) => async (): Promise<T[]> => {
const { id, url, } = params
const storage = new LocalStorage(
window.localStorage,
Serialization.serialize,
Serialization.deserialize
)
const localData = storage.getItem(id)
if (localData === null) {
const remoteItems = await window.fetch(url)
// TODO add custom serialization method
const theItems: T[] = await remoteItems.json()
storage.setItem(id, {
// TODO backend should set expiry
expiry: addTime(new Date(), 30)(TimeDivision.DAYS).getTime(),
items: theItems,
})
return theItems
}

const dataExpiry = new Date(Number(localData.expiry))
const now = new Date()
if (now.getTime() > dataExpiry.getTime()) {
storage.removeItem(id)
const loader: () => Promise<T[]> = loadItems(params)
return loader()
}
return localData.items
}

type SaveItem = <T extends Record<string, unknown>>(p: StorageParams) => (item: T) => Promise<T>

const saveItem: SaveItem = <T extends Record<string, unknown>>(params) => async (item) => {
const { id: storageId, url } = params
const storage = new LocalStorage(
window.localStorage,
Serialization.serialize,
Serialization.deserialize
)
const localData = storage.getItem(storageId, {
expiry: addTime(new Date(), 30)(TimeDivision.DAYS).getTime(),
items: [],
})
const { items: localItems } = localData
const { id: itemId, ...theBody } = item
const theItems: T[] = (
localItems.some(i => i.id === itemId)
? localItems.map(i => i.id === itemId ? item : i)
: [...localItems, item]
)
storage.setItem(storageId, { ...localData, items: theItems })
const response = await window.fetch(`${url}/${itemId}`, {
method: 'put',
body: JSON.stringify(theBody),
headers: {
'Content-Type': 'application/json',
},
})

const responseBody = await response.json()
if (response.status !== 201 && response.status !== 200) {
throw responseBody
}
return responseBody as T
}

type DeleteItem = <T extends Record<string, unknown>>(p: StorageParams) => (item: T) => Promise<boolean>

const deleteItem: DeleteItem = <T extends Record<string, unknown>>(params) => async (item) => {
const { id: storageId, url } = params
const storage = new LocalStorage(
window.localStorage,
Serialization.serialize,
Serialization.deserialize
)

const localData = storage.getItem(storageId, {
expiry: addTime(new Date(), 30)(TimeDivision.DAYS).getTime(),
items: [],
})
const { items: localItems } = localData
const { id: itemId } = item
const theItems: T[] = localItems.filter(i => i.id !== itemId)
storage.setItem(storageId, { ...localData, items: theItems })
const response = await window.fetch(`${url}/${itemId}`, {
method: 'delete',
})
return response.status === 204
}

export const loadNotes = loadItems<Note>({ id: 'notes', url: '/api/notes' })
export const loadFolders = loadItems({ id: 'folders', url: '/api/folders' })
export const saveNote = saveItem<Note>({ id: 'notes', url: '/api/notes' })
export const saveFolder = saveItem({ id: 'folders', url: '/api/folders' })
export const deleteNote = deleteItem<Note>({ id: 'notes', url: '/api/notes' })

+ 0
- 64
packages/app/src/services/entities/Folder.ts View File

@@ -1,64 +0,0 @@
import FolderModel from '../../models/Folder'
import * as ColumnTypes from '../../utilities/ColumnTypes'
import * as Response from '../../utilities/Response'

type Folder = ColumnTypes.InferModel<typeof FolderModel>

export const getSingle = repository => async (id: string) => {
const instance = await repository.findByPk(id)
if (instance === null) {
throw new Response.NotFound({ message: 'Not found.' })
}
return new Response.Retrieved({
data: instance.toJSON() as Folder,
})
}

export const getMultiple = repository => async (query?: Record<string, unknown>) => {
const fetchMethod = async query => {
if (query) {
return repository.findWhere({
attributes: query,
})
}
return repository.findAll()
}
const instances = await fetchMethod(query)
return new Response.Retrieved<Folder[]>({
data: instances.map(i => i.toJSON()),
})
}

export const save = repository => (body: Partial<Folder>) => async (id: string, idColumnName = 'id') => {
const [newInstance, created] = await repository.findOrCreate({
where: { [idColumnName]: id },
defaults: {
...body,
[idColumnName]: id,
},
})

if (created) {
return new Response.Created({
data: newInstance.toJSON() as Folder
})
}

Object.entries(body).forEach(([key, value]) => {
newInstance[key] = value
})
newInstance[idColumnName] = id
const updatedInstance = await newInstance.save()
return new Response.Saved({
data: updatedInstance.toJSON() as Folder
})
}

export const remove = repository => async (id: string) => {
const instanceDAO = repository.findByPk(id)
if (instanceDAO === null) {
throw new Response.NotFound({ message: 'Not found.' })
}
await instanceDAO.destroy()
return new Response.Destroyed()
}

+ 0
- 78
packages/app/src/services/entities/Note.ts View File

@@ -1,78 +0,0 @@
import Model from '../../models/Note'
import * as ColumnTypes from '../../utilities/ColumnTypes'
import * as Response from '../../utilities/Response'

type Note = ColumnTypes.InferModel<typeof Model>

export const getSingle = repository => async (id: string) => {
const instanceDAO = await repository.findByPk(id)
if (instanceDAO === null) {
throw new Response.NotFound({ message: 'Not found.' })
}
const instance = instanceDAO.toJSON()
return new Response.Retrieved({
data: {
...instance,
content: instance.content ? JSON.parse(instance.content) : null,
},
})
}

export const getMultiple = repository => async (query: Record<string, unknown>) => {
const instances = await repository.findAll()
return new Response.Retrieved({
data: instances.map(instanceDAO => {
const instance = instanceDAO.toJSON()
return {
...instance,
content: instance.content ? JSON.parse(instance.content) : null,
}
}),
})
}

export const save = repository => (body: Partial<Note>) => async (id: string, idColumnName = 'id') => {
const { content: contentRaw, ...etcBody } = body
const content = contentRaw! ? JSON.stringify(contentRaw) : null
const [dao, created] = await repository.findOrCreate({
where: { [idColumnName]: id },
defaults: {
...etcBody,
[idColumnName]: id,
content,
},
})

if (created) {
const newInstance = dao.toJSON()
return new Response.Created({
data: {
...newInstance,
content: newInstance.content ? JSON.parse(newInstance.content) : null,
},
})
}

Object.entries(body).forEach(([key, value]) => {
dao[key] = value
})
dao['content'] = content
dao[idColumnName] = id
const updatedDAO = await dao.save()
const updatedInstance = updatedDAO.toJSON()
return new Response.Saved({
data: {
...updatedInstance,
content: updatedInstance.content ? JSON.parse(updatedInstance.content) : null,
}
})
}

export const remove = repository => async (id: string) => {
const instanceDAO = await repository.findByPk(id)
if (instanceDAO === null) {
throw new Response.NotFound({ message: 'Not found.' })
}
await instanceDAO.destroy()
return new Response.Destroyed()
}

+ 0
- 60
packages/app/src/utilities/ColumnTypes.ts View File

@@ -1,60 +0,0 @@
import {
DataType,
INTEGER,
STRING,
TEXT,
DATE,
DATEONLY,
UUIDV4,
} from 'sequelize'

type ModelAttribute = {
allowNull?: boolean,
primaryKey?: boolean,
type: DataType,
}

export type Model = {
tableName?: string,
modelName?: string,
options?: {
timestamps?: boolean,
paranoid?: boolean,
createdAt?: string | boolean,
updatedAt?: string | boolean,
deletedAt?: string | boolean,
},
attributes: Record<string, ModelAttribute>,
}

type IntegerType = typeof INTEGER | ReturnType<typeof INTEGER>
type NumberType = IntegerType

type VarcharType = typeof STRING
type TextType = typeof TEXT | ReturnType<typeof TEXT>
type StringType = VarcharType | TextType

type DateTimeType = typeof DATE
type DateOnlyType = typeof DATEONLY
type DateType = DateTimeType | DateOnlyType

type InferType<V extends DataType> = (
V extends NumberType ? number :
V extends StringType ? string :
V extends DateType ? Date :
V extends typeof UUIDV4 ? string :
unknown
)

export type InferModel<M extends Model> = {
[K in keyof M['attributes']]-?: InferType<M['attributes'][K]['type']>
}

export {
INTEGER,
STRING,
TEXT,
DATEONLY,
DATE,
UUIDV4,
}

+ 0
- 98
packages/app/src/utilities/Date.ts View File

@@ -1,98 +0,0 @@
type FormatDate = (d: Date) => string

export const formatDate: FormatDate = d => {
const yearRaw = d.getFullYear()
const monthRaw = d.getMonth() + 1
const dateRaw = d.getDate()
const hourRaw = d.getHours()
const minuteRaw = d.getMinutes()
const year = yearRaw.toString().padStart(4, '0')
const month = monthRaw.toString().padStart(2, '0')
const date = dateRaw.toString().padStart(2, '0')
const hour = hourRaw.toString().padStart(2, '0')
const minute = minuteRaw.toString().padStart(2, '0')

return `${year}-${month}-${date} ${hour}:${minute}`
}

export enum TimeDivision {
MILLISECONDS,
SECONDS,
MINUTES,
HOURS,
DAYS
}

type GetTimeDifference = (a: Date, b: Date) => (c: TimeDivision) => number

export const getTimeDifference: GetTimeDifference = (a, b) => {
const ms = b.getTime() - a.getTime()
const absoluteMs = ms < 0 ? -ms : ms

return c => {
let divisionDifference = absoluteMs
if (c === TimeDivision.MILLISECONDS) {
return divisionDifference
}

divisionDifference /= 1000
if (c === TimeDivision.SECONDS) {
return divisionDifference
}

divisionDifference /= 60
if (c === TimeDivision.MINUTES) {
return divisionDifference
}

divisionDifference /= 60
if (c === TimeDivision.HOURS) {
return divisionDifference
}

divisionDifference /= 24
if (c === TimeDivision.DAYS) {
return divisionDifference
}

throw new Error('Unknown time division.')
}
}

type AddTime = (refDate: Date, increment: number) => (c: TimeDivision) => Date

export const addTime: AddTime = (refDate, increment) => {
const futureDate = new Date(refDate.getTime())
return c => {
let msIncrement = increment
if (c === TimeDivision.MILLISECONDS) {
futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement)
return futureDate
}

msIncrement *= 1000
if (c === TimeDivision.SECONDS) {
futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement)
return futureDate
}

msIncrement *= 60
if (c === TimeDivision.MINUTES) {
futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement)
return futureDate
}

msIncrement *= 60
if (c === TimeDivision.HOURS) {
futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement)
return futureDate
}

msIncrement *= 24
if (c === TimeDivision.DAYS) {
futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement)
return futureDate
}
throw new Error('Unknown time division.')
}
}

+ 0
- 5
packages/app/src/utilities/Id.ts View File

@@ -1,5 +0,0 @@
import { v4 } from 'uuid'

const generateId = () => v4()

export default generateId

+ 0
- 51
packages/app/src/utilities/ORM.ts View File

@@ -1,51 +0,0 @@
import { Sequelize, Dialect, } from 'sequelize'
import * as ColumnTypes from './ColumnTypes'

export enum DatabaseKind {
MSSQL = 'mssql',
SQLITE = 'sqlite',
MYSQL = 'mysql',
MARIADB = 'mariadb',
POSTGRESQL = 'postgres',
}

type RepositoryParams = {
url: string,
kind: DatabaseKind,
}

export default class ORM {
private readonly instance: Sequelize

constructor(params: RepositoryParams) {
const { kind, url, } = params
switch (kind as DatabaseKind) {
case DatabaseKind.SQLITE:
this.instance = new Sequelize({
dialect: kind as string as Dialect,
storage: url,
})
return
case DatabaseKind.POSTGRESQL:
case DatabaseKind.MARIADB:
case DatabaseKind.MSSQL:
case DatabaseKind.MYSQL:
this.instance = new Sequelize({
dialect: kind as string as Dialect,
host: url,
})
return
// TODO add nosql dbs
default:
break
}
throw new Error(`Database kind "${kind as string}" not yet supported.`)
}

getRepository(model: ColumnTypes.Model) {
if (this.instance instanceof Sequelize) {
return this.instance.define(model.modelName, model.attributes, model.options)
}
throw new Error('Interface not yet initialized.')
}
}

+ 0
- 49
packages/app/src/utilities/Response.ts View File

@@ -1,49 +0,0 @@
interface Response<T extends {} = {}> {
status: number,
data?: T,
}

interface ErrorResponse extends Response {
message: string,
}

type ResponseParams<T extends {} = {}> = {
message?: string,
data?: T,
}

export class NotFound implements ErrorResponse {
public readonly status = 404
public readonly message: string
constructor(params: ResponseParams) {
this.message = params.message
}
}

export class Created<T extends {}> implements Response {
public readonly status = 201
public readonly data: T
constructor(params: ResponseParams<T>) {
this.data = params.data
}
}

export class Saved<T extends {}> implements Response {
public readonly status = 200
public readonly data: T
constructor(params: ResponseParams<T>) {
this.data = params.data
}
}

export class Retrieved<T extends {}> implements Response {
public readonly status = 200
public readonly data: T
constructor(params: ResponseParams<T>) {
this.data = params.data
}
}

export class Destroyed implements Response {
public readonly status = 204
}

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

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

export const deserialize = JSON.parse

+ 0
- 35
packages/app/tsconfig.json View File

@@ -1,35 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"declaration": true,
"declarationDir": "./dist",
"sourceMap": true,
"strict": false
},
"exclude": [
"node_modules",
"**/*.test.ts",
"**/*.test.tsx"
],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
]
}

+ 0
- 6112
packages/app/yarn.lock
File diff suppressed because it is too large
View File


+ 0
- 9
packages/app/zeichen.config.ts View File

@@ -1,9 +0,0 @@
import LocalStorage from '../plugin-local-storage/src'
import RemoteStorage from '../plugin-remote-storage/src'

export default {
plugins: [
LocalStorage,
RemoteStorage({ baseUrl: 'https://localhost:3000/api', }),
]
}

+ 1
- 0
packages/core

@@ -0,0 +1 @@
Subproject commit f1019fe92d89b6285be4e56543e53b9dd908ddb6

+ 0
- 2
packages/core/.npmignore View File

@@ -1,2 +0,0 @@
*.ts
tsconfig.json

+ 0
- 3
packages/core/README.md View File

@@ -1,3 +0,0 @@
# Zeichen - Core

Library for authoring Zeichen plugins and themes.

+ 0
- 11
packages/core/package.json View File

@@ -1,11 +0,0 @@
{
"name": "zeichen-core",
"description": "Library for authoring Zeichen plugins and themes.",
"version": "0.1.0",
"devDependencies": {
"typescript": "^4.0.3"
},
"scripts": {
"build": "tsc **/*.ts"
}
}

+ 0
- 5
packages/core/src/plugin.ts View File

@@ -1,5 +0,0 @@
export type State = {
currentUserId: string,
}

export type Plugin<T = {}> = (config: T) => (state: State) => void

+ 0
- 16
packages/core/src/storage.ts View File

@@ -1,16 +0,0 @@
export default interface Storage<T> {
queryItems(): Promise<T[]>
saveItem(item: T): Promise<void>
deleteItem(item: T): Promise<boolean>
}

export interface Collection<T> {
items: T[],
lastModifiedBy: string,
lastModifiedAt: Date,
}

export type Serializer<T = unknown> = (t: T) => string
export type Deserializer<T = unknown> = (s: string) => T

export class OutOfSyncError {}

+ 0
- 28
packages/core/tsconfig.json View File

@@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"declaration": true,
"declarationDir": "./dist",
"emitDeclarationOnly": true,
"sourceMap": true,
"strict": false
},
"exclude": [
"node_modules",
"**/*.test.ts"
],
"include": [
"**/*.ts"
]
}

+ 1
- 0
packages/plugin-local-storage

@@ -0,0 +1 @@
Subproject commit 2fb6af99ba99525cf9ed62849992b4495381a7b5

+ 0
- 2
packages/plugin-local-storage/.npmignore View File

@@ -1,2 +0,0 @@
*.ts
tsconfig.json

+ 0
- 3
packages/plugin-local-storage/README.md View File

@@ -1,3 +0,0 @@
# Zeichen - Local Storage Plugin

Save and load notes in Zeichen using the browser's local storage.

+ 0
- 18
packages/plugin-local-storage/package.json View File

@@ -1,18 +0,0 @@
{
"name": "zeichen-plugin-local-storage",
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"version": "0.1.0",
"description": "Save and load notes in Zeichen using the browser's local storage.",
"keywords": [
"zeichen",
"plugin",
"storage",
"local"
],
"devDependencies": {
"typescript": "^4.0.3"
},
"scripts": {
"build": "tsc **/*.ts"
}
}

+ 0
- 23
packages/plugin-local-storage/src/Engine.ts View File

@@ -1,23 +0,0 @@
export default class LocalStorage<T> {
constructor(
private readonly source: Storage,
private readonly serializer: (t: T) => string = JSON.stringify,
private readonly deserializer: (s: string) => T = JSON.parse,
) {}

getCollection(collectionId: string) {
const raw = this.source.getItem(collectionId)
if (raw !== null) {
return this.deserializer(raw)
}
return null
}

replaceCollection(collectionId: string, collectionData: T) {
this.source.setItem(collectionId, this.serializer(collectionData))
}

removeCollection(collectionId: string) {
this.source.removeItem(collectionId)
}
}

+ 0
- 55
packages/plugin-local-storage/src/Storage.ts View File

@@ -1,55 +0,0 @@
import Storage, { Collection, OutOfSyncError } from '../../core/src/storage'
import Engine from './Engine'

export default class LocalStorage<T> implements Storage<T> {
private readonly engine: Engine<Collection<T>>

constructor(
private readonly ownerId: string,
private readonly storageId: string,
private readonly getItemId = item => item['id'],
) {
this.engine = new Engine<Collection<T>>(window.localStorage)
}

private getMeta() {
const oldMeta = this.engine.getCollection(this.storageId)
if (oldMeta === null) {
throw new OutOfSyncError()
}
return oldMeta
}

private existenceCheck(newItem: T) {
return oldItem => this.getItemId(oldItem) === this.getItemId(newItem)
}

async queryItems() {
return this.getMeta().items
}

async saveItem(newItem: T) {
const oldMeta = this.getMeta()
const isExistingItem = oldMeta.items.some(this.existenceCheck(newItem))
const newMeta: Collection<T> = {
items: isExistingItem
? oldMeta.items.map(oldItem => this.existenceCheck(newItem)(oldItem) ? newItem : oldItem)
: [...oldMeta.items, newItem],
lastModifiedBy: this.ownerId,
lastModifiedAt: new Date(),
}
this.engine.replaceCollection(this.storageId, newMeta)
}

async deleteItem(newItem: T) {
const oldMeta = this.getMeta()
const newItems = oldMeta.items.filter(oldItem => !this.existenceCheck(newItem)(oldItem))
const newMeta: Collection<T> = {
items: newItems,
lastModifiedBy: this.ownerId,
lastModifiedAt: new Date(),
}
this.engine.replaceCollection(this.storageId, newMeta)
return oldMeta.items.length !== newItems.length
}
}

+ 0
- 13
packages/plugin-local-storage/src/index.ts View File

@@ -1,13 +0,0 @@
import { Plugin } from '../../core/src/plugin'
import Storage from './Storage'

type PluginConfig = {}

const LocalStoragePlugin: Plugin<PluginConfig> = config => ({
currentUserId,
}) => {
new Storage(currentUserId, 'notes')
new Storage(currentUserId, 'folders')
}

export default LocalStoragePlugin

+ 0
- 27
packages/plugin-local-storage/tsconfig.json View File

@@ -1,27 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"declaration": true,
"declarationDir": "./dist",
"sourceMap": true,
"strict": false
},
"exclude": [
"node_modules",
"**/*.test.ts"
],
"include": [
"**/*.ts"
]
}

+ 1
- 0
packages/plugin-remote-storage

@@ -0,0 +1 @@
Subproject commit 6983443f63334697fa640693f8be724c4d5a61b1

+ 0
- 2
packages/plugin-remote-storage/.npmignore View File

@@ -1,2 +0,0 @@
*.ts
tsconfig.json

+ 0
- 3
packages/plugin-remote-storage/README.md View File

@@ -1,3 +0,0 @@
# Zeichen - Remote Storage Plugin

Save and load notes in Zeichen using an external API.

+ 0
- 18
packages/plugin-remote-storage/package.json View File

@@ -1,18 +0,0 @@
{
"name": "zeichen-plugin-remote-storage",
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"version": "0.1.0",
"description": "Save and load notes in Zeichen using an external API.",
"keywords": [
"zeichen",
"plugin",
"storage",
"API"
],
"devDependencies": {
"typescript": "^4.0.3"
},
"scripts": {
"build": "tsc **/*.ts"
}
}

+ 0
- 70
packages/plugin-remote-storage/src/Engine.ts View File

@@ -1,70 +0,0 @@
import { Deserializer, Serializer } from '../../core/src/storage'

const CREATED = 201
const NO_CONTENT = 204
const NOT_FOUND = 404
const GONE = 410

export default class RemoteStorage<U, T> {
constructor(
private readonly baseUrl: string,
private readonly getItemId = item => item['id'],
private readonly serializers: Map<string, Serializer> = new Map([
['*/*', JSON.stringify],
['application/json', JSON.stringify],
['text/json', JSON.stringify],
]),
private readonly deserializers: Map<string, Deserializer> = new Map([
['*/*', JSON.parse],
['application/json', JSON.parse],
['text/json', JSON.parse],
]),
) {}

async getCollection(collectionId: string) {
const response = await window.fetch([this.baseUrl, collectionId].join('/'))
const contentType = response.headers.get('content-type')
const { [contentType]: deserializer = this.deserializers.get('*/*'), } = Object.fromEntries(this.deserializers.entries())
const payload = await response.text()
return deserializer(payload)
}

async setItem(collectionId: string, item: U, contentType = 'application/json') {
const { [contentType]: serializer = this.serializers.get('application/json'), } = Object.fromEntries(this.serializers.entries())
const response = await window.fetch([this.baseUrl, collectionId, this.getItemId(item)].join('/'), {
method: 'put',
body: serializer(item),
})
// resource is created
if (response.status === CREATED) {
return
}
if (100 <= response.status && response.status <= 399) {
console.warn(`Expected response is ${CREATED}, got ${response.status}.`)
return
}
throw new Error(response.statusText)
}

async removeItem(collectionId: string, item: U) {
const response = await window.fetch([this.baseUrl, collectionId, this.getItemId(item)].join('/'), {
method: 'delete',
})
// resource is deleted
if (response.status === NO_CONTENT) {
return true
}
// resource is already deleted
if (response.status === NOT_FOUND || response.status === GONE) {
return false
}
if (100 <= response.status && response.status <= 399) {
console.warn(`Expected response is ${NO_CONTENT}, got ${response.status}.`)
// assume there's a change in the collection
return true
}
throw new Error(response.statusText)
}

// TODO do removeCollection for account closing?
}

+ 0
- 27
packages/plugin-remote-storage/src/Storage.ts View File

@@ -1,27 +0,0 @@
import Storage, { Collection } from '../../core/src/storage'
import Engine from './Engine'

export default class RemoteStorage<T> implements Storage<T> {
private readonly engine: Engine<T, Collection<T>>

constructor(
private ownerId: string,
private baseUrl: string,
private collectionId: string,
) {
this.engine = new Engine(baseUrl)
}

async deleteItem(item: T) {
return this.engine.removeItem(this.collectionId, item)
}

async queryItems() {
const response = await this.engine.getCollection(this.collectionId) as Collection<T>;
return response.items;
}

async saveItem(newItem: T) {
return this.engine.setItem(this.collectionId, newItem)
}
}

+ 0
- 15
packages/plugin-remote-storage/src/index.ts View File

@@ -1,15 +0,0 @@
import { Plugin } from '../../core/src/plugin'
import Storage from './Storage'

type PluginConfig = {
baseUrl: string,
}

const RemoteStoragePlugin: Plugin<PluginConfig> = config => ({
currentUserId,
}) => {
new Storage(currentUserId, config.baseUrl, 'notes')
new Storage(currentUserId, config.baseUrl, 'folders')
}

export default RemoteStoragePlugin

+ 0
- 27
packages/plugin-remote-storage/tsconfig.json View File

@@ -1,27 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"declaration": true,
"declarationDir": "./dist",
"sourceMap": true,
"strict": false
},
"exclude": [
"node_modules",
"**/*.test.ts"
],
"include": [
"**/*.ts"
]
}

Loading…
Cancel
Save