@@ -0,0 +1,104 @@ | |||
/node_modules | |||
/.pnp | |||
.pnp.js | |||
/coverage | |||
/.next/ | |||
/out/ | |||
/build | |||
.DS_Store | |||
*.pem | |||
npm-debug.log* | |||
yarn-debug.log* | |||
yarn-error.log* | |||
.env.local | |||
.env.development.local | |||
.env.test.local | |||
.env.production.local | |||
.env | |||
.vercel | |||
.idea/ | |||
logs | |||
*.log | |||
lerna-debug.log* | |||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |||
pids | |||
*.pid | |||
*.seed | |||
*.pid.lock | |||
lib-cov | |||
coverage | |||
*.lcov | |||
.nyc_output | |||
.grunt | |||
bower_components | |||
.lock-wscript | |||
build/Release | |||
node_modules/ | |||
jspm_packages/ | |||
web_modules/ | |||
*.tsbuildinfo | |||
.npm | |||
.eslintcache | |||
.rpt2_cache/ | |||
.rts2_cache_cjs/ | |||
.rts2_cache_es/ | |||
.rts2_cache_umd/ | |||
.node_repl_history | |||
*.tgz | |||
.yarn-integrity | |||
.env.test | |||
.cache | |||
.parcel-cache | |||
.next | |||
out | |||
.nuxt | |||
dist | |||
.cache/ | |||
.vuepress/dist | |||
.serverless/ | |||
.fusebox/ | |||
.dynamodb/ | |||
.tern-port | |||
.vscode-test | |||
.yarn/cache | |||
.yarn/unplugged | |||
.yarn/build-state.yml | |||
.yarn/install-state.gz | |||
.pnp.* | |||
Thumbs.db | |||
Thumbs.db:encryptable | |||
ehthumbs.db | |||
ehthumbs_vista.db | |||
*.stackdump | |||
[Dd]esktop.ini | |||
$RECYCLE.BIN/ | |||
*.cab | |||
*.msi | |||
*.msix | |||
*.msm | |||
*.msp | |||
*.lnk | |||
cmake-build-*/ | |||
*.iws | |||
out/ | |||
.idea_modules/ | |||
atlassian-ide-plugin.xml | |||
com_crashlytics_export_strings.xml | |||
crashlytics.properties | |||
crashlytics-build.properties | |||
fabric.properties | |||
.AppleDouble | |||
.LSOverride | |||
._* | |||
.DocumentRevisions-V100 | |||
.fseventsd | |||
.Spotlight-V100 | |||
.TemporaryItems | |||
.Trashes | |||
.VolumeIcon.icns | |||
.com.apple.timemachine.donotpresent | |||
.AppleDB | |||
.AppleDesktop | |||
Network Trash Folder | |||
Temporary Items | |||
.apdisk |
@@ -0,0 +1,34 @@ | |||
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.ts`. The page auto-updates as you edit the file. | |||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. | |||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. | |||
## 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/new?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,0 +1,4 @@ | |||
module.exports = { | |||
preset: 'ts-jest', | |||
testEnvironment: 'node', | |||
}; |
@@ -0,0 +1,2 @@ | |||
/// <reference types="next" /> | |||
/// <reference types="next/types/global" /> |
@@ -0,0 +1,3 @@ | |||
module.exports = { | |||
basePath: '', | |||
} |
@@ -0,0 +1,31 @@ | |||
{ | |||
"name": "@zeichen/app-web", | |||
"version": "0.1.0", | |||
"private": true, | |||
"scripts": { | |||
"dev": "next dev", | |||
"build": "next build", | |||
"start": "next start", | |||
"test": "jest" | |||
}, | |||
"homepage": "https://note.modal.sh", | |||
"dependencies": { | |||
"@tesseract-design/react-common": "^0.3.0", | |||
"@tesseract-design/viewfinder": "0.1.1", | |||
"mobiledoc-kit": "^0.13.2", | |||
"next": "10.2.0", | |||
"react": "17.0.2", | |||
"react-dom": "17.0.2", | |||
"react-mobiledoc-editor": "^0.11.1", | |||
"styled-components": "5.2.3" | |||
}, | |||
"devDependencies": { | |||
"@types/jest": "^26.0.23", | |||
"@types/node": "^15.0.1", | |||
"@types/react": "^17.0.4", | |||
"@types/styled-components": "^5.1.9", | |||
"jest": "^26.6.3", | |||
"ts-jest": "^26.5.5", | |||
"typescript": "^4.2.4" | |||
} | |||
} |
@@ -0,0 +1,77 @@ | |||
body { | |||
margin: 0; | |||
/* overflow: overlay; */ | |||
} | |||
h1 { | |||
font-size: 3em; | |||
text-transform: lowercase; | |||
} | |||
h2 { | |||
font-size: 2em; | |||
text-transform: lowercase; | |||
} | |||
h3 { | |||
font-size: 1.75em; | |||
text-transform: lowercase; | |||
} | |||
h4 { | |||
font-size: 1.5em; | |||
text-transform: lowercase; | |||
} | |||
h5 { | |||
font-size: 1.25em; | |||
text-transform: lowercase; | |||
} | |||
h6 { | |||
font-size: 1.125em; | |||
text-transform: lowercase; | |||
} | |||
p { | |||
margin: 2em 0; | |||
} | |||
li { | |||
margin: 2em 0; | |||
} | |||
small { | |||
font-size: 0.75em; | |||
} | |||
a:focus { | |||
outline: 0; | |||
} | |||
pre { | |||
overflow: auto; | |||
margin: 0 -1rem; | |||
padding: 0 1rem; | |||
line-height: 1.2; | |||
box-sizing: border-box; | |||
} | |||
@media only print { | |||
pre, pre * { | |||
color: inherit !important; | |||
} | |||
code, code * { | |||
color: inherit !important; | |||
} | |||
img { | |||
filter: grayscale(100%); | |||
} | |||
:root { | |||
--color-accent: black !important; | |||
--color-active: black !important; | |||
} | |||
} |
@@ -0,0 +1,166 @@ | |||
@font-face { | |||
font-family: 'mononoki'; | |||
font-weight: 400; | |||
font-style: normal; | |||
font-display: swap; | |||
src: | |||
local('mononoki'), | |||
url(fonts/mononoki/mononoki-Regular.woff2) format('woff2'); | |||
} | |||
@font-face { | |||
font-family: 'mononoki'; | |||
font-weight: 700; | |||
font-style: normal; | |||
font-display: swap; | |||
src: | |||
local('mononoki Bold'), | |||
local('mononoki'), | |||
url(fonts/mononoki/mononoki-Bold.woff2) format('woff2'); | |||
} | |||
@font-face { | |||
font-family: 'mononoki'; | |||
font-weight: 400; | |||
font-style: italic; | |||
font-display: swap; | |||
src: | |||
local('mononoki Italic'), | |||
local('mononoki'), | |||
url(fonts/mononoki/mononoki-Italic.woff2) format('woff2'); | |||
} | |||
@font-face { | |||
font-family: 'mononoki'; | |||
font-weight: 700; | |||
font-style: italic; | |||
font-display: swap; | |||
src: | |||
local('mononoki Bold Italic'), | |||
local('mononoki BoldItalic'), | |||
local('mononoki'), | |||
url(fonts/mononoki/mononoki-BoldItalic.woff2) format('woff2'); | |||
} | |||
:root { | |||
--font-family-base: 'Encode Sans', system-ui; | |||
--font-stretch-base: semi-expanded; | |||
--font-weight-base: 400; | |||
--line-height-base: 1.75em; | |||
--font-family-headings: 'Encode Sans', system-ui; | |||
--font-stretch-headings: condensed; | |||
--font-weight-headings: 100; | |||
--line-height-headings: 1.125em; | |||
--font-family-monospace: 'mononoki'; | |||
--font-size-root: 16px; | |||
--opacity-light: 0.25; | |||
--opacity-lighter: 0.5; | |||
--opacity-lightest: 0.75; | |||
} | |||
:root { | |||
--color-bg: var(--color-negative, white); | |||
--color-fg: var(--color-positive, black); | |||
--color-accent: var(--color-primary, blue); | |||
--color-active: var(--color-secondary, red); | |||
} | |||
@media (prefers-color-scheme: dark) { | |||
:root { | |||
--color-bg: var(--color-negative, black); | |||
--color-fg: var(--color-positive, white); | |||
} | |||
} | |||
:root { | |||
font-size: var(--font-size-root); | |||
font-family: var(--font-family-base), sans-serif; | |||
font-stretch: var(--font-stretch-base, normal); | |||
font-weight: var(--font-weight-base, 400); | |||
line-height: var(--line-height-base, 1.75em); | |||
transition-property: color, background-color; | |||
transition-timing-function: ease; | |||
transition-duration: 350ms; | |||
} | |||
@media only screen { | |||
:root { | |||
background-color: var(--color-bg); | |||
color: var(--color-fg); | |||
} | |||
} | |||
h1 { | |||
font-family: var(--font-family-headings), sans-serif; | |||
font-stretch: var(--font-stretch-headings, normal); | |||
font-weight: var(--font-weight-headings, 400); | |||
line-height: var(--line-height-headings, 1.5); | |||
} | |||
h2 { | |||
font-family: var(--font-family-headings), sans-serif; | |||
font-stretch: var(--font-stretch-headings, normal); | |||
font-weight: calc(var(--font-weight-headings, 400) / 100 * 150); | |||
line-height: var(--line-height-headings, 1.5); | |||
} | |||
h3 { | |||
font-family: var(--font-family-headings), sans-serif; | |||
font-stretch: var(--font-stretch-headings, normal); | |||
font-weight: calc(var(--font-weight-headings, 400) / 100 * 250); | |||
line-height: var(--line-height-headings, 1.5); | |||
} | |||
h4 { | |||
font-family: var(--font-family-headings), sans-serif; | |||
font-stretch: var(--font-stretch-headings, normal); | |||
font-weight: var(--font-weight-headings, 400); | |||
line-height: var(--line-height-headings, 1.5); | |||
} | |||
h5 { | |||
font-family: var(--font-family-headings), sans-serif; | |||
font-stretch: var(--font-stretch-headings, normal); | |||
font-weight: var(--font-weight-headings, 400); | |||
line-height: var(--line-height-headings, 1.5); | |||
} | |||
h6 { | |||
font-family: var(--font-family-headings), sans-serif; | |||
font-stretch: var(--font-stretch-headings, normal); | |||
font-weight: var(--font-weight-headings, 400); | |||
line-height: var(--line-height-headings, 1.5); | |||
} | |||
code { | |||
font-family: var(--font-family-monospace), monospace; | |||
} | |||
pre { | |||
font-family: var(--font-family-monospace), monospace; | |||
} | |||
a { | |||
color: inherit; | |||
text-decoration: none; | |||
} | |||
@media only screen { | |||
a { | |||
color: var(--color-accent); | |||
text-decoration: underline; | |||
} | |||
a:focus { | |||
color: var(--color-active); | |||
} | |||
} | |||
::selection { | |||
background-color: var(--color-active); | |||
color: var(--color-fg); | |||
} | |||
:root { | |||
caret-color: var(--color-active); | |||
} |
@@ -0,0 +1,19 @@ | |||
:root { | |||
--color-shade: #000; | |||
--color-negative: #222; | |||
--color-positive: #eee; | |||
--color-primary: #C78AB3; | |||
--color-secondary: #f90; | |||
--color-code-number: #74f95e; | |||
--color-code-keyword: #ff4389; | |||
--color-code-type: #5097D2; | |||
--color-code-instance-attribute: #76a7d2; | |||
--color-code-function: #67c252; | |||
--color-code-parameter: #915ec2; | |||
--color-code-property: #ffa1c9; | |||
--color-code-string: #eed371; | |||
--color-code-variable: #8bc275; | |||
--color-code-regexp: #74A72B; | |||
--color-code-url: #0099CC; | |||
--color-code-global: #C28050; | |||
} |
@@ -0,0 +1,19 @@ | |||
:root { | |||
--color-shade: #fff; | |||
--color-negative: #f8f8f8; | |||
--color-positive: #333; | |||
--color-primary: #ba6a9c; | |||
--color-secondary: #f90; | |||
--color-code-number: #72b507; | |||
--color-code-keyword: #ee5189; | |||
--color-code-type: #427fb1; | |||
--color-code-instance-attribute: #76a7d2; | |||
--color-code-function: #5a984a; | |||
--color-code-parameter: #915ec2; | |||
--color-code-property: #b76e8d; | |||
--color-code-string: #b59e36; | |||
--color-code-variable: #61864e; | |||
--color-code-regexp: #4f7e03; | |||
--color-code-url: #0099CC; | |||
--color-code-global: #C28050; | |||
} |
@@ -0,0 +1,4 @@ | |||
<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,0 +1,41 @@ | |||
import Link from '../Link' | |||
import styled from 'styled-components' | |||
const BrandBase = styled(Link)({ | |||
display: 'block', | |||
textDecoration: 'none', | |||
fontSize: '1.5rem', | |||
fontWeight: 'bold', | |||
fontStretch: '75%', | |||
textTransform: 'uppercase', | |||
width: '2rem', | |||
textAlign: 'center', | |||
'@media (min-width: 720px)': { | |||
width: '8rem', | |||
textAlign: 'left', | |||
}, | |||
}) | |||
const Hide = styled('span')({ | |||
display: 'none', | |||
'@media (min-width: 720px)': { | |||
display: 'inline', | |||
}, | |||
}) | |||
const Brand = () => { | |||
return ( | |||
<BrandBase | |||
href={{ | |||
pathname: '/', | |||
}} | |||
> | |||
Z | |||
<Hide> | |||
eichen | |||
</Hide> | |||
</BrandBase> | |||
) | |||
} | |||
export default Brand |
@@ -0,0 +1,90 @@ | |||
import * as React from 'react' | |||
import * as T from '@tesseract-design/react-common' | |||
import {Container, Editor as MobiledocEditor, MarkupButton, LinkButton} from 'react-mobiledoc-editor' | |||
import styled, {CSSObject} from 'styled-components' | |||
const TOOLBAR_BUTTON_COMMON_STYLES: CSSObject = { | |||
border: 0, | |||
backgroundColor: 'transparent', | |||
padding: 0, | |||
margin: 0, | |||
height: '3rem', | |||
width: '3rem', | |||
color: 'inherit', | |||
font: 'inherit', | |||
} | |||
const StyledMarkupButton = styled(MarkupButton)(TOOLBAR_BUTTON_COMMON_STYLES) | |||
const StyledLinkButton = styled(LinkButton)(TOOLBAR_BUTTON_COMMON_STYLES) | |||
const RawInput = styled('textarea')({ | |||
color: 'inherit', | |||
font: 'inherit', | |||
padding: 0, | |||
border: 0, | |||
backgroundColor: 'transparent', | |||
outline: 0, | |||
display: 'block', | |||
width: '100%', | |||
resize: 'vertical', | |||
}) | |||
const ToolbarBase = styled('div')({ | |||
display: 'flex', | |||
alignItems: 'center', | |||
flexWrap: 'wrap', | |||
margin: '1rem 0', | |||
}) | |||
const StyledEditor = styled('div')({ | |||
'> *': { | |||
outline: 0, | |||
}, | |||
}) | |||
const Editor = () => { | |||
const [hydrated, setHydrated] = React.useState(false) | |||
React.useEffect(() => { | |||
setHydrated(true) | |||
}, []) | |||
if (hydrated) { | |||
return ( | |||
<Container> | |||
<ToolbarBase> | |||
<StyledMarkupButton | |||
tag="strong" | |||
> | |||
<T.Icon | |||
name="bold" | |||
/> | |||
</StyledMarkupButton> | |||
<StyledMarkupButton | |||
tag="em" | |||
> | |||
<T.Icon | |||
name="italic" | |||
/> | |||
</StyledMarkupButton> | |||
<StyledLinkButton> | |||
<T.Icon | |||
name="link" | |||
/> | |||
</StyledLinkButton> | |||
</ToolbarBase> | |||
<StyledEditor> | |||
<MobiledocEditor /> | |||
</StyledEditor> | |||
</Container> | |||
) | |||
} | |||
return ( | |||
<RawInput | |||
rows={20} | |||
/> | |||
) | |||
} | |||
export default Editor |
@@ -0,0 +1,68 @@ | |||
import * as React from 'react' | |||
import * as T from '@tesseract-design/react-common' | |||
import styled from 'styled-components' | |||
const Base = styled('div')({ | |||
display: 'flex', | |||
alignItems: 'center', | |||
height: '4rem', | |||
}) | |||
const Content = styled('div')({ | |||
lineHeight: 1.5, | |||
flex: 'auto', | |||
}) | |||
const Title = styled('strong')({ | |||
display: 'block', | |||
whiteSpace: 'nowrap', | |||
}) | |||
const Subtitle = styled('small')({ | |||
display: 'block', | |||
whiteSpace: 'nowrap', | |||
}) | |||
const IconContainer = styled('div')({ | |||
marginRight: '1rem', | |||
}) | |||
type Props = { | |||
title: string, | |||
subtitle?: string, | |||
} | |||
const FolderLinkContent: React.FC<Props> = ({ | |||
title, | |||
subtitle, | |||
}) => { | |||
return ( | |||
<Base> | |||
<IconContainer> | |||
<T.Icon | |||
name="folder" | |||
/> | |||
</IconContainer> | |||
<Content> | |||
<Title> | |||
{title} | |||
</Title> | |||
{ | |||
typeof (subtitle as string) | |||
&& ( | |||
<Subtitle> | |||
{subtitle} | |||
</Subtitle> | |||
) | |||
} | |||
</Content> | |||
<div> | |||
<T.Icon | |||
name="chevron-right" | |||
/> | |||
</div> | |||
</Base> | |||
) | |||
} | |||
export default FolderLinkContent |
@@ -0,0 +1,39 @@ | |||
import * as React from 'react' | |||
import NextLink from 'next/link' | |||
import {UrlObject} from 'url' | |||
type Props = { | |||
href: UrlObject, | |||
as?: UrlObject, | |||
prefetch?: boolean, | |||
replace?: boolean, | |||
shallow?: boolean, | |||
component?: React.ElementType, | |||
} | |||
const Link: React.FC<Props> = ({ | |||
href, | |||
as, | |||
prefetch, | |||
replace, | |||
shallow, | |||
component: Component = 'a', | |||
...etcProps | |||
}) => { | |||
return ( | |||
<NextLink | |||
href={href} | |||
as={as} | |||
passHref | |||
replace={replace} | |||
shallow={shallow} | |||
prefetch={prefetch} | |||
> | |||
<Component | |||
{...etcProps} | |||
/> | |||
</NextLink> | |||
) | |||
} | |||
export default Link |
@@ -0,0 +1,63 @@ | |||
import * as React from 'react' | |||
import * as T from '@tesseract-design/react-common' | |||
import styled from 'styled-components' | |||
const Base = styled('div')({ | |||
display: 'flex', | |||
alignItems: 'center', | |||
height: '4rem', | |||
}) | |||
const Content = styled('div')({ | |||
lineHeight: 1.5, | |||
flex: 'auto', | |||
}) | |||
const Title = styled('strong')({ | |||
display: 'block', | |||
whiteSpace: 'nowrap', | |||
}) | |||
const Subtitle = styled('small')({ | |||
display: 'block', | |||
whiteSpace: 'nowrap', | |||
}) | |||
const IconContainer = styled('div')({ | |||
marginRight: '1rem', | |||
}) | |||
type Props = { | |||
title: string, | |||
subtitle?: string, | |||
} | |||
const NoteLinkContent: React.FC<Props> = ({ | |||
title, | |||
subtitle, | |||
}) => { | |||
return ( | |||
<Base> | |||
<IconContainer> | |||
<T.Icon | |||
name="file-text" | |||
/> | |||
</IconContainer> | |||
<Content> | |||
<Title> | |||
{title} | |||
</Title> | |||
{ | |||
typeof (subtitle as string) | |||
&& ( | |||
<Subtitle> | |||
{subtitle} | |||
</Subtitle> | |||
) | |||
} | |||
</Content> | |||
</Base> | |||
) | |||
} | |||
export default NoteLinkContent |
@@ -0,0 +1,83 @@ | |||
import * as React from 'react' | |||
import {LeftSidebarWithMenu} from '@tesseract-design/viewfinder' | |||
import Link from '../../molecules/Link' | |||
import {Subpage} from '../../../utils/query' | |||
type Props = { | |||
subpage: Subpage, | |||
} | |||
const BinView: React.FC<Props> = ({ | |||
subpage | |||
}) => { | |||
return ( | |||
<LeftSidebarWithMenu.Layout | |||
brand={"Brand"} | |||
sidebarMenuItems={[ | |||
{ | |||
id: 'notes', | |||
url: { | |||
pathname: '/my/notes', | |||
}, | |||
label: 'Notes', | |||
icon: 'N', | |||
}, | |||
{ | |||
id: 'bin', | |||
url: { | |||
pathname: '/my/bin' | |||
}, | |||
icon: 'B', | |||
label: 'Bin', | |||
secondary: true, | |||
}, | |||
]} | |||
sidebarMainOpen={subpage === Subpage.SIDEBAR} | |||
moreItemsOpen={subpage === Subpage.MORE} | |||
sidebarMain={ | |||
<LeftSidebarWithMenu.SidebarMainContainer> | |||
Sidebar | |||
</LeftSidebarWithMenu.SidebarMainContainer> | |||
} | |||
moreLinkMenuItem={{ | |||
label: 'More', | |||
icon: 'M', | |||
url: { | |||
query: { | |||
subpage: 'more', | |||
} | |||
} | |||
}} | |||
moreLinkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
<LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
</Link> | |||
)} | |||
linkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.SidebarMenuContainer> | |||
<LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.SidebarMenuContainer> | |||
</Link> | |||
)} | |||
> | |||
<LeftSidebarWithMenu.ContentContainer> | |||
Hello | |||
</LeftSidebarWithMenu.ContentContainer> | |||
</LeftSidebarWithMenu.Layout> | |||
) | |||
} | |||
export default BinView |
@@ -0,0 +1,232 @@ | |||
import * as React from 'react' | |||
import Head from 'next/head' | |||
import * as T from '@tesseract-design/react-common' | |||
import {LeftSidebarWithMenu} from '@tesseract-design/viewfinder' | |||
import styled from 'styled-components' | |||
import Link from '../../molecules/Link' | |||
import {Subpage} from '../../../utils/query' | |||
import Note from '../../../models/Note' | |||
import Brand from '../../molecules/Brand' | |||
import NoteLinkContent from '../../molecules/NoteLinkContent' | |||
import Folder from '../../../models/Folder' | |||
import FolderLinkContent from '../../molecules/FolderLinkContent' | |||
import Editor from '../../molecules/Editor' | |||
const SidebarLink = styled(Link)({ | |||
textDecoration: 'none', | |||
}) | |||
const CenteredContent = styled(LeftSidebarWithMenu.ContentContainer)({ | |||
height: 'calc(100vh - var(--height-topbar, 4rem) - var(--size-menu, 4rem))', | |||
display: 'grid', | |||
placeContent: 'center', | |||
textAlign: 'center', | |||
'@media (min-width: 720px)': { | |||
height: 'calc(100vh - var(--height-topbar, 4rem))', | |||
}, | |||
}) | |||
// @ts-ignore | |||
const TitleInput = styled('input')({ | |||
color: 'inherit', | |||
font: 'inherit', | |||
fontSize: '3rem', | |||
padding: 0, | |||
border: 0, | |||
backgroundColor: 'transparent', | |||
fontFamily: 'var(--font-family-headings), sans-serif', | |||
fontStretch: 'var(--font-stretch-headings, normal)', | |||
fontWeight: 'var(--font-weight-headings, 400)', | |||
height: '4rem', | |||
outline: 0, | |||
width: '100%', | |||
display: 'block', | |||
marginBottom: '1rem', | |||
}) | |||
const SidebarTitle = styled('h1')({ | |||
margin: 0, | |||
lineHeight: '4rem', | |||
}) | |||
const TitleContainer = styled(LeftSidebarWithMenu.ContentContainer)({ | |||
display: 'block', | |||
}) | |||
const SidebarSubtitle = styled('p')({ | |||
margin: '2rem 0', | |||
}) | |||
type Props = { | |||
subpage: Subpage, | |||
notes: Note[], | |||
subfolders: Folder[], | |||
currentFolder: Folder, | |||
currentNote?: Note, | |||
} | |||
const NoteView: React.FC<Props> = ({ | |||
subpage, | |||
notes, | |||
subfolders, | |||
currentFolder, | |||
currentNote, | |||
}) => { | |||
const APP_NAME = 'Zeichen' | |||
return ( | |||
<> | |||
<Head> | |||
<title> | |||
{currentNote ? `${currentNote.title} | ${APP_NAME}` : APP_NAME} | |||
</title> | |||
</Head> | |||
<LeftSidebarWithMenu.Layout | |||
brand={ | |||
<Brand /> | |||
} | |||
sidebarMenuItems={[ | |||
{ | |||
id: 'notes', | |||
url: { | |||
pathname: '/my/notes', | |||
}, | |||
label: 'Notes', | |||
icon: ( | |||
<T.Icon | |||
name="book" | |||
/> | |||
), | |||
}, | |||
{ | |||
id: 'bin', | |||
url: { | |||
pathname: '/my/bin' | |||
}, | |||
icon: ( | |||
<T.Icon | |||
name="trash2" | |||
/> | |||
), | |||
label: 'Bin', | |||
secondary: true, | |||
}, | |||
]} | |||
sidebarMainOpen={subpage === Subpage.SIDEBAR} | |||
moreItemsOpen={subpage === Subpage.MORE} | |||
sidebarMain={ | |||
<> | |||
{currentFolder && ( | |||
<LeftSidebarWithMenu.SidebarMainContainer> | |||
<SidebarTitle> | |||
{currentFolder.name} | |||
</SidebarTitle> | |||
{currentFolder.description && ( | |||
<SidebarSubtitle> | |||
{currentFolder.description} | |||
</SidebarSubtitle> | |||
)} | |||
</LeftSidebarWithMenu.SidebarMainContainer> | |||
)} | |||
{subfolders.map(f => ( | |||
<SidebarLink | |||
href={{ | |||
pathname: '/my/notes', | |||
query: { | |||
folderId: f.id, | |||
}, | |||
}} | |||
> | |||
<LeftSidebarWithMenu.SidebarMainContainer> | |||
<FolderLinkContent | |||
title={f.name} | |||
/> | |||
</LeftSidebarWithMenu.SidebarMainContainer> | |||
</SidebarLink> | |||
))} | |||
{notes.map(n => ( | |||
<SidebarLink | |||
href={{ | |||
pathname: '/my/notes/[noteId]', | |||
query: { | |||
noteId: n.id, | |||
}, | |||
}} | |||
> | |||
<LeftSidebarWithMenu.SidebarMainContainer> | |||
<NoteLinkContent | |||
title={n.title} | |||
subtitle={n.contentVersions[0].createdAt.toString()} | |||
/> | |||
</LeftSidebarWithMenu.SidebarMainContainer> | |||
</SidebarLink> | |||
))} | |||
</> | |||
} | |||
moreLinkMenuItem={{ | |||
label: 'More', | |||
icon: ( | |||
<T.Icon | |||
name="more-horizontal" | |||
/> | |||
), | |||
url: { | |||
query: { | |||
subpage: 'more', | |||
} | |||
} | |||
}} | |||
moreLinkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
<LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
</Link> | |||
)} | |||
linkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.SidebarMenuContainer> | |||
<LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.SidebarMenuContainer> | |||
</Link> | |||
)} | |||
> | |||
{!currentNote && ( | |||
<CenteredContent> | |||
<T.Icon | |||
name="folder" | |||
size="4rem" | |||
/> | |||
Select a note from the menu. | |||
</CenteredContent> | |||
)} | |||
{currentNote && ( | |||
<> | |||
<TitleContainer | |||
as="label" | |||
> | |||
<TitleInput | |||
placeholder="Title" | |||
/> | |||
</TitleContainer> | |||
<LeftSidebarWithMenu.ContentContainer> | |||
<Editor /> | |||
</LeftSidebarWithMenu.ContentContainer> | |||
</> | |||
)} | |||
</LeftSidebarWithMenu.Layout> | |||
</> | |||
) | |||
} | |||
export default NoteView |
@@ -0,0 +1,7 @@ | |||
export default class Folder { | |||
id: string | |||
name: string | |||
description?: string | |||
} |
@@ -0,0 +1,15 @@ | |||
import Folder from './Folder' | |||
import User from './User' | |||
import NoteVersion from './NoteVersion' | |||
export default class Note { | |||
id: string | |||
title: string | |||
folder?: Folder | |||
authorUser: User | |||
contentVersions: NoteVersion[] | |||
} |
@@ -0,0 +1,11 @@ | |||
import Note from './Note' | |||
export default class NoteVersion { | |||
id: string | |||
content: string | |||
createdAt: Date | string | |||
parent?: NoteVersion | |||
} |
@@ -0,0 +1,9 @@ | |||
import UserProfile from './UserProfile' | |||
export default class User { | |||
id: string | |||
username: string | |||
profile: UserProfile | |||
} |
@@ -0,0 +1,9 @@ | |||
export default class UserLink { | |||
id: string | |||
title: string | |||
description?: string | |||
url: string | |||
} |
@@ -0,0 +1,11 @@ | |||
import UserLink from './UserLink' | |||
export default class UserProfile { | |||
id: string | |||
email: string | |||
bio?: string | |||
links: UserLink[] | |||
} |
@@ -0,0 +1,5 @@ | |||
const MyApp = ({ Component, pageProps }) => { | |||
return <Component {...pageProps} /> | |||
} | |||
export default MyApp |
@@ -0,0 +1,61 @@ | |||
import Document, {Html, Head, Main, NextScript} from 'next/document' | |||
import {ServerStyleSheet} from 'styled-components' | |||
import pkg from '../../package.json' | |||
import config from '../../next.config' | |||
const publicUrl = process.env.NODE_ENV === 'production' ? pkg.homepage : config.basePath | |||
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: ( | |||
<> | |||
{initialProps.styles} | |||
<link rel="preconnect" href="https://fonts.gstatic.com" /> | |||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Encode+Sans:wdth,wght@75..112.5,100..900&display=swap" /> | |||
<link rel="stylesheet" href={`${publicUrl}/global.css`} /> | |||
<link rel="stylesheet" href={`${publicUrl}/theme.css`} /> | |||
<link rel="stylesheet" title="Dark" href={`${publicUrl}/theme/dark.css`} /> | |||
<link rel="alternate stylesheet" title="Light" href={`${publicUrl}/theme/light.css`} /> | |||
{sheet.getStyleElement()} | |||
</> | |||
), | |||
} | |||
} catch (err) { | |||
console.error(err) | |||
} finally { | |||
sheet.seal() | |||
} | |||
} | |||
render() { | |||
return ( | |||
<Html | |||
lang="en-PH" | |||
> | |||
<Head /> | |||
<body> | |||
<Main /> | |||
<NextScript /> | |||
</body> | |||
</Html> | |||
) | |||
} | |||
} |
@@ -0,0 +1,19 @@ | |||
import {GetServerSideProps, NextPage} from 'next' | |||
type Props = {} | |||
const Page: NextPage<Props> = () => { | |||
return ( | |||
<> | |||
Hello | |||
</> | |||
); | |||
}; | |||
export default Page | |||
export const getServerSideProps: GetServerSideProps = async (ctx) => { | |||
return { | |||
props: {}, | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
import {GetServerSideProps, NextPage} from 'next' | |||
import BinView from '../../../components/templates/BinView' | |||
import {QueryFragment, Subpage} from '../../../utils/query' | |||
type Props = { | |||
subpage: Subpage, | |||
} | |||
const Page: NextPage<Props> = ({ | |||
subpage, | |||
}) => { | |||
return ( | |||
<BinView | |||
subpage={subpage} | |||
/> | |||
); | |||
}; | |||
export default Page | |||
export const getServerSideProps: GetServerSideProps = async (ctx) => { | |||
const { [QueryFragment.SUBPAGE]: subpage = '' } = ctx.query | |||
return { | |||
props: { | |||
subpage, | |||
}, | |||
} | |||
} |
@@ -0,0 +1,85 @@ | |||
import {GetServerSideProps, NextPage} from 'next' | |||
import NoteView from '../../../components/templates/NoteView' | |||
import {QueryFragment, Subpage} from '../../../utils/query' | |||
import Note from '../../../models/Note' | |||
import Folder from '../../../models/Folder' | |||
type Props = { | |||
subpage: Subpage, | |||
notes: Note[], | |||
currentFolder?: Folder, | |||
subfolders: Folder[], | |||
currentNote: Note, | |||
} | |||
const Page: NextPage<Props> = ({ | |||
subpage, | |||
notes, | |||
currentFolder, | |||
subfolders, | |||
currentNote, | |||
}) => { | |||
return ( | |||
<NoteView | |||
subpage={subpage} | |||
notes={notes} | |||
subfolders={subfolders} | |||
currentFolder={currentFolder} | |||
currentNote={currentNote} | |||
/> | |||
); | |||
}; | |||
export default Page | |||
export const getServerSideProps: GetServerSideProps = async (ctx) => { | |||
const { [QueryFragment.SUBPAGE]: subpage = '', noteId, } = ctx.query | |||
const authorUser = { | |||
id: '0', | |||
profile: { | |||
id: '0', | |||
email: 'hello@example.com', | |||
links: [], | |||
}, | |||
username: 'johndoe' | |||
} | |||
const notes: Note[] = [ | |||
{ | |||
id: '0', | |||
title: 'Hello', | |||
authorUser: authorUser, | |||
contentVersions: [ | |||
{ | |||
id: '0', | |||
content: 'Note content', | |||
createdAt: new Date().toISOString(), | |||
} | |||
], | |||
} | |||
] | |||
const subfolders: Folder[] = [ | |||
{ | |||
id: '0', | |||
name: 'Child Folder', | |||
description: 'Where we put other notes', | |||
} | |||
] | |||
const currentNote: Note = { | |||
id: noteId as string, | |||
title: 'This Note', | |||
authorUser, | |||
contentVersions: [], | |||
} | |||
return { | |||
props: { | |||
subpage, | |||
notes, | |||
subfolders, | |||
currentFolder: { | |||
name: 'Root Folder', | |||
description: 'Default location of your notes.', | |||
}, | |||
currentNote, | |||
}, | |||
} | |||
} |
@@ -0,0 +1,75 @@ | |||
import {GetServerSideProps, NextPage} from 'next' | |||
import NoteView from '../../../components/templates/NoteView' | |||
import {QueryFragment, Subpage} from '../../../utils/query' | |||
import Note from '../../../models/Note' | |||
import Folder from '../../../models/Folder' | |||
type Props = { | |||
subpage: Subpage, | |||
notes: Note[], | |||
currentFolder?: Folder, | |||
subfolders: Folder[], | |||
} | |||
const Page: NextPage<Props> = ({ | |||
subpage, | |||
notes, | |||
currentFolder, | |||
subfolders, | |||
}) => { | |||
return ( | |||
<NoteView | |||
subpage={subpage} | |||
notes={notes} | |||
subfolders={subfolders} | |||
currentFolder={currentFolder} | |||
/> | |||
); | |||
}; | |||
export default Page | |||
export const getServerSideProps: GetServerSideProps = async (ctx) => { | |||
const { [QueryFragment.SUBPAGE]: subpage = '' } = ctx.query | |||
const authorUser = { | |||
id: '0', | |||
profile: { | |||
id: '0', | |||
email: 'hello@example.com', | |||
links: [], | |||
}, | |||
username: 'johndoe' | |||
} | |||
const notes: Note[] = [ | |||
{ | |||
id: '0', | |||
title: 'Hello', | |||
authorUser: authorUser, | |||
contentVersions: [ | |||
{ | |||
id: '0', | |||
content: 'Note content', | |||
createdAt: new Date().toISOString(), | |||
} | |||
], | |||
} | |||
] | |||
const subfolders: Folder[] = [ | |||
{ | |||
id: '0', | |||
name: 'Child Folder', | |||
description: 'Where we put other notes', | |||
} | |||
] | |||
return { | |||
props: { | |||
subpage, | |||
notes, | |||
subfolders, | |||
currentFolder: { | |||
name: 'Root Folder', | |||
description: 'Default location of your notes.', | |||
}, | |||
}, | |||
} | |||
} |
@@ -0,0 +1,19 @@ | |||
import {deserialize} from './content' | |||
describe('deserialize', () => { | |||
it('should parse BBCode', () => { | |||
expect(deserialize(` | |||
hello world | |||
`)).toEqual({ | |||
version: '0.3.2', | |||
markups: [], | |||
atoms: [], | |||
cards: [], | |||
sections: [ | |||
[1, 'p', [ | |||
[0, [], 0, 'hello world'] | |||
]] | |||
], | |||
}) | |||
}) | |||
}) |
@@ -0,0 +1,160 @@ | |||
enum MarkerType { | |||
TEXT = 0, | |||
ATOM = 1, | |||
} | |||
enum SectionType { | |||
MARKUP = 1, | |||
IMAGE = 2, | |||
LIST = 3, | |||
CARD = 10, | |||
} | |||
// TODO!!!!! | |||
// Try parsing BBCode before converting it to Mobiledoc and vv | |||
const generateBBCodeAST = (bbcode: string) => ( | |||
bbcode | |||
.trim() | |||
.split('') | |||
.reduce( | |||
(parserState, c) => { | |||
const {tokenStack, tagStack, tree} = parserState | |||
const [lastToken] = tokenStack.slice(-1) | |||
switch (c) { | |||
case '[': | |||
return { | |||
...parserState, | |||
tokenStack: [ | |||
...tokenStack, | |||
'', | |||
], | |||
} | |||
case ']': | |||
if ( | |||
lastToken.startsWith('/') | |||
&& lastToken.slice('/'.length) === tagStack.slice(-1)[0] | |||
) { | |||
return { | |||
...parserState, | |||
tokenStack: tokenStack.slice(0, -1), | |||
tagStack: tagStack.slice(0, -1), | |||
} | |||
} | |||
return { | |||
...parserState, | |||
tokenStack: tokenStack.slice(0, -1), | |||
tagStack: [ | |||
...tagStack, | |||
lastToken, | |||
] | |||
} | |||
default: | |||
break | |||
} | |||
return { | |||
...parserState, | |||
tokenStack: [ | |||
...tokenStack.slice(0, -1), | |||
lastToken + c, | |||
], | |||
} | |||
}, | |||
{ | |||
tree: [], | |||
tokenStack: [''], | |||
tagStack: [], | |||
} | |||
) | |||
) | |||
const convertBBCodeToMobiledoc = (bbcode: string, { components, version = '0.3.2,' }) => { | |||
return bbcode | |||
.trim() | |||
.split('') | |||
.reduce( | |||
(parserState, c) => { | |||
switch (c) { | |||
case '[': | |||
return { | |||
...parserState, | |||
tokenStack: [ | |||
...parserState.tokenStack, | |||
'', | |||
] | |||
} | |||
case ']': | |||
if (parserState.lastToken.startsWith('/')) { | |||
return { | |||
...parserState, | |||
// pop token stack | |||
tokenStack: parserState.tokenStack.slice(0, -1), | |||
tagStack: parserState.tagStack.slice(0, -1), | |||
} | |||
} | |||
return { | |||
...parserState, | |||
// pop token stack | |||
tokenStack: parserState.tokenStack.slice(0, -1), | |||
tagStack: [ | |||
...parserState.tagStack, | |||
parserState.lastToken, | |||
], | |||
lastToken: '', | |||
} | |||
} | |||
let [lastSection = [SectionType.MARKUP, 'p', []]] = parserState.state.sections.slice(-1) | |||
let [sectionType = SectionType.MARKUP, sectionTag = 'p', sectionMarkers = []] = lastSection | |||
let [lastMarker = [MarkerType.TEXT, [], 0, '']] = sectionMarkers.slice(-1) | |||
let [textTypeIdentifier, openMarkupsIndexes, numberOfClosedMarkups, value] = lastMarker | |||
return { | |||
...parserState, | |||
sections: [ | |||
...parserState.state.sections, | |||
[ | |||
sectionType, | |||
sectionTag, | |||
[ | |||
...sectionMarkers, | |||
[ | |||
textTypeIdentifier, | |||
openMarkupsIndexes, | |||
numberOfClosedMarkups, | |||
value + parserState.lastCharacter, | |||
], | |||
], | |||
], | |||
], | |||
lastCharacter: c, | |||
} | |||
}, | |||
{ | |||
state: { | |||
version, | |||
markups: [], | |||
atoms: [], | |||
cards: [], | |||
sections: [], | |||
}, | |||
lastCharacter: '', | |||
lastToken: '', | |||
tokenStack: [], | |||
tagStack: [], | |||
} | |||
) | |||
.state | |||
} | |||
const convertMobiledocToBBCode = (mobiledoc: any, { components }) => { | |||
} | |||
export const deserialize = (text: string) => { | |||
return convertBBCodeToMobiledoc(text, { components: [] }) | |||
} | |||
export const serialize = (data: any) => { | |||
return convertMobiledocToBBCode(data, { components: [] }) | |||
} |
@@ -0,0 +1,8 @@ | |||
export enum Subpage { | |||
SIDEBAR = 'sidebar', | |||
MORE = 'more', | |||
} | |||
export enum QueryFragment { | |||
SUBPAGE = 'subpage', | |||
} |
@@ -0,0 +1,29 @@ | |||
{ | |||
"compilerOptions": { | |||
"target": "es5", | |||
"lib": [ | |||
"dom", | |||
"dom.iterable", | |||
"esnext" | |||
], | |||
"allowJs": true, | |||
"skipLibCheck": true, | |||
"strict": false, | |||
"forceConsistentCasingInFileNames": true, | |||
"noEmit": true, | |||
"esModuleInterop": true, | |||
"module": "esnext", | |||
"moduleResolution": "node", | |||
"resolveJsonModule": true, | |||
"isolatedModules": true, | |||
"jsx": "preserve" | |||
}, | |||
"include": [ | |||
"next-env.d.ts", | |||
"**/*.ts", | |||
"**/*.tsx" | |||
], | |||
"exclude": [ | |||
"node_modules" | |||
] | |||
} |