[main]: front start
This commit is contained in:
parent
7dc9d18ccd
commit
cbfbb036ee
6
front/.babelrc.json
Normal file
6
front/.babelrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
"@babel/preset-react"
|
||||||
|
]
|
||||||
|
}
|
1
front/.gitignore
vendored
Normal file
1
front/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/node_modules
|
6
front/.prettierrc.json
Normal file
6
front/.prettierrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
BIN
front/README copy.md
Normal file
BIN
front/README copy.md
Normal file
Binary file not shown.
4
front/custom.d.ts
vendored
Normal file
4
front/custom.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.svg' {
|
||||||
|
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
|
||||||
|
export default content;
|
||||||
|
}
|
19
front/eslint.config.js
Normal file
19
front/eslint.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
'simple-import-sort': simpleImportSort,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'simple-import-sort/imports': 'warn',
|
||||||
|
'simple-import-sort/exports': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
17990
front/package-lock.json
generated
Normal file
17990
front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
66
front/package.json
Normal file
66
front/package.json
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"name": "template",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"build-storybook": "storybook build",
|
||||||
|
"lint": "eslint src/**/*.{js,ts,tsx}",
|
||||||
|
"lint-fix": "npm run lint -- --fix",
|
||||||
|
"start": "webpack serve --mode development",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"style-lint": "npx stylelint '**/*.{css,scss}'",
|
||||||
|
"style-lint-fix": "npm run style-lint -- --fix",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@svgr/webpack": "^8.1.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "6.26.0",
|
||||||
|
"zustand": "5.0.0-rc.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/preset-react": "^7.24.7",
|
||||||
|
"@babel/preset-typescript": "^7.24.7",
|
||||||
|
"@chromatic-com/storybook": "^1.6.1",
|
||||||
|
"@eslint/js": "^9.9.0",
|
||||||
|
"@storybook/addon-essentials": "^8.2.9",
|
||||||
|
"@storybook/addon-interactions": "^8.2.9",
|
||||||
|
"@storybook/addon-links": "^8.2.9",
|
||||||
|
"@storybook/addon-onboarding": "^8.2.9",
|
||||||
|
"@storybook/addon-webpack5-compiler-swc": "^1.0.5",
|
||||||
|
"@storybook/blocks": "^8.2.9",
|
||||||
|
"@storybook/react": "^8.2.9",
|
||||||
|
"@storybook/react-webpack5": "^8.2.9",
|
||||||
|
"@storybook/test": "^8.2.9",
|
||||||
|
"@types/eslint__js": "^8.42.3",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"babel-loader": "^9.1.3",
|
||||||
|
"css-loader": "^7.1.2",
|
||||||
|
"eslint": "^9.9.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
|
"html-webpack-plugin": "^5.5.0",
|
||||||
|
"lint-staged": "^15.2.9",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"sass": "^1.77.8",
|
||||||
|
"sass-loader": "^16.0.0",
|
||||||
|
"storybook": "^8.2.9",
|
||||||
|
"style-loader": "^4.0.0",
|
||||||
|
"stylelint": "^16.8.1",
|
||||||
|
"stylelint-config-standard-scss": "^13.1.0",
|
||||||
|
"ts-jest": "^29.2.4",
|
||||||
|
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||||
|
"typescript": "^5.5.4",
|
||||||
|
"typescript-eslint": "^8.1.0",
|
||||||
|
"typescript-plugin-css-modules": "^5.1.0",
|
||||||
|
"webpack": "^5.93.0",
|
||||||
|
"webpack-cli": "^5.1.4",
|
||||||
|
"webpack-dev-server": "^5.0.4"
|
||||||
|
}
|
||||||
|
}
|
BIN
front/public/fonts/rubik/bold.woff2
Normal file
BIN
front/public/fonts/rubik/bold.woff2
Normal file
Binary file not shown.
BIN
front/public/fonts/rubik/medium.woff2
Normal file
BIN
front/public/fonts/rubik/medium.woff2
Normal file
Binary file not shown.
BIN
front/public/fonts/rubik/regular.woff2
Normal file
BIN
front/public/fonts/rubik/regular.woff2
Normal file
Binary file not shown.
20
front/public/fonts/rubik/styles.css
Normal file
20
front/public/fonts/rubik/styles.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: Rubik;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('./regular.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Rubik;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url('./medium.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Rubik;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url('./bold.woff2') format('woff2');
|
||||||
|
}
|
1
front/public/fonts/styles.css
Normal file
1
front/public/fonts/styles.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import "./rubik/styles.css";
|
16
front/public/images/svg/hide.svg
Normal file
16
front/public/images/svg/hide.svg
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 13 13" style="enable-background:new 0 0 13 13;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M6.5,9C7.88,9,9,7.88,9,6.5c0-0.12-0.01-0.24-0.03-0.35L6.15,8.97C6.26,8.99,6.38,9,6.5,9z"/>
|
||||||
|
<path d="M5.12,8.58L5.12,8.58l0.73-0.73l0,0l1.99-1.99l0.73-0.73l2.09-2.09L11.8,1.9L11.1,1.2L9.85,2.44
|
||||||
|
C8.86,1.85,7.71,1.5,6.5,1.5c-2.65,0-5,1.58-6.01,4.04c-0.25,0.61-0.25,1.31,0,1.92c0.41,1,1.05,1.84,1.84,2.51L1.2,11.1L1.9,11.8
|
||||||
|
l2.6-2.6L5.12,8.58z M5.15,7.14C5.06,6.95,5,6.73,5,6.5C5,5.67,5.67,5,6.5,5c0.23,0,0.45,0.06,0.64,0.15L5.15,7.14z M1.42,7.08
|
||||||
|
c-0.15-0.37-0.15-0.8,0-1.17C2.27,3.84,4.26,2.5,6.5,2.5c0.94,0,1.83,0.25,2.62,0.68L7.88,4.42C7.48,4.15,7.01,4,6.5,4
|
||||||
|
C5.12,4,4,5.12,4,6.5c0,0.51,0.15,0.98,0.42,1.38L3.04,9.26C2.34,8.69,1.77,7.96,1.42,7.08z"/>
|
||||||
|
<path d="M12.51,5.54c-0.28-0.68-0.67-1.27-1.13-1.8l-0.71,0.71c0.37,0.43,0.69,0.92,0.91,1.47c0.15,0.37,0.15,0.79,0,1.17
|
||||||
|
C10.73,9.16,8.74,10.5,6.5,10.5c-0.56,0-1.1-0.1-1.62-0.26L4.1,11.02c0.75,0.3,1.56,0.48,2.4,0.48c2.65,0,5.01-1.58,6.01-4.04
|
||||||
|
C12.76,6.85,12.76,6.15,12.51,5.54z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
10
front/public/images/svg/show.svg
Normal file
10
front/public/images/svg/show.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 13 13" style="enable-background:new 0 0 13 13;" xml:space="preserve">
|
||||||
|
<path d="M6.5,11.5c-2.65,0-5-1.58-6.01-4.04c-0.25-0.61-0.25-1.31,0-1.92C1.5,3.09,3.85,1.5,6.5,1.5c2.65,0,5.01,1.58,6.01,4.04
|
||||||
|
c0.25,0.61,0.25,1.31,0,1.92C11.51,9.91,9.15,11.5,6.5,11.5z M6.5,2.5c-2.24,0-4.23,1.34-5.08,3.42c-0.15,0.37-0.15,0.79,0,1.17
|
||||||
|
C2.27,9.16,4.26,10.5,6.5,10.5c2.24,0,4.23-1.34,5.08-3.42c0.15-0.37,0.15-0.8,0-1.17C10.73,3.84,8.74,2.5,6.5,2.5z"/>
|
||||||
|
<path d="M6.5,9C5.12,9,4,7.88,4,6.5S5.12,4,6.5,4S9,5.12,9,6.5S7.88,9,6.5,9z M6.5,5C5.67,5,5,5.67,5,6.5S5.67,8,6.5,8S8,7.33,8,6.5
|
||||||
|
S7.33,5,6.5,5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 876 B |
11
front/public/index.html
Normal file
11
front/public/index.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
37
front/src/components/app/_theme.scss
Normal file
37
front/src/components/app/_theme.scss
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
@mixin light {
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
--clr-primary: #363a4e;
|
||||||
|
|
||||||
|
--clr-secondary: #bca59f;
|
||||||
|
|
||||||
|
--clr-accent: #80845c;
|
||||||
|
--clr-accent-o50: #80845c80;
|
||||||
|
|
||||||
|
--clr-layer-100: #EAECF1;
|
||||||
|
--clr-layer-200: #DFE1E6;
|
||||||
|
--clr-layer-300: #FFFFFF;
|
||||||
|
|
||||||
|
--clr-text-100: #0f1015;
|
||||||
|
|
||||||
|
--clr-shadow: #363a4e1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
|
--clr-primary: #b1b5c9;
|
||||||
|
|
||||||
|
--clr-secondary: #604943;
|
||||||
|
|
||||||
|
--clr-accent: #9fa37b;
|
||||||
|
--clr-accent-o50: #9fa37b80;
|
||||||
|
|
||||||
|
--clr-layer-100: #0E1015;
|
||||||
|
--clr-layer-200: #191B20;
|
||||||
|
--clr-layer-300: #2E3139;
|
||||||
|
|
||||||
|
--clr-text-100: #eaebf0;
|
||||||
|
|
||||||
|
--clr-shadow: transparent;
|
||||||
|
}
|
22
front/src/components/app/index.tsx
Normal file
22
front/src/components/app/index.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import './styles.scss';
|
||||||
|
import '@public/fonts/styles.css';
|
||||||
|
|
||||||
|
import { MainLayout } from '@components/layouts';
|
||||||
|
import { About, Home } from '@components/pages';
|
||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route element={<MainLayout />}>
|
||||||
|
<Route path={'/'} element={<Home />} />
|
||||||
|
<Route path={'/about'} element={<About />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
20
front/src/components/app/reset.scss
Normal file
20
front/src/components/app/reset.scss
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, h1, h2, h3, h4, p,
|
||||||
|
figure, blockquote, dl, dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg,
|
||||||
|
picture {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font: inherit;
|
||||||
|
}
|
26
front/src/components/app/styles.scss
Normal file
26
front/src/components/app/styles.scss
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
@use './reset';
|
||||||
|
@use './theme' as theme;
|
||||||
|
|
||||||
|
html[data-theme='light'] {
|
||||||
|
@include theme.light;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] {
|
||||||
|
@include theme.dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='default'] {
|
||||||
|
@include theme.light;
|
||||||
|
@media (prefers-color-scheme:dark) {
|
||||||
|
@include theme.dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Rubik, sans-serif;
|
||||||
|
background-color: var(--clr-layer-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100dvh;
|
||||||
|
}
|
3
front/src/components/layouts/index.tsx
Normal file
3
front/src/components/layouts/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import MainLayout from './main-layout';
|
||||||
|
|
||||||
|
export { MainLayout };
|
18
front/src/components/layouts/main-layout/index.tsx
Normal file
18
front/src/components/layouts/main-layout/index.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Header } from '@components/ux';
|
||||||
|
import React from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
function MainLayout() {
|
||||||
|
return (
|
||||||
|
<div className={styles.mainLayout}>
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MainLayout;
|
@ -0,0 +1,8 @@
|
|||||||
|
.mainLayout {
|
||||||
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
|
grid-template:
|
||||||
|
'header' auto
|
||||||
|
'main' minmax(0, 1fr)
|
||||||
|
/ minmax(0, 1fr);
|
||||||
|
}
|
15
front/src/components/pages/about/index.tsx
Normal file
15
front/src/components/pages/about/index.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Button } from '@components/ui';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
function About() {
|
||||||
|
return (
|
||||||
|
<div className={styles.about}>
|
||||||
|
<p>About Page</p>
|
||||||
|
<Button variant="secondary">Button</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default About;
|
3
front/src/components/pages/about/styles.module.scss
Normal file
3
front/src/components/pages/about/styles.module.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.about {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
30
front/src/components/pages/home/index.tsx
Normal file
30
front/src/components/pages/home/index.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Button, PasswordTextField, TextField } from '@components/ui';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
return (
|
||||||
|
<div className={styles.home}>
|
||||||
|
<div className={styles.box}>
|
||||||
|
<Button>Button</Button>
|
||||||
|
<Button variant="secondary">Button</Button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.box}>
|
||||||
|
<Button scale="s">Button</Button>
|
||||||
|
<Button>Button</Button>
|
||||||
|
<Button scale="l">Button</Button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.box}>
|
||||||
|
<TextField scale="m" />
|
||||||
|
<PasswordTextField scale="m" />
|
||||||
|
</div>
|
||||||
|
<div className={styles.box}>
|
||||||
|
<TextField scale="l" />
|
||||||
|
<PasswordTextField scale="l" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
12
front/src/components/pages/home/styles.module.scss
Normal file
12
front/src/components/pages/home/styles.module.scss
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.home {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center
|
||||||
|
}
|
4
front/src/components/pages/index.tsx
Normal file
4
front/src/components/pages/index.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import About from './about';
|
||||||
|
import Home from './home/index';
|
||||||
|
|
||||||
|
export { About, Home };
|
23
front/src/components/ui/button/index.tsx
Normal file
23
front/src/components/ui/button/index.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { RawButton } from '../raw';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { ButtonProps } from './types.js';
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
variant = 'primary',
|
||||||
|
scale = 'm',
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ButtonProps) {
|
||||||
|
const classes = clsx(
|
||||||
|
styles.button,
|
||||||
|
styles[variant],
|
||||||
|
styles[scale],
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
return <RawButton className={classes} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button;
|
39
front/src/components/ui/button/styles.module.scss
Normal file
39
front/src/components/ui/button/styles.module.scss
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
.button {
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.1s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
filter: brightness(0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background-color: var(--clr-primary);
|
||||||
|
color: var(--clr-layer-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
background-color: var(--clr-secondary);
|
||||||
|
color: var(--clr-text-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l {
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
6
front/src/components/ui/button/types.ts
Normal file
6
front/src/components/ui/button/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { RawButtonProps } from '../raw/raw-button/types';
|
||||||
|
|
||||||
|
export type ButtonProps = {
|
||||||
|
variant?: 'primary' | 'secondary';
|
||||||
|
scale?: 's' | 'm' | 'l';
|
||||||
|
} & RawButtonProps;
|
23
front/src/components/ui/control-label/index.tsx
Normal file
23
front/src/components/ui/control-label/index.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { ControlLabelProps } from './types';
|
||||||
|
|
||||||
|
export default function ControlLabel({
|
||||||
|
text,
|
||||||
|
position = 'top',
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ControlLabelProps) {
|
||||||
|
const classes = clsx(styles.label, styles[position], className);
|
||||||
|
const reversed = position === 'right' || position === 'bottom';
|
||||||
|
return (
|
||||||
|
<label className={classes} {...props}>
|
||||||
|
{reversed && children}
|
||||||
|
{text && <p>{text}</p>}
|
||||||
|
{!reversed && children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
15
front/src/components/ui/control-label/styles.module.scss
Normal file
15
front/src/components/ui/control-label/styles.module.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left,
|
||||||
|
.right {
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top,
|
||||||
|
.bottom {
|
||||||
|
gap: 5px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
4
front/src/components/ui/control-label/types.ts
Normal file
4
front/src/components/ui/control-label/types.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type ControlLabelProps = {
|
||||||
|
text?: string;
|
||||||
|
position?: 'left' | 'top' | 'right' | 'bottom';
|
||||||
|
} & React.ComponentPropsWithoutRef<'label'>;
|
15
front/src/components/ui/icon-button/index.tsx
Normal file
15
front/src/components/ui/icon-button/index.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { RawButton } from '../raw';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { IconButtonProps } from './types';
|
||||||
|
|
||||||
|
export default function IconButton({
|
||||||
|
scale = 'm',
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: IconButtonProps) {
|
||||||
|
const classes = clsx(styles.button, styles[scale], className);
|
||||||
|
return <RawButton className={classes} {...props} />;
|
||||||
|
}
|
32
front/src/components/ui/icon-button/styles.module.scss
Normal file
32
front/src/components/ui/icon-button/styles.module.scss
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
.button {
|
||||||
|
border-radius: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--clr-accent-o50);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--clr-primary);
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.s {
|
||||||
|
padding: 4px;
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m {
|
||||||
|
padding: 6px;
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l {
|
||||||
|
padding: 8px;
|
||||||
|
height: 44px;
|
||||||
|
width: 44px;
|
||||||
|
}
|
5
front/src/components/ui/icon-button/types.ts
Normal file
5
front/src/components/ui/icon-button/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { RawButtonProps } from '../raw/raw-button/types';
|
||||||
|
|
||||||
|
export type IconButtonProps = {
|
||||||
|
scale?: 's' | 'm' | 'l';
|
||||||
|
} & RawButtonProps;
|
19
front/src/components/ui/index.tsx
Normal file
19
front/src/components/ui/index.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import Button from './button';
|
||||||
|
import ControlLabel from './control-label';
|
||||||
|
import IconButton from './icon-button';
|
||||||
|
import Input from './input';
|
||||||
|
import Menu from './menu';
|
||||||
|
import PasswordTextField from './password-text-field';
|
||||||
|
import Select from './select';
|
||||||
|
import TextField from './text-field';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Button,
|
||||||
|
ControlLabel,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Menu,
|
||||||
|
PasswordTextField,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
};
|
44
front/src/components/ui/input/index.tsx
Normal file
44
front/src/components/ui/input/index.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import RawInput from '../raw/raw-input';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { InputProps } from './types';
|
||||||
|
|
||||||
|
export default function Input({
|
||||||
|
scale = 'm',
|
||||||
|
wrapper = {},
|
||||||
|
leftNode,
|
||||||
|
rightNode,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InputProps) {
|
||||||
|
const [focus, setFocus] = useState<boolean>(false);
|
||||||
|
const wrapperClassName = clsx(
|
||||||
|
styles.wrapper,
|
||||||
|
focus && styles.wrapperFocus,
|
||||||
|
wrapper.className,
|
||||||
|
);
|
||||||
|
const inputClassName = clsx(styles.input, styles[scale], className);
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
setFocus(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setFocus(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...wrapper} className={wrapperClassName}>
|
||||||
|
{leftNode}
|
||||||
|
<RawInput
|
||||||
|
className={inputClassName}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{rightNode}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
29
front/src/components/ui/input/styles.module.scss
Normal file
29
front/src/components/ui/input/styles.module.scss
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
outline: solid 0px var(--clr-accent-o50);
|
||||||
|
transition: all 0.1s ease-in-out;
|
||||||
|
background-color: var(--clr-layer-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapperFocus {
|
||||||
|
outline-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
color: var(--clr-text-100);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l {
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
10
front/src/components/ui/input/types.ts
Normal file
10
front/src/components/ui/input/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { RawInputProps } from '../raw/raw-input/types';
|
||||||
|
|
||||||
|
type InputProps = {
|
||||||
|
scale?: 'm' | 'l';
|
||||||
|
wrapper?: React.ComponentPropsWithoutRef<'div'>;
|
||||||
|
leftNode?: React.ReactNode;
|
||||||
|
rightNode?: React.ReactNode;
|
||||||
|
} & RawInputProps;
|
||||||
|
|
||||||
|
export { InputProps };
|
50
front/src/components/ui/menu/index.tsx
Normal file
50
front/src/components/ui/menu/index.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { RawButton } from '../raw';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { MenuProps } from './types';
|
||||||
|
|
||||||
|
export default function Menu<T>({
|
||||||
|
options,
|
||||||
|
selected,
|
||||||
|
getOptionKey,
|
||||||
|
getOptionLabel,
|
||||||
|
onSelect,
|
||||||
|
visible = true,
|
||||||
|
className,
|
||||||
|
nodeRef,
|
||||||
|
...props
|
||||||
|
}: MenuProps<T>) {
|
||||||
|
const handleButtonClick = (option: T) => {
|
||||||
|
onSelect?.(option);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcButtonClassName = (option: T) => {
|
||||||
|
const isSelected =
|
||||||
|
selected && getOptionKey(option) === getOptionKey(selected);
|
||||||
|
return clsx(
|
||||||
|
styles.button,
|
||||||
|
isSelected ? styles.buttonSelected : styles.buttonUnselected,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={clsx(className, styles.menu)} ref={nodeRef} {...props}>
|
||||||
|
{options.map((option) => (
|
||||||
|
<li key={getOptionKey(option)} className={styles.item}>
|
||||||
|
<RawButton
|
||||||
|
onClick={() => handleButtonClick(option)}
|
||||||
|
className={calcButtonClassName(option)}
|
||||||
|
>
|
||||||
|
{getOptionLabel(option)}
|
||||||
|
</RawButton>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
31
front/src/components/ui/menu/styles.module.scss
Normal file
31
front/src/components/ui/menu/styles.module.scss
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.menu {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: var(--clr-layer-300);
|
||||||
|
box-shadow: 0px 1px 2px var(--clr-shadow);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 20px;
|
||||||
|
color: var(--clr-text-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonSelected {
|
||||||
|
background-color: var(--clr-accent);
|
||||||
|
color: var(--clr-layer-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonUnselected {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--clr-accent-o50);
|
||||||
|
}
|
||||||
|
}
|
9
front/src/components/ui/menu/types.ts
Normal file
9
front/src/components/ui/menu/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export type MenuProps<T> = {
|
||||||
|
options: T[];
|
||||||
|
selected?: T;
|
||||||
|
getOptionKey: (option: T) => string;
|
||||||
|
getOptionLabel: (option: T) => string;
|
||||||
|
onSelect?: (option: T) => void;
|
||||||
|
visible?: boolean;
|
||||||
|
nodeRef?: React.ForwardedRef<HTMLUListElement>;
|
||||||
|
} & Omit<React.ComponentPropsWithoutRef<'ul'>, 'onSelect'>;
|
35
front/src/components/ui/password-text-field/index.tsx
Normal file
35
front/src/components/ui/password-text-field/index.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import HideIcon from '@public/images/svg/hide.svg';
|
||||||
|
import ShowIcon from '@public/images/svg/show.svg';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import IconButton from '../icon-button';
|
||||||
|
import TextField from '../text-field';
|
||||||
|
import { PasswordTextFieldProps } from './types';
|
||||||
|
|
||||||
|
export default function PasswordTextField({
|
||||||
|
rightNode,
|
||||||
|
scale,
|
||||||
|
...props
|
||||||
|
}: PasswordTextFieldProps) {
|
||||||
|
const [visible, setVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleShowButtonClick = () => {
|
||||||
|
setVisible(!visible);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
type={visible ? 'text' : 'password'}
|
||||||
|
scale={scale}
|
||||||
|
rightNode={
|
||||||
|
<>
|
||||||
|
<IconButton scale={scale} onClick={handleShowButtonClick}>
|
||||||
|
{visible ? <HideIcon /> : <ShowIcon />}
|
||||||
|
</IconButton>
|
||||||
|
{rightNode}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
3
front/src/components/ui/password-text-field/types.ts
Normal file
3
front/src/components/ui/password-text-field/types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { TextFieldProps } from '../text-field/types';
|
||||||
|
|
||||||
|
export type PasswordTextFieldProps = {} & Omit<TextFieldProps, 'type'>;
|
3
front/src/components/ui/raw/index.ts
Normal file
3
front/src/components/ui/raw/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import RawButton from './raw-button';
|
||||||
|
|
||||||
|
export { RawButton };
|
15
front/src/components/ui/raw/raw-button/index.tsx
Normal file
15
front/src/components/ui/raw/raw-button/index.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { RawButtonProps } from './types';
|
||||||
|
|
||||||
|
export default function RawButton({
|
||||||
|
type = 'button',
|
||||||
|
className,
|
||||||
|
nodeRef,
|
||||||
|
...props
|
||||||
|
}: RawButtonProps) {
|
||||||
|
const classes = clsx(styles.rawButton, className);
|
||||||
|
return <button className={classes} ref={nodeRef} type={type} {...props} />;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
.rawButton {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
font: inherit;
|
||||||
|
}
|
3
front/src/components/ui/raw/raw-button/types.ts
Normal file
3
front/src/components/ui/raw/raw-button/types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export type RawButtonProps = {
|
||||||
|
nodeRef?: React.ForwardedRef<HTMLButtonElement>;
|
||||||
|
} & React.ComponentPropsWithoutRef<'button'>;
|
10
front/src/components/ui/raw/raw-input/index.tsx
Normal file
10
front/src/components/ui/raw/raw-input/index.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { RawInputProps } from './types';
|
||||||
|
|
||||||
|
export default function RawInput({ className, ...props }: RawInputProps) {
|
||||||
|
const classes = clsx(styles.rawInput, className);
|
||||||
|
return <input className={classes} {...props} />;
|
||||||
|
}
|
6
front/src/components/ui/raw/raw-input/styles.module.scss
Normal file
6
front/src/components/ui/raw/raw-input/styles.module.scss
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.rawInput {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
font: inherit;
|
||||||
|
}
|
5
front/src/components/ui/raw/raw-input/types.ts
Normal file
5
front/src/components/ui/raw/raw-input/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
type RawInputProps = {
|
||||||
|
nodeRef?: React.ForwardedRef<HTMLInputElement>;
|
||||||
|
} & React.ComponentPropsWithoutRef<'input'>;
|
||||||
|
|
||||||
|
export { RawInputProps };
|
49
front/src/components/ui/select/index.tsx
Normal file
49
front/src/components/ui/select/index.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { useMissClick } from '@utils/miss-click';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import Input from '../input';
|
||||||
|
import Menu from '../menu';
|
||||||
|
import { RawButton } from '../raw';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { SelectProps } from './types';
|
||||||
|
|
||||||
|
export default function Select<T>({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
getOptionKey,
|
||||||
|
getOptionLabel,
|
||||||
|
onChange,
|
||||||
|
}: SelectProps<T>) {
|
||||||
|
const toggleButtonRef = useRef<HTMLButtonElement>();
|
||||||
|
const menuRef = useRef<HTMLUListElement>();
|
||||||
|
const [menuVisible, setMenuVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useMissClick([toggleButtonRef, menuRef], () => setMenuVisible(false));
|
||||||
|
|
||||||
|
const handleToggleButtonClick = () => {
|
||||||
|
setMenuVisible(!menuVisible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuSelect = (option: T) => {
|
||||||
|
setMenuVisible(false);
|
||||||
|
onChange?.(option);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.select}>
|
||||||
|
<RawButton onClick={handleToggleButtonClick} nodeRef={toggleButtonRef}>
|
||||||
|
<Input value={value && getOptionLabel(value)} readOnly />
|
||||||
|
</RawButton>
|
||||||
|
<Menu
|
||||||
|
options={options}
|
||||||
|
selected={value}
|
||||||
|
getOptionKey={getOptionKey}
|
||||||
|
getOptionLabel={getOptionLabel}
|
||||||
|
onSelect={handleMenuSelect}
|
||||||
|
visible={menuVisible}
|
||||||
|
className={styles.menu}
|
||||||
|
nodeRef={menuRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
9
front/src/components/ui/select/styles.module.scss
Normal file
9
front/src/components/ui/select/styles.module.scss
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.select {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 5px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
7
front/src/components/ui/select/types.ts
Normal file
7
front/src/components/ui/select/types.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export type SelectProps<T> = {
|
||||||
|
options: T[];
|
||||||
|
value?: T;
|
||||||
|
getOptionKey: (option: T) => string;
|
||||||
|
getOptionLabel: (option: T) => string;
|
||||||
|
onChange?: (option: T) => void;
|
||||||
|
} & Omit<React.ComponentPropsWithoutRef<'div'>, 'onChange'>;
|
13
front/src/components/ui/text-field/index.tsx
Normal file
13
front/src/components/ui/text-field/index.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ControlLabel from '../control-label';
|
||||||
|
import Input from '../input';
|
||||||
|
import { TextFieldProps } from './types';
|
||||||
|
|
||||||
|
export default function TextField({ label = {}, ...props }: TextFieldProps) {
|
||||||
|
return (
|
||||||
|
<ControlLabel {...label}>
|
||||||
|
<Input {...props} />
|
||||||
|
</ControlLabel>
|
||||||
|
);
|
||||||
|
}
|
6
front/src/components/ui/text-field/types.ts
Normal file
6
front/src/components/ui/text-field/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ControlLabelProps } from '../control-label/types';
|
||||||
|
import { InputProps } from '../input/types';
|
||||||
|
|
||||||
|
export type TextFieldProps = {
|
||||||
|
label?: ControlLabelProps;
|
||||||
|
} & InputProps;
|
14
front/src/components/ux/header/index.tsx
Normal file
14
front/src/components/ux/header/index.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ThemeSelect from '../theme-select';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<header className={styles.header}>
|
||||||
|
<ThemeSelect />
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
5
front/src/components/ux/header/styles.module.scss
Normal file
5
front/src/components/ux/header/styles.module.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
3
front/src/components/ux/index.tsx
Normal file
3
front/src/components/ux/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Header from './header';
|
||||||
|
|
||||||
|
export { Header };
|
22
front/src/components/ux/theme-select/index.tsx
Normal file
22
front/src/components/ux/theme-select/index.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Select } from '@components/ui';
|
||||||
|
import { useColorThemeStore } from '@store/color-theme';
|
||||||
|
import { ColorTheme, THEMES } from '@utils/color-theme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function ThemeSelect() {
|
||||||
|
const { theme, setTheme } = useColorThemeStore();
|
||||||
|
|
||||||
|
const handleChange = (newTheme: ColorTheme) => {
|
||||||
|
setTheme(newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
options={THEMES}
|
||||||
|
value={theme}
|
||||||
|
getOptionKey={(t) => t.key}
|
||||||
|
getOptionLabel={(t) => t.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
1
front/src/custom.d.ts
vendored
Normal file
1
front/src/custom.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module '*.module.scss';
|
9
front/src/index.tsx
Normal file
9
front/src/index.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import App from './components/app';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
const root = createRoot(rootElement);
|
||||||
|
|
||||||
|
root.render(<App />);
|
3
front/src/storage/constants.ts
Normal file
3
front/src/storage/constants.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const STORAGE_KEYS = {
|
||||||
|
colorTheme: 'color-theme',
|
||||||
|
};
|
15
front/src/store/color-theme/index.tsx
Normal file
15
front/src/store/color-theme/index.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { initializeColorTheme, setColorTheme } from '@utils/color-theme';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { ColorThemeStore } from './types';
|
||||||
|
|
||||||
|
const initialTheme = initializeColorTheme();
|
||||||
|
|
||||||
|
export const useColorThemeStore = create<ColorThemeStore>((set) => ({
|
||||||
|
theme: initialTheme,
|
||||||
|
setTheme: (theme) =>
|
||||||
|
set(() => {
|
||||||
|
setColorTheme(theme);
|
||||||
|
return { theme };
|
||||||
|
}),
|
||||||
|
}));
|
6
front/src/store/color-theme/types.ts
Normal file
6
front/src/store/color-theme/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ColorTheme } from '@utils/color-theme';
|
||||||
|
|
||||||
|
export type ColorThemeStore = {
|
||||||
|
theme: ColorTheme;
|
||||||
|
setTheme: (theme: ColorTheme) => void;
|
||||||
|
};
|
1
front/src/utils/array/index.ts
Normal file
1
front/src/utils/array/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './utils';
|
10
front/src/utils/array/utils.ts
Normal file
10
front/src/utils/array/utils.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export const arrayToObject = <T>(
|
||||||
|
array: T[],
|
||||||
|
getItemKey: (item: T) => string,
|
||||||
|
) => {
|
||||||
|
const obj: Record<string, T> = {};
|
||||||
|
array.forEach((item) => {
|
||||||
|
obj[getItemKey(item)] = item;
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
};
|
7
front/src/utils/color-theme/constants.ts
Normal file
7
front/src/utils/color-theme/constants.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { ColorTheme } from './types';
|
||||||
|
|
||||||
|
export const LIGHT_THEME = { key: 'light', name: 'Light' };
|
||||||
|
export const DARK_THEME = { key: 'dark', name: 'Dark' };
|
||||||
|
export const DEFAULT_THEME = { key: 'default', name: 'Default' };
|
||||||
|
|
||||||
|
export const THEMES: ColorTheme[] = [LIGHT_THEME, DARK_THEME, DEFAULT_THEME];
|
3
front/src/utils/color-theme/index.tsx
Normal file
3
front/src/utils/color-theme/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './constants';
|
||||||
|
export * from './types';
|
||||||
|
export * from './utils';
|
4
front/src/utils/color-theme/types.ts
Normal file
4
front/src/utils/color-theme/types.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type ColorTheme = {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
};
|
35
front/src/utils/color-theme/utils.ts
Normal file
35
front/src/utils/color-theme/utils.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { arrayToObject } from '@utils/array';
|
||||||
|
import { STORAGE_KEYS } from 'src/storage/constants';
|
||||||
|
|
||||||
|
import { DEFAULT_THEME, THEMES } from './constants';
|
||||||
|
import { ColorTheme } from './types';
|
||||||
|
|
||||||
|
const themeMap = arrayToObject(THEMES, (t) => t.key);
|
||||||
|
|
||||||
|
export const setHTMLColorTheme = (theme: ColorTheme) => {
|
||||||
|
document.documentElement.dataset['theme'] = theme.key;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getColorTheme = (): ColorTheme => {
|
||||||
|
const storageTheme = localStorage.getItem(STORAGE_KEYS.colorTheme);
|
||||||
|
if (!storageTheme || !themeMap[storageTheme]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return themeMap[storageTheme];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setColorTheme = (theme: ColorTheme) => {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.colorTheme, theme.key);
|
||||||
|
setHTMLColorTheme(theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initializeColorTheme = () => {
|
||||||
|
const theme = getColorTheme();
|
||||||
|
if (!theme) {
|
||||||
|
setColorTheme(DEFAULT_THEME);
|
||||||
|
return DEFAULT_THEME;
|
||||||
|
} else {
|
||||||
|
setHTMLColorTheme(theme);
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
};
|
25
front/src/utils/miss-click/index.tsx
Normal file
25
front/src/utils/miss-click/index.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export const useMissClick = (
|
||||||
|
ignore: React.MutableRefObject<HTMLElement>[],
|
||||||
|
callback: () => void,
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (event: MouseEvent) => {
|
||||||
|
const { target } = event;
|
||||||
|
if (!(target instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < ignore.length; i += 1) {
|
||||||
|
if (ignore[i].current?.contains(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
window.addEventListener('click', handler);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', handler);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
21
front/tsconfig.json
Normal file
21
front/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react",
|
||||||
|
"target": "ES6",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@public/*": ["public/*"],
|
||||||
|
"@components/*": ["src/components/*"],
|
||||||
|
"@storage/*": ["src/storage/*"],
|
||||||
|
"@store/*": ["src/store/*"],
|
||||||
|
"@utils/*": ["src/utils/*"],
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "typescript-plugin-css-modules"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
60
front/webpack.config.js
Normal file
60
front/webpack.config.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||||
|
import path from 'path';
|
||||||
|
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
|
||||||
|
|
||||||
|
const __dirname = path.resolve();
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
entry: './src/index.tsx',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'main.js',
|
||||||
|
publicPath: '/',
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
historyApiFallback: true,
|
||||||
|
static: {
|
||||||
|
directory: path.join(__dirname, 'build'),
|
||||||
|
},
|
||||||
|
port: 5000,
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(ts|tsx)$/,
|
||||||
|
use: ['babel-loader'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.module\.scss$/,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: { modules: { namedExport: false } },
|
||||||
|
},
|
||||||
|
'sass-loader',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(scss|css)$/,
|
||||||
|
exclude: /\.module\.scss$/,
|
||||||
|
use: ['style-loader', 'css-loader', 'sass-loader'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(svg)$/i,
|
||||||
|
use: ['@svgr/webpack'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: path.join(__dirname, 'public', 'index.html'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.ts', '.tsx'],
|
||||||
|
plugins: [new TsconfigPathsPlugin({})],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
Loading…
x
Reference in New Issue
Block a user