[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…
Reference in New Issue
Block a user