1
0
Fork 0
mirror of https://github.com/kou029w/_.git synced 2025-01-30 22:08:02 +00:00

create frourio project

This commit is contained in:
Nebel 2020-10-12 13:35:33 +09:00
parent fbc5a580bc
commit bfd67b52e7
50 changed files with 11108 additions and 0 deletions

5
frourio/.eslintignore Normal file
View file

@ -0,0 +1,5 @@
node_modules
.next
dist
server/migration
server/index.js

38
frourio/.eslintrc.js Normal file
View file

@ -0,0 +1,38 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:prettier/recommended',
'prettier/@typescript-eslint'
],
plugins: ['@typescript-eslint', 'react'],
parser: '@typescript-eslint/parser',
env: {
browser: true,
node: true,
es6: true
},
settings: {
react: {
version: 'detect'
}
},
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off'
},
overrides: [
{
files: ['*.js'],
rules: { '@typescript-eslint/no-var-requires': ['off'] }
}
]
}

40
frourio/.gitignore vendored Normal file
View file

@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# server
server/**/$*.ts
server/**/*.db
server/index.js
server/database.json
server/.upload
server/public/icons/*
!server/public/icons/dammy.svg

5
frourio/.prettierrc Normal file
View file

@ -0,0 +1,5 @@
{
"semi": false,
"trailingComma": "none",
"singleQuote": true
}

3
frourio/.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "prisma.prisma"]
}

6
frourio/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"eslint.run": "onSave",
"editor.codeActionsOnSave": { "source.fixAll.eslint": true },
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib"
}

30
frourio/README.md Normal file
View file

@ -0,0 +1,30 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

6
frourio/aspida.config.js Normal file
View file

@ -0,0 +1,6 @@
require('dotenv').config({ path: 'server/.env' })
module.exports = {
input: 'server/api',
baseURL: `${process.env.API_ORIGIN || ''}${process.env.BASE_PATH || ''}`
}

View file

@ -0,0 +1,65 @@
import { useState, useCallback, ChangeEvent } from 'react'
import styles from '~/styles/UserBanner.module.css'
import { apiClient } from '~/utils/apiClient'
import { UserInfo } from '$/types'
const UserBanner = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [token, setToken] = useState('')
const [userInfo, setUserInfo] = useState({} as UserInfo)
const editIcon = useCallback(
async (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files?.length) return
setUserInfo(
await apiClient.user.$post({
headers: { token },
body: { icon: e.target.files[0] }
})
)
},
[token]
)
const login = useCallback(async () => {
const id = prompt('Enter the user id (See server/.env)')
const pass = prompt('Enter the user pass (See server/.env)')
if (!id || !pass) return alert('Login failed')
let newToken = ''
try {
newToken = (await apiClient.token.$post({ body: { id, pass } })).token
setToken(newToken)
} catch (e) {
return alert('Login failed')
}
setUserInfo(await apiClient.user.$get({ headers: { token: newToken } }))
setIsLoggedIn(true)
}, [])
const logout = useCallback(async () => {
await apiClient.token.delete({ headers: { token } })
setToken('')
setIsLoggedIn(false)
}, [token])
return (
<div className={styles.userBanner}>
{isLoggedIn ? (
<div>
<img src={userInfo.icon} className={styles.userIcon} />
<span>{userInfo.name}</span>
<input type="file" accept="image/*" onChange={editIcon} />
<button onClick={logout}>LOGOUT</button>
</div>
) : (
<button onClick={login}>LOGIN</button>
)}
</div>
)
}
export default UserBanner

2
frourio/next-env.d.ts vendored Normal file
View file

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

54
frourio/package.json Normal file
View file

@ -0,0 +1,54 @@
{
"name": "frourio",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "npm run migrate:up && run-p dev:*",
"dev:front": "next dev",
"dev:server": "cd server && node_modules/.bin/webpack --watch --mode=development",
"dev:aspida": "aspida --watch",
"dev:frourio": "cd server && node_modules/.bin/frourio --watch",
"dev:prisma": "cd server && node_modules/.bin/prisma generate --watch",
"build": "run-p build:front build:server",
"build:front": "aspida && next build && next export",
"build:server": "npm run migrate:up && npm run build:frourio && cd server && node_modules/.bin/webpack --mode=production",
"build:frourio": "cd server && node_modules/.bin/prisma generate && node_modules/.bin/frourio",
"build:types": "aspida && npm run build:frourio",
"install:server": "cd server && yarn install",
"lint": "eslint --ext .ts,.js,.tsx .",
"lint:fix": "npm run lint -- --fix",
"start": "run-p start:*",
"start:front": "next start",
"start:server": "cd server && cross-env NODE_ENV=production node index.js",
"typecheck": "npm run build:types && tsc --noEmit && tsc --noEmit -p server",
"migrate": "npm run migrate:save && npm run migrate:up",
"migrate:save": "cd server && node_modules/.bin/prisma migrate save --create-db --experimental",
"migrate:up": "cd server && node_modules/.bin/prisma migrate up --create-db --experimental",
"migrate:down": "cd server && node_modules/.bin/prisma migrate down --experimental"
},
"dependencies": {
"@aspida/fetch": "^0.10.0",
"@aspida/swr": "^0.2.0",
"class-validator": "^0.12.2",
"next": "9.5.3",
"react": "16.13.1",
"react-dom": "16.13.1",
"swr": "^0.3.5"
},
"devDependencies": {
"@types/node": "^14.11.2",
"@types/react": "^16.9.49",
"@typescript-eslint/eslint-plugin": "^4.3.0",
"@typescript-eslint/parser": "^4.3.0",
"cross-env": "^7.0.2",
"dotenv": "^8.2.0",
"eslint": "^7.10.0",
"eslint-config-prettier": "^6.12.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.2",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.2",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3"
}
}

8
frourio/pages/_app.tsx Normal file
View file

@ -0,0 +1,8 @@
import { AppProps } from 'next/app'
import '../styles/globals.css'
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default MyApp

100
frourio/pages/index.tsx Normal file
View file

@ -0,0 +1,100 @@
import Head from 'next/head'
import { useCallback, useState, FormEvent, ChangeEvent } from 'react'
import useAspidaSWR from '@aspida/swr'
import styles from '~/styles/Home.module.css'
import { apiClient } from '~/utils/apiClient'
import { Task } from '$/types'
import UserBanner from '~/components/UserBanner'
const Home = () => {
const { data: tasks, error, mutate: setTasks } = useAspidaSWR(apiClient.tasks)
const [label, setLabel] = useState('')
const inputLavel = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setLabel(e.target.value),
[]
)
const createTask = useCallback(
async (e: FormEvent) => {
e.preventDefault()
if (!label) return
await apiClient.tasks.post({ body: { label } })
setLabel('')
setTasks(await apiClient.tasks.$get())
},
[label]
)
const toggleDone = useCallback(async (task: Task) => {
await apiClient.tasks._taskId(task.id).patch({ body: { done: !task.done } })
setTasks(await apiClient.tasks.$get())
}, [])
const deleteTask = useCallback(async (task: Task) => {
await apiClient.tasks._taskId(task.id).delete()
setTasks(await apiClient.tasks.$get())
}, [])
if (error) return <div>failed to load</div>
if (!tasks) return <div>loading...</div>
return (
<div className={styles.container}>
<Head>
<title>frourio-todo-app</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<UserBanner />
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<p className={styles.description}>frourio-todo-app</p>
<div>
<form style={{ textAlign: 'center' }} onSubmit={createTask}>
<input value={label} type="text" onChange={inputLavel} />
<input type="submit" value="ADD" />
</form>
<ul className={styles.tasks}>
{tasks.map((task) => (
<li key={task.id}>
<label>
<input
type="checkbox"
checked={task.done}
onChange={() => toggleDone(task)}
/>
<span>{task.label}</span>
</label>
<input
type="button"
value="DELETE"
style={{ float: 'right' }}
onClick={() => deleteTask(task)}
/>
</li>
))}
</ul>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<img src="/vercel.svg" alt="Vercel Logo" className={styles.logo} />
</a>
</footer>
</div>
)
}
export default Home

BIN
frourio/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

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

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,5 @@
SERVER_PORT=8080
BASE_PATH=/api
API_ORIGIN=http://localhost:8080
USER_ID=id
USER_PASS=pass

View file

@ -0,0 +1,5 @@
import { defineController } from './$relay'
export default defineController(() => ({
get: () => ({ status: 200, body: 'Hello, world!' })
}))

View file

@ -0,0 +1,5 @@
export type Methods = {
get: {
resBody: string
}
}

View file

@ -0,0 +1,13 @@
import { defineController } from './$relay'
import { updateTask, deleteTask } from '$/service/tasks'
export default defineController(() => ({
patch: async ({ body, params }) => {
await updateTask(params.taskId, body)
return { status: 204 }
},
delete: async ({ params }) => {
await deleteTask(params.taskId)
return { status: 204 }
}
}))

View file

@ -0,0 +1,11 @@
import { Task } from '$/types'
export type Methods = {
patch: {
reqBody: Partial<Pick<Task, 'label' | 'done'>>
status: 204
}
delete: {
status: 204
}
}

View file

@ -0,0 +1,10 @@
import { defineController } from './$relay'
import { getTasks, createTask } from '$/service/tasks'
export default defineController(() => ({
get: async () => ({ status: 200, body: await getTasks() }),
post: async ({ body }) => ({
status: 201,
body: await createTask(body.label)
})
}))

View file

@ -0,0 +1,15 @@
import { Task } from '$/types'
export type Methods = {
get: {
query?: {
limit?: number
}
resBody: Task[]
}
post: {
reqBody: Pick<Task, 'label'>
resBody: Task
}
}

View file

@ -0,0 +1,14 @@
import { defineController } from './$relay'
import { validateUser, createToken, deleteToken } from '$/service/user'
export default defineController(() => ({
post: ({ body }) =>
validateUser(body.id, body.pass)
? { status: 201, body: { token: createToken() } }
: { status: 401 },
delete: ({ headers }) => {
deleteToken(headers.token)
return { status: 204 }
}
}))

View file

@ -0,0 +1,14 @@
import { LoginBody, TokenHeader } from '$/validators'
export type Methods = {
post: {
reqBody: LoginBody
resBody: {
token: string
}
}
delete: {
reqHeaders: TokenHeader
}
}

View file

@ -0,0 +1,10 @@
import { defineController } from './$relay'
import { getUserInfoById, changeIcon } from '$/service/user'
export default defineController(() => ({
get: ({ user }) => ({ status: 200, body: getUserInfoById(user.id) }),
post: async ({ user, body }) => ({
status: 201,
body: await changeIcon(user.id, body.icon)
})
}))

View file

@ -0,0 +1,25 @@
import { defineHooks } from './$relay'
import { getUserIdByToken } from '$/service/user'
export type User = {
id: string
}
export default defineHooks((fastify) => ({
preHandler: fastify.auth([
(req, _, done) => {
const user =
typeof req.headers.token === 'string' &&
getUserIdByToken(req.headers.token)
if (user) {
// eslint-disable-next-line
// @ts-expect-error
req.user = user
done()
} else {
done(new Error('Unauthorized'))
}
}
])
}))

View file

@ -0,0 +1,16 @@
import { TokenHeader } from '$/validators'
import { UserInfo } from '$/types'
export type Methods = {
get: {
reqHeaders: TokenHeader
resBody: UserInfo
}
post: {
reqHeaders: TokenHeader
reqFormat: FormData
reqBody: { icon: Blob }
resBody: UserInfo
}
}

21
frourio/server/index.ts Normal file
View file

@ -0,0 +1,21 @@
import path from 'path'
import Fastify from 'fastify'
import helmet from 'fastify-helmet'
import cors from 'fastify-cors'
import fastifyStatic from 'fastify-static'
import fastifyAuth from 'fastify-auth'
import { SERVER_PORT, BASE_PATH } from './service/envValues'
import server from './$server'
const fastify = Fastify()
fastify.register(helmet)
fastify.register(cors)
fastify.register(fastifyStatic, {
root: path.join(__dirname, 'public'),
prefix: BASE_PATH
})
fastify.register(fastifyAuth).after(() => {
server(fastify, { basePath: BASE_PATH })
})
fastify.listen(SERVER_PORT)

View file

@ -0,0 +1,42 @@
{
"name": "frourio-server",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "npm run migrate:up && run-p dev:*",
"dev:server": "webpack --watch --mode=development",
"dev:frourio": "frourio --watch",
"dev:prisma": "prisma generate --watch",
"build": "npm run migrate:up && prisma generate && frourio && webpack --mode=production",
"migrate": "npm run migrate:save && npm run migrate:up",
"migrate:save": "prisma migrate save --create-db --experimental",
"migrate:up": "prisma migrate up --create-db --experimental",
"migrate:down": "prisma migrate down --experimental",
"start": "cross-env NODE_ENV=production node index.js"
},
"dependencies": {
"@prisma/client": "^2.8.0",
"class-validator": "^0.12.2",
"dotenv": "^8.2.0",
"fastify": "^3.5.1",
"fastify-auth": "^1.0.1",
"fastify-cors": "^4.1.0",
"fastify-helmet": "^5.0.3",
"fastify-multipart": "^3.2.0",
"fastify-static": "^3.2.0"
},
"devDependencies": {
"@prisma/cli": "^2.8.0",
"cross-env": "^7.0.2",
"frourio": "^0.17.1",
"nodemon-webpack-plugin": "^4.3.2",
"npm-run-all": "^4.1.5",
"ts-loader": "^8.0.4",
"ts-node": "^9.0.0",
"tsconfig-paths-webpack-plugin": "^3.3.0",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"webpack-node-externals": "^2.5.2"
}
}

View file

@ -0,0 +1 @@
DATABASE_URL=file:DATABASE_FILE

View file

@ -0,0 +1,41 @@
# Migration `20201001130532`
This migration has been generated by solufa at 10/1/2020, 10:05:32 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE TABLE `Task` (
`id` int NOT NULL AUTO_INCREMENT,
`label` varchar(191) NOT NULL ,
`done` boolean NOT NULL DEFAULT false,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration ..20201001130532
--- datamodel.dml
+++ datamodel.dml
@@ -1,0 +1,14 @@
+datasource db {
+ provider = "sqlite"
+ url = "***"
+}
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+model Task {
+ id Int @id @default(autoincrement())
+ label String
+ done Boolean @default(false)
+}
```

View file

@ -0,0 +1,14 @@
datasource db {
provider = "sqlite"
url = "***"
}
generator client {
provider = "prisma-client-js"
}
model Task {
id Int @id @default(autoincrement())
label String
done Boolean @default(false)
}

View file

@ -0,0 +1,113 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateSource",
"source": "db"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "provider",
"value": "\"sqlite\""
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "url",
"value": "\"***\""
},
{
"tag": "CreateModel",
"model": "Task"
},
{
"tag": "CreateField",
"model": "Task",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "Task",
"field": "label",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Task",
"field": "done",
"type": "Boolean",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "done"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "done"
},
"directive": "default"
},
"argument": "",
"value": "false"
}
]
}

View file

@ -0,0 +1,3 @@
# Prisma Migrate lockfile v1
20201001130532

View file

@ -0,0 +1,14 @@
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Task {
id Int @id @default(autoincrement())
label String
done Boolean @default(false)
}

View file

@ -0,0 +1,12 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512">
<style type="text/css">
.st0{fill:#4B4B4B;}
</style>
<g>
<path class="st0" d="M256,265.308c73.252,0,132.644-59.391,132.644-132.654C388.644,59.412,329.252,0,256,0
c-73.262,0-132.643,59.412-132.643,132.654C123.357,205.917,182.738,265.308,256,265.308z" style="fill: rgb(75, 75, 75);"></path>
<path class="st0" d="M425.874,393.104c-5.922-35.474-36-84.509-57.552-107.465c-5.829-6.212-15.948-3.628-19.504-1.427
c-27.04,16.672-58.782,26.399-92.819,26.399c-34.036,0-65.778-9.727-92.818-26.399c-3.555-2.201-13.675-4.785-19.505,1.427
c-21.55,22.956-51.628,71.991-57.551,107.465C71.573,480.444,164.877,512,256,512C347.123,512,440.427,480.444,425.874,393.104z" style="fill: rgb(75, 75, 75);"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 839 B

View file

@ -0,0 +1,11 @@
import dotenv from 'dotenv'
dotenv.config()
const USER_ID = process.env.USER_ID ?? ''
const USER_PASS = process.env.USER_PASS ?? ''
const SERVER_PORT = +(process.env.SERVER_PORT ?? '8080')
const BASE_PATH = process.env.BASE_PATH ?? ''
const API_ORIGIN = process.env.API_ORIGIN ?? ''
export { USER_ID, USER_PASS, SERVER_PORT, BASE_PATH, API_ORIGIN }

View file

@ -0,0 +1,17 @@
import { PrismaClient } from '@prisma/client'
import { Task } from '$/types'
const prisma = new PrismaClient()
export const getTasks = async (limit?: number) => (await prisma.task.findMany()).slice(0, limit)
export const createTask = (label: Task['label']) =>
prisma.task.create({ data: { label } })
export const updateTask = (
id: Task['id'],
partialTask: Partial<Pick<Task, 'label' | 'done'>>
) => prisma.task.update({ where: { id }, data: partialTask })
export const deleteTask = (id: Task['id']) =>
prisma.task.delete({ where: { id } })

View file

@ -0,0 +1,51 @@
import fs from 'fs'
import path from 'path'
import { Multipart } from 'fastify-multipart'
import { API_ORIGIN, BASE_PATH, USER_ID, USER_PASS } from './envValues'
const iconsDir = 'public/icons'
const createIconURL = (name: string) =>
`${API_ORIGIN}${BASE_PATH}/icons/${name}`
const userInfo = {
name: 'sample user',
icon: createIconURL(
fs
.readdirSync(path.resolve(iconsDir))
.filter((n) => n !== 'dammy.svg')
.pop() ?? 'dammy.svg'
)
}
let userToken: string | null = null
export const validateUser = (id: string, pass: string) =>
id === USER_ID && pass === USER_PASS
export const validateToken = (token: string) =>
userToken !== null && token === userToken
export const getUserIdByToken = (token: string) =>
validateToken(token) && { id: USER_ID }
export const getUserInfoById = (id: string) => ({ id, ...userInfo })
export const createToken = () => {
userToken = `token:${Date.now()}`
return userToken
}
export const deleteToken = (token: string) => {
if (validateToken(token)) userToken = null
}
export const changeIcon = async (id: string, iconFile: Multipart) => {
const iconName = `${Date.now()}${path.extname(iconFile.filename)}`
await fs.promises.writeFile(
path.resolve(iconsDir, iconName),
await iconFile.toBuffer()
)
userInfo.icon = createIconURL(iconName)
return { id, ...userInfo }
}

View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"downlevelIteration": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"lib": ["dom"],
"module": "esnext",
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"$/*": ["./*"]
},
"rootDir": ".",
"strict": true,
"strictPropertyInitialization": false,
"target": "es6"
}
}

View file

@ -0,0 +1,11 @@
export type Task = {
id: number
label: string
done: boolean
}
export type UserInfo = {
id: string
name: string
icon: string
}

View file

@ -0,0 +1,15 @@
import { MinLength, IsString } from 'class-validator'
export class LoginBody {
@MinLength(2)
id: string
@MinLength(4)
pass: string
}
export class TokenHeader {
@IsString()
@MinLength(10)
token: string
}

View file

@ -0,0 +1,24 @@
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
const nodeExternals = require('webpack-node-externals')
const NodemonPlugin = require('nodemon-webpack-plugin')
module.exports = {
entry: './index.ts',
target: 'node',
node: {
__dirname: false
},
output: {
filename: 'index.js',
path: __dirname
},
module: {
rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
},
plugins: [new NodemonPlugin()],
resolve: {
extensions: ['.ts', '.js'],
plugins: [new TsconfigPathsPlugin()]
},
externals: [nodeExternals()]
}

4135
frourio/server/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,80 @@
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
width: 100%;
height: 100px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
}
.footer img {
margin-left: 0.5rem;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
line-height: 1.5;
font-size: 1.5rem;
}
.logo {
height: 1em;
}
.tasks {
width: 300px;
padding: 0;
margin: 20px auto 0;
list-style-type: none;
text-align: left;
}
.tasks > li {
margin-top: 10px;
border-bottom: 1px solid #eee;
}

View file

@ -0,0 +1,13 @@
.userBanner {
position: fixed;
top: 0;
right: 0;
padding: 20px;
}
.userIcon {
width: 32px;
height: 32px;
background: #ddd;
vertical-align: bottom;
}

View file

@ -0,0 +1,16 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

27
frourio/tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"baseUrl": ".",
"paths": {
"~/*": ["./*"],
"$/*": ["./server/*"]
},
"allowJs": true,
"skipLibCheck": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"strict": false
},
"exclude": ["node_modules", "server"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

View file

@ -0,0 +1,4 @@
import aspida from '@aspida/fetch'
import api from '~/server/api/$api'
export const apiClient = api(aspida())

5915
frourio/yarn.lock Normal file

File diff suppressed because it is too large Load diff