The current adapters have been implemented for server-side routes, mainly for action-redirect routes (/api/a/) and resource routes (/api). In addition, the required data under getServerSideProps has been included to share one presenter code.master
@@ -35,3 +35,5 @@ yarn-error.log* | |||||
.vercel | .vercel | ||||
.vs/ | .vs/ | ||||
.idea/ | |||||
*.sqlite |
@@ -16,7 +16,7 @@ You can start editing the page by modifying `pages/index.js`. The page auto-upda | |||||
[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`. | [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. | |||||
The `pages/api` directory is mapped to `/sdfapi/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. | |||||
## Learn More | ## Learn More | ||||
@@ -5,7 +5,8 @@ | |||||
"scripts": { | "scripts": { | ||||
"dev": "next dev", | "dev": "next dev", | ||||
"build": "next build", | "build": "next build", | ||||
"start": "next start" | |||||
"start": "next start", | |||||
"migrate": "node ./scripts/migrate.js" | |||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@auth0/nextjs-auth0": "^1.3.0", | "@auth0/nextjs-auth0": "^1.3.0", | ||||
@@ -14,12 +15,18 @@ | |||||
"react": "17.0.2", | "react": "17.0.2", | ||||
"react-dom": "17.0.2", | "react-dom": "17.0.2", | ||||
"react-feather": "^2.0.9", | "react-feather": "^2.0.9", | ||||
"styled-components": "^5.2.3" | |||||
"react-hook-form": "^7.0.4", | |||||
"sqlite3": "^5.0.2", | |||||
"styled-components": "^5.2.3", | |||||
"uuid": "^8.3.2" | |||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@types/node": "^14.14.37", | "@types/node": "^14.14.37", | ||||
"@types/react": "^17.0.3", | "@types/react": "^17.0.3", | ||||
"@types/sqlite3": "^3.1.7", | |||||
"@types/styled-components": "^5.1.9", | "@types/styled-components": "^5.1.9", | ||||
"@types/uuid": "^8.3.0", | |||||
"dotenv": "^8.2.0", | |||||
"typescript": "^4.2.3" | "typescript": "^4.2.3" | ||||
} | } | ||||
} | } |
@@ -0,0 +1,46 @@ | |||||
const dotenv = require('dotenv') | |||||
const SQLite = require('sqlite3') | |||||
dotenv.config() | |||||
const sqlite = SQLite.verbose() | |||||
const db = new sqlite.Database(process.env.DATABASE_PATH) | |||||
db.serialize(() => { | |||||
db.run(` | |||||
CREATE TABLE IF NOT EXISTS items ( | |||||
id CHARACTER(32) PRIMARY KEY, | |||||
name VARCHAR(256) NOT NULL, | |||||
url VARCHAR(256) NOT NULL, | |||||
folder_id CHARACTER(32) NOT NULL, | |||||
created_at DATETIME NOT NULL, | |||||
updated_at DATETIME NOT NULL, | |||||
deleted_at DATETIME | |||||
) | |||||
`) | |||||
db.run(` | |||||
CREATE TABLE IF NOT EXISTS folders ( | |||||
id CHARACTER(32) PRIMARY KEY, | |||||
name VARCHAR(256) NOT NULL, | |||||
parent_id CHARACTER(32), | |||||
created_at DATETIME NOT NULL, | |||||
updated_at DATETIME NOT NULL, | |||||
deleted_at DATETIME | |||||
) | |||||
`) | |||||
db.run(` | |||||
CREATE TABLE IF NOT EXISTS users ( | |||||
id CHARACTER(32) PRIMARY KEY, | |||||
name VARCHAR(256) NOT NULL, | |||||
root_folder_id CHARACTER(32) NOT NULL, | |||||
created_at DATETIME NOT NULL, | |||||
updated_at DATETIME NOT NULL, | |||||
deleted_at DATETIME | |||||
) | |||||
`) | |||||
}) | |||||
db.close() |
@@ -1,3 +1,4 @@ | |||||
import * as React from 'react' | |||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
const Base = styled('input')({ | const Base = styled('input')({ | ||||
@@ -12,10 +13,17 @@ const Base = styled('input')({ | |||||
cursor: 'pointer', | cursor: 'pointer', | ||||
}) | }) | ||||
const TextInput = (props) => { | |||||
type Props = { | |||||
name: string, | |||||
} | |||||
const TextInput = React.forwardRef<HTMLInputElement, Props>((props, ref) => { | |||||
return ( | return ( | ||||
<Base {...props} /> | |||||
<Base | |||||
{...props} | |||||
ref={ref} | |||||
/> | |||||
) | ) | ||||
} | |||||
}) | |||||
export default TextInput | export default TextInput |
@@ -1,10 +1,15 @@ | |||||
import Head from 'next/head' | |||||
import Link from 'next/link' | |||||
import styled, { createGlobalStyle } from 'styled-components' | |||||
import { Folder, Settings, Trash2 } from 'react-feather' | |||||
import Button from '../../molecules/Button' | |||||
import TopBar from '../../organisms/widgets/TopBar' | |||||
import FolderItem from '../../organisms/tree/FolderItem' | |||||
import Head from 'next/head'; | |||||
import Link from 'next/link'; | |||||
import styled, {createGlobalStyle} from 'styled-components'; | |||||
import {Folder, Settings, Trash2} from 'react-feather'; | |||||
import Button from '../../molecules/Button'; | |||||
import TopBar from '../../organisms/widgets/TopBar'; | |||||
import FolderItem from '../../organisms/tree/FolderItem'; | |||||
import FolderViewMode from '../../../models/FolderViewMode'; | |||||
import TextInput from '../../molecules/TextInput'; | |||||
import {forClientSideState, Form} from '../../../utilities/handler'; | |||||
import Method from '../../../utilities/handler/Method'; | |||||
import {useRouter} from 'next/router'; | |||||
const Variables = createGlobalStyle({ | const Variables = createGlobalStyle({ | ||||
':root': { | ':root': { | ||||
@@ -71,22 +76,22 @@ const SidebarContents = styled('div')({ | |||||
position: 'relative', | position: 'relative', | ||||
}) | }) | ||||
const Tree = styled('div')({ | |||||
const FolderListWrapper = styled('div')({ | |||||
width: '100%', | width: '100%', | ||||
height: '100%', | height: '100%', | ||||
overflow: 'auto', | overflow: 'auto', | ||||
}) | }) | ||||
const TreeParent = styled('ul')({ | |||||
const FolderList = styled('ul')({ | |||||
margin: 0, | margin: 0, | ||||
padding: 0, | padding: 0, | ||||
}) | }) | ||||
const TreeChild = styled('li')({ | |||||
const FolderListItem = styled('li')({ | |||||
display: 'block', | display: 'block', | ||||
}) | }) | ||||
const TreeActions = styled('div')({ | |||||
const FolderActions = styled('div')({ | |||||
position: 'sticky', | position: 'sticky', | ||||
display: 'flex', | display: 'flex', | ||||
justifyContent: 'flex-end', | justifyContent: 'flex-end', | ||||
@@ -100,17 +105,15 @@ const TreeActions = styled('div')({ | |||||
boxSizing: 'border-box', | boxSizing: 'border-box', | ||||
}) | }) | ||||
const TreeLink = styled('a')({ | |||||
const FolderLink = styled('a')({ | |||||
display: 'inline-flex', | display: 'inline-flex', | ||||
minWidth: '100%', | |||||
verticalAlign: 'top', | verticalAlign: 'top', | ||||
height: '2rem', | |||||
height: '3rem', | |||||
alignItems: 'center', | alignItems: 'center', | ||||
whiteSpace: 'nowrap', | whiteSpace: 'nowrap', | ||||
padding: '0 1rem', | padding: '0 1rem', | ||||
boxSizing: 'border-box', | boxSizing: 'border-box', | ||||
[`+ ${TreeParent}`]: { | |||||
paddingLeft: '1.5rem', | |||||
}, | |||||
}) | }) | ||||
const FolderGrid = styled('div')({ | const FolderGrid = styled('div')({ | ||||
@@ -142,30 +145,39 @@ const Header = styled('div')({ | |||||
backgroundColor: 'var(--color-bg, white)', | backgroundColor: 'var(--color-bg, white)', | ||||
}) | }) | ||||
const renderChild = c => ( | |||||
<TreeChild | |||||
key={c.id} | |||||
> | |||||
<TreeLink> | |||||
{c.name} | |||||
</TreeLink> | |||||
<TreeParent> | |||||
{c.children.map(renderChild)} | |||||
</TreeParent> | |||||
</TreeChild> | |||||
) | |||||
const FolderIdInput = styled('input')({ | |||||
fontFamily: 'monospace', | |||||
fontSize: '0.875rem', | |||||
display: 'block', | |||||
width: '20em', | |||||
border: 0, | |||||
backgroundColor: 'transparent', | |||||
color: 'inherit', | |||||
padding: 0, | |||||
margin: 0, | |||||
outline: 0, | |||||
}) | |||||
const FolderIdBox = styled('span')({ | |||||
display: 'inline-block', | |||||
verticalAlign: 'middle', | |||||
}) | |||||
const FolderTemplate = ({ | const FolderTemplate = ({ | ||||
path = [], | |||||
query = '', | |||||
id = '', | |||||
query, | |||||
children, | children, | ||||
items = [], | items = [], | ||||
mode, | |||||
hierarchy, | |||||
}) => { | }) => { | ||||
const router = useRouter() | |||||
const [currentFolder] = hierarchy.slice(-1) | |||||
return ( | return ( | ||||
<> | <> | ||||
<Head> | <Head> | ||||
<title>{ Array.isArray(path) && path.length > 0 ? `${path.slice(-1)[0]} | Bruhbot` : 'Bruhbot' }</title> | |||||
<title> | |||||
{currentFolder.name} | Bruhbot | |||||
</title> | |||||
<link rel="icon" href="/favicon.ico" /> | <link rel="icon" href="/favicon.ico" /> | ||||
</Head> | </Head> | ||||
<Variables /> | <Variables /> | ||||
@@ -217,16 +229,69 @@ const FolderTemplate = ({ | |||||
</SidebarLink> | </SidebarLink> | ||||
</Link> | </Link> | ||||
<InnerSidebar> | <InnerSidebar> | ||||
<Tree> | |||||
<TreeActions> | |||||
<Button> | |||||
New Folder | |||||
</Button> | |||||
</TreeActions> | |||||
<TreeParent> | |||||
{children.map(renderChild)} | |||||
</TreeParent> | |||||
</Tree> | |||||
<FolderListWrapper> | |||||
<FolderActions> | |||||
<form | |||||
action="/api/a/folders/new" | |||||
method={Method.GET} | |||||
onSubmit={forClientSideState({ router, })(() => { | |||||
})} | |||||
> | |||||
{( | |||||
<Button | |||||
type="submit" | |||||
> | |||||
New Folder | |||||
</Button> | |||||
)} | |||||
</form> | |||||
</FolderActions> | |||||
{ | |||||
mode === FolderViewMode.NEW_FOLDER | |||||
&& ( | |||||
<FolderList> | |||||
<form | |||||
action="/api/a/folders/create" | |||||
method={Method.POST} | |||||
> | |||||
<input | |||||
type="hidden" | |||||
name="parent_id" | |||||
value={currentFolder.id} | |||||
/> | |||||
<TextInput | |||||
name="name" | |||||
/> | |||||
<Button> | |||||
Create | |||||
</Button> | |||||
</form> | |||||
</FolderList> | |||||
) | |||||
} | |||||
<FolderList> | |||||
{children.map(c => ( | |||||
<FolderListItem | |||||
key={c.id} | |||||
> | |||||
<Link | |||||
href={{ | |||||
pathname: '/my/folders/[id]', | |||||
query: { | |||||
id: c.id, | |||||
}, | |||||
}} | |||||
passHref | |||||
> | |||||
<FolderLink> | |||||
{c.name} | |||||
</FolderLink> | |||||
</Link> | |||||
</FolderListItem> | |||||
))} | |||||
</FolderList> | |||||
</FolderListWrapper> | |||||
</InnerSidebar> | </InnerSidebar> | ||||
</SidebarContents> | </SidebarContents> | ||||
</SideBar> | </SideBar> | ||||
@@ -234,9 +299,33 @@ const FolderTemplate = ({ | |||||
<Header> | <Header> | ||||
<FolderInfoContainer> | <FolderInfoContainer> | ||||
<div> | <div> | ||||
Folder ID: {id} | |||||
{hierarchy.map(h => ( | |||||
<> | |||||
{'/ '} | |||||
<Link | |||||
href={{ | |||||
pathname: '/my/folders/[id]', | |||||
query: { | |||||
id: h.id, | |||||
}, | |||||
}} | |||||
> | |||||
<a> | |||||
{h.name} | |||||
</a> | |||||
</Link> | |||||
{' '} | |||||
</> | |||||
))} | |||||
</div> | </div> | ||||
<div> | <div> | ||||
<FolderIdBox> | |||||
Folder ID | |||||
<FolderIdInput | |||||
readOnly | |||||
value={currentFolder.id} | |||||
/> | |||||
</FolderIdBox> | |||||
<Button> | <Button> | ||||
Delete | Delete | ||||
</Button> | </Button> | ||||
@@ -244,22 +333,37 @@ const FolderTemplate = ({ | |||||
</FolderInfoContainer> | </FolderInfoContainer> | ||||
</Header> | </Header> | ||||
<Container> | <Container> | ||||
<FolderGrid> | |||||
{ | |||||
items.map(i => ( | |||||
<FolderGridCell | |||||
key={i.id} | |||||
> | |||||
<FolderGridContents> | |||||
<FolderItem | |||||
name={i.name} | |||||
url={i.url} | |||||
/> | |||||
</FolderGridContents> | |||||
</FolderGridCell> | |||||
)) | |||||
} | |||||
</FolderGrid> | |||||
{ | |||||
Array.isArray(items) | |||||
&& items.length < 1 | |||||
&& ( | |||||
<> | |||||
This folder is empty. | |||||
</> | |||||
) | |||||
} | |||||
{ | |||||
Array.isArray(items) | |||||
&& items.length > 0 | |||||
&& ( | |||||
<FolderGrid> | |||||
{ | |||||
items.map(i => ( | |||||
<FolderGridCell | |||||
key={i.id} | |||||
> | |||||
<FolderGridContents> | |||||
<FolderItem | |||||
name={i.name} | |||||
url={i.url} | |||||
/> | |||||
</FolderGridContents> | |||||
</FolderGridCell> | |||||
)) | |||||
} | |||||
</FolderGrid> | |||||
) | |||||
} | |||||
</Container> | </Container> | ||||
</Main> | </Main> | ||||
</> | </> | ||||
@@ -0,0 +1,143 @@ | |||||
import FolderViewMode from '../../models/FolderViewMode' | |||||
import Folder from '../../models/Folder' | |||||
import FolderService from './service' | |||||
import Item from '../../models/Item' | |||||
import {_} from '../../utilities/messages' | |||||
export default class FolderPresenter { | |||||
private readonly folderService = new FolderService() | |||||
private async getCommonProps(folderId: string) { | |||||
return Promise.all([ | |||||
this.folderService.getHierarchy(folderId), | |||||
this.folderService.getChildren(folderId), | |||||
this.folderService.getItems(folderId), | |||||
]) | |||||
} | |||||
async getRootFolderViewState({ session, query, mode, }) { | |||||
// TODO make this authentication checking more like a middleware. | |||||
if (!session) { | |||||
return { | |||||
redirect: { | |||||
url: new URL('/', process.env.BASE_URL) | |||||
}, | |||||
} | |||||
} | |||||
const folderId = session.user.rootFolder.id | |||||
const [hierarchy, children, items] = await this.getCommonProps(folderId) | |||||
return { | |||||
body: { | |||||
data: { | |||||
query, | |||||
mode, | |||||
items: items.map(Item.formatForView), | |||||
children: children.map(c => Folder.formatForView(c)), | |||||
hierarchy: hierarchy.map(h => Folder.formatForView(h)), | |||||
}, | |||||
}, | |||||
} | |||||
} | |||||
async getDescendantFolderViewState({ session, query, mode, id, }) { | |||||
// TODO make this authentication checking more like a middleware. | |||||
if (!session) { | |||||
return { | |||||
redirect: { | |||||
url: new URL('/', process.env.BASE_URL) | |||||
}, | |||||
} | |||||
} | |||||
const folderId = id as string | |||||
const [hierarchy, children, items] = await this.getCommonProps(folderId) | |||||
return { | |||||
body: { | |||||
data: { | |||||
query, | |||||
mode, | |||||
items: items.map(Item.formatForView), | |||||
children: children.map(Folder.formatForView), | |||||
hierarchy: hierarchy.map(Folder.formatForView), | |||||
}, | |||||
}, | |||||
} | |||||
} | |||||
async getFolder({ id }) { | |||||
try { | |||||
const data = await this.folderService.get(id) | |||||
// TODO compute owner of folder by getting the root folder | |||||
if (data) { | |||||
return { | |||||
status: 200, | |||||
title: _('FOLDER_FOUND_TITLE'), | |||||
body: { | |||||
description: _('FOLDER_FOUND_DESCRIPTION'), | |||||
data, | |||||
}, | |||||
} | |||||
} | |||||
return { | |||||
status: 404, | |||||
title: _('FOLDER_NOT_FOUND_TITLE'), | |||||
body: { | |||||
description: _('FOLDER_NOT_FOUND_DESCRIPTION'), | |||||
}, | |||||
} | |||||
} catch { | |||||
return { | |||||
status: 500, | |||||
title: _('ERROR_RETRIEVING_FOLDER_TITLE'), | |||||
body: { | |||||
description: _('ERROR_RETRIEVING_FOLDER_DESCRIPTION'), | |||||
}, | |||||
} | |||||
} | |||||
} | |||||
async createFolder({ name, parentId, origin = null }) { | |||||
try { | |||||
const newFolder = await this.folderService.create(name, parentId) | |||||
return { | |||||
status: 201, | |||||
title: _('FOLDER_CREATED_TITLE'), | |||||
body: { | |||||
description: _('FOLDER_CREATED_DESCRIPTION'), | |||||
data: newFolder, | |||||
}, | |||||
redirect: { | |||||
url: new URL(`/my/folders/${newFolder.id}`, process.env.BASE_URL), | |||||
}, | |||||
} | |||||
} catch { | |||||
return { | |||||
status: 500, | |||||
title: _('ERROR_CREATING_FOLDER_TITLE'), | |||||
body: { | |||||
description: _('ERROR_CREATING_FOLDER_DESCRIPTION'), | |||||
}, | |||||
redirect: { | |||||
url: new URL(origin), | |||||
}, | |||||
} | |||||
} | |||||
} | |||||
generateRedirectToCreateFolder({ sourceURL, }) { | |||||
const url = sourceURL ? new URL(sourceURL) : new URL('/my/folders', process.env.BASE_URL) | |||||
const search = Object.fromEntries(url.searchParams.entries()) | |||||
url.search = new URLSearchParams({ | |||||
...search, | |||||
'mode': FolderViewMode.NEW_FOLDER, | |||||
}).toString() | |||||
return { | |||||
redirect: { | |||||
url | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,106 @@ | |||||
import { Database } from 'sqlite3' | |||||
import { v4 } from 'uuid' | |||||
import Folder from '../../models/Folder' | |||||
import Item from '../../models/Item' | |||||
export default class FolderService { | |||||
private readonly db = new Database(process.env.DATABASE_PATH) | |||||
async getChildren(id: string): Promise<Folder[]> { | |||||
return new Promise((resolve, reject) => { | |||||
const statement = this.db.prepare(` | |||||
SELECT * | |||||
FROM folders | |||||
WHERE parent_id = (?) | |||||
`) | |||||
statement.all(id, (err, rows) => { | |||||
if (err) { | |||||
reject(err) | |||||
return | |||||
} | |||||
resolve(rows.map(row => Folder.resolve(row))) | |||||
}) | |||||
}) | |||||
} | |||||
async get(id: string): Promise<Folder> { | |||||
return new Promise((resolve, reject) => { | |||||
const statement = this.db.prepare(` | |||||
SELECT * | |||||
FROM folders | |||||
WHERE id = (?) | |||||
`) | |||||
statement.get(id, (err, row) => { | |||||
if (err) { | |||||
reject(err) | |||||
return | |||||
} | |||||
if (row) { | |||||
resolve(Folder.resolve(row)) | |||||
return | |||||
} | |||||
resolve(null) | |||||
}) | |||||
}) | |||||
} | |||||
async create(name: string, parentId?: string): Promise<Folder> { | |||||
return new Promise((resolve, reject) => { | |||||
const id = v4() | |||||
const statement = this.db.prepare(` | |||||
INSERT INTO folders (id, name, parent_id, created_at, updated_at, deleted_at) VALUES (?, ?, ?, ?, ?, NULL) | |||||
`) | |||||
const now = new Date() | |||||
const createdAt = now.toISOString() | |||||
const updatedAt = now.toISOString() | |||||
statement.run(id, name, parentId, createdAt, updatedAt, async (err) => { | |||||
if (err) { | |||||
reject(err) | |||||
return | |||||
} | |||||
resolve(Folder.resolve({ | |||||
'id': id, | |||||
'name': name, | |||||
'parent_id': parentId || null, | |||||
'created_at': createdAt, | |||||
'updated_at': updatedAt, | |||||
'deleted_at': null, | |||||
})) | |||||
}) | |||||
}) | |||||
} | |||||
async getItems(id: string): Promise<Item[]> { | |||||
return new Promise((resolve, reject) => { | |||||
const statement = this.db.prepare(` | |||||
SELECT * | |||||
FROM items | |||||
WHERE folder_id = (?) | |||||
`) | |||||
statement.all(id, (err, rows) => { | |||||
if (err) { | |||||
reject(err) | |||||
return | |||||
} | |||||
resolve(rows.map(item => Item.resolve(item))) | |||||
}) | |||||
}) | |||||
} | |||||
async getHierarchy(id: string): Promise<Folder[]> { | |||||
let currentId: string = id as string | |||||
const hierarchy: Folder[] = [] | |||||
let selectedFolder: Folder | |||||
while (currentId !== null) { | |||||
selectedFolder = await this.get(currentId) | |||||
hierarchy.unshift(selectedFolder) | |||||
currentId = selectedFolder.parent?.id || null | |||||
} | |||||
return hierarchy | |||||
} | |||||
} |
@@ -0,0 +1,67 @@ | |||||
import { Database } from 'sqlite3' | |||||
import User from '../../models/User' | |||||
import {v4} from 'uuid' | |||||
import FolderService from '../Folder/service' | |||||
export default class UserService { | |||||
private readonly db = new Database(process.env.DATABASE_PATH) | |||||
private readonly folderService = new FolderService() | |||||
async getByUsername(username: string): Promise<User> { | |||||
return new Promise((resolve, reject) => { | |||||
const statement = this.db.prepare(` | |||||
SELECT * | |||||
FROM users | |||||
WHERE name = (?) | |||||
`) | |||||
statement.get(username, (err, result) => { | |||||
if (err) { | |||||
reject(err) | |||||
return | |||||
} | |||||
if (result) { | |||||
resolve(User.resolve(result)) | |||||
return | |||||
} | |||||
resolve(null) | |||||
}) | |||||
}) | |||||
} | |||||
async createUser(username: string): Promise<User> { | |||||
const now = new Date() | |||||
const userFolder = await this.folderService.create(username) | |||||
return new Promise((resolve, reject) => { | |||||
const userId = v4() | |||||
const userStatement = this.db.prepare(` | |||||
INSERT INTO users ( | |||||
id, | |||||
name, | |||||
root_folder_id, | |||||
created_at, | |||||
updated_at, | |||||
deleted_at | |||||
) VALUES (?, ?, ?, ?, ?, NULL) | |||||
`) | |||||
const createdAt = now.toISOString() | |||||
const updatedAt = now.toISOString() | |||||
const rootFolderId = userFolder.id | |||||
userStatement.run(userId, username, rootFolderId, createdAt, updatedAt, (err) => { | |||||
if (err) { | |||||
reject(err) | |||||
return | |||||
} | |||||
resolve(User.resolve({ | |||||
'id': userId, | |||||
'name': username, | |||||
'root_folder_id': rootFolderId, | |||||
'created_at': now, | |||||
'updated_at': now, | |||||
'deleted_at': null, | |||||
})) | |||||
}) | |||||
}) | |||||
} | |||||
} |
@@ -0,0 +1,12 @@ | |||||
{ | |||||
"FOLDER_FOUND_TITLE": "Folder Found", | |||||
"FOLDER_FOUND_DESCRIPTION": "Folder retrieved successfully.", | |||||
"FOLDER_NOT_FOUND_TITLE": "Folder Not Found", | |||||
"FOLDER_NOT_FOUND_DESCRIPTION": "The folder does not exist.", | |||||
"ERROR_RETRIEVING_FOLDER_TITLE": "Error Retrieving Folder", | |||||
"ERROR_RETRIEVING_FOLDER_DESCRIPTION": "The folder was not retrieved due to an internal error.", | |||||
"FOLDER_CREATED_TITLE": "Folder Created", | |||||
"FOLDER_CREATED_DESCRIPTION": "Folder created successfully.", | |||||
"ERROR_CREATING_FOLDER_TITLE": "Error Creating Folder", | |||||
"ERROR_CREATING_FOLDER_DESCRIPTION": "The folder was not created." | |||||
} |
@@ -0,0 +1,5 @@ | |||||
import { registerLocale, setLocale } from '../utilities/messages' | |||||
import enPH from './en/PH.json' | |||||
registerLocale('en-PH', enPH) | |||||
setLocale('en-PH') |
@@ -0,0 +1,28 @@ | |||||
export default class Folder { | |||||
id?: string | |||||
name: string | |||||
parent?: Folder | |||||
createdAt: Date | |||||
updatedAt: Date | |||||
deletedAt?: Date | |||||
static resolve(data: Record<string, unknown>): Folder { | |||||
return { | |||||
id: data['id'] as string, | |||||
name: data['name'] as string, | |||||
parent: data['parent_id'] ? { id: data['parent_id'] } as Folder : null, | |||||
createdAt: new Date(data['created_at'] as string), | |||||
updatedAt: new Date(data['updated_at'] as string), | |||||
deletedAt: data['deleted_at'] ? new Date(data['deleted_at'] as string) : null, | |||||
} | |||||
} | |||||
static formatForView(folder: Folder) { | |||||
return { | |||||
...folder, | |||||
createdAt: folder.createdAt.toISOString(), | |||||
updatedAt: folder.updatedAt.toISOString(), | |||||
deletedAt: folder.deletedAt?.toISOString() || null, | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,7 @@ | |||||
enum FolderViewMode { | |||||
DEFAULT = '', | |||||
NEW_FOLDER = 'new_folder', | |||||
DELETE_FOLDER = 'delete_folder', | |||||
} | |||||
export default FolderViewMode |
@@ -0,0 +1,32 @@ | |||||
import Folder from './Folder' | |||||
export default class Item { | |||||
id?: string | |||||
name: string | |||||
url: string | |||||
folder: Folder | |||||
createdAt: Date | |||||
updatedAt: Date | |||||
deletedAt?: Date | |||||
static resolve(data: Record<string, unknown>): Item { | |||||
return { | |||||
id: data['id'] as string, | |||||
name: data['name'] as string, | |||||
url: data['url'] as string, | |||||
folder: { id: data['root_folder_id'] } as Folder, | |||||
createdAt: new Date(data['created_at'] as string), | |||||
updatedAt: new Date(data['updated_at'] as string), | |||||
deletedAt: data['deleted_at'] ? new Date(data['deleted_at'] as string) : null, | |||||
} | |||||
} | |||||
static formatForView(item: Item) { | |||||
return { | |||||
...item, | |||||
createdAt: item.createdAt.toISOString(), | |||||
updatedAt: item.updatedAt.toISOString(), | |||||
deletedAt: item.deletedAt?.toISOString() || null, | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,21 @@ | |||||
import Folder from './Folder' | |||||
export default class User { | |||||
id?: string | |||||
name: string | |||||
rootFolder: Folder | |||||
createdAt: Date | |||||
updatedAt: Date | |||||
deletedAt?: Date | |||||
static resolve(data: Record<string, unknown>): User { | |||||
return { | |||||
id: data['id'] as string, | |||||
name: data['name'] as string, | |||||
rootFolder: { id: data['root_folder_id'] } as Folder, | |||||
createdAt: new Date(data['created_at'] as string), | |||||
updatedAt: new Date(data['updated_at'] as string), | |||||
deletedAt: data['deleted_at'] ? new Date(data['deleted_at'] as string) : null, | |||||
} | |||||
} | |||||
} |
@@ -1,12 +1,14 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import { UserProvider } from '@auth0/nextjs-auth0' | import { UserProvider } from '@auth0/nextjs-auth0' | ||||
import '../messages' | |||||
import '../styles/globals.css' | import '../styles/globals.css' | ||||
export default function App({ Component, pageProps }) { | |||||
const App = ({ Component, pageProps }) => { | |||||
return ( | return ( | ||||
<UserProvider> | <UserProvider> | ||||
<Component {...pageProps} /> | <Component {...pageProps} /> | ||||
</UserProvider> | </UserProvider> | ||||
); | ); | ||||
} | } | ||||
export default App |
@@ -0,0 +1,27 @@ | |||||
import {createDefaultAuth} from '../../../../utilities/auth' | |||||
import UserService from '../../../../domains/User/service' | |||||
const bindToUserAccount = async (req, res, session) => { | |||||
const userService = new UserService() | |||||
const username = session.user.name | |||||
const user = await userService.getByUsername(username) | |||||
const effectiveUser = user || await userService.createUser(username) | |||||
session.user.id = effectiveUser.id | |||||
session.user.rootFolder = effectiveUser.rootFolder | |||||
return session | |||||
} | |||||
export default async (req, res) => { | |||||
try { | |||||
const auth = createDefaultAuth() | |||||
await auth.handleCallback( | |||||
req, | |||||
res, | |||||
{ | |||||
afterCallback: bindToUserAccount, | |||||
} | |||||
) | |||||
} catch (err) { | |||||
res.status(err.status || 500).end(err.message) | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
import {createDefaultAuth} from '../../../../utilities/auth' | |||||
export default async (req, res) => { | |||||
try { | |||||
const auth = createDefaultAuth() | |||||
await auth.handleLogin(req, res) | |||||
} catch (err) { | |||||
res.status(err.status || 500).end(err.message) | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
import {createDefaultAuth} from '../../../../utilities/auth' | |||||
export default async (req, res) => { | |||||
try { | |||||
const auth = createDefaultAuth() | |||||
await auth.handleLogout(req, res) | |||||
} catch (err) { | |||||
res.status(err.status || 500).end(err.message) | |||||
} | |||||
} |
@@ -0,0 +1,12 @@ | |||||
import FolderPresenter from '../../../../domains/Folder/presenter' | |||||
import {forActionRoute} from '../../../../utilities/handler' | |||||
import '../../../../messages' | |||||
export default forActionRoute(async (req) => { | |||||
const folderController = new FolderPresenter() | |||||
return folderController.createFolder({ | |||||
name: req.body['name'], | |||||
parentId: req.body['parent_id'], | |||||
origin: req.headers['referer'], | |||||
}) | |||||
}) |
@@ -0,0 +1,10 @@ | |||||
import FolderPresenter from '../../../../domains/Folder/presenter' | |||||
import {forActionRoute} from '../../../../utilities/handler' | |||||
import '../../../../messages' | |||||
export default forActionRoute(async (req) => { | |||||
const folderController = new FolderPresenter() | |||||
return folderController.generateRedirectToCreateFolder({ | |||||
sourceURL: req.headers['referer'], | |||||
}) | |||||
}) |
@@ -1,3 +0,0 @@ | |||||
import { handleAuth } from '@auth0/nextjs-auth0' | |||||
export default handleAuth() |
@@ -0,0 +1,26 @@ | |||||
import FolderPresenter from '../../../domains/Folder/presenter' | |||||
import Method from '../../../utilities/handler/Method' | |||||
import {forResourceRoute} from '../../../utilities/handler' | |||||
import '../../../messages' | |||||
export default forResourceRoute(async (req) => { | |||||
const folderController = new FolderPresenter() | |||||
switch (req.method) { | |||||
case Method.GET: | |||||
return folderController.getFolder({ | |||||
id: req.query['id'], | |||||
}) | |||||
case Method.PUT: | |||||
// TODO replace folder | |||||
// should we need this method? | |||||
break | |||||
case Method.PATCH: | |||||
// TODO rename folder | |||||
break | |||||
case Method.DELETE: | |||||
// TODO soft-delete folder | |||||
break | |||||
default: | |||||
break | |||||
} | |||||
}) |
@@ -0,0 +1,20 @@ | |||||
import Method from '../../../utilities/handler/Method' | |||||
import FolderPresenter from '../../../domains/Folder/presenter' | |||||
import {forResourceRoute} from '../../../utilities/handler' | |||||
import '../../../messages' | |||||
export default forResourceRoute(async (req) => { | |||||
const folderController = new FolderPresenter() | |||||
switch (req.method) { | |||||
case Method.GET: | |||||
// GET all folders by the user | |||||
break | |||||
case Method.POST: | |||||
return folderController.createFolder({ | |||||
name: req.body['name'], | |||||
parentId: req.body['parent_id'] | |||||
}) | |||||
default: | |||||
break | |||||
} | |||||
}) |
@@ -1,14 +1,5 @@ | |||||
import { getSession } from '@auth0/nextjs-auth0' | |||||
import { GetServerSideProps } from 'next' | |||||
import Head from 'next/head' | |||||
import styled from 'styled-components' | |||||
const Container = styled('div')({ | |||||
maxWidth: 720, | |||||
margin: '0 auto', | |||||
padding: '0 1rem', | |||||
boxSizing: 'border-box', | |||||
}) | |||||
import { createDefaultAuth } from '../utilities/auth' | |||||
import { GetServerSideProps } from 'next' | |||||
const Home = () => { | const Home = () => { | ||||
return ( | return ( | ||||
@@ -17,18 +8,18 @@ const Home = () => { | |||||
} | } | ||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { | export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { | ||||
const session = getSession(req, res) | |||||
const auth = createDefaultAuth() | |||||
const session = auth.getSession(req, res) | |||||
if (!session) { | if (!session) { | ||||
return { | return { | ||||
redirect: { | redirect: { | ||||
destination: '/api/auth/login', | |||||
destination: '/api/a/auth/log-in', | |||||
permanent: false, | permanent: false, | ||||
}, | }, | ||||
} | } | ||||
} | } | ||||
return { | return { | ||||
redirect: { | redirect: { | ||||
destination: '/my/folders', | destination: '/my/folders', | ||||
@@ -1,194 +0,0 @@ | |||||
import { GetServerSideProps } from 'next' | |||||
import FolderTemplate from '../../../components/templates/Folder' | |||||
const Home = ({ path, id, children, }) => { | |||||
return ( | |||||
<FolderTemplate | |||||
path={path} | |||||
id={id} | |||||
children={children} | |||||
/> | |||||
) | |||||
} | |||||
export const getServerSideProps: GetServerSideProps = async ({ query: nextQuery }) => { | |||||
const { q: query } = nextQuery | |||||
return { | |||||
props: { | |||||
query, | |||||
items: [ | |||||
{ | |||||
name: 'Bruh', | |||||
url: 'https://placehold.it/300', | |||||
} | |||||
], | |||||
children: [ | |||||
{ | |||||
id: '00000000-0000-0000-000000000001', | |||||
name: 'Foo Folder 1', | |||||
children: [ | |||||
{ | |||||
id: '00000000-0000-0000-000000000002', | |||||
name: 'Bar Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '00000000-0000-0000-000000000003', | |||||
name: 'Bar Folder 2', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '00000000-0000-0000-000000000004', | |||||
name: 'Bar Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '00000000-0000-0000-000000000005', | |||||
name: 'Baz Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '00000000-0000-0000-00000000006', | |||||
name: 'Baz Folder 2 With A Very Long Name That Causes Overflow Somehow', | |||||
children: [], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
{ | |||||
id: '10000000-0000-0000-000000000001', | |||||
name: 'Foo Folder 2', | |||||
children: [ | |||||
{ | |||||
id: '10000000-0000-0000-000000000002', | |||||
name: 'Bar Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '10000000-0000-0000-000000000003', | |||||
name: 'Bar Folder 2', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '10000000-0000-0000-000000000004', | |||||
name: 'Bar Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '10000000-0000-0000-000000000005', | |||||
name: 'Baz Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '10000000-0000-0000-00000000006', | |||||
name: 'Baz Folder 2 With A Very Long Name That Causes Overflow Somehow', | |||||
children: [], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
{ | |||||
id: '20000000-0000-0000-000000000001', | |||||
name: 'Foo Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '20000000-0000-0000-000000000002', | |||||
name: 'Bar Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '20000000-0000-0000-000000000003', | |||||
name: 'Bar Folder 2', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '20000000-0000-0000-000000000004', | |||||
name: 'Bar Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '20000000-0000-0000-000000000005', | |||||
name: 'Baz Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '20000000-0000-0000-00000000006', | |||||
name: 'Baz Folder 2 With A Very Long Name That Causes Overflow Somehow', | |||||
children: [], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
{ | |||||
id: '30000000-0000-0000-000000000001', | |||||
name: 'Foo Folder 4', | |||||
children: [ | |||||
{ | |||||
id: '30000000-0000-0000-000000000002', | |||||
name: 'Bar Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '30000000-0000-0000-000000000003', | |||||
name: 'Bar Folder 2', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '30000000-0000-0000-000000000004', | |||||
name: 'Bar Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '30000000-0000-0000-000000000005', | |||||
name: 'Baz Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '30000000-0000-0000-00000000006', | |||||
name: 'Baz Folder 2 With A Very Long Name That Causes Overflow Somehow', | |||||
children: [], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
{ | |||||
id: '40000000-0000-0000-000000000001', | |||||
name: 'Foo Folder 5', | |||||
children: [ | |||||
{ | |||||
id: '40000000-0000-0000-000000000002', | |||||
name: 'Bar Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '40000000-0000-0000-000000000003', | |||||
name: 'Bar Folder 2', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '40000000-0000-0000-000000000004', | |||||
name: 'Bar Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '40000000-0000-0000-000000000005', | |||||
name: 'Baz Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '40000000-0000-0000-00000000006', | |||||
name: 'Baz Folder 2 With A Very Long Name That Causes Overflow Somehow', | |||||
children: [], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
} | |||||
} | |||||
export default Home |
@@ -0,0 +1,37 @@ | |||||
import { GetServerSideProps } from 'next' | |||||
import FolderTemplate from '../../../components/templates/Folder' | |||||
import FolderPresenter from '../../../domains/Folder/presenter' | |||||
import {createDefaultAuth} from '../../../utilities/auth'; | |||||
import {forGetServerSideProps} from '../../../utilities/handler' | |||||
import FolderViewMode from '../../../models/FolderViewMode' | |||||
const DescendantFolder = ({ children, items, mode, hierarchy, query, }) => { | |||||
return ( | |||||
<FolderTemplate | |||||
query={query} | |||||
children={children} | |||||
items={items} | |||||
mode={mode} | |||||
hierarchy={hierarchy} | |||||
/> | |||||
) | |||||
} | |||||
export const getServerSideProps: GetServerSideProps = forGetServerSideProps((ctx) => { | |||||
const auth = createDefaultAuth() | |||||
const session = auth.getSession(ctx.req, ctx.res) | |||||
const folderController = new FolderPresenter() | |||||
const { | |||||
'q': query = '', | |||||
'mode': mode = FolderViewMode.DEFAULT, | |||||
'id': id, | |||||
} = ctx.query | |||||
return folderController.getDescendantFolderViewState({ | |||||
session, | |||||
query, | |||||
mode, | |||||
id, | |||||
}) | |||||
}) | |||||
export default DescendantFolder |
@@ -1,196 +1,35 @@ | |||||
import { GetServerSideProps } from 'next' | import { GetServerSideProps } from 'next' | ||||
import FolderTemplate from '../../../components/templates/Folder' | import FolderTemplate from '../../../components/templates/Folder' | ||||
import FolderPresenter from '../../../domains/Folder/presenter' | |||||
import {createDefaultAuth} from '../../../utilities/auth'; | |||||
import {forGetServerSideProps} from '../../../utilities/handler' | |||||
import FolderViewMode from '../../../models/FolderViewMode'; | |||||
const Home = ({ path, id, children, items, }) => { | |||||
const RootFolder = ({ children, items, mode, hierarchy, query, }) => { | |||||
return ( | return ( | ||||
<FolderTemplate | <FolderTemplate | ||||
path={path} | |||||
id={id} | |||||
query={query} | |||||
children={children} | children={children} | ||||
items={items} | items={items} | ||||
mode={mode} | |||||
hierarchy={hierarchy} | |||||
/> | /> | ||||
) | ) | ||||
} | } | ||||
export const getServerSideProps: GetServerSideProps = async ({ query: nextQuery }) => { | |||||
const { q: query = '' } = nextQuery | |||||
return { | |||||
props: { | |||||
query, | |||||
items: new Array(24).fill(0).map((_, i) => ( | |||||
{ | |||||
id: `11111111-1111-1111-11111111111${i}`, | |||||
name: 'Bruh', | |||||
url: 'http://placehold.it/300', | |||||
} | |||||
)), | |||||
children: [ | |||||
{ | |||||
id: '00000000-0000-0000-000000000001', | |||||
name: 'Foo Folder 1', | |||||
children: [ | |||||
{ | |||||
id: '00000000-0000-0000-000000000002', | |||||
name: 'Bar Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '00000000-0000-0000-000000000003', | |||||
name: 'Bar Folder 2', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '00000000-0000-0000-000000000004', | |||||
name: 'Bar Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '00000000-0000-0000-000000000005', | |||||
name: 'Baz Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '00000000-0000-0000-00000000006', | |||||
name: 'Baz Folder 2 With A Very Long Name That Causes Overflow Somehow', | |||||
children: [], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
export const getServerSideProps: GetServerSideProps = forGetServerSideProps((ctx) => { | |||||
const auth = createDefaultAuth() | |||||
const session = auth.getSession(ctx.req, ctx.res) | |||||
const folderController = new FolderPresenter() | |||||
const { | |||||
'q': query = '', | |||||
'mode': mode = FolderViewMode.DEFAULT, | |||||
} = ctx.query | |||||
return folderController.getRootFolderViewState({ | |||||
session, | |||||
query, | |||||
mode, | |||||
}) | |||||
}) | |||||
{ | |||||
id: '10000000-0000-0000-000000000001', | |||||
name: 'Foo Folder 2', | |||||
children: [ | |||||
{ | |||||
id: '10000000-0000-0000-000000000002', | |||||
name: 'Bar Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '10000000-0000-0000-000000000003', | |||||
name: 'Bar Folder 2', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '10000000-0000-0000-000000000004', | |||||
name: 'Bar Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '10000000-0000-0000-000000000005', | |||||
name: 'Baz Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '10000000-0000-0000-00000000006', | |||||
name: 'Baz Folder 2 With A Very Long Name That Causes Overflow Somehow', | |||||
children: [], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
{ | |||||
id: '20000000-0000-0000-000000000001', | |||||
name: 'Foo Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '20000000-0000-0000-000000000002', | |||||
name: 'Bar Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '20000000-0000-0000-000000000003', | |||||
name: 'Bar Folder 2', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '20000000-0000-0000-000000000004', | |||||
name: 'Bar Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '20000000-0000-0000-000000000005', | |||||
name: 'Baz Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '20000000-0000-0000-00000000006', | |||||
name: 'Baz Folder 2 With A Very Long Name That Causes Overflow Somehow', | |||||
children: [], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
{ | |||||
id: '30000000-0000-0000-000000000001', | |||||
name: 'Foo Folder 4', | |||||
children: [ | |||||
{ | |||||
id: '30000000-0000-0000-000000000002', | |||||
name: 'Bar Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '30000000-0000-0000-000000000003', | |||||
name: 'Bar Folder 2', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '30000000-0000-0000-000000000004', | |||||
name: 'Bar Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '30000000-0000-0000-000000000005', | |||||
name: 'Baz Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '30000000-0000-0000-00000000006', | |||||
name: 'Baz Folder 2 With A Very Long Name That Causes Overflow Somehow', | |||||
children: [], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
{ | |||||
id: '40000000-0000-0000-000000000001', | |||||
name: 'Foo Folder 5', | |||||
children: [ | |||||
{ | |||||
id: '40000000-0000-0000-000000000002', | |||||
name: 'Bar Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '40000000-0000-0000-000000000003', | |||||
name: 'Bar Folder 2', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '40000000-0000-0000-000000000004', | |||||
name: 'Bar Folder 3', | |||||
children: [ | |||||
{ | |||||
id: '40000000-0000-0000-000000000005', | |||||
name: 'Baz Folder 1', | |||||
children: [], | |||||
}, | |||||
{ | |||||
id: '40000000-0000-0000-00000000006', | |||||
name: 'Baz Folder 2 With A Very Long Name That Causes Overflow Somehow', | |||||
children: [], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
], | |||||
}, | |||||
} | |||||
} | |||||
export default Home | |||||
export default RootFolder |
@@ -0,0 +1,24 @@ | |||||
const createFetchClient = ({ baseURL, headers: baseHeaders = {} }) => { | |||||
return async ({ | |||||
method = 'GET', | |||||
url, | |||||
query = undefined, | |||||
headers = {}, | |||||
body = undefined, | |||||
}) => { | |||||
const theURL = new URL(url, baseURL) | |||||
if (query) { | |||||
theURL.search = new URLSearchParams(query).toString() | |||||
} | |||||
return fetch(theURL.toString(), { | |||||
method, | |||||
body, | |||||
headers: { | |||||
...baseHeaders, | |||||
...headers, | |||||
} | |||||
}) | |||||
} | |||||
} | |||||
export default createFetchClient |
@@ -0,0 +1,28 @@ | |||||
import { initAuth0 } from '@auth0/nextjs-auth0' | |||||
const createAuth = (params) => { | |||||
return initAuth0({ | |||||
...params, | |||||
enableTelemetry: false, | |||||
routes: { | |||||
callback: '/api/a/auth/callback', | |||||
postLogoutRedirect: process.env.BASE_URL, | |||||
}, | |||||
}) | |||||
} | |||||
export const createDefaultAuth = () => createAuth({ | |||||
secret: process.env.AUTH_SECRET, | |||||
issuerBaseURL: process.env.AUTH_ISSUER_BASE_URL, | |||||
baseURL: process.env.BASE_URL, | |||||
clientID: process.env.AUTH_CLIENT_ID, | |||||
clientSecret: process.env.AUTH_CLIENT_SECRET | |||||
}) | |||||
export class SessionNotFoundError extends Error { | |||||
constructor() { | |||||
super('No session found.') | |||||
} | |||||
} | |||||
export default createAuth |
@@ -0,0 +1,51 @@ | |||||
import * as React from 'react' | |||||
import {useForm} from 'react-hook-form' | |||||
import Method from './Method' | |||||
type Props = { | |||||
method: Method, | |||||
action: string, | |||||
href?: string, | |||||
headers?: Record<string, string>, | |||||
onResponse?: (...args: unknown[]) => unknown, | |||||
onError?: (...args: unknown[]) => unknown, | |||||
onSubmit?: (...args: unknown[]) => unknown, | |||||
children: (...args: unknown[]) => unknown, | |||||
} | |||||
const Form: React.FC<Props> = ({ | |||||
method, | |||||
action, | |||||
href, | |||||
children, | |||||
onResponse, | |||||
onError, | |||||
headers, | |||||
onSubmit, | |||||
}) => { | |||||
const onFormSubmit = async (body) => { | |||||
const response = await fetch(href, { | |||||
method, | |||||
body, | |||||
headers, | |||||
}) | |||||
const data = await response.json() | |||||
if (typeof (onResponse as unknown) === 'function') { | |||||
onResponse(data) | |||||
} | |||||
} | |||||
const {register, handleSubmit} = useForm() | |||||
return ( | |||||
<form | |||||
method={method === Method.GET ? 'get' : 'post'} | |||||
action={action} | |||||
onSubmit={handleSubmit(onSubmit || onFormSubmit, onError)} | |||||
> | |||||
{children({ register })} | |||||
</form> | |||||
) | |||||
} | |||||
export default Form |
@@ -0,0 +1,9 @@ | |||||
enum Method { | |||||
GET = 'GET', | |||||
POST = 'POST', | |||||
PUT = 'PUT', | |||||
PATCH = 'PATCH', | |||||
DELETE = 'DELETE', | |||||
} | |||||
export default Method |
@@ -0,0 +1,74 @@ | |||||
export { default as Form } from './Form' | |||||
// for noscript form submissions | |||||
export const forActionRoute = (fn) => async (req, res) => { | |||||
const { redirect, body, status, title, } = await fn(req, res) | |||||
if (redirect) { | |||||
res.redirect(redirect.url.toString()) | |||||
return | |||||
} | |||||
res.status(status) | |||||
res.statusMessage = title | |||||
res.json(body.data) | |||||
} | |||||
// for fetching data for SSR of pages | |||||
export const forGetServerSideProps = (fn) => async (ctx) => { | |||||
const { redirect, body, status, } = await fn(ctx) | |||||
if (redirect) { | |||||
return { | |||||
redirect: { | |||||
destination: redirect.url.toString(), | |||||
permanent: Boolean(redirect.permanent), | |||||
}, | |||||
props: body.data, | |||||
} | |||||
} | |||||
if (status === 404) { | |||||
return { | |||||
notFound: true, | |||||
props: body.data, | |||||
} | |||||
} | |||||
return { | |||||
props: body.data, | |||||
} | |||||
} | |||||
// for handling of AJAX (fetch) requests from clients | |||||
export const forResourceRoute = (fn) => async (req, res) => { | |||||
const { body, status, title, } = await fn(req) || { | |||||
status: 405, | |||||
} | |||||
res.status(status) | |||||
if (title) { | |||||
res.statusMessage = title | |||||
} | |||||
if (body) { | |||||
res.json(body) | |||||
return | |||||
} | |||||
res.end('') | |||||
} | |||||
// for form submit handlers | |||||
export const forClientSideState = ({ router, ...state }) => (fn) => async (e) => { | |||||
const { redirect, status, } = await fn(state) | |||||
if (redirect) { | |||||
if (redirect.permanent) { | |||||
router.replace(redirect.url) | |||||
} else { | |||||
router.push(redirect.url) | |||||
} | |||||
return | |||||
} | |||||
if (status > 299) { | |||||
// noop | |||||
return | |||||
} | |||||
} | |||||
@@ -0,0 +1,3 @@ | |||||
import { useForm } from 'react-hook-form' | |||||
export default useForm |
@@ -0,0 +1,25 @@ | |||||
class Messages { | |||||
private readonly messageStore = {} | |||||
private locale: string | |||||
registerLocale(locale: string, messages: Record<string, string>) { | |||||
this.messageStore[locale] = messages | |||||
this.locale = this.locale || locale | |||||
} | |||||
setLocale(locale: string) { | |||||
this.locale = locale | |||||
} | |||||
translate(msgId: string) { | |||||
return this.messageStore[this.locale][msgId] | |||||
} | |||||
} | |||||
const defaultMessages = new Messages() | |||||
export const registerLocale = defaultMessages.registerLocale.bind(defaultMessages) | |||||
export const setLocale = defaultMessages.setLocale.bind(defaultMessages) | |||||
export const _ = defaultMessages.translate.bind(defaultMessages) |