[test-entity]: front
This commit is contained in:
parent
4d40a2cacb
commit
ee6bbdca8b
7
front/public/images/svg/arrow-left.svg
Normal file
7
front/public/images/svg/arrow-left.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 4 6" style="enable-background:new 0 0 4 6;" xml:space="preserve">
|
||||||
|
<path d="M0.49,2.99c0-0.13,0.05-0.26,0.15-0.35l2-2c0.2-0.2,0.51-0.2,0.71,0s0.2,0.51,0,0.71L1.71,2.99l1.65,1.65
|
||||||
|
c0.2,0.2,0.2,0.51,0,0.71s-0.51,0.2-0.71,0l-2-2C0.54,3.25,0.49,3.12,0.49,2.99z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 548 B |
7
front/public/images/svg/arrow-right.svg
Normal file
7
front/public/images/svg/arrow-right.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 4 6" style="enable-background:new 0 0 4 6;" xml:space="preserve">
|
||||||
|
<path d="M3.51,3.01c0,0.13-0.05,0.26-0.15,0.35l-2,2c-0.2,0.2-0.51,0.2-0.71,0c-0.2-0.2-0.2-0.51,0-0.71l1.64-1.64L0.64,1.36
|
||||||
|
c-0.2-0.2-0.2-0.51,0-0.71s0.51-0.2,0.71,0l2,2C3.46,2.75,3.51,2.88,3.51,3.01z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 558 B |
8
front/public/images/svg/close.svg
Normal file
8
front/public/images/svg/close.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?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">
|
||||||
|
<path d="M1.23,5.47L3,3.71l1.77,1.77c0.2,0.2,0.51,0.2,0.71,0c0.2-0.2,0.2-0.51,0-0.71L3.71,3l1.77-1.77c0.2-0.2,0.2-0.51,0-0.71
|
||||||
|
c-0.2-0.2-0.51-0.2-0.71,0L3,2.29L1.23,0.53c-0.2-0.2-0.51-0.2-0.71,0s-0.2,0.51,0,0.71L2.29,3L0.53,4.77c-0.2,0.2-0.2,0.51,0,0.71
|
||||||
|
C0.72,5.67,1.04,5.67,1.23,5.47z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 646 B |
20
front/public/images/svg/menu.svg
Normal file
20
front/public/images/svg/menu.svg
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?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">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<line class="st0" x1="0.5" y1="1" x2="5.5" y2="1"/>
|
||||||
|
<path d="M5.5,1.5h-5C0.22,1.5,0,1.28,0,1s0.22-0.5,0.5-0.5h5C5.78,0.5,6,0.72,6,1S5.78,1.5,5.5,1.5z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<line class="st0" x1="0.5" y1="3" x2="5.5" y2="3"/>
|
||||||
|
<path d="M5.5,3.5h-5C0.22,3.5,0,3.28,0,3s0.22-0.5,0.5-0.5h5C5.78,2.5,6,2.72,6,3S5.78,3.5,5.5,3.5z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<line class="st0" x1="0.5" y1="5" x2="5.5" y2="5"/>
|
||||||
|
<path d="M5.5,5.5h-5C0.22,5.5,0,5.28,0,5s0.22-0.5,0.5-0.5h5C5.78,4.5,6,4.72,6,5S5.78,5.5,5.5,5.5z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 914 B |
@ -1,2 +1,2 @@
|
|||||||
export * from './service';
|
export * from './service';
|
||||||
export { type Park, type TurbineType } from './types';
|
export * from './types';
|
||||||
|
@ -1,30 +1,38 @@
|
|||||||
import { ParkFormValues, TurbineTypeFormValues } from '@components/ux';
|
import { ApiResponse } from '@api/types';
|
||||||
|
import { TurbineTypeFormValues } from '@components/pages/turbine-type-page/types';
|
||||||
|
|
||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import { WIND_ENDPOINTS } from './constants';
|
import { WIND_ENDPOINTS } from './constants';
|
||||||
import { Park, ParkTurbine, TurbineType } from './types';
|
import { Park, ParkTurbine, ParkWithTurbines, TurbineType } from './types';
|
||||||
import { packParkFormValues, packTurbineTypeFormValues } from './utils';
|
import { packTurbineTypes } from './utils';
|
||||||
|
|
||||||
export const getTurbineTypes = () => {
|
export const getTurbineTypes = () => {
|
||||||
return api.get<TurbineType[]>(WIND_ENDPOINTS.turbines);
|
return api.get<TurbineType[]>(WIND_ENDPOINTS.turbines);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTurbineTypes = (values: Partial<TurbineTypeFormValues>) => {
|
export const getTurbineType = (id: string) => {
|
||||||
return api.post(
|
const url = `${WIND_ENDPOINTS.turbineType}/${id}`;
|
||||||
|
return api.get<TurbineType>(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTurbineTypes = (
|
||||||
|
formValues: Partial<TurbineTypeFormValues>,
|
||||||
|
) => {
|
||||||
|
return api.post<TurbineType>(
|
||||||
WIND_ENDPOINTS.turbineType,
|
WIND_ENDPOINTS.turbineType,
|
||||||
packTurbineTypeFormValues(values),
|
packTurbineTypes(formValues),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editTurbineTypes = (
|
export const editTurbineTypes = (
|
||||||
values: Partial<TurbineTypeFormValues>,
|
formValues: Partial<TurbineTypeFormValues>,
|
||||||
id: number,
|
id: string,
|
||||||
) => {
|
) => {
|
||||||
const url = `${WIND_ENDPOINTS.turbineType}/${id}`;
|
const url = `${WIND_ENDPOINTS.turbineType}/${id}`;
|
||||||
return api.put(url, packTurbineTypeFormValues(values));
|
return api.put<TurbineType>(url, packTurbineTypes(formValues));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTurbineTypes = (id: number) => {
|
export const deleteTurbineType = (id: number) => {
|
||||||
const url = `${WIND_ENDPOINTS.turbineType}/${id}`;
|
const url = `${WIND_ENDPOINTS.turbineType}/${id}`;
|
||||||
return api.delete(url);
|
return api.delete(url);
|
||||||
};
|
};
|
||||||
@ -33,16 +41,25 @@ export const getParks = () => {
|
|||||||
return api.get<Park[]>(WIND_ENDPOINTS.parks);
|
return api.get<Park[]>(WIND_ENDPOINTS.parks);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createPark = (values: Partial<ParkFormValues>) => {
|
export const getPark = (id: string) => {
|
||||||
return api.post(WIND_ENDPOINTS.park, packParkFormValues(values));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const editPark = (values: Partial<ParkFormValues>, id: number) => {
|
|
||||||
const url = `${WIND_ENDPOINTS.park}/${id}`;
|
const url = `${WIND_ENDPOINTS.park}/${id}`;
|
||||||
return api.put(url, packParkFormValues(values));
|
return api.get<Park>(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getParkTurines = (id: number) => {
|
export const getParkTurbines = (id: string) => {
|
||||||
const url = `${WIND_ENDPOINTS.parks}/${id}/turbines`;
|
const url = `${WIND_ENDPOINTS.parks}/${id}/turbines`;
|
||||||
return api.get<ParkTurbine[]>(url);
|
return api.get<ParkTurbine[]>(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getParkWithTurbines = async (
|
||||||
|
id: string,
|
||||||
|
): Promise<ApiResponse<ParkWithTurbines>> => {
|
||||||
|
const parkURL = `${WIND_ENDPOINTS.park}/${id}`;
|
||||||
|
const turbinesURL = `${WIND_ENDPOINTS.parks}/${id}/turbines`;
|
||||||
|
const parkPesponse = await api.get<Park>(parkURL);
|
||||||
|
const turbinesResponse = await api.get<ParkTurbine[]>(turbinesURL);
|
||||||
|
return {
|
||||||
|
data: { ...parkPesponse.data, turbines: turbinesResponse.data },
|
||||||
|
error: parkPesponse.error || turbinesResponse.error || null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -13,10 +13,20 @@ export type Park = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ParkTurbine = {
|
export type ParkTurbine = {
|
||||||
windParkId: number;
|
id: number;
|
||||||
turbineId: number;
|
name: string;
|
||||||
|
height: number;
|
||||||
|
bladeLength: number;
|
||||||
xOffset: number;
|
xOffset: number;
|
||||||
yOffset: number;
|
yOffset: number;
|
||||||
angle: number;
|
angle: number;
|
||||||
comment: string;
|
comment: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ParkWithTurbines = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
centerLatitude: number;
|
||||||
|
centerLongitude: number;
|
||||||
|
turbines: ParkTurbine[];
|
||||||
|
};
|
||||||
|
@ -1,19 +1,9 @@
|
|||||||
import { ParkFormValues, TurbineTypeFormValues } from '@components/ux';
|
import { TurbineTypeFormValues } from '@components/pages/turbine-type-page/types';
|
||||||
|
|
||||||
export const packTurbineTypeFormValues = (
|
export const packTurbineTypes = (values: Partial<TurbineTypeFormValues>) => {
|
||||||
values: Partial<TurbineTypeFormValues>,
|
|
||||||
) => {
|
|
||||||
return {
|
return {
|
||||||
Name: values.name ?? '',
|
Name: values.name ?? '',
|
||||||
Height: parseInt(values.height ?? '0'),
|
Height: parseInt(values.height ?? '0'),
|
||||||
BladeLength: parseInt(values.bladeLength ?? '0'),
|
BladeLength: parseInt(values.bladeLength ?? '0'),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const packParkFormValues = (values: Partial<ParkFormValues>) => {
|
|
||||||
return {
|
|
||||||
Name: values.name ?? '',
|
|
||||||
CenterLatitude: parseInt(values.centerLatitude ?? '0'),
|
|
||||||
CenterLongitude: parseInt(values.centerLongitude ?? '0'),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
5
front/src/components/_mixins.scss
Normal file
5
front/src/components/_mixins.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@mixin on-mobile {
|
||||||
|
@media (width <= 800px) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
@ -2,14 +2,12 @@
|
|||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
|
|
||||||
--clr-primary: #4176FF;
|
--clr-primary: #4176FF;
|
||||||
--clr-primary-o50: #3865DA80;
|
--clr-primary-o50: #4176FF80;
|
||||||
--clr-primary-hover: #638FFF;
|
--clr-primary-hover: #638FFF;
|
||||||
--clr-primary-disabled: #3D68D7;
|
|
||||||
--clr-on-primary: #FFFFFF;
|
--clr-on-primary: #FFFFFF;
|
||||||
|
|
||||||
--clr-secondary: #EAEAEA;
|
--clr-secondary: #E1EAF8;
|
||||||
--clr-secondary-hover: #EFEFEF;
|
--clr-secondary-hover: #E8ECF0;
|
||||||
--clr-secondary-disabled: #E1E1E1;
|
|
||||||
--clr-on-secondary: #0D0D0D;
|
--clr-on-secondary: #0D0D0D;
|
||||||
|
|
||||||
--clr-layer-100: #EBEEF0;
|
--clr-layer-100: #EBEEF0;
|
||||||
@ -20,6 +18,7 @@
|
|||||||
--clr-text-100: #8D8D8D;
|
--clr-text-100: #8D8D8D;
|
||||||
--clr-text-200: #6C7480;
|
--clr-text-200: #6C7480;
|
||||||
--clr-text-300: #1D1F20;
|
--clr-text-300: #1D1F20;
|
||||||
|
--clr-text-primary: #3865DA;
|
||||||
|
|
||||||
--clr-border-100: #DFDFDF;
|
--clr-border-100: #DFDFDF;
|
||||||
--clr-border-200: #D8D8D8;
|
--clr-border-200: #D8D8D8;
|
||||||
@ -28,6 +27,8 @@
|
|||||||
--clr-shadow-200: #00000026;
|
--clr-shadow-200: #00000026;
|
||||||
|
|
||||||
--clr-ripple: #1D1F2026;
|
--clr-ripple: #1D1F2026;
|
||||||
|
|
||||||
|
--clr-error: #E54B4B;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
@ -36,7 +37,7 @@
|
|||||||
--clr-primary: #3865DA;
|
--clr-primary: #3865DA;
|
||||||
--clr-primary-o50: #3865DA80;
|
--clr-primary-o50: #3865DA80;
|
||||||
--clr-primary-hover: #4073F7;
|
--clr-primary-hover: #4073F7;
|
||||||
--clr-primary-disabled: #2A4DA7;
|
--clr-primary-disabled: #334570;
|
||||||
--clr-on-primary: #FFFFFF;
|
--clr-on-primary: #FFFFFF;
|
||||||
|
|
||||||
--clr-secondary: #3F3F3F;
|
--clr-secondary: #3F3F3F;
|
||||||
@ -52,6 +53,7 @@
|
|||||||
--clr-text-100: #888888;
|
--clr-text-100: #888888;
|
||||||
--clr-text-200: #C5C5C5;
|
--clr-text-200: #C5C5C5;
|
||||||
--clr-text-300: #F0F0F0;
|
--clr-text-300: #F0F0F0;
|
||||||
|
--clr-text-primary: #4176FF;
|
||||||
|
|
||||||
--clr-border-100: #3D3D3D;
|
--clr-border-100: #3D3D3D;
|
||||||
--clr-border-200: #545454;
|
--clr-border-200: #545454;
|
||||||
@ -60,4 +62,6 @@
|
|||||||
--clr-shadow-200: #00000026;
|
--clr-shadow-200: #00000026;
|
||||||
|
|
||||||
--clr-ripple: #F0F0F026;
|
--clr-ripple: #F0F0F026;
|
||||||
|
|
||||||
|
--clr-error: #FF6363;
|
||||||
}
|
}
|
||||||
|
@ -2,30 +2,34 @@ import './styles.scss';
|
|||||||
import '@public/fonts/styles.css';
|
import '@public/fonts/styles.css';
|
||||||
|
|
||||||
import { MainLayout } from '@components/layouts';
|
import { MainLayout } from '@components/layouts';
|
||||||
import { ParksPage, TurbineTypesPage } from '@components/pages';
|
import {
|
||||||
|
ParkPage,
|
||||||
|
ParksPage,
|
||||||
|
TurbineTypePage,
|
||||||
|
TurbineTypesPage,
|
||||||
|
} from '@components/pages';
|
||||||
import { ROUTES } from '@utils/route';
|
import { ROUTES } from '@utils/route';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
<Route
|
|
||||||
index
|
|
||||||
element={<Navigate to={ROUTES.turbineTypes.path} replace />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path={ROUTES.turbineTypes.path}
|
path={ROUTES.turbineTypes.path}
|
||||||
element={<TurbineTypesPage />}
|
element={<TurbineTypesPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route path={ROUTES.turbineType.path} element={<TurbineTypePage />} />
|
||||||
<Route path={ROUTES.parks.path} element={<ParksPage />} />
|
<Route path={ROUTES.parks.path} element={<ParksPage />} />
|
||||||
|
<Route path={ROUTES.park.path} element={<ParkPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={<Navigate to={ROUTES.turbineTypes.path} replace />}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
|
1
front/src/components/app/index.ts
Normal file
1
front/src/components/app/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { App } from './component';
|
@ -23,7 +23,7 @@ html[data-theme='default'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
--td-100: 0.2s;
|
--td-100: 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
1
front/src/components/layouts/index.ts
Normal file
1
front/src/components/layouts/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { MainLayout } from './main-layout';
|
@ -1,3 +0,0 @@
|
|||||||
import MainLayout from './main-layout';
|
|
||||||
|
|
||||||
export { MainLayout };
|
|
22
front/src/components/layouts/main-layout/component.tsx
Normal file
22
front/src/components/layouts/main-layout/component.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Header, Sidebar } from '@components/ux';
|
||||||
|
import { useDeviceType } from '@utils/device';
|
||||||
|
import React from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
export function MainLayout() {
|
||||||
|
const deviceType = useDeviceType();
|
||||||
|
return (
|
||||||
|
<div className={styles.mainLayout}>
|
||||||
|
{deviceType === 'mobile' ? (
|
||||||
|
<Header className={styles.header} />
|
||||||
|
) : (
|
||||||
|
<Sidebar className={styles.sidebar} />
|
||||||
|
)}
|
||||||
|
<main className={styles.main}>
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,20 +1 @@
|
|||||||
import { Sidebar } from '@components/ux';
|
export * from './component';
|
||||||
import React from 'react';
|
|
||||||
import { Outlet } from 'react-router-dom';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
function MainLayout() {
|
|
||||||
return (
|
|
||||||
<div className={styles.mainLayout}>
|
|
||||||
<Sidebar />
|
|
||||||
<main className={styles.main}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MainLayout;
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@use '@components/mixins.scss' as m;
|
||||||
|
|
||||||
.mainLayout {
|
.mainLayout {
|
||||||
display: grid;
|
display: grid;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -6,16 +8,31 @@
|
|||||||
/ auto minmax(0, 1fr);
|
/ auto minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.sidebar {
|
||||||
display: flex;
|
grid-area: sidebar;
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.header {
|
||||||
display: grid;
|
grid-area: header;
|
||||||
width: 1000px;
|
}
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
grid-template-rows: minmax(0, 1fr);
|
.main {
|
||||||
|
display: grid;
|
||||||
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
grid-area: main;
|
||||||
|
grid-template-columns: 1fr minmax(0, 1000px) 1fr;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include m.on-mobile {
|
||||||
|
.mainLayout {
|
||||||
|
grid-template:
|
||||||
|
'header' auto
|
||||||
|
'main' minmax(0, 1fr)
|
||||||
|
/ minmax(0, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
|
export * from './park-page';
|
||||||
export * from './parks-page';
|
export * from './parks-page';
|
||||||
|
export * from './turbine-type-page';
|
||||||
export * from './turbine-types-page';
|
export * from './turbine-types-page';
|
||||||
|
83
front/src/components/pages/park-page/component.tsx
Normal file
83
front/src/components/pages/park-page/component.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { getParkWithTurbines, ParkWithTurbines } from '@api/wind';
|
||||||
|
import { Button, Heading, NumberInput, TextInput } from '@components/ui';
|
||||||
|
import { Controller, useForm } from '@utils/form';
|
||||||
|
import { useRoute } from '@utils/route';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { ParkTurbines } from './components';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { ParkFormValues } from './types';
|
||||||
|
import { unpackPark } from './utils';
|
||||||
|
|
||||||
|
export function ParkPage() {
|
||||||
|
const [park, setPark] = useState<ParkWithTurbines>(null);
|
||||||
|
const [pending, setPending] = useState<boolean>(false);
|
||||||
|
const params = useParams();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const { register, control, getValues, reset } = useForm<ParkFormValues>({});
|
||||||
|
|
||||||
|
const { id } = params;
|
||||||
|
const isEdit = id !== 'new';
|
||||||
|
const heading = isEdit ? 'Edit' : 'Create new';
|
||||||
|
|
||||||
|
const fetchPark = async () => {
|
||||||
|
const response = await getParkWithTurbines(id);
|
||||||
|
setPark(response.data);
|
||||||
|
reset(unpackPark(response.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEdit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchPark();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
if (isEdit) {
|
||||||
|
reset(unpackPark(park));
|
||||||
|
} else {
|
||||||
|
reset({});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<Heading tag="h1">{route.title}</Heading>
|
||||||
|
<form className={styles.form}>
|
||||||
|
<header>
|
||||||
|
<Heading tag="h3">{heading}</Heading>
|
||||||
|
</header>
|
||||||
|
<TextInput {...register('name')} label={{ text: 'Name' }} />
|
||||||
|
<div className={styles.inputBox}>
|
||||||
|
<Controller
|
||||||
|
{...control('centerLatitude')}
|
||||||
|
render={(props) => (
|
||||||
|
<NumberInput label={{ text: 'Center Latitude' }} {...props} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
{...control('centerLongitude')}
|
||||||
|
render={(props) => (
|
||||||
|
<NumberInput label={{ text: 'Center Longitude' }} {...props} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttonBox}>
|
||||||
|
<Button variant="secondary" onClick={handleReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" pending={pending}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<Controller
|
||||||
|
{...control('turbines')}
|
||||||
|
render={(props) => <ParkTurbines {...props} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
front/src/components/pages/park-page/components/index.ts
Normal file
1
front/src/components/pages/park-page/components/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './park-turbines';
|
@ -0,0 +1,19 @@
|
|||||||
|
import { DataGrid } from '@components/ui/data-grid';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { columns } from './constants';
|
||||||
|
import { ParkTurbinesProps } from './types';
|
||||||
|
|
||||||
|
export function ParkTurbines({ value = [] }: ParkTurbinesProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div></div>
|
||||||
|
<DataGrid
|
||||||
|
items={value}
|
||||||
|
columns={columns}
|
||||||
|
getItemKey={({ id }) => String(id)}
|
||||||
|
selectedItems={[]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { DataGridColumnConfig } from '@components/ui/data-grid/types';
|
||||||
|
import { ParkTurbine } from 'src/api/wind';
|
||||||
|
|
||||||
|
export const columns: DataGridColumnConfig<ParkTurbine>[] = [
|
||||||
|
{ name: 'Id', getText: (t) => String(t.id) },
|
||||||
|
{ name: 'Name', getText: (t) => t.name },
|
||||||
|
{ name: 'X', getText: (t) => String(t.xOffset) },
|
||||||
|
{ name: 'Y', getText: (t) => String(t.yOffset) },
|
||||||
|
{ name: 'Angle', getText: (t) => String(t.angle) },
|
||||||
|
{ name: 'Comment', getText: (t) => String(t.comment), flex: '2' },
|
||||||
|
];
|
@ -0,0 +1,6 @@
|
|||||||
|
import { ParkTurbine } from '@api/wind';
|
||||||
|
|
||||||
|
export type ParkTurbinesProps = {
|
||||||
|
value?: ParkTurbine[];
|
||||||
|
onChange?: (value: ParkTurbine[]) => void;
|
||||||
|
};
|
28
front/src/components/pages/park-page/styles.module.scss
Normal file
28
front/src/components/pages/park-page/styles.module.scss
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.page {
|
||||||
|
display: grid;
|
||||||
|
padding: 40px 20px;
|
||||||
|
gap: 20px;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: grid;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: var(--clr-layer-200);
|
||||||
|
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBox {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonBox {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
padding-top: 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
8
front/src/components/pages/park-page/types.ts
Normal file
8
front/src/components/pages/park-page/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { ParkTurbine } from '@api/wind';
|
||||||
|
|
||||||
|
export type ParkFormValues = {
|
||||||
|
name: string;
|
||||||
|
centerLatitude: string;
|
||||||
|
centerLongitude: string;
|
||||||
|
turbines: ParkTurbine[];
|
||||||
|
};
|
@ -1,11 +1,12 @@
|
|||||||
import { Park } from 'src/api/wind';
|
import { ParkWithTurbines } from '@api/wind';
|
||||||
|
|
||||||
import { ParkFormValues } from './types';
|
import { ParkFormValues } from './types';
|
||||||
|
|
||||||
export const parkToFormValues = (park: Park): ParkFormValues => {
|
export const unpackPark = (park: ParkWithTurbines): ParkFormValues => {
|
||||||
return {
|
return {
|
||||||
name: park.name,
|
name: park.name,
|
||||||
centerLatitude: String(park.centerLatitude),
|
centerLatitude: String(park.centerLatitude),
|
||||||
centerLongitude: String(park.centerLongitude),
|
centerLongitude: String(park.centerLongitude),
|
||||||
|
turbines: park.turbines,
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -1,58 +1,52 @@
|
|||||||
|
import { getParks, Park } from '@api/wind';
|
||||||
import { Button, Heading } from '@components/ui';
|
import { Button, Heading } from '@components/ui';
|
||||||
import { DataGrid } from '@components/ui/data-grid';
|
import { DataGrid } from '@components/ui/data-grid';
|
||||||
import { ParkModal } from '@components/ux';
|
import { ROUTES, useRoute } from '@utils/route';
|
||||||
import { useRoute } from '@utils/route';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { getParks, Park } from 'src/api/wind';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { columns } from './constants';
|
import { columns } from './constants';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
export function ParksPage() {
|
export function ParksPage() {
|
||||||
const [createModalOpen, setCreateModalOpen] = useState<boolean>(false);
|
|
||||||
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
|
|
||||||
const [parks, setParks] = useState<Park[]>([]);
|
const [parks, setParks] = useState<Park[]>([]);
|
||||||
const [selected, setSelected] = useState<Park>(null);
|
const [selected, setSelected] = useState<Park>(null);
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const fetchParks = async () => {
|
const fetchParks = async () => {
|
||||||
const res = await getParks();
|
const res = await getParks();
|
||||||
setParks(res.data ?? []);
|
setParks(res.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchParks();
|
fetchParks();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateButtonClick = () => {
|
|
||||||
setCreateModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditButtonClick = () => {
|
|
||||||
setEditModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleParkSelect = (items: Park[]) => {
|
const handleParkSelect = (items: Park[]) => {
|
||||||
setSelected(items[0] ?? null);
|
setSelected(items[0] ?? null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteButtonClick = async () => {
|
||||||
|
//
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<Heading tag="h1" className={styles.heading}>
|
<Heading tag="h1">{route.title}</Heading>
|
||||||
{route.title}
|
|
||||||
</Heading>
|
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Button onClick={handleCreateButtonClick}>Create new</Button>
|
<Link to={ROUTES.park.path.replace(':id', 'new')}>
|
||||||
<Button
|
<Button>Create new</Button>
|
||||||
variant="secondary"
|
</Link>
|
||||||
onClick={handleEditButtonClick}
|
{selected && (
|
||||||
disabled={!selected}
|
<Link to={ROUTES.park.path.replace(':id', String(selected.id))}>
|
||||||
>
|
<Button variant="secondary">Edit</Button>
|
||||||
Edit
|
</Link>
|
||||||
</Button>
|
)}
|
||||||
<Button variant="secondary" disabled={!selected}>
|
{selected && (
|
||||||
Delete
|
<Button variant="secondary" onClick={handleDeleteButtonClick}>
|
||||||
</Button>
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.dataGridWrapper}>
|
<div className={styles.dataGridWrapper}>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
@ -64,18 +58,6 @@ export function ParksPage() {
|
|||||||
multiselect={false}
|
multiselect={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ParkModal
|
|
||||||
park={null}
|
|
||||||
open={createModalOpen}
|
|
||||||
onClose={() => setCreateModalOpen(false)}
|
|
||||||
onSuccess={fetchParks}
|
|
||||||
/>
|
|
||||||
<ParkModal
|
|
||||||
park={selected}
|
|
||||||
open={editModalOpen}
|
|
||||||
onClose={() => setEditModalOpen(false)}
|
|
||||||
onSuccess={fetchParks}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@ import { Park } from 'src/api/wind';
|
|||||||
|
|
||||||
export const columns: DataGridColumnConfig<Park>[] = [
|
export const columns: DataGridColumnConfig<Park>[] = [
|
||||||
{ name: 'Name', getText: (t) => t.name, flex: '2' },
|
{ name: 'Name', getText: (t) => t.name, flex: '2' },
|
||||||
{ name: 'Center latitude', getText: (t) => String(t.centerLatitude) },
|
{ name: 'Center Latitude', getText: (t) => String(t.centerLatitude) },
|
||||||
{ name: 'Center longitude', getText: (t) => String(t.centerLongitude) },
|
{ name: 'Center Longitude', getText: (t) => String(t.centerLongitude) },
|
||||||
];
|
];
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.page {
|
.page {
|
||||||
display: grid;
|
display: grid;
|
||||||
padding: 40px 20px 20px;
|
padding: 40px 20px;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
grid-template-rows: auto auto minmax(0, 1fr);
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid var(--clr-border-100);
|
border: 1px solid var(--clr-border-100);
|
||||||
border-radius: 10px;
|
border-radius: 15px;
|
||||||
background-color: var(--clr-layer-200);
|
background-color: var(--clr-layer-200);
|
||||||
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
100
front/src/components/pages/turbine-type-page/component.tsx
Normal file
100
front/src/components/pages/turbine-type-page/component.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import {
|
||||||
|
createTurbineTypes,
|
||||||
|
editTurbineTypes,
|
||||||
|
getTurbineType,
|
||||||
|
TurbineType,
|
||||||
|
} from '@api/wind';
|
||||||
|
import { Button, Heading, NumberInput, TextInput } from '@components/ui';
|
||||||
|
import { Controller, useForm } from '@utils/form';
|
||||||
|
import { ROUTES, useRoute } from '@utils/route';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { TurbineTypeFormValues } from './types';
|
||||||
|
import { unpackTurbineType } from './utils';
|
||||||
|
|
||||||
|
export function TurbineTypePage() {
|
||||||
|
const [turbineType, setTurbineType] = useState<TurbineType>(null);
|
||||||
|
const [pending, setPending] = useState<boolean>(false);
|
||||||
|
const params = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const { register, control, getValues, reset } =
|
||||||
|
useForm<TurbineTypeFormValues>({});
|
||||||
|
|
||||||
|
const { id } = params;
|
||||||
|
const isEdit = id !== 'new';
|
||||||
|
const heading = isEdit ? 'Edit' : 'Create new';
|
||||||
|
|
||||||
|
const fetchTurbineType = async () => {
|
||||||
|
const response = await getTurbineType(id);
|
||||||
|
setTurbineType(response.data);
|
||||||
|
reset(unpackTurbineType(response.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEdit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchTurbineType();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setPending(true);
|
||||||
|
if (isEdit) {
|
||||||
|
const response = await editTurbineTypes(getValues(), id);
|
||||||
|
setTurbineType(response.data);
|
||||||
|
} else {
|
||||||
|
const response = await createTurbineTypes(getValues());
|
||||||
|
navigate(
|
||||||
|
ROUTES.turbineType.path.replace(':id', String(response.data.id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setPending(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
if (isEdit) {
|
||||||
|
reset(unpackTurbineType(turbineType));
|
||||||
|
} else {
|
||||||
|
reset({});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.page} onSubmit={handleSubmit}>
|
||||||
|
<Heading tag="h1">{route.title}</Heading>
|
||||||
|
<form className={styles.form}>
|
||||||
|
<header>
|
||||||
|
<Heading tag="h3">{heading}</Heading>
|
||||||
|
</header>
|
||||||
|
<TextInput {...register('name')} label={{ text: 'Name' }} />
|
||||||
|
<div className={styles.inputBox}>
|
||||||
|
<Controller
|
||||||
|
{...control('height')}
|
||||||
|
render={(props) => (
|
||||||
|
<NumberInput label={{ text: 'Height' }} {...props} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
{...control('bladeLength')}
|
||||||
|
render={(props) => (
|
||||||
|
<NumberInput label={{ text: 'Blade length' }} {...props} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttonBox}>
|
||||||
|
<Button variant="secondary" onClick={handleReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" pending={pending}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
front/src/components/pages/turbine-type-page/index.ts
Normal file
1
front/src/components/pages/turbine-type-page/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './component';
|
@ -0,0 +1,28 @@
|
|||||||
|
.page {
|
||||||
|
display: grid;
|
||||||
|
padding: 40px 20px;
|
||||||
|
gap: 20px;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: grid;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: var(--clr-layer-200);
|
||||||
|
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBox {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonBox {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
padding-top: 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
5
front/src/components/pages/turbine-type-page/types.ts
Normal file
5
front/src/components/pages/turbine-type-page/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type TurbineTypeFormValues = {
|
||||||
|
name: string;
|
||||||
|
height: string;
|
||||||
|
bladeLength: string;
|
||||||
|
};
|
@ -1,8 +1,8 @@
|
|||||||
import { TurbineType } from 'src/api/wind';
|
import { TurbineType } from '@api/wind';
|
||||||
|
|
||||||
import { TurbineTypeFormValues } from './types';
|
import { TurbineTypeFormValues } from './types';
|
||||||
|
|
||||||
export const turbineTypeToFormValues = (
|
export const unpackTurbineType = (
|
||||||
turbineType: TurbineType,
|
turbineType: TurbineType,
|
||||||
): TurbineTypeFormValues => {
|
): TurbineTypeFormValues => {
|
||||||
return {
|
return {
|
@ -1,16 +1,14 @@
|
|||||||
|
import { deleteTurbineType, getTurbineTypes, TurbineType } from '@api/wind';
|
||||||
import { Button, Heading } from '@components/ui';
|
import { Button, Heading } from '@components/ui';
|
||||||
import { DataGrid } from '@components/ui/data-grid';
|
import { DataGrid } from '@components/ui/data-grid';
|
||||||
import { TurbineTypeModal } from '@components/ux';
|
import { ROUTES, useRoute } from '@utils/route';
|
||||||
import { useRoute } from '@utils/route';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { deleteTurbineTypes, getTurbineTypes, TurbineType } from 'src/api/wind';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { columns } from './constants';
|
import { columns } from './constants';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
export function TurbineTypesPage() {
|
export function TurbineTypesPage() {
|
||||||
const [createModalOpen, setCreateModalOpen] = useState<boolean>(false);
|
|
||||||
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
|
|
||||||
const [turbineTypes, setTurbineTypes] = useState<TurbineType[]>([]);
|
const [turbineTypes, setTurbineTypes] = useState<TurbineType[]>([]);
|
||||||
const [selected, setSelected] = useState<TurbineType>(null);
|
const [selected, setSelected] = useState<TurbineType>(null);
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@ -24,44 +22,34 @@ export function TurbineTypesPage() {
|
|||||||
fetchTurbineTypes();
|
fetchTurbineTypes();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateButtonClick = () => {
|
|
||||||
setCreateModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditButtonClick = () => {
|
|
||||||
setEditModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteButtonClick = async () => {
|
|
||||||
await deleteTurbineTypes(selected.id);
|
|
||||||
fetchTurbineTypes();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTurbineTypeSelect = (items: TurbineType[]) => {
|
const handleTurbineTypeSelect = (items: TurbineType[]) => {
|
||||||
setSelected(items[0] ?? null);
|
setSelected(items[0] ?? null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteButtonClick = async () => {
|
||||||
|
await deleteTurbineType(selected.id);
|
||||||
|
fetchTurbineTypes();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<Heading tag="h1" className={styles.heading}>
|
<Heading tag="h1">{route.title}</Heading>
|
||||||
{route.title}
|
|
||||||
</Heading>
|
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Button onClick={handleCreateButtonClick}>Create new</Button>
|
<Link to={ROUTES.turbineType.path.replace(':id', 'new')}>
|
||||||
<Button
|
<Button>Create new</Button>
|
||||||
variant="secondary"
|
</Link>
|
||||||
onClick={handleEditButtonClick}
|
{selected && (
|
||||||
disabled={!selected}
|
<Link
|
||||||
>
|
to={ROUTES.turbineType.path.replace(':id', String(selected.id))}
|
||||||
Edit
|
>
|
||||||
</Button>
|
<Button variant="secondary">Edit</Button>
|
||||||
<Button
|
</Link>
|
||||||
variant="secondary"
|
)}
|
||||||
onClick={handleDeleteButtonClick}
|
{selected && (
|
||||||
disabled={!selected}
|
<Button variant="secondary" onClick={handleDeleteButtonClick}>
|
||||||
>
|
Delete
|
||||||
Delete
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.dataGridWrapper}>
|
<div className={styles.dataGridWrapper}>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
@ -73,18 +61,6 @@ export function TurbineTypesPage() {
|
|||||||
multiselect={false}
|
multiselect={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TurbineTypeModal
|
|
||||||
turbineType={null}
|
|
||||||
open={createModalOpen}
|
|
||||||
onClose={() => setCreateModalOpen(false)}
|
|
||||||
onSuccess={fetchTurbineTypes}
|
|
||||||
/>
|
|
||||||
<TurbineTypeModal
|
|
||||||
turbineType={selected}
|
|
||||||
open={editModalOpen}
|
|
||||||
onClose={() => setEditModalOpen(false)}
|
|
||||||
onSuccess={fetchTurbineTypes}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.page {
|
.page {
|
||||||
display: grid;
|
display: grid;
|
||||||
padding: 40px 20px 20px;
|
padding: 40px 20px;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
grid-template-rows: auto auto minmax(0, 1fr);
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid var(--clr-border-100);
|
border: 1px solid var(--clr-border-100);
|
||||||
border-radius: 10px;
|
border-radius: 15px;
|
||||||
background-color: var(--clr-layer-200);
|
background-color: var(--clr-layer-200);
|
||||||
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { ForwardedRef, forwardRef, useEffect, useState } from 'react';
|
import React, {
|
||||||
|
CSSProperties,
|
||||||
|
ForwardedRef,
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { FadeProps } from './types';
|
import { FadeProps } from './types';
|
||||||
@ -14,7 +20,7 @@ export function FadeInner(
|
|||||||
}: Omit<FadeProps, 'ref'>,
|
}: Omit<FadeProps, 'ref'>,
|
||||||
ref: ForwardedRef<HTMLDivElement>,
|
ref: ForwardedRef<HTMLDivElement>,
|
||||||
) {
|
) {
|
||||||
const [visibleInner, setVisibleInner] = useState<boolean>(visible);
|
const [visibleInternal, setVisibleInternal] = useState<boolean>(visible);
|
||||||
|
|
||||||
const classNames = clsx(
|
const classNames = clsx(
|
||||||
styles.fade,
|
styles.fade,
|
||||||
@ -25,22 +31,21 @@ export function FadeInner(
|
|||||||
const inlineStyle = {
|
const inlineStyle = {
|
||||||
...style,
|
...style,
|
||||||
'--animation-duration': `${duration}ms`,
|
'--animation-duration': `${duration}ms`,
|
||||||
} as React.CSSProperties;
|
} as CSSProperties;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
setVisibleInner(true);
|
setVisibleInternal(true);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
const handleAnimationEnd = (event: React.AnimationEvent) => {
|
const handleAnimationEnd = (event: React.AnimationEvent) => {
|
||||||
if (event.animationName === styles.fadeout) {
|
if (event.animationName === styles.fadeout) {
|
||||||
setVisibleInner(false);
|
setVisibleInternal(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!visibleInner) {
|
if (!visibleInternal) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.fade {
|
.fade {
|
||||||
animation: fadein var(--animation-duration);
|
animation: fadein var(--animation-duration) ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invisible {
|
.invisible {
|
||||||
@ -9,17 +9,21 @@
|
|||||||
@keyframes fadein {
|
@keyframes fadein {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
transform: scale(0.9) translateY(-30px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeout {
|
@keyframes fadeout {
|
||||||
from {
|
from {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
transform: scale(0.9) translateY(-30px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
export type FadeProps = {
|
export type FadeProps = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
} & React.ComponentProps<'div'>;
|
} & ComponentProps<'div'>;
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export * from './fade';
|
export * from './fade';
|
||||||
export * from './ripple';
|
export * from './ripple';
|
||||||
export * from './slide';
|
|
||||||
|
@ -50,6 +50,7 @@ export function Ripple() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerDown = (event: React.MouseEvent) => {
|
const handlePointerDown = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
const { pageX, pageY } = event;
|
const { pageX, pageY } = event;
|
||||||
addWave(pageX, pageY);
|
addWave(pageX, pageY);
|
||||||
};
|
};
|
||||||
|
@ -10,14 +10,15 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
background-color: var(--clr-ripple);
|
background-color: var(--clr-ripple);
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
animation: fadein 0.3s linear;
|
animation: fadein 0.25s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invisible {
|
.invisible {
|
||||||
animation: fadeout 0.3s linear forwards;
|
animation: fadeout 0.25s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadein {
|
@keyframes fadein {
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
import React, { ForwardedRef, forwardRef, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import { SlideProps } from './types';
|
|
||||||
|
|
||||||
export function SlideInner(
|
|
||||||
{
|
|
||||||
visible,
|
|
||||||
duration = 200,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
...props
|
|
||||||
}: Omit<SlideProps, 'ref'>,
|
|
||||||
ref: ForwardedRef<HTMLDivElement>,
|
|
||||||
) {
|
|
||||||
const [visibleInner, setVisibleInner] = useState<boolean>(visible);
|
|
||||||
|
|
||||||
const classNames = clsx(
|
|
||||||
styles.slide,
|
|
||||||
{ [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 Slide = forwardRef(SlideInner);
|
|
@ -1,29 +0,0 @@
|
|||||||
.slide {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export type SlideProps = {
|
|
||||||
visible: boolean;
|
|
||||||
duration?: number;
|
|
||||||
} & React.ComponentProps<'div'>;
|
|
@ -26,8 +26,6 @@ function AutocompleteInner<T>(
|
|||||||
label = {},
|
label = {},
|
||||||
name,
|
name,
|
||||||
id,
|
id,
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: Omit<AutocompleteProps<T>, 'ref'>,
|
}: Omit<AutocompleteProps<T>, 'ref'>,
|
||||||
ref: ForwardedRef<HTMLDivElement>,
|
ref: ForwardedRef<HTMLDivElement>,
|
||||||
) {
|
) {
|
||||||
@ -39,18 +37,15 @@ function AutocompleteInner<T>(
|
|||||||
|
|
||||||
useImperativeHandle(ref, () => autocompleteRef.current, []);
|
useImperativeHandle(ref, () => autocompleteRef.current, []);
|
||||||
|
|
||||||
useMissClick(
|
useMissClick({
|
||||||
[autocompleteRef, menuRef],
|
callback: () => setMenuVisible(false),
|
||||||
() => setMenuVisible(false),
|
enabled: menuVisible,
|
||||||
menuVisible,
|
whitelist: [autocompleteRef, menuRef],
|
||||||
);
|
});
|
||||||
|
|
||||||
const autocompleteClassName = clsx(
|
const autocompleteClassName = clsx(styles.autocomplete, styles[scale], {
|
||||||
styles.autocomplete,
|
[styles.menuVisible]: menuVisible,
|
||||||
styles[scale],
|
});
|
||||||
{ [styles.menuVisible]: menuVisible },
|
|
||||||
className,
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredOptions = options.filter((option) => {
|
const filteredOptions = options.filter((option) => {
|
||||||
const label = getOptionLabel(option).toLocaleLowerCase();
|
const label = getOptionLabel(option).toLocaleLowerCase();
|
||||||
@ -80,7 +75,7 @@ function AutocompleteInner<T>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={autocompleteClassName} ref={autocompleteRef} {...props}>
|
<div className={autocompleteClassName} ref={autocompleteRef}>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={value ? getOptionLabel(value) : text}
|
value={value ? getOptionLabel(value) : text}
|
||||||
onClick={handleInputClick}
|
onClick={handleInputClick}
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
|
import { ComponentProps, Key } from 'react';
|
||||||
|
|
||||||
import { LabelProps } from '../label';
|
import { LabelProps } from '../label';
|
||||||
import { Scale } from '../types';
|
import { Scale } from '../types';
|
||||||
|
|
||||||
export type AutocompleteProps<T> = {
|
export type AutocompleteProps<T> = {
|
||||||
options: T[];
|
options: T[];
|
||||||
value?: T;
|
value?: T;
|
||||||
getOptionKey: (option: T) => React.Key;
|
getOptionKey: (option: T) => Key;
|
||||||
getOptionLabel: (option: T) => string;
|
getOptionLabel: (option: T) => string;
|
||||||
onChange?: (option: T) => void;
|
onChange?: (option: T) => void;
|
||||||
scale?: Scale;
|
scale?: Scale;
|
||||||
label?: LabelProps;
|
label?: LabelProps;
|
||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
} & Omit<React.ComponentProps<'div'>, 'onChange'>;
|
} & Omit<ComponentProps<'div'>, 'onChange'>;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React, { ForwardedRef, forwardRef } from 'react';
|
||||||
|
|
||||||
import { Ripple } from '../animation/ripple/component';
|
import { Ripple } from '../animation/ripple/component';
|
||||||
import { Comet } from '../comet';
|
import { Comet } from '../comet';
|
||||||
@ -8,16 +8,18 @@ import { COMET_VARIANT_MAP } from './constants';
|
|||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { ButtonProps } from './types.js';
|
import { ButtonProps } from './types.js';
|
||||||
|
|
||||||
export function Button({
|
function ButtonInner(
|
||||||
variant = 'primary',
|
{
|
||||||
scale = 'm',
|
variant = 'primary',
|
||||||
pending = false,
|
scale = 'm',
|
||||||
className,
|
pending = false,
|
||||||
children,
|
className,
|
||||||
disabled,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps,
|
||||||
const classNames = clsx(
|
ref: ForwardedRef<HTMLButtonElement>,
|
||||||
|
) {
|
||||||
|
const buttonClassName = clsx(
|
||||||
styles.button,
|
styles.button,
|
||||||
styles[variant],
|
styles[variant],
|
||||||
styles[scale],
|
styles[scale],
|
||||||
@ -25,11 +27,7 @@ export function Button({
|
|||||||
className,
|
className,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<RawButton
|
<RawButton className={buttonClassName} ref={ref} {...props}>
|
||||||
className={classNames}
|
|
||||||
disabled={pending ? true : disabled}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{pending && (
|
{pending && (
|
||||||
<div className={styles.cometWrapper}>
|
<div className={styles.cometWrapper}>
|
||||||
<Comet scale={scale} variant={COMET_VARIANT_MAP[variant]} />
|
<Comet scale={scale} variant={COMET_VARIANT_MAP[variant]} />
|
||||||
@ -40,3 +38,5 @@ export function Button({
|
|||||||
</RawButton>
|
</RawButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Button = forwardRef(ButtonInner);
|
||||||
|
@ -3,14 +3,9 @@
|
|||||||
.button {
|
.button {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0px 2px 2px var(--clr-shadow-200);
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all var(--td-100) ease-in-out;
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:disabled) {
|
&:not(:disabled) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -28,6 +23,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pending {
|
.pending {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
.childrenWrapper {
|
.childrenWrapper {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
@ -37,27 +34,21 @@
|
|||||||
background-color: var(--clr-primary);
|
background-color: var(--clr-primary);
|
||||||
color: var(--clr-on-primary);
|
color: var(--clr-on-primary);
|
||||||
|
|
||||||
&:hover {
|
@media (hover: hover) {
|
||||||
background-color: var(--clr-primary-hover);
|
&:hover {
|
||||||
}
|
background-color: var(--clr-primary-hover);
|
||||||
|
}
|
||||||
&:disabled,
|
|
||||||
&.pending {
|
|
||||||
background-color: var(--clr-primary-disabled);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
background-color: var(--clr-secondary);
|
background-color: var(--clr-secondary);
|
||||||
color: var(--clr-on-secondary);
|
color: var(--clr-on-secondary);
|
||||||
|
|
||||||
&:hover {
|
@media (hover: hover) {
|
||||||
background-color: var(--clr-secondary-hover);
|
&:hover {
|
||||||
}
|
background-color: var(--clr-secondary-hover);
|
||||||
|
}
|
||||||
&:disabled,
|
|
||||||
&.pending {
|
|
||||||
background-color: var(--clr-secondary-disabled);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
export type CalendarProps = {
|
export type CalendarProps = {
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
min: Date | null;
|
min: Date | null;
|
||||||
max: Date | null;
|
max: Date | null;
|
||||||
} & Omit<React.ComponentProps<'div'>, 'onChange'>;
|
} & Omit<ComponentProps<'div'>, 'onChange'>;
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
.checkbox {
|
.checkbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 2px solid var(--clr-border-200);
|
border: 1px solid var(--clr-border-200);
|
||||||
background-color: var(--clr-layer-300);
|
background-color: var(--clr-layer-300);
|
||||||
box-shadow: 0px 2px 2px var(--clr-shadow-200);
|
box-shadow: 0px 2px 2px var(--clr-shadow-200);
|
||||||
transition: all var(--td-100) ease-in-out;
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
import { Scale } from '../types';
|
import { Scale } from '../types';
|
||||||
|
|
||||||
export type CometProps = {
|
export type CometProps = {
|
||||||
scale?: Scale;
|
scale?: Scale;
|
||||||
variant?: 'onPrimary' | 'onSecondary';
|
variant?: 'onPrimary' | 'onSecondary';
|
||||||
} & React.ComponentProps<'div'>;
|
} & ComponentProps<'div'>;
|
||||||
|
@ -1,71 +1,67 @@
|
|||||||
import { arrayToObject } from '@utils/array';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { DataGridHeader, DataGridRow } from './components';
|
import { DataGridHeader, DataGridRow } from './components';
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import { DataGridProps } from './types';
|
import { DataGridProps } from './types';
|
||||||
|
|
||||||
export function DataGrid<T>({
|
export function DataGrid<T>({
|
||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
getItemKey,
|
getItemKey,
|
||||||
className,
|
|
||||||
selectedItems,
|
selectedItems,
|
||||||
onItemsSelect,
|
onItemsSelect,
|
||||||
multiselect = true,
|
multiselect,
|
||||||
|
className,
|
||||||
...props
|
...props
|
||||||
}: DataGridProps<T>) {
|
}: DataGridProps<T>) {
|
||||||
const [allRowsSelected, setAllRowsSelected] = useState<boolean>(false);
|
const [allItemsSelected, setAllItemsSelected] = useState<boolean>(false);
|
||||||
|
|
||||||
const selectedItemsMap = useMemo(
|
const selectedItemsMap = useMemo(() => {
|
||||||
() => arrayToObject(selectedItems, (i) => getItemKey(i)),
|
const map: Record<string, T> = {};
|
||||||
[selectedItems],
|
for (let i = 0; i < selectedItems.length; i += 1) {
|
||||||
);
|
const item = selectedItems[i];
|
||||||
|
map[String(getItemKey(item))] = item;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [selectedItems]);
|
||||||
|
|
||||||
const handleSelectAllRows = () => {
|
const handleSelectAllItems = () => {
|
||||||
if (!multiselect) {
|
if (!multiselect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAllRowsSelected(!allRowsSelected);
|
onItemsSelect?.(allItemsSelected ? [] : [...items]);
|
||||||
if (allRowsSelected) {
|
setAllItemsSelected(!allItemsSelected);
|
||||||
onItemsSelect([]);
|
|
||||||
} else {
|
|
||||||
onItemsSelect([...items]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRowSelect = (item: T) => {
|
const handleItemSelect = (key: string, item: T) => {
|
||||||
setAllRowsSelected(false);
|
const selected = Boolean(selectedItemsMap[key]);
|
||||||
const key = getItemKey(item);
|
|
||||||
const selected = selectedItemsMap[key];
|
|
||||||
if (!multiselect) {
|
if (!multiselect) {
|
||||||
onItemsSelect(selected ? [] : [item]);
|
onItemsSelect(selected ? [] : [item]);
|
||||||
} else {
|
return;
|
||||||
onItemsSelect(
|
|
||||||
selected
|
|
||||||
? selectedItems.filter((i) => key !== getItemKey(i))
|
|
||||||
: [...selectedItems, item],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
onItemsSelect?.(
|
||||||
|
selected
|
||||||
|
? selectedItems.filter((i) => key !== getItemKey(i))
|
||||||
|
: [...selectedItems, item],
|
||||||
|
);
|
||||||
|
setAllItemsSelected(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.dataGrid, className)} {...props}>
|
<div className={className} {...props}>
|
||||||
<DataGridHeader
|
<DataGridHeader
|
||||||
columns={columns}
|
columns={columns}
|
||||||
allRowsSelected={allRowsSelected}
|
allItemsSelected={allItemsSelected}
|
||||||
onSelectAllRows={handleSelectAllRows}
|
onSelectAllItems={handleSelectAllItems}
|
||||||
/>
|
/>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const key = getItemKey(item);
|
const key = String(getItemKey(item));
|
||||||
return (
|
return (
|
||||||
<DataGridRow
|
<DataGridRow
|
||||||
object={item}
|
object={item}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
selected={selectedItemsMap[key] ? true : false}
|
selected={Boolean(selectedItemsMap[key])}
|
||||||
onSelect={() => handleRowSelect(item)}
|
onSelect={() => handleItemSelect(key, item)}
|
||||||
key={key}
|
key={getItemKey(item)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -12,8 +12,8 @@ import { DataGridHeaderProps } from './types';
|
|||||||
|
|
||||||
export function DataGridHeader<T>({
|
export function DataGridHeader<T>({
|
||||||
columns,
|
columns,
|
||||||
allRowsSelected,
|
allItemsSelected,
|
||||||
onSelectAllRows,
|
onSelectAllItems,
|
||||||
}: DataGridHeaderProps<T>) {
|
}: DataGridHeaderProps<T>) {
|
||||||
const [sort, setSort] = useState<DataGridSort>({ order: 'asc', column: '' });
|
const [sort, setSort] = useState<DataGridSort>({ order: 'asc', column: '' });
|
||||||
|
|
||||||
@ -32,8 +32,8 @@ export function DataGridHeader<T>({
|
|||||||
return (
|
return (
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={allRowsSelected}
|
checked={allItemsSelected}
|
||||||
onChange={onSelectAllRows}
|
onChange={onSelectAllItems}
|
||||||
label={{ className: styles.checkboxLabel }}
|
label={{ className: styles.checkboxLabel }}
|
||||||
/>
|
/>
|
||||||
{columns.map((column) => {
|
{columns.map((column) => {
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: solid 1px var(--clr-border-100);
|
border: solid 1px var(--clr-border-100);
|
||||||
background-color: var(--clr-layer-300);
|
background-color: var(--clr-layer-300);
|
||||||
|
border-top-left-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
@ -18,12 +19,17 @@
|
|||||||
border: solid 1px var(--clr-border-100);
|
border: solid 1px var(--clr-border-100);
|
||||||
background-color: var(--clr-layer-300);
|
background-color: var(--clr-layer-300);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
transition: all var(--td-100) ease-in-out;
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
|
||||||
&:hover {
|
&:last-of-type {
|
||||||
background-color: var(--clr-layer-300-hover);
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--clr-layer-300-hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,6 @@ import { DataGridColumnConfig } from '../../types';
|
|||||||
|
|
||||||
export type DataGridHeaderProps<T> = {
|
export type DataGridHeaderProps<T> = {
|
||||||
columns: DataGridColumnConfig<T>[];
|
columns: DataGridColumnConfig<T>[];
|
||||||
allRowsSelected: boolean;
|
allItemsSelected: boolean;
|
||||||
onSelectAllRows: () => void;
|
onSelectAllItems: () => void;
|
||||||
};
|
};
|
||||||
|
@ -24,7 +24,7 @@ export function DataGridRow<T>({
|
|||||||
style={{ flex: column.flex }}
|
style={{ flex: column.flex }}
|
||||||
key={column.name}
|
key={column.name}
|
||||||
>
|
>
|
||||||
<Span color="t200">{column.getText(object)}</Span>
|
<Span>{column.getText(object)}</Span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
import { PreviewArticle } from '@components/ui/preview';
|
import { PreviewArticle } from '@components/ui/preview';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { DataGrid } from './component';
|
import { DataGrid } from './component';
|
||||||
import { Cat, DataGridColumnConfig } from './types';
|
import { DataGridColumnConfig } from './types';
|
||||||
|
|
||||||
|
type Cat = {
|
||||||
|
name: string;
|
||||||
|
breed: string;
|
||||||
|
age: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function DataGridPreview() {
|
export function DataGridPreview() {
|
||||||
|
const [selectedItems, setSelectedItems] = useState<Cat[]>([]);
|
||||||
|
|
||||||
const items: Cat[] = [
|
const items: Cat[] = [
|
||||||
{ name: 'Luna', breed: 'British Shorthair', color: 'Gray', age: '2' },
|
{ name: 'Luna', breed: 'British Shorthair', color: 'Gray', age: '2' },
|
||||||
{ name: 'Simba', breed: 'Siamese', color: 'Cream', age: '1' },
|
{ name: 'Simba', breed: 'Siamese', color: 'Cream', age: '1' },
|
||||||
@ -14,15 +23,24 @@ export function DataGridPreview() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const columns: DataGridColumnConfig<Cat>[] = [
|
const columns: DataGridColumnConfig<Cat>[] = [
|
||||||
{ name: 'Name', getText: (cat) => cat.name, flex: '2' },
|
{ name: 'Name', getText: (cat) => cat.name },
|
||||||
{ name: 'Breed', getText: (cat) => cat.breed },
|
{ name: 'Breed', getText: (cat) => cat.breed, scale: 2 },
|
||||||
{ name: 'Age', getText: (cat) => cat.age },
|
{ name: 'Age', getText: (cat) => cat.age },
|
||||||
{ name: 'Color', getText: (cat) => cat.color },
|
{ name: 'Color', getText: (cat) => cat.color },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreviewArticle title="DataGrid">
|
<PreviewArticle title="DataGrid">
|
||||||
<DataGrid style={{ width: '100%' }} items={items} columns={columns} />
|
<div style={{ width: '100%', overflow: 'auto' }}>
|
||||||
|
<DataGrid
|
||||||
|
style={{ minWidth: 500 }}
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
getItemKey={({ name }) => name}
|
||||||
|
selectedItems={selectedItems}
|
||||||
|
onItemsSelect={setSelectedItems}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</PreviewArticle>
|
</PreviewArticle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
.dataGrid {
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0px 2px 2px var(--clr-shadow-200);
|
|
||||||
}
|
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ComponentPropsWithoutRef, Key } from 'react';
|
||||||
|
|
||||||
export type DataGridColumnConfig<T> = {
|
export type DataGridColumnConfig<T> = {
|
||||||
name: string;
|
name: string;
|
||||||
getText: (object: T) => string;
|
getText: (object: T) => string;
|
||||||
@ -13,15 +15,8 @@ export type DataGridSort = {
|
|||||||
export type DataGridProps<T> = {
|
export type DataGridProps<T> = {
|
||||||
items: T[];
|
items: T[];
|
||||||
columns: DataGridColumnConfig<T>[];
|
columns: DataGridColumnConfig<T>[];
|
||||||
getItemKey: (object: T) => string;
|
getItemKey: (item: T) => Key;
|
||||||
selectedItems: T[];
|
selectedItems?: T[];
|
||||||
onItemsSelect: (selectedItems: T[]) => void;
|
onItemsSelect?: (selectedItems: T[]) => void;
|
||||||
multiselect?: boolean;
|
multiselect?: boolean;
|
||||||
} & React.ComponentPropsWithoutRef<'div'>;
|
} & ComponentPropsWithoutRef<'div'>;
|
||||||
|
|
||||||
export type Cat = {
|
|
||||||
name: string;
|
|
||||||
breed: string;
|
|
||||||
age: string;
|
|
||||||
color: string;
|
|
||||||
};
|
|
||||||
|
@ -37,11 +37,11 @@ export function DateInput({
|
|||||||
setDirtyDate(valueToDirtyDate(value));
|
setDirtyDate(valueToDirtyDate(value));
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
useMissClick(
|
useMissClick({
|
||||||
[wrapperRef, calendarWrapperRef],
|
callback: () => setCalendarVisible(false),
|
||||||
() => setCalendarVisible(false),
|
enabled: calendarVisible,
|
||||||
calendarVisible,
|
whitelist: [wrapperRef, calendarWrapperRef],
|
||||||
);
|
});
|
||||||
|
|
||||||
const handleCalendarButtonClick = () => {
|
const handleCalendarButtonClick = () => {
|
||||||
setCalendarVisible(!calendarVisible);
|
setCalendarVisible(!calendarVisible);
|
||||||
|
43
front/src/components/ui/dialog/component.tsx
Normal file
43
front/src/components/ui/dialog/component.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useMissClick } from '@utils/miss-click';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
|
||||||
|
import { Fade } from '../animation';
|
||||||
|
import { Heading } from '../heading';
|
||||||
|
import { Overlay } from '../overlay';
|
||||||
|
import { Paragraph } from '../paragraph';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { DialogProps } from './types';
|
||||||
|
|
||||||
|
export function Dialog({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
heading,
|
||||||
|
message,
|
||||||
|
triggerElementRef,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: DialogProps) {
|
||||||
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useMissClick({
|
||||||
|
callback: onClose,
|
||||||
|
enabled: open,
|
||||||
|
whitelist: [dialogRef, triggerElementRef],
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogClassName = clsx(styles.dialog, className);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Overlay open={open} className={styles.overlay}>
|
||||||
|
<Fade visible={open} className={styles.fade}>
|
||||||
|
<div className={dialogClassName} ref={dialogRef} {...props}>
|
||||||
|
<Heading tag="h3">{heading}</Heading>
|
||||||
|
<Paragraph color="t300">{message}</Paragraph>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Fade>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
2
front/src/components/ui/dialog/index.ts
Normal file
2
front/src/components/ui/dialog/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './component';
|
||||||
|
export * from './preview';
|
32
front/src/components/ui/dialog/preview.tsx
Normal file
32
front/src/components/ui/dialog/preview.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { PreviewArticle } from '@components/ui/preview';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '../button';
|
||||||
|
import { Dialog } from './component';
|
||||||
|
|
||||||
|
export function DialogPreview() {
|
||||||
|
const openButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreviewArticle title="Dialog">
|
||||||
|
<Button onClick={() => setOpen(true)} ref={openButtonRef}>
|
||||||
|
Open dialog
|
||||||
|
</Button>
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
heading="Dialog"
|
||||||
|
message="Short dialog message"
|
||||||
|
triggerElementRef={openButtonRef}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', gap: '10px' }}>
|
||||||
|
<Button variant="secondary" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setOpen(false)}>Confirm</Button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</PreviewArticle>
|
||||||
|
);
|
||||||
|
}
|
26
front/src/components/ui/dialog/styles.module.scss
Normal file
26
front/src/components/ui/dialog/styles.module.scss
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
@use '@components/func.scss' as f;
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
display: grid;
|
||||||
|
padding: 20px;
|
||||||
|
grid-template:
|
||||||
|
'. . .' 1fr
|
||||||
|
'. fade .' auto
|
||||||
|
'. . .' 2fr
|
||||||
|
/ 1fr auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade {
|
||||||
|
grid-area: fade;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: grid;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: var(--clr-layer-300);
|
||||||
|
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||||
|
gap: 10px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
}
|
9
front/src/components/ui/dialog/types.ts
Normal file
9
front/src/components/ui/dialog/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ComponentPropsWithoutRef, MutableRefObject } from 'react';
|
||||||
|
|
||||||
|
export type DialogProps = {
|
||||||
|
heading: string;
|
||||||
|
message: string;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
triggerElementRef?: MutableRefObject<HTMLElement>;
|
||||||
|
} & ComponentPropsWithoutRef<'div'>;
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ComponentPropsWithoutRef } from 'react';
|
||||||
|
|
||||||
import { LabelProps } from '../label';
|
import { LabelProps } from '../label';
|
||||||
import { RawInputProps } from '../raw';
|
import { RawInputProps } from '../raw';
|
||||||
import { Scale } from '../types';
|
import { Scale } from '../types';
|
||||||
@ -8,4 +10,4 @@ export type FileUploaderProps = {
|
|||||||
scale?: Scale;
|
scale?: Scale;
|
||||||
label?: LabelProps;
|
label?: LabelProps;
|
||||||
input?: Omit<RawInputProps, 'type'>;
|
input?: Omit<RawInputProps, 'type'>;
|
||||||
} & Omit<React.ComponentPropsWithoutRef<'button'>, 'onChange'>;
|
} & Omit<ComponentPropsWithoutRef<'button'>, 'onChange'>;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
import { TextColor } from '../types';
|
import { TextColor } from '../types';
|
||||||
|
|
||||||
export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||||
@ -5,4 +7,4 @@ export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|||||||
export type HeadingProps<T extends HeadingTag> = {
|
export type HeadingProps<T extends HeadingTag> = {
|
||||||
tag: T;
|
tag: T;
|
||||||
color?: TextColor;
|
color?: TextColor;
|
||||||
} & React.ComponentProps<T>;
|
} & ComponentProps<T>;
|
||||||
|
@ -1,22 +1,33 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React, { ForwardedRef, forwardRef } from 'react';
|
||||||
|
|
||||||
import { Ripple } from '../animation';
|
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 function IconButton({
|
function IconButtonInner(
|
||||||
scale = 'm',
|
{
|
||||||
className,
|
variant = 'circle',
|
||||||
children,
|
scale = 'm',
|
||||||
...props
|
className,
|
||||||
}: IconButtonProps) {
|
children,
|
||||||
const classes = clsx(styles.button, styles[scale], className);
|
...props
|
||||||
|
}: IconButtonProps,
|
||||||
|
ref: ForwardedRef<HTMLButtonElement>,
|
||||||
|
) {
|
||||||
|
const buttonClassName = clsx(
|
||||||
|
styles.button,
|
||||||
|
styles[scale],
|
||||||
|
styles[variant],
|
||||||
|
className,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<RawButton className={classes} {...props}>
|
<RawButton className={buttonClassName} ref={ref} {...props}>
|
||||||
{children}
|
{children}
|
||||||
<Ripple />
|
<Ripple />
|
||||||
</RawButton>
|
</RawButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const IconButton = forwardRef(IconButtonInner);
|
||||||
|
@ -3,44 +3,75 @@
|
|||||||
.button {
|
.button {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 100%;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--td-100) ease-in-out;
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--clr-ripple);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--clr-text-300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
fill: var(--clr-text-100);
|
fill: var(--clr-text-100);
|
||||||
transition: all var(--td-100) ease-in-out;
|
transition: all var(--td-100) ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--clr-ripple);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--clr-text-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
$size: 26px;
|
$size: 26px;
|
||||||
$padding: 4px;
|
$rect-padding: 6px;
|
||||||
|
$circle-padding: 4px;
|
||||||
|
$border-radius: 8px;
|
||||||
|
|
||||||
.s {
|
.s {
|
||||||
width: $size;
|
width: $size;
|
||||||
height: $size;
|
height: $size;
|
||||||
padding: $padding;
|
|
||||||
|
&.rect {
|
||||||
|
padding: $rect-padding;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.circle {
|
||||||
|
padding: $circle-padding;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.m {
|
.m {
|
||||||
width: f.m($size);
|
width: f.m($size);
|
||||||
height: f.m($size);
|
height: f.m($size);
|
||||||
padding: f.m($padding);
|
|
||||||
|
&.rect {
|
||||||
|
padding: f.m($rect-padding);
|
||||||
|
border-radius: f.m($border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.circle {
|
||||||
|
padding: f.m($circle-padding);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.l {
|
.l {
|
||||||
width: f.l($size);
|
width: f.l($size);
|
||||||
height: f.l($size);
|
height: f.l($size);
|
||||||
padding: f.l($padding);
|
|
||||||
|
&.rect {
|
||||||
|
padding: f.l($rect-padding);
|
||||||
|
border-radius: f.l($border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.circle {
|
||||||
|
padding: f.l($circle-padding);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,6 @@ import { Scale } from '@components/ui/types';
|
|||||||
import { RawButtonProps } from '../raw';
|
import { RawButtonProps } from '../raw';
|
||||||
|
|
||||||
export type IconButtonProps = {
|
export type IconButtonProps = {
|
||||||
|
variant?: 'circle' | 'rect';
|
||||||
scale?: Scale;
|
scale?: Scale;
|
||||||
} & RawButtonProps;
|
} & RawButtonProps;
|
||||||
|
@ -6,4 +6,4 @@ export type ImageFileManagerProps = {
|
|||||||
onChange?: (value: File | null) => void;
|
onChange?: (value: File | null) => void;
|
||||||
scale?: Scale;
|
scale?: Scale;
|
||||||
label?: LabelProps;
|
label?: LabelProps;
|
||||||
} & Omit<React.ComponentPropsWithoutRef<'div'>, 'onChange'>;
|
} & Omit<ComponentPropsWithoutRef<'div'>, 'onChange'>;
|
||||||
|
@ -7,9 +7,12 @@ export { FileUploader } from './file-uploader';
|
|||||||
export { Heading } from './heading';
|
export { Heading } from './heading';
|
||||||
export { IconButton } from './icon-button';
|
export { IconButton } from './icon-button';
|
||||||
export { ImageFileManager } from './image-file-manager';
|
export { ImageFileManager } from './image-file-manager';
|
||||||
|
export { LinkButton } from './link-button';
|
||||||
export { Menu } from './menu';
|
export { Menu } from './menu';
|
||||||
|
export { Dialog } from './dialog';
|
||||||
export { NumberInput } from './number-input';
|
export { NumberInput } from './number-input';
|
||||||
export { Overlay } from './overlay';
|
export { Overlay } from './overlay';
|
||||||
|
export { Pagination } from './pagination';
|
||||||
export { Paragraph } from './paragraph';
|
export { Paragraph } from './paragraph';
|
||||||
export { PasswordInput } from './password-input';
|
export { PasswordInput } from './password-input';
|
||||||
export { RadioGroup } from './radio-group';
|
export { RadioGroup } from './radio-group';
|
@ -11,6 +11,7 @@ function InputInner(
|
|||||||
wrapper = {},
|
wrapper = {},
|
||||||
leftNode,
|
leftNode,
|
||||||
rightNode,
|
rightNode,
|
||||||
|
invalid,
|
||||||
className,
|
className,
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
@ -24,6 +25,7 @@ function InputInner(
|
|||||||
styles.wrapper,
|
styles.wrapper,
|
||||||
focus && styles.wrapperFocus,
|
focus && styles.wrapperFocus,
|
||||||
wrapper.className,
|
wrapper.className,
|
||||||
|
invalid && styles.invalid,
|
||||||
);
|
);
|
||||||
const inputClassNames = clsx(styles.input, className);
|
const inputClassNames = clsx(styles.input, className);
|
||||||
|
|
||||||
|
@ -14,6 +14,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
border-color: var(--clr-error);
|
||||||
|
}
|
||||||
|
|
||||||
.wrapperFocus {
|
.wrapperFocus {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
border-color: var(--clr-primary);
|
border-color: var(--clr-primary);
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { Scale } from '@components/ui/types';
|
import { Scale } from '@components/ui/types';
|
||||||
|
import { ComponentProps, ReactNode } from 'react';
|
||||||
|
|
||||||
import { RawInputProps } from '../raw';
|
import { RawInputProps } from '../raw';
|
||||||
|
|
||||||
type InputProps = {
|
type InputProps = {
|
||||||
scale?: Scale;
|
scale?: Scale;
|
||||||
wrapper?: React.ComponentProps<'div'>;
|
wrapper?: ComponentProps<'div'>;
|
||||||
leftNode?: React.ReactNode;
|
leftNode?: ReactNode;
|
||||||
rightNode?: React.ReactNode;
|
rightNode?: ReactNode;
|
||||||
|
invalid?: boolean;
|
||||||
} & RawInputProps;
|
} & RawInputProps;
|
||||||
|
|
||||||
export { InputProps };
|
export { InputProps };
|
||||||
|
@ -11,6 +11,7 @@ function LabelInner(
|
|||||||
scale,
|
scale,
|
||||||
position = 'top',
|
position = 'top',
|
||||||
required = {},
|
required = {},
|
||||||
|
error,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
@ -29,6 +30,11 @@ function LabelInner(
|
|||||||
</Span>
|
</Span>
|
||||||
)}
|
)}
|
||||||
{!reversed && children}
|
{!reversed && children}
|
||||||
|
{error && (
|
||||||
|
<Span scale={scale} className={styles.error}>
|
||||||
|
{error}
|
||||||
|
</Span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
.label {
|
.label {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--clr-error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.left,
|
.left,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
import { Required, Scale } from '../types';
|
import { Required, Scale } from '../types';
|
||||||
|
|
||||||
export type LabelProps = {
|
export type LabelProps = {
|
||||||
@ -5,4 +7,5 @@ export type LabelProps = {
|
|||||||
scale?: Scale;
|
scale?: Scale;
|
||||||
position?: 'left' | 'top' | 'right' | 'bottom';
|
position?: 'left' | 'top' | 'right' | 'bottom';
|
||||||
required?: Required;
|
required?: Required;
|
||||||
} & React.ComponentProps<'label'>;
|
error?: string;
|
||||||
|
} & ComponentProps<'label'>;
|
||||||
|
23
front/src/components/ui/link-button/component.tsx
Normal file
23
front/src/components/ui/link-button/component.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Ripple } from '../animation';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { LinkButtonProps } from './types';
|
||||||
|
|
||||||
|
export function LinkButton({
|
||||||
|
scale = 'm',
|
||||||
|
className,
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: LinkButtonProps) {
|
||||||
|
const linkClassName = clsx(styles.button, styles[scale], className);
|
||||||
|
return (
|
||||||
|
<Link className={linkClassName} to={href} {...props}>
|
||||||
|
{children}
|
||||||
|
<Ripple />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
1
front/src/components/ui/link-button/index.tsx
Normal file
1
front/src/components/ui/link-button/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './component';
|
37
front/src/components/ui/link-button/styles.module.scss
Normal file
37
front/src/components/ui/link-button/styles.module.scss
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
@use '@components/func.scss' as f;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--clr-text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
|
||||||
|
&:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$padding: 10px 16px;
|
||||||
|
$border-radius: 8px;
|
||||||
|
$font-size: 12px;
|
||||||
|
|
||||||
|
.s {
|
||||||
|
padding: $padding;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
font-size: $font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m {
|
||||||
|
padding: f.m($padding);
|
||||||
|
border-radius: f.m($border-radius);
|
||||||
|
font-size: f.m($font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.l {
|
||||||
|
padding: f.l($padding);
|
||||||
|
border-radius: f.l($border-radius);
|
||||||
|
font-size: f.l($font-size);
|
||||||
|
}
|
7
front/src/components/ui/link-button/types.ts
Normal file
7
front/src/components/ui/link-button/types.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { ComponentPropsWithoutRef } from 'react';
|
||||||
|
|
||||||
|
import { Scale } from '../types';
|
||||||
|
|
||||||
|
export type LinkButtonProps = {
|
||||||
|
scale?: Scale;
|
||||||
|
} & ComponentPropsWithoutRef<'a'>;
|
@ -1,7 +1,9 @@
|
|||||||
|
import { ComponentProps, Key } from 'react';
|
||||||
|
|
||||||
export type MenuProps<T> = {
|
export type MenuProps<T> = {
|
||||||
options: T[];
|
options: T[];
|
||||||
selected?: T;
|
selected?: T;
|
||||||
getOptionKey: (option: T) => React.Key;
|
getOptionKey: (option: T) => Key;
|
||||||
getOptionLabel: (option: T) => string;
|
getOptionLabel: (option: T) => string;
|
||||||
onSelect?: (option: T) => void;
|
onSelect?: (option: T) => void;
|
||||||
} & Omit<React.ComponentProps<'ul'>, 'onSelect'>;
|
} & Omit<ComponentProps<'ul'>, 'onSelect'>;
|
||||||
|
@ -1,16 +1,47 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React, { ForwardedRef, forwardRef, useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { Fade } from '../animation';
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { OverlayProps } from './types';
|
import { OverlayProps } from './types';
|
||||||
|
|
||||||
export function Overlay({ open, children, variant = 'small' }: OverlayProps) {
|
function OverlayInner(
|
||||||
|
{ open, className, ...props }: OverlayProps,
|
||||||
|
ref: ForwardedRef<HTMLDivElement>,
|
||||||
|
) {
|
||||||
|
const [openInternal, setOpenInternal] = useState<boolean>(open);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setOpenInternal(true);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const overlayClassName = clsx(
|
||||||
|
styles.overlay,
|
||||||
|
{ [styles.closed]: !open },
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAnimationEnd = (event: React.AnimationEvent) => {
|
||||||
|
if (event.animationName === styles.fadeout) {
|
||||||
|
setOpenInternal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!openInternal) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<Fade visible={open}>
|
<div
|
||||||
<div className={clsx(styles.backdrop, styles[variant])}>{children}</div>
|
className={overlayClassName}
|
||||||
</Fade>,
|
onAnimationEnd={handleAnimationEnd}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>,
|
||||||
document.body,
|
document.body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Overlay = forwardRef(OverlayInner);
|
||||||
|
@ -1,27 +1,30 @@
|
|||||||
.backdrop {
|
.overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
display: grid;
|
width: 100vw;
|
||||||
width: 100%;
|
height: 100dvh;
|
||||||
height: 100%;
|
animation: fadein 0.25s forwards ease-in-out;
|
||||||
padding: 20px;
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
background-color: rgba(0 0 0 / 0.75);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
.closed {
|
||||||
grid-template:
|
animation: fadeout 0.25s forwards ease-in-out;
|
||||||
'. . .' 1fr
|
|
||||||
'. form .' auto
|
|
||||||
'. . .' 5fr
|
|
||||||
/ 1fr minmax(0, 400px) 1fr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.large {
|
@keyframes fadein {
|
||||||
grid-template:
|
from {
|
||||||
'. . .' 1fr
|
background-color: rgba(0 0 0 / 0);
|
||||||
'. form .' auto
|
}
|
||||||
'. . .' 5fr
|
to {
|
||||||
/ 1fr minmax(0, 600px) 1fr;
|
background-color: rgba(0 0 0 / 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
from {
|
||||||
|
background-color: rgba(0 0 0 / 0.5);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-color: rgba(0 0 0 / 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ComponentPropsWithoutRef } from 'react';
|
||||||
|
|
||||||
export type OverlayProps = {
|
export type OverlayProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
children: ReactNode;
|
} & ComponentPropsWithoutRef<'div'>;
|
||||||
variant?: 'small' | 'large';
|
|
||||||
};
|
|
||||||
|
71
front/src/components/ui/pagination/component.tsx
Normal file
71
front/src/components/ui/pagination/component.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import ArrowLeftIcon from '@public/images/svg/arrow-left.svg';
|
||||||
|
import ArrowRightIcon from '@public/images/svg/arrow-right.svg';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { PaginationItem } from './components';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { PaginationProps } from './types';
|
||||||
|
import { getPageNumbers } from './utils';
|
||||||
|
|
||||||
|
export function Pagination({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
total,
|
||||||
|
sibling = 2,
|
||||||
|
boundary = 1,
|
||||||
|
scale = 'm',
|
||||||
|
}: PaginationProps) {
|
||||||
|
const pageNumbers = getPageNumbers(value, total, sibling, boundary);
|
||||||
|
const paginationClassNames = clsx(styles.pagination, styles[scale]);
|
||||||
|
|
||||||
|
const handleBackButtonClick = () => {
|
||||||
|
onChange?.(Math.max(value - 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextButtonClick = () => {
|
||||||
|
onChange?.(Math.min(value + 1, total));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={paginationClassNames}>
|
||||||
|
<PaginationItem
|
||||||
|
scale={scale}
|
||||||
|
variant="default"
|
||||||
|
onClick={handleBackButtonClick}
|
||||||
|
disabled={value === 1}
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon className={styles.icon} />
|
||||||
|
</PaginationItem>
|
||||||
|
{pageNumbers.map((number, index) => {
|
||||||
|
if (number === null) {
|
||||||
|
return (
|
||||||
|
<PaginationItem scale={scale} variant="dots" disabled key={index}>
|
||||||
|
...
|
||||||
|
</PaginationItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const isCurrent = number === value;
|
||||||
|
return (
|
||||||
|
<PaginationItem
|
||||||
|
scale={scale}
|
||||||
|
variant={isCurrent ? 'current' : 'default'}
|
||||||
|
disabled={isCurrent}
|
||||||
|
key={index}
|
||||||
|
onClick={() => onChange?.(number)}
|
||||||
|
>
|
||||||
|
{number}
|
||||||
|
</PaginationItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<PaginationItem
|
||||||
|
scale={scale}
|
||||||
|
variant="default"
|
||||||
|
onClick={handleNextButtonClick}
|
||||||
|
disabled={value === total}
|
||||||
|
>
|
||||||
|
<ArrowRightIcon className={styles.icon} />
|
||||||
|
</PaginationItem>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
front/src/components/ui/pagination/components/index.ts
Normal file
1
front/src/components/ui/pagination/components/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './pagination-item';
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Ripple } from '@components/ui/animation';
|
||||||
|
import { RawButton } from '@components/ui/raw';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { PaginationItemProps } from './types';
|
||||||
|
|
||||||
|
export function PaginationItem({
|
||||||
|
scale,
|
||||||
|
variant,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: PaginationItemProps) {
|
||||||
|
const itemClassName = clsx(
|
||||||
|
styles.item,
|
||||||
|
styles[variant],
|
||||||
|
styles[scale],
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<RawButton className={itemClassName} {...props}>
|
||||||
|
{children}
|
||||||
|
<Ripple />
|
||||||
|
</RawButton>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './component';
|
@ -0,0 +1,69 @@
|
|||||||
|
@use '@components/func.scss' as f;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all var(--td-100) ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current {
|
||||||
|
background-color: var(--clr-primary);
|
||||||
|
color: var(--clr-on-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dots {
|
||||||
|
color: var(--clr-on-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.default {
|
||||||
|
background-color: var(--clr-secondary);
|
||||||
|
color: var(--clr-on-secondary);
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--clr-secondary-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$size: 30px;
|
||||||
|
$border-radius: 8px;
|
||||||
|
$font-size: 12px;
|
||||||
|
|
||||||
|
.s {
|
||||||
|
min-width: $size;
|
||||||
|
height: $size;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
font-size: $font-size;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: $font-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m {
|
||||||
|
min-width: f.m($size);
|
||||||
|
height: f.m($size);
|
||||||
|
border-radius: f.m($border-radius);
|
||||||
|
font-size: f.m($font-size);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: f.m($font-size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.l {
|
||||||
|
min-width: f.l($size);
|
||||||
|
height: f.l($size);
|
||||||
|
border-radius: f.l($border-radius);
|
||||||
|
font-size: f.l($font-size);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: f.l($font-size);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { Scale } from '@components/ui/types';
|
||||||
|
import { ComponentPropsWithoutRef } from 'react';
|
||||||
|
|
||||||
|
export type PaginationItemProps = {
|
||||||
|
scale: Scale;
|
||||||
|
variant: 'current' | 'dots' | 'default';
|
||||||
|
} & ComponentPropsWithoutRef<'button'>;
|
2
front/src/components/ui/pagination/index.ts
Normal file
2
front/src/components/ui/pagination/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { Pagination } from './component';
|
||||||
|
export { PaginationPreview } from './preview';
|
18
front/src/components/ui/pagination/preview.tsx
Normal file
18
front/src/components/ui/pagination/preview.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { PreviewArticle } from '@components/ui/preview';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { Pagination } from './component';
|
||||||
|
|
||||||
|
export function PaginationPreview() {
|
||||||
|
const [value1, setValue1] = useState<number>(1);
|
||||||
|
const [value2, setValue2] = useState<number>(1);
|
||||||
|
const [value3, setValue3] = useState<number>(1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreviewArticle title="Pagination">
|
||||||
|
<Pagination value={value1} onChange={setValue1} total={20} scale="s" />
|
||||||
|
<Pagination value={value2} onChange={setValue2} total={20} scale="m" />
|
||||||
|
<Pagination value={value3} onChange={setValue3} total={20} scale="l" />
|
||||||
|
</PreviewArticle>
|
||||||
|
);
|
||||||
|
}
|
25
front/src/components/ui/pagination/styles.module.scss
Normal file
25
front/src/components/ui/pagination/styles.module.scss
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@use '@components/func.scss' as f;
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--clr-on-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
$gap: 5px;
|
||||||
|
|
||||||
|
.s {
|
||||||
|
gap: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m {
|
||||||
|
gap: f.m($gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.l {
|
||||||
|
gap: f.l($gap);
|
||||||
|
}
|
10
front/src/components/ui/pagination/types.ts
Normal file
10
front/src/components/ui/pagination/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Scale } from '../types';
|
||||||
|
|
||||||
|
export type PaginationProps = {
|
||||||
|
value: number;
|
||||||
|
onChange?: (value: number) => void;
|
||||||
|
total: number;
|
||||||
|
sibling?: number;
|
||||||
|
boundary?: number;
|
||||||
|
scale?: Scale;
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user