Browse Source

Add route adapters

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
TheoryOfNekomata 3 years ago
parent
commit
6e9dc4afce
36 changed files with 1826 additions and 476 deletions
  1. +2
    -0
      .gitignore
  2. +1
    -1
      README.md
  3. +9
    -2
      package.json
  4. +46
    -0
      scripts/migrate.js
  5. +11
    -3
      src/components/molecules/TextInput/index.tsx
  6. +163
    -59
      src/components/templates/Folder/index.tsx
  7. +143
    -0
      src/domains/Folder/presenter.ts
  8. +106
    -0
      src/domains/Folder/service.ts
  9. +67
    -0
      src/domains/User/service.ts
  10. +12
    -0
      src/messages/en/PH.json
  11. +5
    -0
      src/messages/index.ts
  12. +28
    -0
      src/models/Folder.ts
  13. +7
    -0
      src/models/FolderViewMode.ts
  14. +32
    -0
      src/models/Item.ts
  15. +21
    -0
      src/models/User.ts
  16. +4
    -2
      src/pages/_app.tsx
  17. +27
    -0
      src/pages/api/a/auth/callback.ts
  18. +10
    -0
      src/pages/api/a/auth/log-in.ts
  19. +10
    -0
      src/pages/api/a/auth/log-out.ts
  20. +12
    -0
      src/pages/api/a/folders/create.ts
  21. +10
    -0
      src/pages/api/a/folders/new.ts
  22. +0
    -3
      src/pages/api/auth/[...auth0].ts
  23. +26
    -0
      src/pages/api/folders/[id].ts
  24. +20
    -0
      src/pages/api/folders/index.ts
  25. +5
    -14
      src/pages/index.tsx
  26. +0
    -194
      src/pages/my/folders/[...path].tsx
  27. +37
    -0
      src/pages/my/folders/[id].tsx
  28. +23
    -184
      src/pages/my/folders/index.tsx
  29. +24
    -0
      src/utilities/api/FetchClient.ts
  30. +28
    -0
      src/utilities/auth.ts
  31. +51
    -0
      src/utilities/handler/Form.tsx
  32. +9
    -0
      src/utilities/handler/Method.ts
  33. +74
    -0
      src/utilities/handler/index.ts
  34. +3
    -0
      src/utilities/handler/useForm.ts
  35. +25
    -0
      src/utilities/messages.ts
  36. +775
    -14
      yarn.lock

+ 2
- 0
.gitignore View File

@@ -35,3 +35,5 @@ yarn-error.log*
.vercel

.vs/
.idea/
*.sqlite

+ 1
- 1
README.md View File

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

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



+ 9
- 2
package.json View File

@@ -5,7 +5,8 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"migrate": "node ./scripts/migrate.js"
},
"dependencies": {
"@auth0/nextjs-auth0": "^1.3.0",
@@ -14,12 +15,18 @@
"react": "17.0.2",
"react-dom": "17.0.2",
"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": {
"@types/node": "^14.14.37",
"@types/react": "^17.0.3",
"@types/sqlite3": "^3.1.7",
"@types/styled-components": "^5.1.9",
"@types/uuid": "^8.3.0",
"dotenv": "^8.2.0",
"typescript": "^4.2.3"
}
}

+ 46
- 0
scripts/migrate.js View File

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

+ 11
- 3
src/components/molecules/TextInput/index.tsx View File

@@ -1,3 +1,4 @@
import * as React from 'react'
import styled from 'styled-components'
const Base = styled('input')({
@@ -12,10 +13,17 @@ const Base = styled('input')({
cursor: 'pointer',
})
const TextInput = (props) => {
type Props = {
name: string,
}
const TextInput = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
return (
<Base {...props} />
<Base
{...props}
ref={ref}
/>
)
}
})
export default TextInput

+ 163
- 59
src/components/templates/Folder/index.tsx View File

