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 | |||
.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`. | |||
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 | |||
@@ -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" | |||
} | |||
} |
@@ -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' | |||
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 |
@@ -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> | |||
</> | |||
@@ -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 { 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 |
@@ -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 = () => { | |||
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', | |||
@@ -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 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 |
@@ -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) |