1
.gitignore
vendored
@ -17,7 +17,6 @@ eggs/
|
|||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
lib/
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
|
||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
wheels/
|
wheels/
|
||||||
|
1
front/.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
|
/build
|
||||||
|
7
front/.stylelintrc.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["stylelint-scss", "stylelint-order"],
|
||||||
|
"extends": [
|
||||||
|
"stylelint-config-idiomatic-order"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
}
|
BIN
front/README.md
@ -16,4 +16,13 @@ export default tseslint.config(
|
|||||||
'simple-import-sort/exports': 'warn',
|
'simple-import-sort/exports': 'warn',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{ varsIgnorePattern: '_', argsIgnorePattern: '_' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
8774
front/package-lock.json
generated
@ -1,41 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "template",
|
"name": "app",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"build-storybook": "storybook build",
|
|
||||||
"lint": "eslint src/**/*.{js,ts,tsx}",
|
"lint": "eslint src/**/*.{js,ts,tsx}",
|
||||||
"lint-fix": "npm run lint -- --fix",
|
"lint-fix": "npm run lint -- --fix",
|
||||||
"start": "webpack serve --mode development",
|
"start": "webpack serve --mode development",
|
||||||
"storybook": "storybook dev -p 6006",
|
|
||||||
"style-lint": "npx stylelint '**/*.{css,scss}'",
|
"style-lint": "npx stylelint '**/*.{css,scss}'",
|
||||||
"style-lint-fix": "npm run style-lint -- --fix",
|
"style-lint-fix": "npm run style-lint -- --fix"
|
||||||
"test": "jest"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@svgr/webpack": "^8.1.0",
|
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "6.26.0",
|
"react-router-dom": "^6.26.2",
|
||||||
"zustand": "5.0.0-rc.2"
|
"zustand": "5.0.0-rc.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-react": "^7.24.7",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@babel/preset-typescript": "^7.24.7",
|
"@babel/preset-typescript": "^7.24.7",
|
||||||
"@chromatic-com/storybook": "^1.6.1",
|
|
||||||
"@eslint/js": "^9.9.0",
|
"@eslint/js": "^9.9.0",
|
||||||
"@storybook/addon-essentials": "^8.2.9",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@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/eslint__js": "^8.42.3",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
@ -46,15 +33,14 @@
|
|||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"lint-staged": "^15.2.9",
|
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8",
|
||||||
"sass-loader": "^16.0.0",
|
"sass-loader": "^16.0.0",
|
||||||
"storybook": "^8.2.9",
|
|
||||||
"style-loader": "^4.0.0",
|
"style-loader": "^4.0.0",
|
||||||
"stylelint": "^16.8.1",
|
"stylelint": "^16.9.0",
|
||||||
"stylelint-config-standard-scss": "^13.1.0",
|
"stylelint-config-idiomatic-order": "^10.0.0",
|
||||||
"ts-jest": "^29.2.4",
|
"stylelint-order": "^6.0.4",
|
||||||
|
"stylelint-scss": "^6.7.0",
|
||||||
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"typescript-eslint": "^8.1.0",
|
"typescript-eslint": "^8.1.0",
|
||||||
|
7
front/public/images/svg/arrow-down.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?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 6 4" style="enable-background:new 0 0 6 4;" xml:space="preserve">
|
||||||
|
<path d="M3,3.5c-0.13,0-0.26-0.05-0.35-0.15l-2-2c-0.2-0.2-0.2-0.51,0-0.71s0.51-0.2,0.71,0L3,2.29l1.65-1.65
|
||||||
|
c0.2-0.2,0.51-0.2,0.71,0s0.2,0.51,0,0.71l-2,2C3.26,3.45,3.13,3.5,3,3.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 538 B |
7
front/public/images/svg/arrow-up.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?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 6 4" style="enable-background:new 0 0 6 4;" xml:space="preserve">
|
||||||
|
<path d="M3.01,0.49c0.13,0,0.26,0.05,0.35,0.15l2,2c0.2,0.2,0.2,0.51,0,0.71c-0.2,0.2-0.51,0.2-0.71,0L3.01,1.7L1.36,3.35
|
||||||
|
c-0.2,0.2-0.51,0.2-0.71,0s-0.2-0.51,0-0.71l2-2C2.75,0.54,2.88,0.49,3.01,0.49z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 556 B |
10
front/public/images/svg/calendar.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="M9.68,12H3.32C2.04,12,1,10.88,1,9.5v-5C1,3.12,2.04,2,3.32,2h6.36C10.96,2,12,3.12,12,4.5v5C12,10.88,10.96,12,9.68,12z
|
||||||
|
M3.32,3C2.59,3,2,3.67,2,4.5v5C2,10.33,2.59,11,3.32,11h6.36C10.41,11,11,10.33,11,9.5v-5C11,3.67,10.41,3,9.68,3H3.32z"/>
|
||||||
|
<rect x="1" y="5" width="11" height="1"/>
|
||||||
|
<path d="M4.5,4L4.5,4C4.22,4,4,3.78,4,3.5v-2C4,1.22,4.22,1,4.5,1h0C4.78,1,5,1.22,5,1.5v2C5,3.78,4.78,4,4.5,4z"/>
|
||||||
|
<path d="M8.5,4L8.5,4C8.22,4,8,3.78,8,3.5v-2C8,1.22,8.22,1,8.5,1h0C8.78,1,9,1.22,9,1.5v2C9,3.78,8.78,4,8.5,4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 877 B |
7
front/public/images/svg/check.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?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 6 4" style="enable-background:new 0 0 6 4;" xml:space="preserve">
|
||||||
|
<path d="M2.25,4C2.12,4,1.99,3.95,1.9,3.85l-1.5-1.5c-0.2-0.2-0.2-0.51,0-0.71s0.51-0.2,0.71,0l1.15,1.15L4.9,0.15
|
||||||
|
c0.2-0.2,0.51-0.2,0.71,0s0.2,0.51,0,0.71l-3,3C2.51,3.95,2.38,4,2.25,4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 542 B |
12
front/public/images/svg/delete.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?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="M11.5,3H11H9V2.5C9,1.67,8.33,1,7.5,1h-2C4.67,1,4,1.67,4,2.5V3H2H1.5C1.22,3,1,3.22,1,3.5S1.22,4,1.5,4H2v5.5
|
||||||
|
C2,10.88,3.12,12,4.5,12h4c1.38,0,2.5-1.12,2.5-2.5V4h0.5C11.78,4,12,3.78,12,3.5S11.78,3,11.5,3z M5,2.5C5,2.22,5.22,2,5.5,2h2
|
||||||
|
C7.78,2,8,2.22,8,2.5V3H5V2.5z M10,9.5c0,0.83-0.67,1.5-1.5,1.5h-4C3.67,11,3,10.33,3,9.5V4h7V9.5z"/>
|
||||||
|
<path d="M5.25,9.5c0.28,0,0.5-0.22,0.5-0.5V6c0-0.28-0.22-0.5-0.5-0.5S4.75,5.72,4.75,6v3C4.75,9.28,4.97,9.5,5.25,9.5z"/>
|
||||||
|
<path d="M7.75,9.5c0.28,0,0.5-0.22,0.5-0.5V6c0-0.28-0.22-0.5-0.5-0.5S7.25,5.72,7.25,6v3C7.25,9.28,7.47,9.5,7.75,9.5z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 959 B |
@ -2,15 +2,17 @@
|
|||||||
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<!-- 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"
|
<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">
|
viewBox="0 0 13 13" style="enable-background:new 0 0 13 13;" xml:space="preserve">
|
||||||
|
<path d="M1.55,11.45L1.55,11.45c-0.2-0.2-0.2-0.51,0-0.71l9.19-9.19c0.2-0.2,0.51-0.2,0.71,0l0,0c0.2,0.2,0.2,0.51,0,0.71
|
||||||
|
l-9.19,9.19C2.06,11.65,1.75,11.65,1.55,11.45z"/>
|
||||||
<g>
|
<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="M12.51,5.54c-0.28-0.67-0.66-1.28-1.12-1.8l-0.71,0.71c0.37,0.44,0.68,0.93,0.9,1.48c0.15,0.37,0.15,0.79,0,1.17
|
||||||
<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
|
C10.73,9.16,8.74,10.5,6.5,10.5c-0.56,0-1.11-0.09-1.63-0.25L4.1,11.02c0.75,0.3,1.56,0.48,2.4,0.48c2.65,0,5.01-1.58,6.01-4.04
|
||||||
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"/>
|
C12.76,6.85,12.76,6.15,12.51,5.54z"/>
|
||||||
|
<path d="M1.42,7.08c-0.15-0.37-0.15-0.8,0-1.17C2.27,3.84,4.26,2.5,6.5,2.5c1.3,0,2.5,0.46,3.46,1.24l0.71-0.71
|
||||||
|
C9.53,2.07,8.07,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.54,1.33,1.49,2.39,2.66,3.09l0.73-0.73
|
||||||
|
C2.8,9.23,1.91,8.29,1.42,7.08z"/>
|
||||||
|
<path d="M6.5,9C7.88,9,9,7.88,9,6.5c0-0.12-0.02-0.23-0.03-0.34L6.16,8.97C6.27,8.98,6.38,9,6.5,9z"/>
|
||||||
|
<path d="M6.5,4C5.12,4,4,5.12,4,6.5c0,0.87,0.45,1.64,1.12,2.08l0.73-0.73C5.35,7.61,5,7.1,5,6.5C5,5.67,5.67,5,6.5,5
|
||||||
|
c0.6,0,1.11,0.35,1.35,0.86l0.73-0.73C8.14,4.45,7.37,4,6.5,4z"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
7
front/public/images/svg/plus.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?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="M9.5,6H7V3.5C7,3.22,6.78,3,6.5,3S6,3.22,6,3.5V6H3.5C3.22,6,3,6.22,3,6.5S3.22,7,3.5,7H6v2.5C6,9.78,6.22,10,6.5,10
|
||||||
|
S7,9.78,7,9.5V7h2.5C9.78,7,10,6.78,10,6.5S9.78,6,9.5,6z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 541 B |
6
front/public/images/svg/radio.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?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 6 6" style="enable-background:new 0 0 6 6;" xml:space="preserve">
|
||||||
|
<circle cx="3" cy="3" r="3"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 384 B |
2
front/src/api/floris/constants.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// export const BASE_URL = 'http://localhost:8000/api';
|
||||||
|
export const BASE_URL = 'http://192.168.1.110:8000/api';
|
1
front/src/api/floris/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { downloadImage, getWindmillData } from './service';
|
26
front/src/api/floris/service.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { WindmillFormStore } from '@components/ux/windmill-form';
|
||||||
|
|
||||||
|
import { BASE_URL } from './constants';
|
||||||
|
import { GetWindmillDataRes } from './types';
|
||||||
|
import { getWindmillDataParams } from './utils';
|
||||||
|
|
||||||
|
export const getWindmillData = async (store: Partial<WindmillFormStore>) => {
|
||||||
|
const params = getWindmillDataParams(store);
|
||||||
|
const url = `${BASE_URL}/floris/get_windmill_data?${params}`;
|
||||||
|
const init: RequestInit = {
|
||||||
|
method: 'GET',
|
||||||
|
};
|
||||||
|
const res: Response = await fetch(url, init);
|
||||||
|
const data: GetWindmillDataRes = await res.json();
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const downloadImage = async (imageName: string) => {
|
||||||
|
const url = `${BASE_URL}/floris/download_image/${imageName}`;
|
||||||
|
const init: RequestInit = {
|
||||||
|
method: 'GET',
|
||||||
|
};
|
||||||
|
const res: Response = await fetch(url, init);
|
||||||
|
const data = await res.blob();
|
||||||
|
return data;
|
||||||
|
};
|
4
front/src/api/floris/types.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type GetWindmillDataRes = {
|
||||||
|
file_name: string;
|
||||||
|
data: number[];
|
||||||
|
};
|
9
front/src/api/floris/utils.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { WindmillFormStore } from '@components/ux/windmill-form';
|
||||||
|
|
||||||
|
export const getWindmillDataParams = (store: Partial<WindmillFormStore>) => {
|
||||||
|
const layoutX = store.windmills?.map((row) => `layout_x=${row.x}`).join('&');
|
||||||
|
const layoutY = store.windmills?.map((row) => `layout_y=${row.y}`).join('&');
|
||||||
|
const dateStart = `date_start=${store.dateFrom?.substring(0, 10)}`;
|
||||||
|
const dateEnd = `date_end=${store.dateTo?.substring(0, 10)}`;
|
||||||
|
return `${layoutX}&${layoutY}&${dateStart}&${dateEnd}`;
|
||||||
|
};
|
1
front/src/api/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './floris';
|
0
front/src/components/_mixins.scss
Normal file
@ -1,37 +1,63 @@
|
|||||||
@mixin light {
|
@mixin light {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
|
|
||||||
--clr-primary: #363a4e;
|
--clr-primary: #4176FF;
|
||||||
|
--clr-primary-o50: #3865DA80;
|
||||||
|
--clr-primary-hover: #638FFF;
|
||||||
|
--clr-primary-active: #3D68D7;
|
||||||
|
--clr-on-primary: #FFFFFF;
|
||||||
|
|
||||||
--clr-secondary: #bca59f;
|
--clr-secondary: #EAEAEA;
|
||||||
|
--clr-secondary-hover: #EFEFEF;
|
||||||
|
--clr-secondary-active: #E1E1E1;
|
||||||
|
--clr-on-secondary: #0D0D0D;
|
||||||
|
|
||||||
--clr-accent: #80845c;
|
--clr-layer-100: #EBEEF0;
|
||||||
--clr-accent-o50: #80845c80;
|
--clr-layer-200: #FFFFFF;
|
||||||
|
|
||||||
--clr-layer-100: #EAECF1;
|
|
||||||
--clr-layer-200: #DFE1E6;
|
|
||||||
--clr-layer-300: #FFFFFF;
|
--clr-layer-300: #FFFFFF;
|
||||||
|
--clr-layer-300-hover: #EAEAEA;
|
||||||
|
|
||||||
--clr-text-100: #0f1015;
|
--clr-text-100: #8D8D8D;
|
||||||
|
--clr-text-200: #6C7480;
|
||||||
|
--clr-text-300: #1D1F20;
|
||||||
|
|
||||||
--clr-shadow: #363a4e1A;
|
--clr-border-100: #DFDFDF;
|
||||||
|
--clr-border-200: #D8D8D8;
|
||||||
|
|
||||||
|
--clr-shadow-100: #0000001A;
|
||||||
|
--clr-shadow-200: #00000026;
|
||||||
|
|
||||||
|
--clr-ripple: #1D1F2026;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
|
||||||
--clr-primary: #b1b5c9;
|
--clr-primary: #3865DA;
|
||||||
|
--clr-primary-o50: #3865DA80;
|
||||||
|
--clr-primary-hover: #4073F7;
|
||||||
|
--clr-primary-active: #2A4DA7;
|
||||||
|
--clr-on-primary: #FFFFFF;
|
||||||
|
|
||||||
--clr-secondary: #604943;
|
--clr-secondary: #3F3F3F;
|
||||||
|
--clr-secondary-hover: #4D4D4D;
|
||||||
|
--clr-secondary-active: #323232;
|
||||||
|
--clr-on-secondary: #FFFFFF;
|
||||||
|
|
||||||
--clr-accent: #9fa37b;
|
--clr-layer-100: #1B1B1B;
|
||||||
--clr-accent-o50: #9fa37b80;
|
--clr-layer-200: #232323;
|
||||||
|
--clr-layer-300: #2F2F2F;
|
||||||
|
--clr-layer-300-hover: #3E3E3E;
|
||||||
|
|
||||||
--clr-layer-100: #0E1015;
|
--clr-text-100: #888888;
|
||||||
--clr-layer-200: #191B20;
|
--clr-text-200: #C5C5C5;
|
||||||
--clr-layer-300: #2E3139;
|
--clr-text-300: #F0F0F0;
|
||||||
|
|
||||||
--clr-text-100: #eaebf0;
|
--clr-border-100: #3D3D3D;
|
||||||
|
--clr-border-200: #545454;
|
||||||
|
|
||||||
--clr-shadow: transparent;
|
--clr-shadow-100: #0000001A;
|
||||||
|
--clr-shadow-200: #00000026;
|
||||||
|
|
||||||
|
--clr-ripple: #F0F0F026;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import './styles.scss';
|
|||||||
import '@public/fonts/styles.css';
|
import '@public/fonts/styles.css';
|
||||||
|
|
||||||
import { MainLayout } from '@components/layouts';
|
import { MainLayout } from '@components/layouts';
|
||||||
import { About, Home } from '@components/pages';
|
import { HomePage } from '@components/pages';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
@ -11,8 +11,7 @@ function App() {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
<Route path={'/'} element={<Home />} />
|
<Route path={'/'} element={<HomePage />} />
|
||||||
<Route path={'/about'} element={<About />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
*,
|
|
||||||
*::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;
|
|
||||||
}
|
|
@ -1,6 +1,12 @@
|
|||||||
@use './reset';
|
|
||||||
@use './theme' as theme;
|
@use './theme' as theme;
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
html[data-theme='light'] {
|
html[data-theme='light'] {
|
||||||
@include theme.light;
|
@include theme.light;
|
||||||
}
|
}
|
||||||
@ -16,11 +22,23 @@ html[data-theme='default'] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
--td-100: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Rubik, sans-serif;
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
background-color: var(--clr-layer-100);
|
background-color: var(--clr-layer-100);
|
||||||
|
font-family: Rubik, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg,
|
||||||
|
picture {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@ -8,7 +8,7 @@ function MainLayout() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.mainLayout}>
|
<div className={styles.mainLayout}>
|
||||||
<Header />
|
<Header />
|
||||||
<main>
|
<main className={styles.main}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,3 +6,8 @@
|
|||||||
'main' minmax(0, 1fr)
|
'main' minmax(0, 1fr)
|
||||||
/ minmax(0, 1fr);
|
/ minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
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;
|
|
@ -1,3 +0,0 @@
|
|||||||
.about {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
41
front/src/components/pages/home-page/component.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Heading, Paragraph } from '@components/ui';
|
||||||
|
import { WindmillForm } from '@components/ux';
|
||||||
|
import { WindmillFormResponse } from '@components/ux/windmill-form';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
export function HomePage() {
|
||||||
|
const [result, setResult] = useState<WindmillFormResponse | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleFormSuccess = (response: WindmillFormResponse) => {
|
||||||
|
setResult(response);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormFail = (message: string) => {
|
||||||
|
setError(message);
|
||||||
|
setResult(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<div className={styles.wrapperForm}>
|
||||||
|
<WindmillForm onSuccess={handleFormSuccess} onFail={handleFormFail} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.result}>
|
||||||
|
<Heading tag="h3">Result</Heading>
|
||||||
|
{result && (
|
||||||
|
<>
|
||||||
|
<div className={styles.power}>{result.power.join(' ')}</div>
|
||||||
|
<div className={styles.image}>
|
||||||
|
{result.image && <img src={result.image} alt="Image" />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{error && <Paragraph>{error}</Paragraph>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
front/src/components/pages/home-page/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { HomePage } from './component';
|
41
front/src/components/pages/home-page/styles.module.scss
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
.page {
|
||||||
|
display: grid;
|
||||||
|
padding: 20px;
|
||||||
|
gap: 20px;
|
||||||
|
grid-template:
|
||||||
|
'. form result .' auto
|
||||||
|
/ auto minmax(0, 380px) minmax(0, 700px) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapperForm {
|
||||||
|
grid-area: form;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--clr-layer-200);
|
||||||
|
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||||
|
gap: 20px;
|
||||||
|
grid-area: result;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1000px) {
|
||||||
|
.page {
|
||||||
|
grid-template:
|
||||||
|
'form' auto
|
||||||
|
'result' auto
|
||||||
|
/ 1fr;
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
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;
|
|
@ -1,12 +0,0 @@
|
|||||||
.home {
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center
|
|
||||||
}
|
|
@ -1,4 +1 @@
|
|||||||
import About from './about';
|
export { HomePage } from './home-page';
|
||||||
import Home from './home/index';
|
|
||||||
|
|
||||||
export { About, Home };
|
|
||||||
|
58
front/src/components/ui/animation/fade/component.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { ForwardedRef, forwardRef, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { FadeProps } from './types';
|
||||||
|
|
||||||
|
export function FadeInner(
|
||||||
|
{
|
||||||
|
visible,
|
||||||
|
duration = 200,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: Omit<FadeProps, 'ref'>,
|
||||||
|
ref: ForwardedRef<HTMLDivElement>,
|
||||||
|
) {
|
||||||
|
const [visibleInner, setVisibleInner] = useState<boolean>(visible);
|
||||||
|
|
||||||
|
const classNames = clsx(
|
||||||
|
styles.fade,
|
||||||
|
{ [styles.invisible]: !visible },
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
|
||||||
|
const inlineStyle = {
|
||||||
|
...style,
|
||||||
|
'--animation-duration': `${duration}ms`,
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
setVisibleInner(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
const handleAnimationEnd = (event: React.AnimationEvent) => {
|
||||||
|
if (event.animationName === styles.fadeout) {
|
||||||
|
setVisibleInner(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!visibleInner) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames}
|
||||||
|
ref={ref}
|
||||||
|
style={inlineStyle}
|
||||||
|
onAnimationEnd={handleAnimationEnd}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Fade = forwardRef(FadeInner);
|
2
front/src/components/ui/animation/fade/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { Fade } from './component';
|
||||||
|
export { type FadeProps } from './types';
|
29
front/src/components/ui/animation/fade/styles.module.scss
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.fade {
|
||||||
|
animation: fadein var(--animation-duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
animation: fadeout var(--animation-duration) forwards ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9) translateY(-30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9) translateY(-30px);
|
||||||
|
}
|
||||||
|
}
|
4
front/src/components/ui/animation/fade/types.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type FadeProps = {
|
||||||
|
visible: boolean;
|
||||||
|
duration?: number;
|
||||||
|
} & React.ComponentProps<'div'>;
|
2
front/src/components/ui/animation/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './fade';
|
||||||
|
export * from './ripple';
|
68
front/src/components/ui/animation/ripple/component.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React, {
|
||||||
|
ForwardedRef,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { RippleWave } from './parts/ripple-wave';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { RippleProps } from './types';
|
||||||
|
import { calcRippleWaveStyle } from './utils';
|
||||||
|
|
||||||
|
export function RippleInner(
|
||||||
|
props: RippleProps,
|
||||||
|
ref: ForwardedRef<HTMLDivElement>,
|
||||||
|
) {
|
||||||
|
const rippleRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [waves, setWaves] = useState<React.JSX.Element[]>([]);
|
||||||
|
const [isTouch, setIsTouch] = useState(false);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => rippleRef.current, []);
|
||||||
|
|
||||||
|
const handleWaveOnDone = () => {
|
||||||
|
setWaves((prev) => prev.slice(1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWave = (x: number, y: number) => {
|
||||||
|
const style = calcRippleWaveStyle(x, y, rippleRef.current);
|
||||||
|
const wave = (
|
||||||
|
<RippleWave
|
||||||
|
key={new Date().getTime()}
|
||||||
|
style={style}
|
||||||
|
onDone={handleWaveOnDone}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
setWaves([...waves, wave]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (event: React.MouseEvent) => {
|
||||||
|
if (isTouch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { pageX, pageY } = event;
|
||||||
|
addWave(pageX, pageY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchStart = (event: React.TouchEvent) => {
|
||||||
|
setIsTouch(true);
|
||||||
|
const { touches, changedTouches } = event;
|
||||||
|
const { pageX, pageY } = touches[0] ?? changedTouches[0];
|
||||||
|
addWave(pageX, pageY);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={rippleRef}
|
||||||
|
className={styles.ripple}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{waves}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Ripple = forwardRef(RippleInner);
|
1
front/src/components/ui/animation/ripple/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Ripple } from './component';
|
@ -0,0 +1,39 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import styles from './style.module.scss';
|
||||||
|
import { RippleWaveProps } from './types';
|
||||||
|
|
||||||
|
export function RippleWave({ style, onDone }: RippleWaveProps) {
|
||||||
|
const [isMouseUp, setIsMouseUp] = useState(false);
|
||||||
|
const [isAnimationEnd, setIsAnimationEnd] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mouseUpListener = () => setIsMouseUp(true);
|
||||||
|
document.addEventListener('mouseup', mouseUpListener, { once: true });
|
||||||
|
document.addEventListener('touchend', mouseUpListener, { once: true });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const visible = !isMouseUp || !isAnimationEnd;
|
||||||
|
|
||||||
|
const className = clsx(
|
||||||
|
styles.wave,
|
||||||
|
visible ? styles.visible : styles.invisible,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAnimationEnd = (event: React.AnimationEvent) => {
|
||||||
|
if (event.animationName === styles.fadein) {
|
||||||
|
setIsAnimationEnd(true);
|
||||||
|
} else {
|
||||||
|
onDone();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
onAnimationEnd={handleAnimationEnd}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { RippleWave } from './component';
|
@ -0,0 +1,33 @@
|
|||||||
|
.wave {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: var(--clr-ripple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible {
|
||||||
|
animation: fadein 0.3s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
animation: fadeout 0.3s linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
scale: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export type RippleWaveProps = {
|
||||||
|
style: CSSProperties;
|
||||||
|
onDone: () => void;
|
||||||
|
};
|
@ -0,0 +1,7 @@
|
|||||||
|
.ripple {
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
}
|
1
front/src/components/ui/animation/ripple/types.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type RippleProps = {} & React.ComponentProps<'div'>;
|
14
front/src/components/ui/animation/ripple/utils.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export const calcRippleWaveStyle = (
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
ripple: HTMLDivElement,
|
||||||
|
): CSSProperties => {
|
||||||
|
const wrapperRect = ripple.getBoundingClientRect();
|
||||||
|
const diameter = Math.max(wrapperRect.width, wrapperRect.height);
|
||||||
|
const radius = diameter / 2;
|
||||||
|
const left = x - wrapperRect.left - radius;
|
||||||
|
const top = y - wrapperRect.top - radius;
|
||||||
|
return { left, top, width: diameter, height: diameter };
|
||||||
|
};
|
42
front/src/components/ui/button/component.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Ripple } from '../animation';
|
||||||
|
import { Comet } from '../comet';
|
||||||
|
import { RawButton } from '../raw';
|
||||||
|
import { COMET_VARIANT_MAP } from './constants';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { ButtonProps } from './types.js';
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
variant = 'primary',
|
||||||
|
scale = 'm',
|
||||||
|
pending = false,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
...props
|
||||||
|
}: ButtonProps) {
|
||||||
|
const classNames = clsx(
|
||||||
|
styles.button,
|
||||||
|
styles[variant],
|
||||||
|
styles[scale],
|
||||||
|
{ [styles.pending]: pending },
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<RawButton
|
||||||
|
className={classNames}
|
||||||
|
disabled={pending ? true : disabled}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{pending && (
|
||||||
|
<div className={styles.cometWrapper}>
|
||||||
|
<Comet scale={scale} variant={COMET_VARIANT_MAP[variant]} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.childrenWrapper}>{children}</div>
|
||||||
|
<Ripple />
|
||||||
|
</RawButton>
|
||||||
|
);
|
||||||
|
}
|
10
front/src/components/ui/button/constants.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { CometProps } from '../comet';
|
||||||
|
import { ButtonProps } from './types';
|
||||||
|
|
||||||
|
export const COMET_VARIANT_MAP: Record<
|
||||||
|
ButtonProps['variant'],
|
||||||
|
CometProps['variant']
|
||||||
|
> = {
|
||||||
|
primary: 'onPrimary',
|
||||||
|
secondary: 'onSecondary',
|
||||||
|
};
|
3
front/src/components/ui/button/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { Button } from './component';
|
||||||
|
export { ButtonPreview } from './preview';
|
||||||
|
export { type ButtonProps } from './types';
|
@ -1,23 +0,0 @@
|
|||||||
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;
|
|
49
front/src/components/ui/button/preview.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { PreviewArticle, PreviewBox } from '@components/ui/preview';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Button } from './component';
|
||||||
|
|
||||||
|
export function ButtonPreview() {
|
||||||
|
return (
|
||||||
|
<PreviewArticle title="Button">
|
||||||
|
<PreviewBox>
|
||||||
|
<Button scale="s">Button</Button>
|
||||||
|
<Button scale="m">Button</Button>
|
||||||
|
<Button scale="l">Button</Button>
|
||||||
|
</PreviewBox>
|
||||||
|
<PreviewBox>
|
||||||
|
<Button scale="s" pending>
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
<Button scale="m" pending>
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
<Button scale="l" pending>
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
</PreviewBox>
|
||||||
|
<PreviewBox>
|
||||||
|
<Button scale="s" variant="secondary">
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
<Button scale="m" variant="secondary">
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
<Button scale="l" variant="secondary">
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
</PreviewBox>
|
||||||
|
<PreviewBox>
|
||||||
|
<Button scale="s" variant="secondary" pending>
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
<Button scale="m" variant="secondary" pending>
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
<Button scale="l" variant="secondary" pending>
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
</PreviewBox>
|
||||||
|
</PreviewArticle>
|
||||||
|
);
|
||||||
|
}
|
@ -1,39 +1,76 @@
|
|||||||
.button {
|
.button {
|
||||||
border-radius: 5px;
|
position: relative;
|
||||||
cursor: pointer;
|
overflow: hidden;
|
||||||
|
box-shadow: 0px 2px 2px var(--clr-shadow-200);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.1s ease-in-out;
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
|
||||||
&:hover {
|
&:disabled {
|
||||||
filter: brightness(0.9);
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:not(:disabled) {
|
||||||
filter: brightness(0.8);
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cometWrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending {
|
||||||
|
.childrenWrapper {
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
background-color: var(--clr-primary);
|
background-color: var(--clr-primary);
|
||||||
color: var(--clr-layer-100);
|
color: var(--clr-on-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--clr-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background-color: var(--clr-primary-active);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
background-color: var(--clr-secondary);
|
background-color: var(--clr-secondary);
|
||||||
color: var(--clr-text-100);
|
color: var(--clr-on-secondary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--clr-secondary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background-color: var(--clr-secondary-active);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.s {
|
.s {
|
||||||
padding: 8px 16px;
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.m {
|
.m {
|
||||||
padding: 10px 20px;
|
padding: 14px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.l {
|
.l {
|
||||||
padding: 12px 24px;
|
padding: 18px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { RawButtonProps } from '../raw/raw-button/types';
|
import { Scale } from '@components/ui/types';
|
||||||
|
|
||||||
|
import { RawButtonProps } from '../raw';
|
||||||
|
|
||||||
export type ButtonProps = {
|
export type ButtonProps = {
|
||||||
variant?: 'primary' | 'secondary';
|
variant?: 'primary' | 'secondary';
|
||||||
scale?: 's' | 'm' | 'l';
|
scale?: Scale;
|
||||||
|
pending?: boolean;
|
||||||
} & RawButtonProps;
|
} & RawButtonProps;
|
||||||
|
36
front/src/components/ui/calendar/component.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, { ForwardedRef, forwardRef, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { CalendarDays } from './parts';
|
||||||
|
import { CalendarProps } from './types';
|
||||||
|
|
||||||
|
function CalendarInner(
|
||||||
|
{ value, onChange, min, max, ...props }: Omit<CalendarProps, 'ref'>,
|
||||||
|
ref: ForwardedRef<HTMLDivElement>,
|
||||||
|
) {
|
||||||
|
const [date, setDate] = useState<Date>(value ? new Date(value) : new Date());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDate(value ? new Date(value) : new Date());
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleMonthChange = (delta: number) => {
|
||||||
|
const newDate = new Date(date);
|
||||||
|
newDate.setMonth(newDate.getMonth() + delta);
|
||||||
|
setDate(newDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props} ref={ref}>
|
||||||
|
<CalendarDays
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
date={date}
|
||||||
|
onMonthChange={handleMonthChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Calendar = forwardRef(CalendarInner);
|
2
front/src/components/ui/calendar/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { Calendar } from './component';
|
||||||
|
export { CalendarPreview } from './preview';
|
@ -0,0 +1,81 @@
|
|||||||
|
import { IconButton } from '@components/ui/icon-button';
|
||||||
|
import { RawButton } from '@components/ui/raw';
|
||||||
|
import { Span } from '@components/ui/span';
|
||||||
|
import ArrowDownIcon from '@public/images/svg/arrow-down.svg';
|
||||||
|
import ArrowUpIcon from '@public/images/svg/arrow-up.svg';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { DAYS_OF_THE_WEEK, MONTHS } from './constants';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { CalendarDaysProps } from './types';
|
||||||
|
import { getCalendarDays } from './utils';
|
||||||
|
|
||||||
|
export function CalendarDays({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
date,
|
||||||
|
onMonthChange,
|
||||||
|
}: CalendarDaysProps) {
|
||||||
|
const today = useMemo(() => new Date(), []);
|
||||||
|
|
||||||
|
const days = useMemo(() => {
|
||||||
|
return getCalendarDays({
|
||||||
|
year: date.getFullYear(),
|
||||||
|
monthIndex: date.getMonth(),
|
||||||
|
today,
|
||||||
|
selectedDateStr: value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
});
|
||||||
|
}, [date, min, max]);
|
||||||
|
|
||||||
|
const handleChange = (newValue: string) => {
|
||||||
|
onChange?.(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<Span color="t300" className={styles.title}>
|
||||||
|
{MONTHS[date.getMonth()]} {date.getFullYear()}
|
||||||
|
</Span>
|
||||||
|
<IconButton
|
||||||
|
className={styles.turnButton}
|
||||||
|
onClick={() => onMonthChange(-1)}
|
||||||
|
>
|
||||||
|
<ArrowUpIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
className={styles.turnButton}
|
||||||
|
onClick={() => onMonthChange(1)}
|
||||||
|
>
|
||||||
|
<ArrowDownIcon />
|
||||||
|
</IconButton>
|
||||||
|
</header>
|
||||||
|
<div className={styles.daysGrid}>
|
||||||
|
{DAYS_OF_THE_WEEK.map((day) => (
|
||||||
|
<Span className={styles.dayOfTheWeek} scale="none" key={day}>
|
||||||
|
{day}
|
||||||
|
</Span>
|
||||||
|
))}
|
||||||
|
{days.map((day, index) => (
|
||||||
|
<RawButton
|
||||||
|
key={index}
|
||||||
|
disabled={day.isDisabled}
|
||||||
|
className={clsx(styles.day, {
|
||||||
|
[styles.selectedDay]: day.isSelected,
|
||||||
|
[styles.currentMonthDay]: day.isCurrentMonth,
|
||||||
|
})}
|
||||||
|
onClick={() => handleChange(day.string)}
|
||||||
|
>
|
||||||
|
{day.number}
|
||||||
|
{day.isToday && <div className={styles.todayIndicator} />}
|
||||||
|
</RawButton>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
export const DAYS_OF_THE_WEEK = [
|
||||||
|
'Mon',
|
||||||
|
'Tue',
|
||||||
|
'Wed',
|
||||||
|
'Thu',
|
||||||
|
'Fri',
|
||||||
|
'Sat',
|
||||||
|
'Sun',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MONTHS = [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December',
|
||||||
|
];
|
@ -0,0 +1,2 @@
|
|||||||
|
export { CalendarDays } from './component';
|
||||||
|
export { type CalendarDaysProps } from './types';
|
@ -0,0 +1,85 @@
|
|||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 5px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.turnButton {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daysGrid {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
grid-template-columns: repeat(7, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dayOfTheWeek {
|
||||||
|
display: flex;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--clr-text-100);
|
||||||
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
|
||||||
|
&:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--clr-layer-300-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--clr-text-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.currentMonthDay {
|
||||||
|
color: var(--clr-text-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedDay {
|
||||||
|
background-color: var(--clr-primary);
|
||||||
|
box-shadow: 0px 2px 2px var(--clr-shadow-200);
|
||||||
|
color: var(--clr-on-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--clr-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todayIndicator {
|
||||||
|
background-color: var(--clr-on-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.todayIndicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 12%;
|
||||||
|
left: 50%;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--clr-text-300);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
export type CalendarDay = {
|
||||||
|
number: number;
|
||||||
|
isDisabled: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
isToday: boolean;
|
||||||
|
string: string;
|
||||||
|
isCurrentMonth: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CalendarDaysProps = {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
min: Date | null;
|
||||||
|
max: Date | null;
|
||||||
|
date: Date;
|
||||||
|
onMonthChange: (delta: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetCalendarDaysParams = {
|
||||||
|
year: number;
|
||||||
|
monthIndex: number;
|
||||||
|
today: Date;
|
||||||
|
selectedDateStr?: string;
|
||||||
|
min: Date | null;
|
||||||
|
max: Date | null;
|
||||||
|
};
|
@ -0,0 +1,52 @@
|
|||||||
|
import { dateToInputString } from '@utils/date';
|
||||||
|
|
||||||
|
import { CalendarDay, GetCalendarDaysParams } from './types';
|
||||||
|
|
||||||
|
const addDays = (date: Date, days: number) => {
|
||||||
|
date.setDate(date.getDate() + days);
|
||||||
|
};
|
||||||
|
|
||||||
|
const daysAreEqual = (date1: Date, date2: Date) => {
|
||||||
|
return (
|
||||||
|
date1.getDate() === date2.getDate() &&
|
||||||
|
date1.getMonth() === date2.getMonth() &&
|
||||||
|
date1.getFullYear() === date2.getFullYear()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCalendarDays = ({
|
||||||
|
year,
|
||||||
|
monthIndex,
|
||||||
|
today,
|
||||||
|
selectedDateStr,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
}: GetCalendarDaysParams) => {
|
||||||
|
const selectedDate = new Date(selectedDateStr);
|
||||||
|
|
||||||
|
const firstDayOfMonth = new Date(year, monthIndex, 1);
|
||||||
|
const daysFromPrevMonth = (firstDayOfMonth.getDay() || 7) - 1;
|
||||||
|
|
||||||
|
const date = new Date(year, monthIndex, 1);
|
||||||
|
addDays(date, -daysFromPrevMonth);
|
||||||
|
const days: CalendarDay[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 42; i += 1) {
|
||||||
|
const number = date.getDate();
|
||||||
|
const isDisabled = (min && date < min) || (max && date > max);
|
||||||
|
const isSelected = daysAreEqual(date, selectedDate);
|
||||||
|
const isToday = daysAreEqual(date, today);
|
||||||
|
const string = dateToInputString(date);
|
||||||
|
const isCurrentMonth = date.getMonth() === monthIndex;
|
||||||
|
days.push({
|
||||||
|
number,
|
||||||
|
isDisabled,
|
||||||
|
isSelected,
|
||||||
|
isToday,
|
||||||
|
string,
|
||||||
|
isCurrentMonth,
|
||||||
|
});
|
||||||
|
addDays(date, 1);
|
||||||
|
}
|
||||||
|
return days;
|
||||||
|
};
|
1
front/src/components/ui/calendar/parts/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './calendar-days';
|
19
front/src/components/ui/calendar/preview.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { PreviewArticle } from '@components/ui/preview';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { Calendar } from './component';
|
||||||
|
|
||||||
|
export function CalendarPreview() {
|
||||||
|
const [date, setDate] = useState<string>('2024-10-09T01:07');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreviewArticle title="Calendar">
|
||||||
|
<Calendar
|
||||||
|
value={date}
|
||||||
|
onChange={setDate}
|
||||||
|
min="2024-10-02T00:00"
|
||||||
|
max="2024-10-15T00:00"
|
||||||
|
/>
|
||||||
|
</PreviewArticle>
|
||||||
|
);
|
||||||
|
}
|
0
front/src/components/ui/calendar/styles.module.scss
Normal file
6
front/src/components/ui/calendar/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export type CalendarProps = {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
min: Date | null;
|
||||||
|
max: Date | null;
|
||||||
|
} & Omit<React.ComponentProps<'div'>, 'onChange'>;
|
42
front/src/components/ui/checkbox-group/component.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Checkbox } from '../checkbox';
|
||||||
|
import { Span } from '../span';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { CheckboxGroupProps } from './types';
|
||||||
|
|
||||||
|
export function CheckboxGroup<T>({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
items,
|
||||||
|
onChange,
|
||||||
|
getItemKey,
|
||||||
|
getItemLabel,
|
||||||
|
scale = 'm',
|
||||||
|
label,
|
||||||
|
}: CheckboxGroupProps<T>) {
|
||||||
|
const classNames = clsx(styles.checkBoxGroup, styles[scale]);
|
||||||
|
|
||||||
|
const handleChange = (index: number) => {
|
||||||
|
onChange(value.with(index, !value[index]));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames}>
|
||||||
|
<Span color="t300" scale={scale} className={styles.label}>
|
||||||
|
{label}
|
||||||
|
</Span>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Checkbox
|
||||||
|
name={name}
|
||||||
|
label={{ text: getItemLabel(item) }}
|
||||||
|
checked={value[index]}
|
||||||
|
key={getItemKey(item)}
|
||||||
|
onChange={() => handleChange(index)}
|
||||||
|
scale={scale}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
2
front/src/components/ui/checkbox-group/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { CheckboxGroup } from './component';
|
||||||
|
export { CheckboxGroupPreview } from './preview';
|
46
front/src/components/ui/checkbox-group/preview.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { PreviewArticle } from '@components/ui/preview';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { CheckboxGroup } from './component';
|
||||||
|
|
||||||
|
export function CheckboxGroupPreview() {
|
||||||
|
const [value1, setValue1] = useState<boolean[]>([true, true, false]);
|
||||||
|
const [value2, setValue2] = useState<boolean[]>([true, true, false]);
|
||||||
|
const [value3, setValue3] = useState<boolean[]>([true, true, false]);
|
||||||
|
const items = ['Pop', 'Rap', 'Rock'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreviewArticle title="CheckboxGroup">
|
||||||
|
<CheckboxGroup
|
||||||
|
name="animal-s"
|
||||||
|
value={value1}
|
||||||
|
items={items}
|
||||||
|
onChange={setValue1}
|
||||||
|
getItemKey={(i) => i}
|
||||||
|
getItemLabel={(i) => i}
|
||||||
|
label="What genre of music do you like?"
|
||||||
|
scale="s"
|
||||||
|
/>
|
||||||
|
<CheckboxGroup
|
||||||
|
name="animal-m"
|
||||||
|
value={value2}
|
||||||
|
items={items}
|
||||||
|
onChange={setValue2}
|
||||||
|
getItemKey={(i) => i}
|
||||||
|
getItemLabel={(i) => i}
|
||||||
|
label="What genre of music do you like?"
|
||||||
|
scale="m"
|
||||||
|
/>
|
||||||
|
<CheckboxGroup
|
||||||
|
name="animal-l"
|
||||||
|
value={value3}
|
||||||
|
items={items}
|
||||||
|
onChange={setValue3}
|
||||||
|
getItemKey={(i) => i}
|
||||||
|
getItemLabel={(i) => i}
|
||||||
|
label="What genre of music do you like?"
|
||||||
|
scale="l"
|
||||||
|
/>
|
||||||
|
</PreviewArticle>
|
||||||
|
);
|
||||||
|
}
|
22
front/src/components/ui/checkbox-group/styles.module.scss
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.checkBoxGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s {
|
||||||
|
.label {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m {
|
||||||
|
.label {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.l {
|
||||||
|
.label {
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
}
|
12
front/src/components/ui/checkbox-group/types.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Scale } from '../types';
|
||||||
|
|
||||||
|
export type CheckboxGroupProps<T> = {
|
||||||
|
name: string;
|
||||||
|
value: boolean[];
|
||||||
|
items: T[];
|
||||||
|
onChange: (value: boolean[]) => void;
|
||||||
|
getItemKey: (item: T) => React.Key;
|
||||||
|
getItemLabel: (item: T) => string;
|
||||||
|
scale?: Scale;
|
||||||
|
label?: string;
|
||||||
|
};
|
43
front/src/components/ui/checkbox/component.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import CheckIcon from '@public/images/svg/check.svg';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { ForwardedRef, forwardRef } from 'react';
|
||||||
|
|
||||||
|
import { Ripple } from '../animation';
|
||||||
|
import { Label, LabelProps } from '../label';
|
||||||
|
import { RawInput } from '../raw';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { CheckboxProps } from './types';
|
||||||
|
|
||||||
|
function CheckboxInner(
|
||||||
|
{ scale = 'm', label = {}, required, ...props }: Omit<CheckboxProps, 'ref'>,
|
||||||
|
ref: ForwardedRef<HTMLInputElement>,
|
||||||
|
) {
|
||||||
|
const wrapperClassName = clsx(styles.wrapper, styles[scale]);
|
||||||
|
|
||||||
|
const labelProps: LabelProps = {
|
||||||
|
position: 'right',
|
||||||
|
scale: scale,
|
||||||
|
...label,
|
||||||
|
required: { value: required, ...label.required },
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label {...labelProps}>
|
||||||
|
<div className={wrapperClassName}>
|
||||||
|
<RawInput
|
||||||
|
className={styles.input}
|
||||||
|
type="checkbox"
|
||||||
|
ref={ref}
|
||||||
|
required={required}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<div className={styles.checkbox}>
|
||||||
|
<CheckIcon className={styles.icon} />
|
||||||
|
</div>
|
||||||
|
<Ripple />
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox = forwardRef(CheckboxInner);
|
2
front/src/components/ui/checkbox/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { Checkbox } from './component';
|
||||||
|
export { type CheckboxProps } from './types';
|
88
front/src/components/ui/checkbox/styles.module.scss
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.checkbox {
|
||||||
|
background-color: var(--clr-layer-300-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
& ~ .checkbox {
|
||||||
|
border-width: 0;
|
||||||
|
background-color: var(--clr-primary);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 100%;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
& ~ .checkbox {
|
||||||
|
background-color: var(--clr-primary-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--clr-border-200);
|
||||||
|
background-color: var(--clr-layer-300);
|
||||||
|
box-shadow: 0px 2px 2px var(--clr-shadow-200);
|
||||||
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 0%;
|
||||||
|
fill: var(--clr-text-200);
|
||||||
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s {
|
||||||
|
padding: 3px;
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m {
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.l {
|
||||||
|
padding: 7px;
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
}
|
8
front/src/components/ui/checkbox/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { LabelProps } from '../label';
|
||||||
|
import { RawInputProps } from '../raw';
|
||||||
|
import { Scale } from '../types';
|
||||||
|
|
||||||
|
export type CheckboxProps = {
|
||||||
|
scale?: Scale;
|
||||||
|
label?: LabelProps;
|
||||||
|
} & Omit<RawInputProps, 'type'>;
|
19
front/src/components/ui/comet/component.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { CometProps } from './types';
|
||||||
|
|
||||||
|
export function Comet({
|
||||||
|
scale = 'm',
|
||||||
|
variant = 'onPrimary',
|
||||||
|
className,
|
||||||
|
}: CometProps) {
|
||||||
|
const classNames = clsx(
|
||||||
|
styles.comet,
|
||||||
|
styles[scale],
|
||||||
|
styles[variant],
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
return <div className={classNames}></div>;
|
||||||
|
}
|
2
front/src/components/ui/comet/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { Comet } from './component';
|
||||||
|
export { type CometProps } from './types';
|
36
front/src/components/ui/comet/styles.module.scss
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
.comet {
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spinner-comet 1s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner-comet {
|
||||||
|
to {
|
||||||
|
transform: rotate(1turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.s {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
mask: radial-gradient(farthest-side, #0000 calc(100% - 2px), #000 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
mask: radial-gradient(farthest-side, #0000 calc(100% - 2.5px), #000 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.l {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
mask: radial-gradient(farthest-side, #0000 calc(100% - 3px), #000 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onPrimary {
|
||||||
|
background: conic-gradient(#0000 10%, var(--clr-on-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.onSecondary {
|
||||||
|
background: conic-gradient(#0000 10%, var(--clr-on-secondary));
|
||||||
|
}
|
6
front/src/components/ui/comet/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { Scale } from '../types';
|
||||||
|
|
||||||
|
export type CometProps = {
|
||||||
|
scale?: Scale;
|
||||||
|
variant?: 'onPrimary' | 'onSecondary';
|
||||||
|
} & React.ComponentProps<'div'>;
|
@ -1,23 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export type ControlLabelProps = {
|
|
||||||
text?: string;
|
|
||||||
position?: 'left' | 'top' | 'right' | 'bottom';
|
|
||||||
} & React.ComponentPropsWithoutRef<'label'>;
|
|
131
front/src/components/ui/date-input/component.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import CalendarIcon from '@public/images/svg/calendar.svg';
|
||||||
|
import { px } from '@utils/css';
|
||||||
|
import { useMissClick } from '@utils/miss-click';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { Calendar } from '../calendar';
|
||||||
|
import { IconButton } from '../icon-button';
|
||||||
|
import { Popover } from '../popover';
|
||||||
|
import { TextInput } from '../text-input';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { DateInputProps } from './types';
|
||||||
|
import { dirtyDateToValue, inputToDirtyDate, valueToDirtyDate } from './utils';
|
||||||
|
|
||||||
|
export function DateInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
rightNode,
|
||||||
|
scale,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
...props
|
||||||
|
}: DateInputProps) {
|
||||||
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const inputWrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const calendarWrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [calendarVisible, setCalendarVisible] = useState<boolean>(false);
|
||||||
|
const [dirtyDate, setDirtyDate] = useState<string>('');
|
||||||
|
|
||||||
|
const minDate = useMemo(() => {
|
||||||
|
return min ? new Date(min) : null;
|
||||||
|
}, [min]);
|
||||||
|
|
||||||
|
const maxDate = useMemo(() => {
|
||||||
|
return max ? new Date(max) : null;
|
||||||
|
}, [max]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDirtyDate(valueToDirtyDate(value));
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useMissClick(
|
||||||
|
[wrapperRef, calendarWrapperRef],
|
||||||
|
() => setCalendarVisible(false),
|
||||||
|
calendarVisible,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCalendarButtonClick = () => {
|
||||||
|
setCalendarVisible(!calendarVisible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newDirtyDate = inputToDirtyDate(event.target.value);
|
||||||
|
if (newDirtyDate.length === 10) {
|
||||||
|
const newValue = dirtyDateToValue(newDirtyDate);
|
||||||
|
const date = new Date(newValue);
|
||||||
|
if (
|
||||||
|
!isNaN(date.getDate()) &&
|
||||||
|
(!minDate || date >= minDate) &&
|
||||||
|
(!maxDate || date <= maxDate)
|
||||||
|
) {
|
||||||
|
onChange?.(newValue);
|
||||||
|
} else {
|
||||||
|
onChange?.('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setDirtyDate(newDirtyDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCalendarChange = (newValue: string) => {
|
||||||
|
onChange?.(newValue);
|
||||||
|
setCalendarVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcPopoverStyles = (calendarRect: DOMRect) => {
|
||||||
|
if (calendarRect === null) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputWrapperRect = inputWrapperRef.current.getBoundingClientRect();
|
||||||
|
const { left, bottom, top } = inputWrapperRect;
|
||||||
|
|
||||||
|
const rightSpace = window.innerWidth - left;
|
||||||
|
const rightOverflow = calendarRect.width - rightSpace;
|
||||||
|
const bottomSpace = window.innerHeight - bottom;
|
||||||
|
|
||||||
|
const popoverLeft = rightOverflow <= 0 ? left : left - rightOverflow;
|
||||||
|
|
||||||
|
const popoverTop =
|
||||||
|
bottomSpace >= calendarRect.height ? bottom : top - calendarRect.height;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: px(popoverLeft),
|
||||||
|
top: px(popoverTop),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper} ref={wrapperRef}>
|
||||||
|
<TextInput
|
||||||
|
{...props}
|
||||||
|
scale={scale}
|
||||||
|
value={dirtyDate}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
wrapper={{ ref: inputWrapperRef }}
|
||||||
|
rightNode={
|
||||||
|
<>
|
||||||
|
<IconButton scale={scale} onClick={handleCalendarButtonClick}>
|
||||||
|
<CalendarIcon />
|
||||||
|
</IconButton>
|
||||||
|
{rightNode}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Popover
|
||||||
|
visible={calendarVisible}
|
||||||
|
calcStyles={calcPopoverStyles}
|
||||||
|
element={
|
||||||
|
<div className={styles.calendarWrapper} ref={calendarWrapperRef}>
|
||||||
|
<Calendar
|
||||||
|
value={value}
|
||||||
|
onChange={handleCalendarChange}
|
||||||
|
className={styles.calendar}
|
||||||
|
min={minDate}
|
||||||
|
max={maxDate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
3
front/src/components/ui/date-input/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { DateInput } from './component';
|
||||||
|
export { DateInputPreview } from './preview';
|
||||||
|
export { type DateInputProps } from './types';
|
33
front/src/components/ui/date-input/preview.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { PreviewArticle } from '@components/ui/preview';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { DateInput } from './component';
|
||||||
|
|
||||||
|
export function DateInputPreview() {
|
||||||
|
const [date, setDate] = useState<string>();
|
||||||
|
return (
|
||||||
|
<PreviewArticle title="DateInput">
|
||||||
|
<DateInput
|
||||||
|
value={date}
|
||||||
|
onChange={(v) => setDate(v)}
|
||||||
|
scale="s"
|
||||||
|
label={{ text: 'Date' }}
|
||||||
|
name="date"
|
||||||
|
/>
|
||||||
|
<DateInput
|
||||||
|
value={date}
|
||||||
|
onChange={(v) => setDate(v)}
|
||||||
|
scale="m"
|
||||||
|
label={{ text: 'Date' }}
|
||||||
|
name="date"
|
||||||
|
/>
|
||||||
|
<DateInput
|
||||||
|
value={date}
|
||||||
|
onChange={(v) => setDate(v)}
|
||||||
|
scale="l"
|
||||||
|
label={{ text: 'Date' }}
|
||||||
|
name="date"
|
||||||
|
/>
|
||||||
|
</PreviewArticle>
|
||||||
|
);
|
||||||
|
}
|
20
front/src/components/ui/date-input/styles.module.scss
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarWrapper {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar {
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid var(--clr-border-200);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--clr-layer-300);
|
||||||
|
box-shadow: 0px 4px 6px var(--clr-shadow-100);
|
||||||
|
}
|
8
front/src/components/ui/date-input/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { TextInputProps } from '../text-input';
|
||||||
|
|
||||||
|
export type DateInputProps = {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
max?: string;
|
||||||
|
min?: string;
|
||||||
|
} & Omit<TextInputProps, 'type' | 'value' | 'onChange'>;
|
21
front/src/components/ui/date-input/utils.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export const valueToDirtyDate = (value: string = '') => {
|
||||||
|
const year = value.substring(0, 4);
|
||||||
|
const month = value.substring(5, 7);
|
||||||
|
const day = value.substring(8, 10);
|
||||||
|
return `${day}${month && '.'}${month}${year && '.'}${year}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inputToDirtyDate = (input: string) => {
|
||||||
|
const value = input.replace(/\D/g, '');
|
||||||
|
const day = value.substring(0, 2);
|
||||||
|
const month = value.substring(2, 4);
|
||||||
|
const year = value.substring(4, 8);
|
||||||
|
return `${day}${month && '.'}${month}${year && '.'}${year}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dirtyDateToValue = (dirtyDate: string) => {
|
||||||
|
const day = dirtyDate.substring(0, 2);
|
||||||
|
const month = dirtyDate.substring(3, 5);
|
||||||
|
const year = dirtyDate.substring(6, 10);
|
||||||
|
return `${year}-${month}-${day}T00:00`;
|
||||||
|
};
|
15
front/src/components/ui/heading/component.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { HeadingProps, HeadingTag } from './types';
|
||||||
|
|
||||||
|
export function Heading<T extends HeadingTag>({
|
||||||
|
tag,
|
||||||
|
color = 't300',
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: HeadingProps<T>) {
|
||||||
|
const classNames = clsx(styles.heading, styles[color], className);
|
||||||
|
return createElement(tag, { ...props, className: classNames });
|
||||||
|
}
|
2
front/src/components/ui/heading/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { Heading } from './component';
|
||||||
|
export { type HeadingProps } from './types';
|
15
front/src/components/ui/heading/styles.module.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.heading {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t100 {
|
||||||
|
color: var(--clr-text-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.t200 {
|
||||||
|
color: var(--clr-text-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.t300 {
|
||||||
|
color: var(--clr-text-300);
|
||||||
|
}
|
8
front/src/components/ui/heading/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { TextColor } from '../types';
|
||||||
|
|
||||||
|
export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||||
|
|
||||||
|
export type HeadingProps<T extends HeadingTag> = {
|
||||||
|
tag: T;
|
||||||
|
color?: TextColor;
|
||||||
|
} & React.ComponentProps<T>;
|
@ -1,15 +1,22 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Ripple } from '../animation';
|
||||||
import { RawButton } from '../raw';
|
import { RawButton } from '../raw';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { IconButtonProps } from './types';
|
import { IconButtonProps } from './types';
|
||||||
|
|
||||||
export default function IconButton({
|
export function IconButton({
|
||||||
scale = 'm',
|
scale = 'm',
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: IconButtonProps) {
|
}: IconButtonProps) {
|
||||||
const classes = clsx(styles.button, styles[scale], className);
|
const classes = clsx(styles.button, styles[scale], className);
|
||||||
return <RawButton className={classes} {...props} />;
|
return (
|
||||||
|
<RawButton className={classes} {...props}>
|
||||||
|
{children}
|
||||||
|
<Ripple />
|
||||||
|
</RawButton>
|
||||||
|
);
|
||||||
}
|
}
|
2
front/src/components/ui/icon-button/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { IconButton } from './component';
|
||||||
|
export { type IconButtonProps } from './types';
|
@ -1,32 +1,41 @@
|
|||||||
.button {
|
.button {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
|
background-color: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--clr-accent-o50);
|
background-color: var(--clr-ripple);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--clr-text-300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: var(--clr-primary);
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
fill: var(--clr-text-100);
|
||||||
|
transition: all var(--td-100) ease-in-out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.s {
|
.s {
|
||||||
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
height: 28px;
|
|
||||||
width: 28px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.m {
|
.m {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
height: 36px;
|
|
||||||
width: 36px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.l {
|
.l {
|
||||||
|
width: 43px;
|
||||||
|
height: 43px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
height: 44px;
|
|
||||||
width: 44px;
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { RawButtonProps } from '../raw/raw-button/types';
|
import { Scale } from '@components/ui/types';
|
||||||
|
|
||||||
|
import { RawButtonProps } from '../raw';
|
||||||
|
|
||||||
export type IconButtonProps = {
|
export type IconButtonProps = {
|
||||||
scale?: 's' | 'm' | 'l';
|
scale?: Scale;
|
||||||
} & RawButtonProps;
|
} & RawButtonProps;
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
import Button from './button';
|
export { Button } from './button';
|
||||||
import ControlLabel from './control-label';
|
export { Checkbox } from './checkbox';
|
||||||
import IconButton from './icon-button';
|
export { CheckboxGroup } from './checkbox-group';
|
||||||
import Input from './input';
|
export { DateInput } from './date-input';
|
||||||
import Menu from './menu';
|
export { Heading } from './heading';
|
||||||
import PasswordTextField from './password-text-field';
|
export { IconButton } from './icon-button';
|
||||||
import Select from './select';
|
export { Menu } from './menu';
|
||||||
import TextField from './text-field';
|
export { Paragraph } from './paragraph';
|
||||||
|
export { PasswordInput } from './password-input';
|
||||||
export {
|
export { RadioGroup } from './radio-group';
|
||||||
Button,
|
export { Select } from './select';
|
||||||
ControlLabel,
|
export { Span } from './span';
|
||||||
IconButton,
|
export { TextInput } from './text-input';
|
||||||
Input,
|
|
||||||
Menu,
|
|
||||||
PasswordTextField,
|
|
||||||
Select,
|
|
||||||
TextField,
|
|
||||||
};
|
|
||||||
|
55
front/src/components/ui/input/component.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { ForwardedRef, forwardRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { RawInput } from '../raw';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { InputProps } from './types';
|
||||||
|
|
||||||
|
function InputInner(
|
||||||
|
{
|
||||||
|
scale = 'm',
|
||||||
|
wrapper = {},
|
||||||
|
leftNode,
|
||||||
|
rightNode,
|
||||||
|
className,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
...props
|
||||||
|
}: Omit<InputProps, 'ref'>,
|
||||||
|
ref: ForwardedRef<HTMLInputElement>,
|
||||||
|
) {
|
||||||
|
const [focus, setFocus] = useState<boolean>(false);
|
||||||
|
const wrapperClassNames = clsx(
|
||||||
|
styles[scale],
|
||||||
|
styles.wrapper,
|
||||||
|
focus && styles.wrapperFocus,
|
||||||
|
wrapper.className,
|
||||||
|
);
|
||||||
|
const inputClassNames = clsx(styles.input, className);
|
||||||
|
|
||||||
|
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
onFocus?.(event);
|
||||||
|
setFocus(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
onBlur?.(event);
|
||||||
|
setFocus(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...wrapper} className={wrapperClassNames}>
|
||||||
|
{leftNode}
|
||||||
|
<RawInput
|
||||||
|
className={inputClassNames}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{rightNode}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Input = forwardRef(InputInner);
|