Add basic FE and prettier
This commit is contained in:
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"experimentalOperatorPosition": "start",
|
||||
"experimentalTernaries": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120,
|
||||
"singleQuote": true
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
# Elysia with Bun runtime
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started with this template, simply paste this command into your terminal:
|
||||
|
||||
```bash
|
||||
bun create elysia ./elysia-example
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
To start the development server run:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Open http://localhost:3000/ with your browser to see the result.
|
||||
Open http://localhost:3000/ with your browser to see the result.
|
||||
|
||||
19
package.json
19
package.json
@@ -1,15 +1,18 @@
|
||||
{
|
||||
"name": "todo3",
|
||||
"version": "0.0.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/jwt": "^1.2.0",
|
||||
"elysia": "^1.2.25"
|
||||
"dev": "bun run --filter '*' dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest"
|
||||
"bun-types": "latest",
|
||||
"prettier": "^3.5.3"
|
||||
},
|
||||
"module": "src/index.js"
|
||||
}
|
||||
"type": "module",
|
||||
"trustedDependencies": [
|
||||
"@swc/core"
|
||||
]
|
||||
}
|
||||
|
||||
2
packages/be/.env
Normal file
2
packages/be/.env
Normal file
@@ -0,0 +1,2 @@
|
||||
JWT_SECRET=FOOBAR
|
||||
PORT=3000
|
||||
11
packages/be/package.json
Normal file
11
packages/be/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "be",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/jwt": "^1.2.0",
|
||||
"elysia": "^1.2.25"
|
||||
}
|
||||
}
|
||||
98
packages/be/src/index.ts
Normal file
98
packages/be/src/index.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { userDTO } from './user-dto';
|
||||
import { jwtConfig } from './jwt.config';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const authBody = t.Object({
|
||||
email: t.String(),
|
||||
password: t.String(),
|
||||
});
|
||||
|
||||
const validTokens = new Set<string>();
|
||||
|
||||
const app = new Elysia()
|
||||
.use(jwtConfig)
|
||||
.derive(async ({ cookie, jwt_auth }) => {
|
||||
const token = cookie.auth_token?.value;
|
||||
if (!token || !validTokens.has(token)) return { user: null };
|
||||
|
||||
const user = await jwt_auth.verify(token);
|
||||
return { user };
|
||||
})
|
||||
.post(
|
||||
'/signup',
|
||||
async ({ body, error, jwt_auth, cookie }) => {
|
||||
const foundUser = userDTO.findUserByEmail(body.email);
|
||||
|
||||
if (foundUser) return error(400, 'User already exists');
|
||||
|
||||
const newUser = await userDTO.createUser({
|
||||
email: body.email,
|
||||
password: body.password,
|
||||
});
|
||||
|
||||
if (!newUser) return error(400, 'Problems creating user');
|
||||
|
||||
const token = await jwt_auth.sign({
|
||||
id: newUser.id,
|
||||
sessionId: randomUUID(),
|
||||
});
|
||||
|
||||
validTokens.add(token);
|
||||
|
||||
cookie.auth_token.set({
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
path: '/',
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{ body: authBody },
|
||||
)
|
||||
.post(
|
||||
'/login',
|
||||
async ({ body, error, jwt_auth, cookie }) => {
|
||||
const foundUser = userDTO.findUserByEmail(body.email);
|
||||
|
||||
if (!foundUser) return error(400, 'User does not exist');
|
||||
|
||||
const isPasswordCorrect = await userDTO.verifyPassword(body.password, foundUser.password_hash);
|
||||
|
||||
if (!isPasswordCorrect) error(400, 'Password is incorrect');
|
||||
|
||||
const token = await jwt_auth.sign({
|
||||
id: foundUser.id,
|
||||
sessionId: randomUUID(),
|
||||
});
|
||||
|
||||
validTokens.add(token);
|
||||
|
||||
cookie.auth_token.set({
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
path: '/',
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{ body: authBody },
|
||||
)
|
||||
.get('/me', ({ user, error }) => {
|
||||
if (!user) return error(401, 'Not Authorized');
|
||||
return { user };
|
||||
})
|
||||
.post('/logout', ({ cookie }) => {
|
||||
const token = cookie.auth_token.value;
|
||||
|
||||
if (token) {
|
||||
validTokens.delete(token);
|
||||
}
|
||||
|
||||
cookie.auth_token.remove();
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
app.listen(process.env.PORT!);
|
||||
6
packages/be/src/jwt.config.ts
Normal file
6
packages/be/src/jwt.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { jwt } from '@elysiajs/jwt';
|
||||
|
||||
export const jwtConfig = jwt({
|
||||
name: 'jwt_auth',
|
||||
secret: process.env.JWT_SECRET!,
|
||||
});
|
||||
@@ -2,14 +2,14 @@ import { Database } from 'bun:sqlite';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
type User = {
|
||||
id: string,
|
||||
email: string,
|
||||
password_hash: string,
|
||||
}
|
||||
id: string;
|
||||
email: string;
|
||||
password_hash: string;
|
||||
};
|
||||
|
||||
type UserWithoutId = Omit<User, 'id' | 'password_hash'> & {
|
||||
password: string,
|
||||
}
|
||||
password: string;
|
||||
};
|
||||
|
||||
const db = new Database('db.sqlite');
|
||||
|
||||
@@ -28,12 +28,12 @@ export const userDTO = {
|
||||
},
|
||||
createUser: async (user: UserWithoutId) => {
|
||||
const password_hash = await Bun.password.hash(user.password);
|
||||
|
||||
|
||||
try {
|
||||
const query = db.query(
|
||||
'INSERT INTO users (id, email, password_hash) VALUES ($id, $email, $password_hash) RETURNING *'
|
||||
'INSERT INTO users (id, email, password_hash) VALUES ($id, $email, $password_hash) RETURNING *',
|
||||
);
|
||||
|
||||
|
||||
return query.get({
|
||||
$id: randomUUID(),
|
||||
$email: user.email,
|
||||
@@ -45,5 +45,5 @@ export const userDTO = {
|
||||
},
|
||||
verifyPassword: async (password: string, hash: string) => {
|
||||
return await Bun.password.verify(password, hash);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
24
packages/fe/.gitignore
vendored
Normal file
24
packages/fe/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
54
packages/fe/README.md
Normal file
54
packages/fe/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
extends: [
|
||||
// Remove ...tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x';
|
||||
import reactDom from 'eslint-plugin-react-dom';
|
||||
|
||||
export default tseslint.config({
|
||||
plugins: {
|
||||
// Add the react-x and react-dom plugins
|
||||
'react-x': reactX,
|
||||
'react-dom': reactDom,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended typescript rules
|
||||
...reactX.configs['recommended-typescript'].rules,
|
||||
...reactDom.configs.recommended.rules,
|
||||
},
|
||||
});
|
||||
```
|
||||
25
packages/fe/eslint.config.js
Normal file
25
packages/fe/eslint.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
},
|
||||
},
|
||||
);
|
||||
24
packages/fe/index.html
Normal file
24
packages/fe/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
as="style"
|
||||
href="https://fonts.googleapis.com/css2?family=Courier+Prime:ital,wght@0,400;0,700;1,400;1,700&display=swap"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Courier+Prime:ital,wght@0,400;0,700;1,400;1,700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Todo 3.0</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
30
packages/fe/package.json
Normal file
30
packages/fe/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "fe",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"be": "workspace:*",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.26.1",
|
||||
"vite": "^6.3.1"
|
||||
}
|
||||
}
|
||||
139
packages/fe/src/App.tsx
Normal file
139
packages/fe/src/App.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button } from './components/core/button';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
import { List } from './components/list';
|
||||
import { ItemType, ListType } from './types';
|
||||
|
||||
const initialState: ListType[] = [
|
||||
{
|
||||
id: '1',
|
||||
items: [
|
||||
{ id: '1', label: 'Item 1', checked: false },
|
||||
{ id: '2', label: 'Item 2', checked: false },
|
||||
{ id: '3', label: 'Item 3', checked: false },
|
||||
],
|
||||
title: 'List 1',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
items: [
|
||||
{ id: '4', label: 'Item 4', checked: false },
|
||||
{ id: '5', label: 'Item 5', checked: false },
|
||||
{ id: '6', label: 'Item 6', checked: false },
|
||||
],
|
||||
title: 'List 2',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
items: [
|
||||
{ id: '7', label: 'Item 7', checked: false },
|
||||
{ id: '8', label: 'Item 8', checked: false },
|
||||
{ id: '9', label: 'Item 9', checked: false },
|
||||
],
|
||||
title: 'List 3',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
const title = ` _____ _ _____ _____
|
||||
|_ _| | | |____ || _ |
|
||||
| | ___ __| | ___ / /| |/' |
|
||||
| |/ _ \\ / _\` |/ _ \\ \\ \\| /| |
|
||||
| | (_) | (_| | (_) | .___/ /\\ |_/ /
|
||||
\\_/\\___/ \\__,_|\\___/ \\____(_)\\___/`;
|
||||
|
||||
function App() {
|
||||
const [ref] = useAutoAnimate();
|
||||
const [state, setState] = useState<ListType[]>(initialState);
|
||||
const [widthPx, setWidthPx] = useState(Math.min(1280, window.innerWidth));
|
||||
const widthCh = useMemo(() => Math.floor(widthPx / 12), [widthPx]);
|
||||
const columns = useMemo(() => Math.ceil(widthCh / (1280 / (12 * 3))), [widthCh]);
|
||||
const listWidthCh = useMemo(() => Math.ceil(widthCh / columns - 1 * (columns - 1)), [columns, widthCh]);
|
||||
const horizontalBorder = useMemo(() => {
|
||||
let string = '+';
|
||||
for (let i = 0; i < widthCh / columns - 2 - 1 * (columns - 1); i++) {
|
||||
string += '-';
|
||||
}
|
||||
string += '+';
|
||||
|
||||
return string;
|
||||
}, [columns, widthCh]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWidthPx(Math.min(1280, window.innerWidth));
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const addList = () => {
|
||||
setState([{ id: window.crypto.randomUUID(), items: [], title: '', createdAt: new Date() }, ...state]);
|
||||
};
|
||||
|
||||
const updateList = (listId: string, list: ListType) => {
|
||||
setState(state.map(l => (l.id === listId ? list : l)));
|
||||
};
|
||||
|
||||
const deleteList = (listId: string) => {
|
||||
setState(state.filter(list => list.id !== listId));
|
||||
};
|
||||
|
||||
const addItem = (listId: string) => {
|
||||
setState(
|
||||
state.map(list =>
|
||||
list.id === listId ?
|
||||
{ ...list, items: [...list.items, { id: window.crypto.randomUUID(), label: '', checked: false }] }
|
||||
: list,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const updateItem = (listId: string, itemId: string, item: ItemType) => {
|
||||
setState(
|
||||
state.map(list =>
|
||||
list.id === listId ? { ...list, items: list.items.map(i => (i.id === itemId ? item : i)) } : list,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteItem = (listId: string, itemId: string) => {
|
||||
setState(
|
||||
state.map(list => (list.id === listId ? { ...list, items: list.items.filter(i => i.id !== itemId) } : list)),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{title}</h1>
|
||||
|
||||
<Button hasBorder onClick={addList}>
|
||||
Add list
|
||||
</Button>
|
||||
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ display: 'grid', gap: 12, gridTemplateColumns: `repeat(${columns}, minmax(${listWidthCh}ch, 1fr))` }}
|
||||
>
|
||||
{state.map(list => (
|
||||
<List
|
||||
key={list.id}
|
||||
{...list}
|
||||
deleteList={deleteList}
|
||||
updateItem={updateItem}
|
||||
deleteItem={deleteItem}
|
||||
addItem={addItem}
|
||||
horizontalBorder={horizontalBorder}
|
||||
listWidthCh={listWidthCh}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
38
packages/fe/src/components/core/button/index.tsx
Normal file
38
packages/fe/src/components/core/button/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ComponentPropsWithRef } from 'react';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
type Props = ComponentPropsWithRef<'button'> & {
|
||||
hasBorder?: boolean;
|
||||
};
|
||||
|
||||
export const Button = ({ className, hasBorder, children, ...props }: Props) => {
|
||||
if (hasBorder) {
|
||||
return (
|
||||
<button className={`${styles.button} ${styles.bordered} ${className}`} {...props}>
|
||||
<span>
|
||||
+-
|
||||
{children
|
||||
?.toString()
|
||||
.split('')
|
||||
.map(() => '-')}
|
||||
-+
|
||||
</span>
|
||||
<span>| {children} |</span>
|
||||
<span>
|
||||
+-
|
||||
{children
|
||||
?.toString()
|
||||
.split('')
|
||||
.map(() => '-')}
|
||||
-+
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={`${styles.button} ${className}`} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
13
packages/fe/src/components/core/button/styles.module.css
Normal file
13
packages/fe/src/components/core/button/styles.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bordered span {
|
||||
display: block;
|
||||
}
|
||||
8
packages/fe/src/components/core/input/index.tsx
Normal file
8
packages/fe/src/components/core/input/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ComponentPropsWithRef } from 'react';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
type Props = ComponentPropsWithRef<'input'>;
|
||||
|
||||
export const Input = ({ className, ...props }: Props) => {
|
||||
return <input className={`${styles.input} ${className}`} {...props} />;
|
||||
};
|
||||
13
packages/fe/src/components/core/input/styles.module.css
Normal file
13
packages/fe/src/components/core/input/styles.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.input {
|
||||
appearance: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
padding: 0;
|
||||
height: 1rem;
|
||||
|
||||
&:focus {
|
||||
color: #fffa;
|
||||
}
|
||||
}
|
||||
31
packages/fe/src/components/item/index.tsx
Normal file
31
packages/fe/src/components/item/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Button } from '../core/button';
|
||||
import { Input } from '../core/input';
|
||||
import { ItemType } from '../../types';
|
||||
|
||||
type Props = ItemType & {
|
||||
listId: string;
|
||||
updateItem: (listId: string, itemId: string, item: ItemType) => void;
|
||||
deleteItem: (listId: string, itemId: string) => void;
|
||||
};
|
||||
|
||||
export const Item = ({ listId, updateItem, deleteItem, ...item }: Props) => {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, max-content) 1fr max-content' }}>
|
||||
<span>|</span>
|
||||
<Button onClick={() => updateItem(listId, item.id, { ...item, checked: !item.checked })}>
|
||||
{' '}
|
||||
{item.checked ? 'X' : 'O'}{' '}
|
||||
</Button>
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<Input
|
||||
className='itemLabel'
|
||||
type='text'
|
||||
value={item.label}
|
||||
onChange={({ target }) => updateItem(listId, item.id, { ...item, label: target.value })}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<span> |</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
57
packages/fe/src/components/list/index.tsx
Normal file
57
packages/fe/src/components/list/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Button } from '../core/button';
|
||||
import { ItemType, ListType } from '../../types';
|
||||
import { Item } from '../item';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
|
||||
type Props = ListType & {
|
||||
deleteList: (listId: string) => void;
|
||||
updateItem: (listId: string, itemId: string, item: ItemType) => void;
|
||||
deleteItem: (listId: string, itemId: string) => void;
|
||||
addItem: (listId: string) => void;
|
||||
horizontalBorder: string;
|
||||
listWidthCh: number;
|
||||
};
|
||||
|
||||
export const List = ({
|
||||
deleteList,
|
||||
updateItem,
|
||||
deleteItem,
|
||||
addItem,
|
||||
horizontalBorder,
|
||||
listWidthCh,
|
||||
...list
|
||||
}: Props) => {
|
||||
const [ref] = useAutoAnimate();
|
||||
return (
|
||||
<div key={list.id} style={{ width: listWidthCh + 'ch', overflow: 'hidden' }}>
|
||||
<div>{horizontalBorder}</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'max-content 1fr repeat(2, max-content)' }}>
|
||||
<span>| </span>
|
||||
<h2>{list.title}</h2>
|
||||
<Button onClick={() => deleteList(list.id)}> X </Button>
|
||||
<span>|</span>
|
||||
</div>
|
||||
|
||||
<div>{horizontalBorder}</div>
|
||||
|
||||
<div ref={ref}>
|
||||
{list.items.map(item => (
|
||||
<Item key={item.id} listId={list.id} updateItem={updateItem} deleteItem={deleteItem} {...item} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'max-content max-content 1fr max-content max-content' }}>
|
||||
<span>| </span>
|
||||
<Button onClick={() => addItem(list.id)} style={{ textDecoration: 'underline' }}>
|
||||
Add item
|
||||
</Button>
|
||||
<span> </span>
|
||||
<i>{list.createdAt.toLocaleDateString()}</i>
|
||||
<span> |</span>
|
||||
</div>
|
||||
|
||||
<div>{horizontalBorder}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
27
packages/fe/src/index.css
Normal file
27
packages/fe/src/index.css
Normal file
@@ -0,0 +1,27 @@
|
||||
:root {
|
||||
font-family: 'Courier Prime', monospace;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
|
||||
color-scheme: dark;
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 1280px;
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
10
packages/fe/src/main.tsx
Normal file
10
packages/fe/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App.tsx';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
12
packages/fe/src/types.ts
Normal file
12
packages/fe/src/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type ItemType = {
|
||||
id: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
};
|
||||
|
||||
export type ListType = {
|
||||
id: string;
|
||||
items: ItemType[];
|
||||
title: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
1
packages/fe/src/vite-env.d.ts
vendored
Normal file
1
packages/fe/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
26
packages/fe/tsconfig.app.json
Normal file
26
packages/fe/tsconfig.app.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
4
packages/fe/tsconfig.json
Normal file
4
packages/fe/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
24
packages/fe/tsconfig.node.json
Normal file
24
packages/fe/tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
7
packages/fe/vite.config.ts
Normal file
7
packages/fe/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
93
src/index.ts
93
src/index.ts
@@ -1,93 +0,0 @@
|
||||
import { Elysia, t } from "elysia"
|
||||
import { userDTO } from "./user-dto"
|
||||
import { jwtConfig } from "./jwt.config"
|
||||
import { randomUUID } from "crypto"
|
||||
|
||||
const authBody = t.Object({
|
||||
email: t.String(),
|
||||
password: t.String(),
|
||||
})
|
||||
|
||||
const validTokens = new Set<string>()
|
||||
|
||||
const app = new Elysia()
|
||||
.use(jwtConfig)
|
||||
.derive(async ({ cookie, jwt_auth }) => {
|
||||
const token = cookie.auth_token?.value
|
||||
if (!token || !validTokens.has(token)) return { user: null }
|
||||
|
||||
const user = await jwt_auth.verify(token)
|
||||
return { user }
|
||||
})
|
||||
.post("/signup", async ({ body, error, jwt_auth, cookie }) => {
|
||||
const foundUser = userDTO.findUserByEmail(body.email)
|
||||
|
||||
if (foundUser) return error(400, "User already exists")
|
||||
|
||||
const newUser = await userDTO.createUser({
|
||||
email: body.email,
|
||||
password: body.password,
|
||||
})
|
||||
|
||||
if (!newUser) return error(400, "Problems creating user")
|
||||
|
||||
const token = await jwt_auth.sign({
|
||||
id: newUser.id,
|
||||
sessionId: randomUUID()
|
||||
})
|
||||
|
||||
validTokens.add(token)
|
||||
|
||||
cookie.auth_token.set({
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
path: '/'
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}, { body: authBody })
|
||||
.post("/login", async ({ body, error, jwt_auth, cookie }) => {
|
||||
const foundUser = userDTO.findUserByEmail(body.email)
|
||||
|
||||
if (!foundUser) return error(400, "User does not exist")
|
||||
|
||||
const isPasswordCorrect = await userDTO.verifyPassword(
|
||||
body.password,
|
||||
foundUser.password_hash,
|
||||
)
|
||||
|
||||
if (!isPasswordCorrect) error(400, "Password is incorrect")
|
||||
|
||||
const token = await jwt_auth.sign({
|
||||
id: foundUser.id,
|
||||
sessionId: randomUUID()
|
||||
})
|
||||
|
||||
validTokens.add(token)
|
||||
|
||||
cookie.auth_token.set({
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
path: '/'
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}, { body: authBody })
|
||||
.get("/me", ({ user, error }) => {
|
||||
if (!user) return error(401, "Not Authorized")
|
||||
return { user }
|
||||
})
|
||||
.post("/logout", ({ cookie }) => {
|
||||
const token = cookie.auth_token.value
|
||||
|
||||
if (token) {
|
||||
validTokens.delete(token)
|
||||
}
|
||||
|
||||
cookie.auth_token.remove()
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
app.listen(3000)
|
||||
@@ -1,6 +0,0 @@
|
||||
import { jwt } from '@elysiajs/jwt'
|
||||
|
||||
export const jwtConfig = jwt({
|
||||
name: 'jwt_auth',
|
||||
secret: process.env.JWT_SECRET!
|
||||
})
|
||||
Reference in New Issue
Block a user