@@ -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({
':root': {
@@ -71,22 +76,22 @@ const SidebarContents = styled('div')({
position: 'relative',
})

const Tree = styled('div')({
const FolderListWrapper = styled('div')({
width: '100%',
height: '100%',
overflow: 'auto',
})

const TreeParent = styled('ul')({
const FolderList = styled('ul')({
margin: 0,
padding: 0,
})

const TreeChild = styled('li')({
const FolderListItem = styled('li')({
display: 'block',
})

const TreeActions = styled('div')({
const FolderActions = styled('div')({
position: 'sticky',
display: 'flex',
justifyContent: 'flex-end',
@@ -100,17 +105,15 @@ const TreeActions = styled('div')({
boxSizing: 'border-box',
})

const TreeLink = styled('a')({
const FolderLink = styled('a')({
display: 'inline-flex',
minWidth: '100%',
verticalAlign: 'top',
height: '2rem',
height: '3rem',
alignItems: 'center',
whiteSpace: 'nowrap',
padding: '0 1rem',
boxSizing: 'border-box',
[`+ ${TreeParent}`]: {
paddingLeft: '1.5rem',
},
})

const FolderGrid = styled('div')({
@@ -142,30 +145,39 @@ const Header = styled('div')({
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 = ({
path = [],
query = '',
id = '',
query,
children,
items = [],
mode,
hierarchy,
}) => {
const router = useRouter()
const [currentFolder] = hierarchy.slice(-1)
return (
<>
<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" />
</Head>
<Variables />
@@ -217,16 +229,69 @@ const FolderTemplate = ({
</SidebarLink>
</Link>
<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>
</SidebarContents>
</SideBar>
@@ -234,9 +299,33 @@ const FolderTemplate = ({
<Header>
<FolderInfoContainer>
<div>
Folder ID: {id}
{hierarchy.map(h => (
<>
{'/ '}
<Link
href={{
pathname: '/my/folders/[id]',
query: {
id: h.id,
},
}}
>
<a>
{h.name}
</a>
</Link>
{' '}
</>
))}
</div>
<div>
<FolderIdBox>
Folder ID
<FolderIdInput
readOnly
value={currentFolder.id}
/>
</FolderIdBox>
<Button>
Delete
</Button>
@@ -244,22 +333,37 @@ const FolderTemplate = ({
</FolderInfoContainer>
</Header>
<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>
</Main>
</>


+ 143
- 0
src/domains/Folder/presenter.ts View File

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

+ 106
- 0
src/domains/Folder/service.ts View File

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

+ 67
- 0
src/domains/User/service.ts View File

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

+ 12
- 0
src/messages/en/PH.json View File

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

+ 5
- 0
src/messages/index.ts View File

@@ -0,0 +1,5 @@
import { registerLocale, setLocale } from '../utilities/messages'
import enPH from './en/PH.json'

registerLocale('en-PH', enPH)
setLocale('en-PH')

+ 28
- 0
src/models/Folder.ts View File

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

+ 7
- 0
src/models/FolderViewMode.ts View File

@@ -0,0 +1,7 @@
enum FolderViewMode {
DEFAULT = '',
NEW_FOLDER = 'new_folder',
DELETE_FOLDER = 'delete_folder',
}

export default FolderViewMode

+ 32
- 0
src/models/Item.ts View File

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

+ 21
- 0
src/models/User.ts View File

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

+ 4
- 2
src/pages/_app.tsx View File

@@ -1,12 +1,14 @@
import * as React from 'react'
import { UserProvider } from '@auth0/nextjs-auth0'
import '../messages'
import '../styles/globals.css'

export default function App({ Component, pageProps }) {
const App = ({ Component, pageProps }) => {
return (
<UserProvider>
<Component {...pageProps} />
</UserProvider>
);
}

export default App

+ 27
- 0
src/pages/api/a/auth/callback.ts View File

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

+ 10
- 0
src/pages/api/a/auth/log-in.ts View File

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

+ 10
- 0
src/pages/api/a/auth/log-out.ts View File

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

+ 12
- 0
src/pages/api/a/folders/create.ts View File

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

+ 10
- 0
src/pages/api/a/folders/new.ts View File

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

+ 0
- 3
src/pages/api/auth/[...auth0].ts View File

@@ -1,3 +0,0 @@
import { handleAuth } from '@auth0/nextjs-auth0'
export default handleAuth()

+ 26
- 0
src/pages/api/folders/[id].ts View File

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

+ 20
- 0
src/pages/api/folders/index.ts View File

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

+ 5
- 14
src/pages/index.tsx View File

@@ -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 = () => {
return (
@@ -17,18 +8,18 @@ const Home = () => {
}

export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
const session = getSession(req, res)
const auth = createDefaultAuth()
const session = auth.getSession(req, res)

if (!session) {
return {
redirect: {
destination: '/api/auth/login',
destination: '/api/a/auth/log-in',
permanent: false,
},
}
}


return {
redirect: {
destination: '/my/folders',


+ 0
- 194
src/pages/my/folders/[...path].tsx View File

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

+ 37
- 0
src/pages/my/folders/[id].tsx View File

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

+ 23
- 184
src/pages/my/folders/index.tsx View File

@@ -1,196 +1,35 @@
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 Home = ({ path, id, children, items, }) => {
const RootFolder = ({ children, items, mode, hierarchy, query, }) => {
return (
<FolderTemplate
path={path}
id={id}
query={query}
children={children}
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

+ 24
- 0
src/utilities/api/FetchClient.ts View File

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

+ 28
- 0
src/utilities/auth.ts View File

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

+ 51
- 0
src/utilities/handler/Form.tsx View File

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

+ 9
- 0
src/utilities/handler/Method.ts View File

@@ -0,0 +1,9 @@
enum Method {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
PATCH = 'PATCH',
DELETE = 'DELETE',
}

export default Method

+ 74
- 0
src/utilities/handler/index.ts View File

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


+ 3
- 0
src/utilities/handler/useForm.ts View File

@@ -0,0 +1,3 @@
import { useForm } from 'react-hook-form'

export default useForm

+ 25
- 0
src/utilities/messages.ts View File

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

+ 775
- 14
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save