feat: initialise template
46
.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
/.swc
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
.marker*
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# logs
|
||||||
|
.logs
|
||||||
12
.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"[yaml]": {
|
||||||
|
"editor.autoIndent": "advanced"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# frontend-template
|
||||||
|
|
||||||
|
Frontend templates for using auth.etwo.dev
|
||||||
6
commitlint.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
rules: {
|
||||||
|
'body-max-line-length': [0],
|
||||||
|
},
|
||||||
|
};
|
||||||
83
eslint.config.cjs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
const tsParser = require('@typescript-eslint/parser');
|
||||||
|
const stylisticPlugin = require('@stylistic/eslint-plugin');
|
||||||
|
const tsPlugin = require ('@typescript-eslint/eslint-plugin')
|
||||||
|
const reactPlugin = require('eslint-plugin-react');
|
||||||
|
const sortPlugin = require('eslint-plugin-simple-import-sort');
|
||||||
|
|
||||||
|
/** @type {import("eslint").FlatConfig[]} */
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
ignores: ['node_modules/**', 'dist/**', 'wailjs/**'],
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
expect: 'readonly',
|
||||||
|
},
|
||||||
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
'@stylistic': stylisticPlugin,
|
||||||
|
'@typescript-eslint': tsPlugin,
|
||||||
|
react: reactPlugin,
|
||||||
|
'simple-import-sort': sortPlugin,
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-misused-new': 'error',
|
||||||
|
semi: ['error', 'always'],
|
||||||
|
'@typescript-eslint/no-empty-function': 'error',
|
||||||
|
'simple-import-sort/imports': 'error',
|
||||||
|
'simple-import-sort/exports': 'error',
|
||||||
|
'@stylistic/member-delimiter-style': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
multiline: { delimiter: 'semi', requireLast: true },
|
||||||
|
singleline: { delimiter: 'semi', requireLast: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-var': 'error',
|
||||||
|
'no-eval': 'error',
|
||||||
|
'eol-last': 'error',
|
||||||
|
'no-console': 'error',
|
||||||
|
'default-case': 'error',
|
||||||
|
semi: 'off',
|
||||||
|
'no-regex-spaces': 'error',
|
||||||
|
'constructor-super': 'error',
|
||||||
|
'no-invalid-regexp': 'error',
|
||||||
|
curly: ['error', 'multi-line'],
|
||||||
|
'no-irregular-whitespace': 'error',
|
||||||
|
'quote-props': ['error', 'as-needed'],
|
||||||
|
'linebreak-style': ['error', 'unix'],
|
||||||
|
'padding-line-between-statements': [
|
||||||
|
'error',
|
||||||
|
{ blankLine: 'always', prev: '*', next: 'return' },
|
||||||
|
],
|
||||||
|
'prefer-const': ['error', { destructuring: 'all' }],
|
||||||
|
'comma-dangle': ['error', 'always-multiline'],
|
||||||
|
indent: [
|
||||||
|
'error',
|
||||||
|
2,
|
||||||
|
{ SwitchCase: 1, ignoredNodes: ['ConditionalExpression', 'TemplateLiteral'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
files: ['.eslintrc.{js,cjs}'],
|
||||||
|
languageOptions: {
|
||||||
|
sourceType: 'script',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
29
global.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.png' {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.gif' {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Single Sign On - etwo.dev</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
10
jest.config.cjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
verbose: true,
|
||||||
|
transform: {
|
||||||
|
'^.+\\.tsx?$': 'ts-jest',
|
||||||
|
},
|
||||||
|
setupFilesAfterEnv: ['./src/setupTests.ts'],
|
||||||
|
};
|
||||||
67
package.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": false,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --open",
|
||||||
|
"test": "jest --watch --maxWorkers=25%",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"setup": "cp .env.template .env",
|
||||||
|
"format": "prettier . --write --ignore-path=.prettierignore",
|
||||||
|
"lint": "eslint --ext .ts,.tsx,.js ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/bricolage-grotesque": "^5.2.8",
|
||||||
|
"@fontsource/inter": "^5.2.6",
|
||||||
|
"@mantine/core": "^8.2.4",
|
||||||
|
"@mantine/dates": "^8.2.4",
|
||||||
|
"@mantine/hooks": "^8.2.4",
|
||||||
|
"@tabler/icons-react": "^3.34.1",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-confetti": "^6.4.0",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"react-router-dom": "^7.9.4",
|
||||||
|
"zustand": "^5.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^19.8.1",
|
||||||
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
|
"@stylistic/eslint-plugin": "^5.2.3",
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
"@testing-library/jest-dom": "^6.6.4",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^24.2.0",
|
||||||
|
"@types/react": "^19.1.9",
|
||||||
|
"@types/react-dom": "^19.1.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
||||||
|
"@typescript-eslint/parser": "^8.39.0",
|
||||||
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
|
"eslint": "^9.32.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
|
"jest": "^30.0.5",
|
||||||
|
"jest-environment-jsdom": "^30.0.5",
|
||||||
|
"lint-staged": "^16.1.4",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"testdouble": "^3.20.2",
|
||||||
|
"ts-jest": "^29.4.1",
|
||||||
|
"typescript": "5.8.3",
|
||||||
|
"vite": "^7.0.6"
|
||||||
|
},
|
||||||
|
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/wailsapp/wails/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||||
|
}
|
||||||
14
postcss.config.cjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
'postcss-preset-mantine': {},
|
||||||
|
'postcss-simple-vars': {
|
||||||
|
variables: {
|
||||||
|
'mantine-breakpoint-xs': '36em',
|
||||||
|
'mantine-breakpoint-sm': '48em',
|
||||||
|
'mantine-breakpoint-md': '62em',
|
||||||
|
'mantine-breakpoint-lg': '75em',
|
||||||
|
'mantine-breakpoint-xl': '88em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
public/android-chrome-192x192.png
Executable file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/android-chrome-512x512.png
Executable file
|
After Width: | Height: | Size: 93 KiB |
BIN
public/apple-touch-icon.png
Executable file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/favicon-16x16.png
Executable file
|
After Width: | Height: | Size: 553 B |
BIN
public/favicon-32x32.png
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/favicon.ico
Executable file
|
After Width: | Height: | Size: 15 KiB |
25
src/App.tsx
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
import '@fontsource/inter/300.css';
|
||||||
|
import '@fontsource/inter/400.css';
|
||||||
|
import '@fontsource/inter/500.css';
|
||||||
|
import '@fontsource/inter/700.css';
|
||||||
|
import '@fontsource/inter/800.css';
|
||||||
|
import '@fontsource/inter/900.css';
|
||||||
|
import '@fontsource/bricolage-grotesque/300.css';
|
||||||
|
import '@fontsource/bricolage-grotesque/400.css';
|
||||||
|
import '@fontsource/bricolage-grotesque/500.css';
|
||||||
|
import '@fontsource/bricolage-grotesque/700.css';
|
||||||
|
import '@fontsource/bricolage-grotesque/800.css';
|
||||||
|
import './global.css';
|
||||||
|
import '@mantine/core/styles.css';
|
||||||
|
|
||||||
|
import { Providers } from '@/lib/providers';
|
||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
import { NotFoundPage } from './components/NotFoundPage';
|
||||||
|
|
||||||
|
export const App = (): React.JSX.Element => (
|
||||||
|
<Providers>
|
||||||
|
<Routes>
|
||||||
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
|
</Routes>
|
||||||
|
</Providers>
|
||||||
|
);
|
||||||
16
src/__test__/App.test.tsx
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
import { render, RenderResult, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { App } from '../App';
|
||||||
|
|
||||||
|
describe('App.tsx', () => {
|
||||||
|
const subject = (): RenderResult => render(<App />);
|
||||||
|
|
||||||
|
describe('when the application is enabled', () => {
|
||||||
|
it('renders the dom', () => {
|
||||||
|
subject();
|
||||||
|
|
||||||
|
expect(screen.getByText('VITE + REACT + TS + WAILS')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
BIN
src/components/NotFoundPage/SAJ.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
42
src/components/NotFoundPage/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Image, Alert, Title, Text, Button, Stack, Group } from '@mantine/core';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { IconArrowLeft } from '@tabler/icons-react';
|
||||||
|
import { FlexCenter } from '../shared/FlexCenter';
|
||||||
|
import SAJEmote from './SAJ.png';
|
||||||
|
|
||||||
|
export const NotFoundPage = (): React.JSX.Element => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlexCenter direction="column">
|
||||||
|
<Alert
|
||||||
|
color="red"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
title={
|
||||||
|
<Group>
|
||||||
|
<Image w="24px" src={SAJEmote} />
|
||||||
|
<Title order={1}>404 – Page Not Found</Title>
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack align="center" gap="xs">
|
||||||
|
<Text size="md" c="dimmed">
|
||||||
|
The page you are looking for doesn’t exist or may have been moved.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
mt="sm"
|
||||||
|
fullWidth
|
||||||
|
color="red"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconArrowLeft size={18} />}
|
||||||
|
onClick={() => navigate('/dashboard')}
|
||||||
|
>
|
||||||
|
Back to Home
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Alert>
|
||||||
|
</FlexCenter>
|
||||||
|
);
|
||||||
|
};
|
||||||
23
src/components/shared/Clock/index.tsx
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface ClockProps {
|
||||||
|
locales?: Intl.LocalesArgument;
|
||||||
|
options?: Intl.DateTimeFormatOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A very simple, unstyled, clock
|
||||||
|
*/
|
||||||
|
export const Clock = ({ locales, options }: ClockProps): React.JSX.Element => {
|
||||||
|
const [time, setTime] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => setTime(new Date()), 1000);
|
||||||
|
|
||||||
|
return (): void => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!time) return <>Loading...</>;
|
||||||
|
|
||||||
|
return <>{time.toLocaleTimeString(locales, options)}</>;
|
||||||
|
};
|
||||||
24
src/components/shared/ColorSchemeToggle/ColorSchemeToggle.module.css
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
.icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
@mixin dark {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
@mixin light {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/components/shared/ColorSchemeToggle/index.tsx
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ActionIcon, MantineSize } from '@mantine/core';
|
||||||
|
import { IconMoon, IconSun } from '@tabler/icons-react';
|
||||||
|
import cx from 'clsx';
|
||||||
|
|
||||||
|
import { useStrictColorScheme } from '@/lib/hooks';
|
||||||
|
|
||||||
|
import styles from './ColorSchemeToggle.module.css';
|
||||||
|
|
||||||
|
interface ColorSchemeToggleProps {
|
||||||
|
size: MantineSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mantine Color Scheme Toggle
|
||||||
|
*
|
||||||
|
* @see https://mantine.dev/theming/color-schemes/#color-scheme-value-caveats
|
||||||
|
*/
|
||||||
|
export const ColorSchemeToggle = ({
|
||||||
|
size,
|
||||||
|
}: ColorSchemeToggleProps): React.JSX.Element => {
|
||||||
|
const { toggleColorScheme } = useStrictColorScheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionIcon onClick={toggleColorScheme} size={size} variant="default">
|
||||||
|
<IconSun className={cx(styles.icon, styles.light)} stroke={1.5} />
|
||||||
|
<IconMoon className={cx(styles.icon, styles.dark)} stroke={1.5} />
|
||||||
|
</ActionIcon>
|
||||||
|
);
|
||||||
|
};
|
||||||
43
src/components/shared/ConfettiButton/index.tsx
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
import { ActionIcon } from '@mantine/core';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Confetti from 'react-confetti';
|
||||||
|
|
||||||
|
interface ConfettiButtonProps {
|
||||||
|
density?: number;
|
||||||
|
duration?: number;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfettiButton = ({
|
||||||
|
icon,
|
||||||
|
density = 500,
|
||||||
|
duration = 1000,
|
||||||
|
}: ConfettiButtonProps): React.JSX.Element => {
|
||||||
|
const [isConfettiActive, setIsConfettiActive] = useState(false);
|
||||||
|
const [isConfettiClean, setIsConfettiClean] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = (): void => {
|
||||||
|
setIsConfettiActive(true);
|
||||||
|
setIsConfettiClean(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsConfettiActive(false);
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsConfettiActive(false);
|
||||||
|
}, duration + 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionIcon variant="transparent" onClick={handleClick}>
|
||||||
|
{icon}
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
|
{isConfettiClean && (
|
||||||
|
<Confetti numberOfPieces={isConfettiActive ? density : 0} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
41
src/components/shared/CopyActionIcon/index.tsx
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
import { ActionIcon, CopyButton, Tooltip } from '@mantine/core';
|
||||||
|
import { IconCheck, IconCopy } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
interface CopyActionIconProps {
|
||||||
|
value: string;
|
||||||
|
tooltip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mantine CopyButton
|
||||||
|
*
|
||||||
|
* @see https://mantine.dev/core/copy-button/
|
||||||
|
*/
|
||||||
|
export const CopyActionIcon = ({
|
||||||
|
value,
|
||||||
|
tooltip,
|
||||||
|
}: CopyActionIconProps): React.JSX.Element => (
|
||||||
|
<CopyButton value={value}>
|
||||||
|
{({ copied, copy }) =>
|
||||||
|
tooltip ? (
|
||||||
|
<Tooltip label={tooltip}>
|
||||||
|
<ActionIcon color="dimmed" onClick={copy} variant="light">
|
||||||
|
{copied ? (
|
||||||
|
<IconCheck color="teal" size={'1rem'} stroke={1.5} />
|
||||||
|
) : (
|
||||||
|
<IconCopy size={'1rem'} stroke={1.5} />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<ActionIcon color="dimmed" onClick={copy} variant="light">
|
||||||
|
{copied ? (
|
||||||
|
<IconCheck color="teal" size={'1rem'} stroke={1.5} />
|
||||||
|
) : (
|
||||||
|
<IconCopy size={'1rem'} stroke={1.5} />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</CopyButton>
|
||||||
|
);
|
||||||
34
src/components/shared/CopyLabelButton/index.tsx
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Button, ButtonProps, CopyButton } from '@mantine/core';
|
||||||
|
|
||||||
|
interface CopyActionIconProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
copyLabel?: string;
|
||||||
|
buttonProps?: ButtonProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mantine CopyButton
|
||||||
|
*
|
||||||
|
* @see https://mantine.dev/core/copy-button/
|
||||||
|
*/
|
||||||
|
export const CopyLabelButton = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
buttonProps,
|
||||||
|
copyLabel = 'copied',
|
||||||
|
}: CopyActionIconProps): React.JSX.Element => (
|
||||||
|
<CopyButton value={value}>
|
||||||
|
{({ copied, copy }) => (
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
onClick={copy}
|
||||||
|
variant="light"
|
||||||
|
color={copied ? 'teal' : 'dimmed'}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
{copied ? copyLabel : label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CopyButton>
|
||||||
|
);
|
||||||
6
src/components/shared/FlexCenter/FlexCenter.module.css
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
.center {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
18
src/components/shared/FlexCenter/index.tsx
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Flex, FlexProps } from '@mantine/core';
|
||||||
|
|
||||||
|
import styles from './FlexCenter.module.css';
|
||||||
|
|
||||||
|
interface FlexCenterProps extends FlexProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlexCenter = ({
|
||||||
|
children,
|
||||||
|
direction,
|
||||||
|
wrap,
|
||||||
|
gap,
|
||||||
|
}: FlexCenterProps): React.JSX.Element => (
|
||||||
|
<Flex gap={gap} direction={direction} wrap={wrap} className={styles.center}>
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
38
src/components/shared/IconLesbian/index.tsx
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
interface IconProps extends React.SVGProps<SVGSVGElement> {
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
heart?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconLesbian: React.FC<IconProps> = ({
|
||||||
|
size = 24,
|
||||||
|
heart = false,
|
||||||
|
color = 'currentColor',
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
fill={color}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
{heart && (
|
||||||
|
<defs>
|
||||||
|
<clipPath id="heartClip">
|
||||||
|
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
)}
|
||||||
|
<g clipPath="url(#heartClip)">
|
||||||
|
<rect fill="#D52D00" width="24" height="4.8" y="0" />
|
||||||
|
<rect fill="#FF9A56" width="24" height="4.8" y="4.8" />
|
||||||
|
<rect fill="#FFF" width="24" height="4.8" y="9.6" />
|
||||||
|
<rect fill="#D362A4" width="24" height="4.8" y="14.4" />
|
||||||
|
<rect fill="#A30262" width="24" height="4.8" y="19.2" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
69
src/components/shared/ProtectedRoute/index.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Navigate, Outlet } from 'react-router-dom';
|
||||||
|
import { Title, Loader, Center } from '@mantine/core';
|
||||||
|
|
||||||
|
import { useFetchCurrentUser } from '@/lib/utils/auth/hooks/useFetchCurrentUser';
|
||||||
|
import { useSession } from '@/lib/utils/auth/hooks/useSession';
|
||||||
|
import { FlexCenter } from '../FlexCenter';
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
roles?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProtectedRoute
|
||||||
|
* ---------------
|
||||||
|
* This route guard ensures that only authenticated users can access
|
||||||
|
* private routes such as the dashboard or internal management pages.
|
||||||
|
*
|
||||||
|
* If the user is not authenticated or the session check fails,
|
||||||
|
* they are redirected to the login page.
|
||||||
|
*
|
||||||
|
* ✅ Example usage:
|
||||||
|
*
|
||||||
|
* <Route element={<ProtectedRoute roles=["whitelisted", "admin"] />}>
|
||||||
|
* <Route path="/dashboard" element={<DashboardLayout />}>
|
||||||
|
* <Route index element={<DashboardHome />} />
|
||||||
|
* <Route path="settings" element={<SettingsPage />} />
|
||||||
|
* </Route>
|
||||||
|
* </Route>
|
||||||
|
*
|
||||||
|
* Behavior:
|
||||||
|
* - While checking session → shows loader
|
||||||
|
* - Authenticated user → allowed to access dashboard
|
||||||
|
* - Unauthenticated user → redirected to /login
|
||||||
|
*
|
||||||
|
* @returns JSX.Element
|
||||||
|
*/
|
||||||
|
export const PublicRoute = ({
|
||||||
|
roles = [],
|
||||||
|
}: ProtectedRouteProps): React.JSX.Element => {
|
||||||
|
const { data, isLoading, error, fetchCurrentUser } = useFetchCurrentUser();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data && !isLoading && !error) fetchCurrentUser();
|
||||||
|
}, [data, isLoading, fetchCurrentUser, error]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<FlexCenter direction="column">
|
||||||
|
<Title order={4} ml="sm">
|
||||||
|
Checking session...
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Loader color="blue" size="lg" />
|
||||||
|
</FlexCenter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !data?.data?.user_id) {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = data.data;
|
||||||
|
if (roles.length > 0 && !roles.includes(user.role)) {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
};
|
||||||
49
src/components/shared/PublicRoute/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { useSession } from '@/lib/utils/auth/hooks/useSession';
|
||||||
|
import { Title, Loader } from '@mantine/core';
|
||||||
|
import { Navigate, Outlet } from 'react-router-dom';
|
||||||
|
import { FlexCenter } from '../FlexCenter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PublicRoute
|
||||||
|
* ------------
|
||||||
|
* This route guard prevents authenticated users from accessing public routes
|
||||||
|
* such as login, register, or landing pages. If a valid session exists,
|
||||||
|
* the user is redirected to the dashboard instead.
|
||||||
|
*
|
||||||
|
* Otherwise, it renders the child route’s component via <Outlet />.
|
||||||
|
*
|
||||||
|
* ✅ Example usage:
|
||||||
|
*
|
||||||
|
* <Route element={<PublicRoute />}>
|
||||||
|
* <Route path="/login" element={<LoginPage />} />
|
||||||
|
* <Route path="/register" element={<RegisterPage />} />
|
||||||
|
* </Route>
|
||||||
|
*
|
||||||
|
* Behavior:
|
||||||
|
* - While checking session → shows loader
|
||||||
|
* - Authenticated user → redirected to /dashboard
|
||||||
|
* - Unauthenticated user → allowed to access the page
|
||||||
|
*
|
||||||
|
* @returns JSX.Element
|
||||||
|
*/
|
||||||
|
export const PublicRoute = (): React.JSX.Element => {
|
||||||
|
const { isLoading, error, isAuthenticated } = useSession();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<FlexCenter direction="column">
|
||||||
|
<Title order={4} ml="sm">
|
||||||
|
Checking session...
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Loader color="blue" size="lg" />
|
||||||
|
</FlexCenter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthenticated && !error) {
|
||||||
|
return <Navigate to="/dashboard" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
};
|
||||||
51
src/components/shared/Tabs/Tabs.module.css
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
.list {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
width: min-content;
|
||||||
|
position: absolute;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
background-color: var(--mantine-color-white);
|
||||||
|
border-radius: var(--mantine-radius-md);
|
||||||
|
border: 1px solid var(--mantine-color-gray-2);
|
||||||
|
box-shadow: var(--mantine-shadow-sm);
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-6);
|
||||||
|
border-color: var(--mantine-color-dark-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
z-index: 3; /* Higher than the indicator */
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
width: min-content;
|
||||||
|
transition: color 100ms ease;
|
||||||
|
color: var(--mantine-color-gray-7);
|
||||||
|
|
||||||
|
&[data-active] {
|
||||||
|
color: var(--mantine-color-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
color: var(--mantine-color-dark-1);
|
||||||
|
|
||||||
|
&[data-active] {
|
||||||
|
color: var(--mantine-color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/components/shared/Tabs/index.tsx
Executable file
@@ -0,0 +1,107 @@
|
|||||||
|
import {
|
||||||
|
Burger,
|
||||||
|
FloatingIndicator,
|
||||||
|
Tabs as MantineTabs,
|
||||||
|
Transition,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import classes from './Tabs.module.css';
|
||||||
|
|
||||||
|
export interface Tab {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
content: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabsProps {
|
||||||
|
tabs: Tab[];
|
||||||
|
burger?: boolean;
|
||||||
|
orientation?: 'vertical' | 'horizontal';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tabs = ({
|
||||||
|
tabs,
|
||||||
|
burger = false,
|
||||||
|
orientation = 'vertical',
|
||||||
|
}: TabsProps): React.JSX.Element => {
|
||||||
|
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const [controlsRefs, setControlsRefs] = useState<
|
||||||
|
Record<string, HTMLButtonElement | null>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const [value, setValue] = useState<string | null>(
|
||||||
|
tabs.length > 0 ? tabs[0].value : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isTabListVisible, setIsTabListVisible] = useState(true);
|
||||||
|
|
||||||
|
const setControlRef =
|
||||||
|
(val: string) =>
|
||||||
|
(node: HTMLButtonElement): void => {
|
||||||
|
controlsRefs[val] = node;
|
||||||
|
setControlsRefs(controlsRefs);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative', height: '100%' }}>
|
||||||
|
{burger && (
|
||||||
|
<Burger
|
||||||
|
className={classes.burger}
|
||||||
|
opened={!isTabListVisible}
|
||||||
|
onClick={(): void => setIsTabListVisible(prev => !prev)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MantineTabs
|
||||||
|
h={'100%'}
|
||||||
|
value={value}
|
||||||
|
variant="none"
|
||||||
|
onChange={setValue}
|
||||||
|
orientation={orientation}
|
||||||
|
>
|
||||||
|
<Transition
|
||||||
|
duration={300}
|
||||||
|
transition="fade"
|
||||||
|
timingFunction="ease"
|
||||||
|
mounted={isTabListVisible}
|
||||||
|
>
|
||||||
|
{styles => (
|
||||||
|
<MantineTabs.List
|
||||||
|
style={styles}
|
||||||
|
ref={setRootRef}
|
||||||
|
mt={burger ? '40px' : '0px'}
|
||||||
|
className={classes.list}
|
||||||
|
>
|
||||||
|
{tabs.map(({ value, label, icon }) => (
|
||||||
|
<MantineTabs.Tab
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
leftSection={icon}
|
||||||
|
className={classes.tab}
|
||||||
|
ref={setControlRef(value)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</MantineTabs.Tab>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<FloatingIndicator
|
||||||
|
parent={rootRef}
|
||||||
|
className={classes.indicator}
|
||||||
|
target={value ? controlsRefs[value] : null}
|
||||||
|
/>
|
||||||
|
</MantineTabs.List>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
{tabs.map(({ value, content }) => (
|
||||||
|
<MantineTabs.Panel key={value} value={value}>
|
||||||
|
{content}
|
||||||
|
</MantineTabs.Panel>
|
||||||
|
))}
|
||||||
|
</MantineTabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
11
src/components/shared/ToggleActionIcon/ToggleActionIcon.module.css
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
.icon {
|
||||||
|
border: 1px solid;
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
border-color: grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/components/shared/ToggleActionIcon/index.tsx
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ActionIcon } from '@mantine/core';
|
||||||
|
|
||||||
|
import styles from './ToggleActionIcon.module.css';
|
||||||
|
|
||||||
|
interface ToggleActionIconProps {
|
||||||
|
isActive?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
children: React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mantine ActionIcon
|
||||||
|
*
|
||||||
|
* @see https://mantine.dev/core/action-icon/
|
||||||
|
*/
|
||||||
|
export const ToggleActionIcon = ({
|
||||||
|
onClick,
|
||||||
|
isActive,
|
||||||
|
children,
|
||||||
|
}: ToggleActionIconProps): React.JSX.Element => (
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
onClick={onClick}
|
||||||
|
className={styles.icon}
|
||||||
|
color={isActive ? '' : 'dimmed'}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ActionIcon>
|
||||||
|
);
|
||||||
21
src/components/shared/UploadFrame/index.tsx
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
import { FileInput, Stack } from '@mantine/core';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface UploadFrameProps {
|
||||||
|
placeholder: string;
|
||||||
|
children: (file: File | null) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UploadFrame = ({
|
||||||
|
children,
|
||||||
|
placeholder,
|
||||||
|
}: UploadFrameProps): React.JSX.Element => {
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<FileInput clearable onChange={setFile} placeholder={placeholder} />
|
||||||
|
{children(file)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
54
src/global.css
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--grid-color-light: rgba(0, 0, 0, 0.1);
|
||||||
|
--grid-color-dark: rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
--hidden-track: transparent;
|
||||||
|
--track-bar: rgba(190, 190, 190, 0.4);
|
||||||
|
--track-bar-hover: rgba(190, 190, 190, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* STYLED SCROLLBARS */
|
||||||
|
/* Firefox */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--track-bar) var(--hidden-track);
|
||||||
|
}
|
||||||
|
/* Chrome, Edge, and Safari */
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 17px;
|
||||||
|
}
|
||||||
|
*::-webkit-scrollbar-track {
|
||||||
|
background: var(--hidden-track);
|
||||||
|
}
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--track-bar);
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 3px solid var(--hidden-track);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--track-bar-hover);
|
||||||
|
}
|
||||||
|
/* EOF STYLED SCROLLBARS */
|
||||||
|
|
||||||
|
.header {
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
z-index: 1000;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: fixed;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--mantine-padding-lg);
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: var(--mantine-radius-md);
|
||||||
|
}
|
||||||
5
src/index.tsx
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import { App } from './App';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root') as HTMLElement).render(<App />);
|
||||||
1
src/lib/hooks/index.ts
Executable file
@@ -0,0 +1 @@
|
|||||||
|
export * from './useStrictColorScheme';
|
||||||
19
src/lib/hooks/useStrictColorScheme/index.ts
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useComputedColorScheme, useMantineColorScheme } from '@mantine/core';
|
||||||
|
|
||||||
|
interface StrictColorSchemeResponse {
|
||||||
|
colorScheme: 'light' | 'dark';
|
||||||
|
toggleColorScheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStrictColorScheme = (): StrictColorSchemeResponse => {
|
||||||
|
const { setColorScheme } = useMantineColorScheme();
|
||||||
|
const computedColorScheme = useComputedColorScheme('light', {
|
||||||
|
getInitialValueInEffect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleColorScheme = (): void => {
|
||||||
|
setColorScheme(computedColorScheme === 'light' ? 'dark' : 'light');
|
||||||
|
};
|
||||||
|
|
||||||
|
return { toggleColorScheme, colorScheme: computedColorScheme };
|
||||||
|
};
|
||||||
19
src/lib/providers/index.tsx
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
import { MantineProvider } from '@mantine/core';
|
||||||
|
import { DatesProvider } from '@mantine/dates';
|
||||||
|
|
||||||
|
import { theme } from '@/lib/theme';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface ProvidersProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Providers = ({ children }: ProvidersProps): React.JSX.Element => (
|
||||||
|
<BrowserRouter>
|
||||||
|
<MantineProvider theme={theme} defaultColorScheme="light">
|
||||||
|
<DatesProvider settings={{ consistentWeeks: true }}>
|
||||||
|
{children}
|
||||||
|
</DatesProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
5
src/lib/store/defaultState.ts
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
import { AppState, defaultAppState } from './slices/appSlice';
|
||||||
|
|
||||||
|
export type StoreState = AppState;
|
||||||
|
|
||||||
|
export const defaultState: AppState = { ...defaultAppState };
|
||||||
9
src/lib/store/index.ts
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { AppSlice, createAppSlice } from './slices/appSlice';
|
||||||
|
|
||||||
|
export type Store = AppSlice;
|
||||||
|
|
||||||
|
export const useStore = create<Store>((set, get, store) => ({
|
||||||
|
...createAppSlice(set, get, store),
|
||||||
|
}));
|
||||||
24
src/lib/store/slices/appSlice.ts
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
import { StateCreator } from 'zustand';
|
||||||
|
|
||||||
|
import { Store } from '..';
|
||||||
|
|
||||||
|
export interface AppState {
|
||||||
|
isDarkMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppActions = {
|
||||||
|
setIsDarkMode(isDarkMode: AppState['isDarkMode']): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AppSlice = AppState & AppActions;
|
||||||
|
|
||||||
|
/** This object enables us to reset state on the fly */
|
||||||
|
export const defaultAppState: AppState = {
|
||||||
|
isDarkMode: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createAppSlice: StateCreator<Store, [], [], AppSlice> = set => ({
|
||||||
|
...defaultAppState,
|
||||||
|
|
||||||
|
setIsDarkMode: isDarkMode => set({ isDarkMode }),
|
||||||
|
});
|
||||||
94
src/lib/theme/index.ts
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
import { createTheme } from '@mantine/core';
|
||||||
|
|
||||||
|
export const theme = createTheme({
|
||||||
|
fontFamily: '"Inter", sans-serif',
|
||||||
|
colors: {
|
||||||
|
brandOrange: [
|
||||||
|
'#ffede3',
|
||||||
|
'#ffdbcc',
|
||||||
|
'#ffb59a',
|
||||||
|
'#ff8d63',
|
||||||
|
'#ff6a36',
|
||||||
|
'#ff5418',
|
||||||
|
'#ff4807',
|
||||||
|
'#e43900',
|
||||||
|
'#cc3100',
|
||||||
|
'#b22500',
|
||||||
|
],
|
||||||
|
brandBlue: [
|
||||||
|
'#f2f8f8',
|
||||||
|
'#e7eced',
|
||||||
|
'#c8d9da',
|
||||||
|
'#a7c4c6',
|
||||||
|
'#8bb3b5',
|
||||||
|
'#79a8ab',
|
||||||
|
'#6ea3a6',
|
||||||
|
'#5d8e91',
|
||||||
|
'#4f7f82',
|
||||||
|
'#3c6e71',
|
||||||
|
],
|
||||||
|
brandYellow: [
|
||||||
|
'#fff9e1',
|
||||||
|
'#fff1cc',
|
||||||
|
'#ffe19b',
|
||||||
|
'#ffd164',
|
||||||
|
'#ffc238',
|
||||||
|
'#ffba1c',
|
||||||
|
'#ffb509',
|
||||||
|
'#e39f00',
|
||||||
|
'#ca8d00',
|
||||||
|
'#af7800',
|
||||||
|
],
|
||||||
|
brandBrown: [
|
||||||
|
'#f8f3f2',
|
||||||
|
'#e8e5e4',
|
||||||
|
'#d3c6c4',
|
||||||
|
'#bea6a1',
|
||||||
|
'#ad8a84',
|
||||||
|
'#a37870',
|
||||||
|
'#9f6f67',
|
||||||
|
'#8b5e56',
|
||||||
|
'#7d534c',
|
||||||
|
'#6f463f',
|
||||||
|
],
|
||||||
|
brandGrey: [
|
||||||
|
'#f5f5f5',
|
||||||
|
'#e7e7e7',
|
||||||
|
'#cdcdcd',
|
||||||
|
'#b2b2b2',
|
||||||
|
'#9a9a9a',
|
||||||
|
'#8b8b8b',
|
||||||
|
'#848484',
|
||||||
|
'#717171',
|
||||||
|
'#656565',
|
||||||
|
'#575757',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
primaryShade: { light: 4, dark: 7 },
|
||||||
|
primaryColor: 'brandOrange',
|
||||||
|
headings: {
|
||||||
|
fontFamily: '"Bricolage Grotesque", sans-serif',
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
brand: {
|
||||||
|
dark: {
|
||||||
|
primary: '#FF6F3C',
|
||||||
|
secondary: '#3C6E71',
|
||||||
|
accent: '#FFD166',
|
||||||
|
background: '#2D2D2D',
|
||||||
|
text: '#FFF8E1',
|
||||||
|
border: '#4A4A4A',
|
||||||
|
hover: '#464646',
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
primary: '#FF6F3C',
|
||||||
|
secondary: '#3C6E71',
|
||||||
|
accent: '#FFD166',
|
||||||
|
background: '#FFF8E1',
|
||||||
|
text: '#3E2723',
|
||||||
|
border: '#DADADA',
|
||||||
|
hover: '#e9e6df',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
54
src/lib/utils/auth/hooks/useAuthorize.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// useLogin.ts
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface AuthorizeRequest {
|
||||||
|
redirect_uri: string;
|
||||||
|
client_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizeResponse {
|
||||||
|
token: string;
|
||||||
|
expires_in: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseAuthorize {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
authorize: (payload: AuthorizeRequest) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthorize = (): UseAuthorize => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse<AuthorizeResponse> | null>(null);
|
||||||
|
|
||||||
|
const authorize = useCallback((payload: AuthorizeRequest): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.post<AuthorizeResponse>('v1/auth/authorize', payload)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse<AuthorizeResponse>(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, authorize };
|
||||||
|
};
|
||||||
62
src/lib/utils/auth/hooks/useCreateClient.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface CreateClientRequest {
|
||||||
|
client_name: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateClientResponse {
|
||||||
|
client_id: string;
|
||||||
|
client_name: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
client_secret: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseCreateClient {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse<CreateClientResponse> | null;
|
||||||
|
createClient: (payload: CreateClientRequest) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreateClient = (): UseCreateClient => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse<CreateClientResponse> | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const createClient = useCallback(
|
||||||
|
(payload: CreateClientRequest): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = 'v1/clients';
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.post<CreateClientResponse>(url, payload)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse<CreateClientResponse>(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isLoading, error, data, createClient };
|
||||||
|
};
|
||||||
50
src/lib/utils/auth/hooks/useDeleteAdminClient.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
interface UseDeleteAdminClient {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
deleteClient: (clientId: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteAdminClient = (): UseDeleteAdminClient => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const deleteClient = useCallback((clientId: string): Promise<void> => {
|
||||||
|
if (!clientId) {
|
||||||
|
setError({ message: 'Client ID is required.' });
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = `v1/admin/clients/${clientId}`;
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.delete(url)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, deleteClient };
|
||||||
|
};
|
||||||
50
src/lib/utils/auth/hooks/useDeleteAdminUser.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
interface UseDeleteAdminUser {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
deleteUser: (userId: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteAdminUser = (): UseDeleteAdminUser => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const deleteUser = useCallback((userId: string): Promise<void> => {
|
||||||
|
if (!userId) {
|
||||||
|
setError({ message: 'User ID is required.' });
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = `v1/admin/users/${userId}`;
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.delete(url)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, deleteUser };
|
||||||
|
};
|
||||||
57
src/lib/utils/auth/hooks/useDeleteUserData.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface UserDeleteRequest {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseDeleteUserData {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
deleteUserData: (payload: UserDeleteRequest) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteUserData = (): UseDeleteUserData => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const deleteUserData = useCallback(
|
||||||
|
(payload: UserDeleteRequest): Promise<void> => {
|
||||||
|
if (!payload?.code) {
|
||||||
|
setError({ message: 'Verification code is required.' });
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = 'v1/user/me/data';
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.delete(url, payload)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isLoading, error, data, deleteUserData };
|
||||||
|
};
|
||||||
66
src/lib/utils/auth/hooks/useFetchAdminClients.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface OauthClient {
|
||||||
|
client_id: string;
|
||||||
|
owner_id: string;
|
||||||
|
client_name: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchClientsResponse {
|
||||||
|
clients: OauthClient[];
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
has_next_page: boolean;
|
||||||
|
has_prev_page: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseFetchAdminClients {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse<FetchClientsResponse> | null;
|
||||||
|
fetchClients: (limit?: number, offset?: number) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFetchAdminClients = (): UseFetchAdminClients => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse<FetchClientsResponse> | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchClients = useCallback((limit = 10, offset = 0): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = `v1/admin/clients?limit=${limit}&offset=${offset}`;
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.get<FetchClientsResponse>(url)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse<FetchClientsResponse>(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, fetchClients };
|
||||||
|
};
|
||||||
71
src/lib/utils/auth/hooks/useFetchAdminUsers.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
user_id: string;
|
||||||
|
email: string;
|
||||||
|
nickname: string;
|
||||||
|
last_logged_in?: number;
|
||||||
|
last_password_change?: number;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
is_active: boolean;
|
||||||
|
role: string;
|
||||||
|
last_data_request?: number;
|
||||||
|
deleted_at?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchUsersResponse {
|
||||||
|
users: User[];
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
has_next_page: boolean;
|
||||||
|
has_prev_page: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseFetchAdminUsers {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse<FetchUsersResponse> | null;
|
||||||
|
fetchUsers: (limit?: number, offset?: number) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFetchAdminUsers = (): UseFetchAdminUsers => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse<FetchUsersResponse> | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchUsers = useCallback((limit = 10, offset = 0): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = `v1/admin/users?limit=${limit}&offset=${offset}`;
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.get<FetchUsersResponse>(url)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse<FetchUsersResponse>(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, fetchUsers };
|
||||||
|
};
|
||||||
53
src/lib/utils/auth/hooks/useFetchClient.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface FetchClientResponse {
|
||||||
|
client_id: string;
|
||||||
|
client_name: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseFetchClientById {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse<FetchClientResponse> | null;
|
||||||
|
fetchClientById: (clientId: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFetchClientById = (): UseFetchClientById => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse<FetchClientResponse> | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchClientById = useCallback((clientId: string): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = `v1/client/${clientId}`;
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.get<FetchClientResponse>(url)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse<FetchClientResponse>(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, fetchClientById };
|
||||||
|
};
|
||||||
55
src/lib/utils/auth/hooks/useFetchClients.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface Client {
|
||||||
|
client_id: string;
|
||||||
|
client_name: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FetchClientsResponse = Client[];
|
||||||
|
|
||||||
|
interface UseFetchClients {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse<FetchClientsResponse> | null;
|
||||||
|
fetchClients: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFetchClients = (): UseFetchClients => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse<FetchClientsResponse> | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchClients = useCallback((): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = 'v1/clients';
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.get<FetchClientsResponse>(url)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse<FetchClientsResponse>(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, fetchClients };
|
||||||
|
};
|
||||||
54
src/lib/utils/auth/hooks/useFetchCurrentUser.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface FetchUserResponse {
|
||||||
|
user_id: string;
|
||||||
|
email: string;
|
||||||
|
nickname: string;
|
||||||
|
role: string;
|
||||||
|
created_at: number;
|
||||||
|
last_data_request: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseFetchCurrentUser {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse<FetchUserResponse> | null;
|
||||||
|
fetchCurrentUser: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFetchCurrentUser = (): UseFetchCurrentUser => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse<FetchUserResponse> | null>(null);
|
||||||
|
|
||||||
|
const fetchCurrentUser = useCallback((): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = 'v1/user/me';
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.get<FetchUserResponse>(url)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse<FetchUserResponse>(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, fetchCurrentUser };
|
||||||
|
};
|
||||||
49
src/lib/utils/auth/hooks/useLogin.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// useLogin.ts
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseLoginReturn {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
login: (payload: LoginRequest) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLogin = (): UseLoginReturn => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const login = useCallback((payload: LoginRequest): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.post('v1/auth/login', payload)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, login };
|
||||||
|
};
|
||||||
44
src/lib/utils/auth/hooks/useLogout.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// useLogin.ts
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
interface UseLogout {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLogout = (): UseLogout => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const logout = useCallback((): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.get('v1/auth/logout')
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, logout };
|
||||||
|
};
|
||||||
45
src/lib/utils/auth/hooks/usePostUserData.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
interface UsePostUserData {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
postUserData: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePostUserData = (): UsePostUserData => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const postUserData = useCallback((): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = 'v1/user/me/data';
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.post(url)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, postUserData };
|
||||||
|
};
|
||||||
44
src/lib/utils/auth/hooks/useRefresh.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// useLogin.ts
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
interface UseRefresh {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRefresh = (): UseRefresh => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const refresh = useCallback((): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.post('v1/auth/token/refresh')
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, refresh };
|
||||||
|
};
|
||||||
50
src/lib/utils/auth/hooks/useRegister.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// useLogin.ts
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
email: string;
|
||||||
|
code: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseRegister {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
register: (payload: RegisterRequest) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRegister = (): UseRegister => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const register = useCallback((payload: RegisterRequest): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.post('v1/auth/register', payload)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, register };
|
||||||
|
};
|
||||||
50
src/lib/utils/auth/hooks/useReset.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// useLogin.ts
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface ResetRequest {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseResetReturn {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
reset: (payload: ResetRequest) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useReset = (): UseResetReturn => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const reset = useCallback((payload: ResetRequest): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.patch('v1/auth/reset', payload)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isLoading, error, data, reset };
|
||||||
|
};
|
||||||
55
src/lib/utils/auth/hooks/useSendEmailCode.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// useLogin.ts
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
OTPTypes,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface SendEmailCodeRequest {
|
||||||
|
email: string;
|
||||||
|
kind: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseSendEmailCode {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
sendEmailCode: (email: string, kind: number) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSendEmailCode = (): UseSendEmailCode => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const sendEmailCode = useCallback(
|
||||||
|
(email: string, kind: OTPTypes): Promise<void> => {
|
||||||
|
const payload: SendEmailCodeRequest = { email, kind };
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.post('v1/auth/send-email-code', payload)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isLoading, error, data, sendEmailCode };
|
||||||
|
};
|
||||||
65
src/lib/utils/auth/hooks/useSession.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
interface UseSession {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useSession
|
||||||
|
* ----------
|
||||||
|
* Automatically checks the user's session on mount.
|
||||||
|
* Returns only state — no internal navigation or manual triggers.
|
||||||
|
*/
|
||||||
|
export const useSession = (): UseSession => {
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
apiClient()
|
||||||
|
.get('v1/auth/session')
|
||||||
|
.then(res => {
|
||||||
|
if (!isMounted) return;
|
||||||
|
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
setError(null);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
setData(null);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
setData(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (isMounted) {
|
||||||
|
setError({ message: 'Network or unexpected error' });
|
||||||
|
setData(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (isMounted) setIsLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isAuthenticated = !!data && !error;
|
||||||
|
|
||||||
|
return { isLoading, error, data, isAuthenticated };
|
||||||
|
};
|
||||||
63
src/lib/utils/auth/hooks/useUpdateClient.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface UpdateClientRequest {
|
||||||
|
client_name: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
regenerate_secret: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateClientResponse {
|
||||||
|
client_secret?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseUpdateClient {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse<UpdateClientResponse> | null;
|
||||||
|
updateClient: (
|
||||||
|
clientId: string,
|
||||||
|
payload: UpdateClientRequest,
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateClient = (): UseUpdateClient => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse<UpdateClientResponse> | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateClient = useCallback(
|
||||||
|
(clientId: string, payload: UpdateClientRequest): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = `v1/client/${clientId}`;
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.patch<UpdateClientResponse>(url, payload)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse<UpdateClientResponse>(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isLoading, error, data, updateClient };
|
||||||
|
};
|
||||||
69
src/lib/utils/auth/hooks/useUpdateCurrentUser.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface UpdateUserRequest {
|
||||||
|
nickname: string;
|
||||||
|
code?: string;
|
||||||
|
email?: string;
|
||||||
|
old_password?: string;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseUpdateCurrentUser {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
updateCurrentUser: (payload: UpdateUserRequest) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateCurrentUser = (): UseUpdateCurrentUser => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const updateCurrentUser = useCallback(
|
||||||
|
(payload: UpdateUserRequest): Promise<void> => {
|
||||||
|
if (payload.email && !payload.code) {
|
||||||
|
setError({
|
||||||
|
message: 'Verification code is required when updating email.',
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (payload.password && !payload.old_password) {
|
||||||
|
setError({
|
||||||
|
message: 'Old password is required when updating password.',
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = 'v1/user/me';
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.patch(url, payload)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isLoading, error, data, updateCurrentUser };
|
||||||
|
};
|
||||||
65
src/lib/utils/auth/hooks/useUpdateUserRole.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
APIResponse,
|
||||||
|
apiClient,
|
||||||
|
isApiResponse,
|
||||||
|
isApiError,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
export interface HandleUpdateRoleRequest {
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseUpdateUserRole {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
data: APIResponse | null;
|
||||||
|
updateUserRole: (
|
||||||
|
userId: string,
|
||||||
|
payload: HandleUpdateRoleRequest,
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateUserRole = (): UseUpdateUserRole => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [data, setData] = useState<APIResponse | null>(null);
|
||||||
|
|
||||||
|
const updateUserRole = useCallback(
|
||||||
|
(userId: string, payload: HandleUpdateRoleRequest): Promise<void> => {
|
||||||
|
if (!userId) {
|
||||||
|
setError({ message: 'User ID is required.' });
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload?.role) {
|
||||||
|
setError({ message: 'Role field is required.' });
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setData(null);
|
||||||
|
|
||||||
|
const url = `v1/admin/users/${userId}/role`;
|
||||||
|
|
||||||
|
return apiClient()
|
||||||
|
.patch(url, payload)
|
||||||
|
.then(res => {
|
||||||
|
if (isApiResponse(res)) {
|
||||||
|
setData(res);
|
||||||
|
} else if (isApiError(res)) {
|
||||||
|
setError(res);
|
||||||
|
} else {
|
||||||
|
setError({ message: 'Received unknown response structure.' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError({ message: 'Network or unexpected error' }))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isLoading, error, data, updateUserRole };
|
||||||
|
};
|
||||||
193
src/lib/utils/auth/index.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
// apiClient.ts
|
||||||
|
export type APIResponseType = 'A' | 'B' | 'C' | 'S' | 'V';
|
||||||
|
|
||||||
|
export enum APIStatusCode {
|
||||||
|
// Generic
|
||||||
|
OK = 0,
|
||||||
|
Default = 1000,
|
||||||
|
InvalidRequest = 1001,
|
||||||
|
NotFound = 1002,
|
||||||
|
Conflict = 1003,
|
||||||
|
RateLimited = 1004,
|
||||||
|
ValidationFailed = 1005,
|
||||||
|
|
||||||
|
// Authentication / Authorization
|
||||||
|
Unauthorized = 2000,
|
||||||
|
Forbidden = 2001,
|
||||||
|
TokenExpired = 2002,
|
||||||
|
TokenInvalid = 2003,
|
||||||
|
SessionExpired = 2004,
|
||||||
|
|
||||||
|
// Server Errors
|
||||||
|
InternalError = 3000,
|
||||||
|
DBError = 3001,
|
||||||
|
CacheError = 3002,
|
||||||
|
ServiceDown = 3003,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OTPTypes {
|
||||||
|
OTPRegister = 3,
|
||||||
|
OTPResetPassword = 7,
|
||||||
|
OTPNewEmail = 9,
|
||||||
|
OTPDataRequest = 15,
|
||||||
|
OTPDeleteRequest = 19,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIResponse<T = any> {
|
||||||
|
data?: T;
|
||||||
|
msg: string;
|
||||||
|
status: APIStatusCode;
|
||||||
|
type: APIResponseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
|
message: string;
|
||||||
|
status?: number;
|
||||||
|
code?: APIStatusCode;
|
||||||
|
type?: APIResponseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiClientOptions {
|
||||||
|
baseUrl?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Type Guards ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime type guard for checking if a value is a valid APIResponse<T>.
|
||||||
|
*/
|
||||||
|
export function isApiResponse<T>(obj: any): obj is APIResponse<T> {
|
||||||
|
return (
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
obj !== null &&
|
||||||
|
typeof obj.msg === 'string' &&
|
||||||
|
typeof obj.status === 'number' &&
|
||||||
|
typeof obj.type === 'string' &&
|
||||||
|
['A', 'B', 'C', 'S', 'V'].includes(obj.type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime type guard for checking if a value is an ApiError.
|
||||||
|
*/
|
||||||
|
export function isApiError(obj: any): obj is ApiError {
|
||||||
|
return (
|
||||||
|
typeof obj === 'object' && obj !== null && typeof obj.message === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- API Client ----------
|
||||||
|
|
||||||
|
export class ApiClient {
|
||||||
|
private baseUrl: string;
|
||||||
|
private headers: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(options: ApiClientOptions = {}) {
|
||||||
|
this.baseUrl = options.baseUrl || 'https://auth.etwo.dev/api/';
|
||||||
|
this.headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(options.headers || {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.token) {
|
||||||
|
this.headers['Authorization'] = `Bearer ${options.token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleResponse<T>(
|
||||||
|
res: Response,
|
||||||
|
): Promise<APIResponse<T> | ApiError> {
|
||||||
|
return res
|
||||||
|
.json()
|
||||||
|
.then((json: any) => {
|
||||||
|
if (isApiResponse<T>(json)) {
|
||||||
|
if (!res.ok || json.status !== APIStatusCode.OK) {
|
||||||
|
return {
|
||||||
|
message: json.msg || res.statusText,
|
||||||
|
status: res.status,
|
||||||
|
code: json.status,
|
||||||
|
type: json.type,
|
||||||
|
} as ApiError;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response was not a valid APIResponse<T>
|
||||||
|
return {
|
||||||
|
message: 'Unexpected response structure from server.',
|
||||||
|
status: res.status,
|
||||||
|
} as ApiError;
|
||||||
|
})
|
||||||
|
.catch(() => ({
|
||||||
|
message: 'Invalid JSON response from server.',
|
||||||
|
status: res.status,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T = any>(
|
||||||
|
path: string,
|
||||||
|
config?: RequestInit,
|
||||||
|
): Promise<APIResponse<T> | ApiError> {
|
||||||
|
return fetch(`${this.baseUrl}${path}`, {
|
||||||
|
...config,
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.headers,
|
||||||
|
}).then(res => this.handleResponse<T>(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
post<T = any>(
|
||||||
|
path: string,
|
||||||
|
body?: any,
|
||||||
|
config?: RequestInit,
|
||||||
|
): Promise<APIResponse<T> | ApiError> {
|
||||||
|
return fetch(`${this.baseUrl}${path}`, {
|
||||||
|
...config,
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
}).then(res => this.handleResponse<T>(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
patch<T = any>(
|
||||||
|
path: string,
|
||||||
|
body?: any,
|
||||||
|
config?: RequestInit,
|
||||||
|
): Promise<APIResponse<T> | ApiError> {
|
||||||
|
return fetch(`${this.baseUrl}${path}`, {
|
||||||
|
...config,
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: this.headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
}).then(res => this.handleResponse<T>(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
put<T = any>(
|
||||||
|
path: string,
|
||||||
|
body?: any,
|
||||||
|
config?: RequestInit,
|
||||||
|
): Promise<APIResponse<T> | ApiError> {
|
||||||
|
return fetch(`${this.baseUrl}${path}`, {
|
||||||
|
...config,
|
||||||
|
method: 'PUT',
|
||||||
|
headers: this.headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
}).then(res => this.handleResponse<T>(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
delete<T = any>(
|
||||||
|
path: string,
|
||||||
|
body?: any,
|
||||||
|
config?: RequestInit,
|
||||||
|
): Promise<APIResponse<T> | ApiError> {
|
||||||
|
return fetch(`${this.baseUrl}${path}`, {
|
||||||
|
...config,
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: this.headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
}).then(res => this.handleResponse<T>(res));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiClient = (options?: ApiClientOptions) => new ApiClient(options);
|
||||||
39
src/lib/utils/copyToClipboard/index.ts
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copies a given string to the system clipboard.
|
||||||
|
*
|
||||||
|
* This function first attempts to use the modern Clipboard API (`navigator.clipboard.writeText`)
|
||||||
|
* if it is available and the context is secure (i.e., HTTPS). If that is not possible,
|
||||||
|
* it falls back to creating a temporary `<textarea>` element and using `document.execCommand('copy')`.
|
||||||
|
*
|
||||||
|
* @param text - The string to be copied to the clipboard.
|
||||||
|
* @returns A Promise that resolves to `true` if the copy succeeded, or `false` otherwise.
|
||||||
|
*/
|
||||||
|
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||||
|
if (!text) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} else {
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
|
const successful = document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
|
||||||
|
if (!successful) throw new Error('Fallback copy command failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Copy to clipboard failed:', err);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/lib/utils/debounce/index.ts
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Debounce helper util
|
||||||
|
*
|
||||||
|
* This method will debounce calls made to the callback using the timeout
|
||||||
|
* provided. Set immediate to `true` to run the function immediately, then
|
||||||
|
* debounce further calls.
|
||||||
|
*/
|
||||||
|
export const debounce = <T extends (...args: unknown[]) => unknown>(
|
||||||
|
callBack: T,
|
||||||
|
wait: number,
|
||||||
|
immediate = false,
|
||||||
|
): VoidFunction => {
|
||||||
|
let timeout: NodeJS.Timeout | null;
|
||||||
|
|
||||||
|
return (...params: unknown[]): void => {
|
||||||
|
const args = params;
|
||||||
|
const later = (): void => {
|
||||||
|
timeout = null;
|
||||||
|
if (!immediate) callBack(...args);
|
||||||
|
};
|
||||||
|
const callNow = immediate && !timeout;
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
if (callNow) callBack(...args);
|
||||||
|
};
|
||||||
|
};
|
||||||
1
src/setupTests.ts
Executable file
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
116
tsconfig.json
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es2020"
|
||||||
|
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||||
|
"jsx": "react-jsx" /* Specify what JSX code is generated. */,
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
/* Modules */
|
||||||
|
// "module": "commonjs", /* Specify what module code is generated. */
|
||||||
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
|
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [],
|
||||||
|
"paths": {
|
||||||
|
"@/components/*": ["./src/components/*"],
|
||||||
|
"@/lib/*": ["./src/lib/*"]
|
||||||
|
} /* Allow multiple folders to be treated as one when resolving modules. */,
|
||||||
|
"typeRoots": [
|
||||||
|
"./types",
|
||||||
|
"./node_modules/@types"
|
||||||
|
] /* Specify multiple folders that act like `./node_modules/@types`. */,
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"jest",
|
||||||
|
"react"
|
||||||
|
] /* Specify type package names to be included without being referenced in a source file. */,
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||||
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||||
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
"noEmit": true /* Disable emitting files from a compilation. */,
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "types"]
|
||||||
|
}
|
||||||
13
vite.config.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||