From ee6bbdca8b90c5b6c7edc270da92485ea2eec2cd Mon Sep 17 00:00:00 2001 From: it-is-not-alright Date: Wed, 20 Nov 2024 00:06:19 +0400 Subject: [PATCH] [test-entity]: front --- front/public/images/svg/arrow-left.svg | 7 ++ front/public/images/svg/arrow-right.svg | 7 ++ front/public/images/svg/close.svg | 8 ++ front/public/images/svg/menu.svg | 20 +++ front/src/api/wind/index.ts | 2 +- front/src/api/wind/service.ts | 51 +++++--- front/src/api/wind/types.ts | 14 ++- front/src/api/wind/utils.ts | 14 +-- front/src/components/_mixins.scss | 5 + front/src/components/app/_theme.scss | 16 ++- .../app/{index.tsx => component.tsx} | 22 ++-- front/src/components/app/index.ts | 1 + front/src/components/app/styles.scss | 2 +- front/src/components/layouts/index.ts | 1 + front/src/components/layouts/index.tsx | 3 - .../layouts/main-layout/component.tsx | 22 ++++ .../components/layouts/main-layout/index.tsx | 21 +--- .../layouts/main-layout/styles.module.scss | 37 ++++-- front/src/components/pages/index.ts | 2 + .../components/pages/park-page/component.tsx | 83 +++++++++++++ .../pages/park-page/components/index.ts | 1 + .../components/park-turbines/component.tsx | 19 +++ .../components/park-turbines/constants.ts | 11 ++ .../components/park-turbines}/index.ts | 0 .../components/park-turbines/types.ts | 6 + .../index.tsx => pages/park-page/index.ts} | 0 .../pages/park-page/styles.module.scss | 28 +++++ front/src/components/pages/park-page/types.ts | 8 ++ .../park-modal => pages/park-page}/utils.ts | 5 +- .../components/pages/parks-page/component.tsx | 62 ++++------ .../components/pages/parks-page/constants.ts | 4 +- .../pages/parks-page/styles.module.scss | 4 +- .../pages/turbine-type-page/component.tsx | 100 +++++++++++++++ .../pages/turbine-type-page/index.ts | 1 + .../turbine-type-page/styles.module.scss | 28 +++++ .../pages/turbine-type-page/types.ts | 5 + .../turbine-type-page}/utils.ts | 4 +- .../pages/turbine-types-page/component.tsx | 72 ++++------- .../turbine-types-page/styles.module.scss | 4 +- .../ui/animation/fade/component.tsx | 19 +-- .../ui/animation/fade/styles.module.scss | 6 +- .../src/components/ui/animation/fade/types.ts | 4 +- front/src/components/ui/animation/index.ts | 1 - .../ui/animation/ripple/component.tsx | 1 + .../ui/animation/ripple/styles.module.scss | 5 +- .../ui/animation/slide/component.tsx | 58 --------- .../ui/animation/slide/styles.module.scss | 29 ----- .../components/ui/animation/slide/types.ts | 4 - .../components/ui/autocomplete/component.tsx | 23 ++-- front/src/components/ui/autocomplete/types.ts | 6 +- front/src/components/ui/button/component.tsx | 32 ++--- .../components/ui/button/styles.module.scss | 31 ++--- front/src/components/ui/calendar/types.ts | 4 +- .../components/ui/checkbox/styles.module.scss | 2 +- front/src/components/ui/comet/types.ts | 4 +- .../src/components/ui/data-grid/component.tsx | 64 +++++----- .../components/DataGridHeader/component.tsx | 8 +- .../DataGridHeader/styles.module.scss | 12 +- .../components/DataGridHeader/types.ts | 4 +- .../components/DataGridRow/component.tsx | 2 +- front/src/components/ui/data-grid/preview.tsx | 28 ++++- .../ui/data-grid/styles.module.scss | 4 - front/src/components/ui/data-grid/types.ts | 17 +-- .../components/ui/date-input/component.tsx | 10 +- front/src/components/ui/dialog/component.tsx | 43 +++++++ front/src/components/ui/dialog/index.ts | 2 + front/src/components/ui/dialog/preview.tsx | 32 +++++ .../components/ui/dialog/styles.module.scss | 26 ++++ front/src/components/ui/dialog/types.ts | 9 ++ .../src/components/ui/file-uploader/types.ts | 4 +- front/src/components/ui/heading/types.ts | 4 +- .../components/ui/icon-button/component.tsx | 29 +++-- .../ui/icon-button/styles.module.scss | 57 +++++++-- front/src/components/ui/icon-button/types.ts | 1 + .../components/ui/image-file-manager/types.ts | 2 +- .../src/components/ui/{index.tsx => index.ts} | 3 + front/src/components/ui/input/component.tsx | 2 + .../components/ui/input/styles.module.scss | 4 + front/src/components/ui/input/types.ts | 8 +- front/src/components/ui/label/component.tsx | 6 + .../components/ui/label/styles.module.scss | 4 + front/src/components/ui/label/types.ts | 5 +- .../components/ui/link-button/component.tsx | 23 ++++ front/src/components/ui/link-button/index.tsx | 1 + .../ui/link-button/styles.module.scss | 37 ++++++ front/src/components/ui/link-button/types.ts | 7 ++ front/src/components/ui/menu/types.ts | 6 +- front/src/components/ui/overlay/component.tsx | 43 ++++++- .../components/ui/overlay/styles.module.scss | 41 ++++--- front/src/components/ui/overlay/types.ts | 6 +- .../components/ui/pagination/component.tsx | 71 +++++++++++ .../ui/pagination/components/index.ts | 1 + .../components/pagination-item/component.tsx | 28 +++++ .../components/pagination-item/index.ts | 1 + .../pagination-item/styles.module.scss | 69 +++++++++++ .../components/pagination-item/types.ts | 7 ++ front/src/components/ui/pagination/index.ts | 2 + .../src/components/ui/pagination/preview.tsx | 18 +++ .../ui/pagination/styles.module.scss | 25 ++++ front/src/components/ui/pagination/types.ts | 10 ++ front/src/components/ui/pagination/utils.ts | 30 +++++ front/src/components/ui/paragraph/types.ts | 4 +- front/src/components/ui/popover/component.tsx | 6 +- front/src/components/ui/popover/types.ts | 6 +- front/src/components/ui/popover/utils.ts | 4 +- .../ui/preview/preview-article/types.ts | 4 +- .../ui/preview/preview-box/types.ts | 4 +- front/src/components/ui/radio-group/types.ts | 4 +- .../components/ui/radio/styles.module.scss | 2 +- .../ui/raw/raw-button/styles.module.scss | 4 + .../src/components/ui/raw/raw-button/types.ts | 4 +- .../src/components/ui/raw/raw-input/types.ts | 4 +- front/src/components/ui/select/component.tsx | 6 +- front/src/components/ui/select/types.ts | 8 +- front/src/components/ui/span/types.ts | 4 +- front/src/components/ui/text-area/types.ts | 4 +- .../components/ui/text-input/component.tsx | 8 +- .../src/components/ui/text-input/preview.tsx | 25 +++- front/src/components/ui/types.ts | 2 +- front/src/components/ux/header/component.tsx | 36 ++++-- .../components/ux/header/styles.module.scss | 9 +- front/src/components/ux/header/types.ts | 3 + front/src/components/ux/index.ts | 9 +- .../ux/mobile-sidebar/component.tsx | 42 +++++++ .../src/components/ux/mobile-sidebar/index.ts | 1 + .../ux/mobile-sidebar/styles.module.scss | 47 +++++++ .../src/components/ux/mobile-sidebar/types.ts | 7 ++ .../components/ux/navigation/component.tsx | 19 +++ front/src/components/ux/navigation/consts.ts | 6 + front/src/components/ux/navigation/index.tsx | 1 + .../ux/navigation/styles.module.scss | 13 ++ .../components/ux/park-modal/component.tsx | 115 ------------------ front/src/components/ux/park-modal/index.ts | 2 - .../ux/park-modal/styles.module.scss | 48 -------- front/src/components/ux/park-modal/types.ts | 14 --- front/src/components/ux/sidebar/component.tsx | 31 +++-- front/src/components/ux/sidebar/constants.ts | 8 -- .../components/ux/sidebar/styles.module.scss | 13 +- front/src/components/ux/sidebar/types.ts | 7 +- .../components/ux/sign-in-form/component.tsx | 40 ++++++ front/src/components/ux/sign-in-form/index.ts | 1 + .../styles.module.scss | 11 +- front/src/components/ux/sign-in-form/types.ts | 8 ++ .../components/ux/sign-up-form/component.tsx | 40 ++++++ front/src/components/ux/sign-up-form/index.ts | 1 + .../ux/sign-up-form/styles.module.scss | 26 ++++ front/src/components/ux/sign-up-form/types.ts | 9 ++ .../components/ux/theme-select/component.tsx | 2 +- front/src/components/ux/theme-select/index.ts | 1 + .../ux/theme-select/styles.module.scss | 4 - .../ux/turbine-type-modal/component.tsx | 79 ------------ .../components/ux/turbine-type-modal/index.ts | 2 - .../components/ux/turbine-type-modal/types.ts | 14 --- front/src/index.tsx | 2 +- front/src/utils/device/index.ts | 1 + front/src/utils/device/types.ts | 1 + front/src/utils/device/utils.ts | 28 +++++ front/src/utils/form/controller/types.ts | 4 +- front/src/utils/form/types.ts | 4 +- front/src/utils/miss-click/index.tsx | 50 +++++--- front/src/utils/miss-click/types.ts | 8 ++ front/src/utils/route/constants.ts | 6 +- front/src/utils/route/types.ts | 2 +- front/tsconfig.json | 1 + 164 files changed, 1761 insertions(+), 863 deletions(-) create mode 100644 front/public/images/svg/arrow-left.svg create mode 100644 front/public/images/svg/arrow-right.svg create mode 100644 front/public/images/svg/close.svg create mode 100644 front/public/images/svg/menu.svg create mode 100644 front/src/components/_mixins.scss rename front/src/components/app/{index.tsx => component.tsx} (60%) create mode 100644 front/src/components/app/index.ts create mode 100644 front/src/components/layouts/index.ts delete mode 100644 front/src/components/layouts/index.tsx create mode 100644 front/src/components/layouts/main-layout/component.tsx create mode 100644 front/src/components/pages/park-page/component.tsx create mode 100644 front/src/components/pages/park-page/components/index.ts create mode 100644 front/src/components/pages/park-page/components/park-turbines/component.tsx create mode 100644 front/src/components/pages/park-page/components/park-turbines/constants.ts rename front/src/components/{ui/animation/slide => pages/park-page/components/park-turbines}/index.ts (100%) create mode 100644 front/src/components/pages/park-page/components/park-turbines/types.ts rename front/src/components/{ux/theme-select/index.tsx => pages/park-page/index.ts} (100%) create mode 100644 front/src/components/pages/park-page/styles.module.scss create mode 100644 front/src/components/pages/park-page/types.ts rename front/src/components/{ux/park-modal => pages/park-page}/utils.ts (55%) create mode 100644 front/src/components/pages/turbine-type-page/component.tsx create mode 100644 front/src/components/pages/turbine-type-page/index.ts create mode 100644 front/src/components/pages/turbine-type-page/styles.module.scss create mode 100644 front/src/components/pages/turbine-type-page/types.ts rename front/src/components/{ux/turbine-type-modal => pages/turbine-type-page}/utils.ts (74%) delete mode 100644 front/src/components/ui/animation/slide/component.tsx delete mode 100644 front/src/components/ui/animation/slide/styles.module.scss delete mode 100644 front/src/components/ui/animation/slide/types.ts delete mode 100644 front/src/components/ui/data-grid/styles.module.scss create mode 100644 front/src/components/ui/dialog/component.tsx create mode 100644 front/src/components/ui/dialog/index.ts create mode 100644 front/src/components/ui/dialog/preview.tsx create mode 100644 front/src/components/ui/dialog/styles.module.scss create mode 100644 front/src/components/ui/dialog/types.ts rename front/src/components/ui/{index.tsx => index.ts} (86%) create mode 100644 front/src/components/ui/link-button/component.tsx create mode 100644 front/src/components/ui/link-button/index.tsx create mode 100644 front/src/components/ui/link-button/styles.module.scss create mode 100644 front/src/components/ui/link-button/types.ts create mode 100644 front/src/components/ui/pagination/component.tsx create mode 100644 front/src/components/ui/pagination/components/index.ts create mode 100644 front/src/components/ui/pagination/components/pagination-item/component.tsx create mode 100644 front/src/components/ui/pagination/components/pagination-item/index.ts create mode 100644 front/src/components/ui/pagination/components/pagination-item/styles.module.scss create mode 100644 front/src/components/ui/pagination/components/pagination-item/types.ts create mode 100644 front/src/components/ui/pagination/index.ts create mode 100644 front/src/components/ui/pagination/preview.tsx create mode 100644 front/src/components/ui/pagination/styles.module.scss create mode 100644 front/src/components/ui/pagination/types.ts create mode 100644 front/src/components/ui/pagination/utils.ts create mode 100644 front/src/components/ux/header/types.ts create mode 100644 front/src/components/ux/mobile-sidebar/component.tsx create mode 100644 front/src/components/ux/mobile-sidebar/index.ts create mode 100644 front/src/components/ux/mobile-sidebar/styles.module.scss create mode 100644 front/src/components/ux/mobile-sidebar/types.ts create mode 100644 front/src/components/ux/navigation/component.tsx create mode 100644 front/src/components/ux/navigation/consts.ts create mode 100644 front/src/components/ux/navigation/index.tsx create mode 100644 front/src/components/ux/navigation/styles.module.scss delete mode 100644 front/src/components/ux/park-modal/component.tsx delete mode 100644 front/src/components/ux/park-modal/index.ts delete mode 100644 front/src/components/ux/park-modal/styles.module.scss delete mode 100644 front/src/components/ux/park-modal/types.ts delete mode 100644 front/src/components/ux/sidebar/constants.ts create mode 100644 front/src/components/ux/sign-in-form/component.tsx create mode 100644 front/src/components/ux/sign-in-form/index.ts rename front/src/components/ux/{turbine-type-modal => sign-in-form}/styles.module.scss (62%) create mode 100644 front/src/components/ux/sign-in-form/types.ts create mode 100644 front/src/components/ux/sign-up-form/component.tsx create mode 100644 front/src/components/ux/sign-up-form/index.ts create mode 100644 front/src/components/ux/sign-up-form/styles.module.scss create mode 100644 front/src/components/ux/sign-up-form/types.ts create mode 100644 front/src/components/ux/theme-select/index.ts delete mode 100644 front/src/components/ux/theme-select/styles.module.scss delete mode 100644 front/src/components/ux/turbine-type-modal/component.tsx delete mode 100644 front/src/components/ux/turbine-type-modal/index.ts delete mode 100644 front/src/components/ux/turbine-type-modal/types.ts create mode 100644 front/src/utils/device/index.ts create mode 100644 front/src/utils/device/types.ts create mode 100644 front/src/utils/device/utils.ts create mode 100644 front/src/utils/miss-click/types.ts diff --git a/front/public/images/svg/arrow-left.svg b/front/public/images/svg/arrow-left.svg new file mode 100644 index 0000000..9577612 --- /dev/null +++ b/front/public/images/svg/arrow-left.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/front/public/images/svg/arrow-right.svg b/front/public/images/svg/arrow-right.svg new file mode 100644 index 0000000..ab416d8 --- /dev/null +++ b/front/public/images/svg/arrow-right.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/front/public/images/svg/close.svg b/front/public/images/svg/close.svg new file mode 100644 index 0000000..4a90caf --- /dev/null +++ b/front/public/images/svg/close.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/front/public/images/svg/menu.svg b/front/public/images/svg/menu.svg new file mode 100644 index 0000000..f60845b --- /dev/null +++ b/front/public/images/svg/menu.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/front/src/api/wind/index.ts b/front/src/api/wind/index.ts index d851581..45237b7 100644 --- a/front/src/api/wind/index.ts +++ b/front/src/api/wind/index.ts @@ -1,2 +1,2 @@ export * from './service'; -export { type Park, type TurbineType } from './types'; +export * from './types'; diff --git a/front/src/api/wind/service.ts b/front/src/api/wind/service.ts index 40d01af..d52c09e 100644 --- a/front/src/api/wind/service.ts +++ b/front/src/api/wind/service.ts @@ -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 { WIND_ENDPOINTS } from './constants'; -import { Park, ParkTurbine, TurbineType } from './types'; -import { packParkFormValues, packTurbineTypeFormValues } from './utils'; +import { Park, ParkTurbine, ParkWithTurbines, TurbineType } from './types'; +import { packTurbineTypes } from './utils'; export const getTurbineTypes = () => { return api.get(WIND_ENDPOINTS.turbines); }; -export const createTurbineTypes = (values: Partial) => { - return api.post( +export const getTurbineType = (id: string) => { + const url = `${WIND_ENDPOINTS.turbineType}/${id}`; + return api.get(url); +}; + +export const createTurbineTypes = ( + formValues: Partial, +) => { + return api.post( WIND_ENDPOINTS.turbineType, - packTurbineTypeFormValues(values), + packTurbineTypes(formValues), ); }; export const editTurbineTypes = ( - values: Partial, - id: number, + formValues: Partial, + id: string, ) => { const url = `${WIND_ENDPOINTS.turbineType}/${id}`; - return api.put(url, packTurbineTypeFormValues(values)); + return api.put(url, packTurbineTypes(formValues)); }; -export const deleteTurbineTypes = (id: number) => { +export const deleteTurbineType = (id: number) => { const url = `${WIND_ENDPOINTS.turbineType}/${id}`; return api.delete(url); }; @@ -33,16 +41,25 @@ export const getParks = () => { return api.get(WIND_ENDPOINTS.parks); }; -export const createPark = (values: Partial) => { - return api.post(WIND_ENDPOINTS.park, packParkFormValues(values)); -}; - -export const editPark = (values: Partial, id: number) => { +export const getPark = (id: string) => { const url = `${WIND_ENDPOINTS.park}/${id}`; - return api.put(url, packParkFormValues(values)); + return api.get(url); }; -export const getParkTurines = (id: number) => { +export const getParkTurbines = (id: string) => { const url = `${WIND_ENDPOINTS.parks}/${id}/turbines`; return api.get(url); }; + +export const getParkWithTurbines = async ( + id: string, +): Promise> => { + const parkURL = `${WIND_ENDPOINTS.park}/${id}`; + const turbinesURL = `${WIND_ENDPOINTS.parks}/${id}/turbines`; + const parkPesponse = await api.get(parkURL); + const turbinesResponse = await api.get(turbinesURL); + return { + data: { ...parkPesponse.data, turbines: turbinesResponse.data }, + error: parkPesponse.error || turbinesResponse.error || null, + }; +}; diff --git a/front/src/api/wind/types.ts b/front/src/api/wind/types.ts index b2a1627..1921287 100644 --- a/front/src/api/wind/types.ts +++ b/front/src/api/wind/types.ts @@ -13,10 +13,20 @@ export type Park = { }; export type ParkTurbine = { - windParkId: number; - turbineId: number; + id: number; + name: string; + height: number; + bladeLength: number; xOffset: number; yOffset: number; angle: number; comment: string; }; + +export type ParkWithTurbines = { + id: number; + name: string; + centerLatitude: number; + centerLongitude: number; + turbines: ParkTurbine[]; +}; diff --git a/front/src/api/wind/utils.ts b/front/src/api/wind/utils.ts index 1c2eb3a..ca76e17 100644 --- a/front/src/api/wind/utils.ts +++ b/front/src/api/wind/utils.ts @@ -1,19 +1,9 @@ -import { ParkFormValues, TurbineTypeFormValues } from '@components/ux'; +import { TurbineTypeFormValues } from '@components/pages/turbine-type-page/types'; -export const packTurbineTypeFormValues = ( - values: Partial, -) => { +export const packTurbineTypes = (values: Partial) => { return { Name: values.name ?? '', Height: parseInt(values.height ?? '0'), BladeLength: parseInt(values.bladeLength ?? '0'), }; }; - -export const packParkFormValues = (values: Partial) => { - return { - Name: values.name ?? '', - CenterLatitude: parseInt(values.centerLatitude ?? '0'), - CenterLongitude: parseInt(values.centerLongitude ?? '0'), - }; -}; diff --git a/front/src/components/_mixins.scss b/front/src/components/_mixins.scss new file mode 100644 index 0000000..58424a5 --- /dev/null +++ b/front/src/components/_mixins.scss @@ -0,0 +1,5 @@ +@mixin on-mobile { + @media (width <= 800px) { + @content; + } +} diff --git a/front/src/components/app/_theme.scss b/front/src/components/app/_theme.scss index 61fbc9a..43450ac 100644 --- a/front/src/components/app/_theme.scss +++ b/front/src/components/app/_theme.scss @@ -2,14 +2,12 @@ color-scheme: light; --clr-primary: #4176FF; - --clr-primary-o50: #3865DA80; + --clr-primary-o50: #4176FF80; --clr-primary-hover: #638FFF; - --clr-primary-disabled: #3D68D7; --clr-on-primary: #FFFFFF; - --clr-secondary: #EAEAEA; - --clr-secondary-hover: #EFEFEF; - --clr-secondary-disabled: #E1E1E1; + --clr-secondary: #E1EAF8; + --clr-secondary-hover: #E8ECF0; --clr-on-secondary: #0D0D0D; --clr-layer-100: #EBEEF0; @@ -20,6 +18,7 @@ --clr-text-100: #8D8D8D; --clr-text-200: #6C7480; --clr-text-300: #1D1F20; + --clr-text-primary: #3865DA; --clr-border-100: #DFDFDF; --clr-border-200: #D8D8D8; @@ -28,6 +27,8 @@ --clr-shadow-200: #00000026; --clr-ripple: #1D1F2026; + + --clr-error: #E54B4B; } @mixin dark { @@ -36,7 +37,7 @@ --clr-primary: #3865DA; --clr-primary-o50: #3865DA80; --clr-primary-hover: #4073F7; - --clr-primary-disabled: #2A4DA7; + --clr-primary-disabled: #334570; --clr-on-primary: #FFFFFF; --clr-secondary: #3F3F3F; @@ -52,6 +53,7 @@ --clr-text-100: #888888; --clr-text-200: #C5C5C5; --clr-text-300: #F0F0F0; + --clr-text-primary: #4176FF; --clr-border-100: #3D3D3D; --clr-border-200: #545454; @@ -60,4 +62,6 @@ --clr-shadow-200: #00000026; --clr-ripple: #F0F0F026; + + --clr-error: #FF6363; } diff --git a/front/src/components/app/index.tsx b/front/src/components/app/component.tsx similarity index 60% rename from front/src/components/app/index.tsx rename to front/src/components/app/component.tsx index bad3af7..ca8aa11 100644 --- a/front/src/components/app/index.tsx +++ b/front/src/components/app/component.tsx @@ -2,30 +2,34 @@ import './styles.scss'; import '@public/fonts/styles.css'; 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 React from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; -function App() { +export function App() { return ( }> - } - /> } /> + } /> } /> + } /> - } /> + } + /> ); } - -export default App; diff --git a/front/src/components/app/index.ts b/front/src/components/app/index.ts new file mode 100644 index 0000000..8e12b7e --- /dev/null +++ b/front/src/components/app/index.ts @@ -0,0 +1 @@ +export { App } from './component'; diff --git a/front/src/components/app/styles.scss b/front/src/components/app/styles.scss index b86f605..21c18b4 100644 --- a/front/src/components/app/styles.scss +++ b/front/src/components/app/styles.scss @@ -23,7 +23,7 @@ html[data-theme='default'] { } html { - --td-100: 0.2s; + --td-100: 0.1s; } body { diff --git a/front/src/components/layouts/index.ts b/front/src/components/layouts/index.ts new file mode 100644 index 0000000..f8c97de --- /dev/null +++ b/front/src/components/layouts/index.ts @@ -0,0 +1 @@ +export { MainLayout } from './main-layout'; diff --git a/front/src/components/layouts/index.tsx b/front/src/components/layouts/index.tsx deleted file mode 100644 index 6da568f..0000000 --- a/front/src/components/layouts/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import MainLayout from './main-layout'; - -export { MainLayout }; diff --git a/front/src/components/layouts/main-layout/component.tsx b/front/src/components/layouts/main-layout/component.tsx new file mode 100644 index 0000000..8949604 --- /dev/null +++ b/front/src/components/layouts/main-layout/component.tsx @@ -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 ( +
+ {deviceType === 'mobile' ? ( +
+ ) : ( + + )} +
+ +
+
+ ); +} diff --git a/front/src/components/layouts/main-layout/index.tsx b/front/src/components/layouts/main-layout/index.tsx index f6033b7..bb82484 100644 --- a/front/src/components/layouts/main-layout/index.tsx +++ b/front/src/components/layouts/main-layout/index.tsx @@ -1,20 +1 @@ -import { Sidebar } from '@components/ux'; -import React from 'react'; -import { Outlet } from 'react-router-dom'; - -import styles from './styles.module.scss'; - -function MainLayout() { - return ( -
- -
-
- -
-
-
- ); -} - -export default MainLayout; +export * from './component'; diff --git a/front/src/components/layouts/main-layout/styles.module.scss b/front/src/components/layouts/main-layout/styles.module.scss index 0a2613b..d5e9068 100644 --- a/front/src/components/layouts/main-layout/styles.module.scss +++ b/front/src/components/layouts/main-layout/styles.module.scss @@ -1,3 +1,5 @@ +@use '@components/mixins.scss' as m; + .mainLayout { display: grid; height: 100%; @@ -6,16 +8,31 @@ / auto minmax(0, 1fr); } -.main { - display: flex; - overflow: auto; - height: 100%; - justify-content: center; +.sidebar { + grid-area: sidebar; } -.content { - display: grid; - width: 1000px; - grid-template-columns: minmax(0, 1fr); - grid-template-rows: minmax(0, 1fr); +.header { + grid-area: header; +} + +.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); + } } diff --git a/front/src/components/pages/index.ts b/front/src/components/pages/index.ts index f1c6e0b..fbb3e4d 100644 --- a/front/src/components/pages/index.ts +++ b/front/src/components/pages/index.ts @@ -1,2 +1,4 @@ +export * from './park-page'; export * from './parks-page'; +export * from './turbine-type-page'; export * from './turbine-types-page'; diff --git a/front/src/components/pages/park-page/component.tsx b/front/src/components/pages/park-page/component.tsx new file mode 100644 index 0000000..a247487 --- /dev/null +++ b/front/src/components/pages/park-page/component.tsx @@ -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(null); + const [pending, setPending] = useState(false); + const params = useParams(); + const route = useRoute(); + + const { register, control, getValues, reset } = useForm({}); + + 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 ( +
+ {route.title} +
+
+ {heading} +
+ +
+ ( + + )} + /> + ( + + )} + /> +
+
+ + +
+ + } + /> +
+ ); +} diff --git a/front/src/components/pages/park-page/components/index.ts b/front/src/components/pages/park-page/components/index.ts new file mode 100644 index 0000000..83cd964 --- /dev/null +++ b/front/src/components/pages/park-page/components/index.ts @@ -0,0 +1 @@ +export * from './park-turbines'; diff --git a/front/src/components/pages/park-page/components/park-turbines/component.tsx b/front/src/components/pages/park-page/components/park-turbines/component.tsx new file mode 100644 index 0000000..8dfe1dc --- /dev/null +++ b/front/src/components/pages/park-page/components/park-turbines/component.tsx @@ -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 ( +
+
+ String(id)} + selectedItems={[]} + /> +
+ ); +} diff --git a/front/src/components/pages/park-page/components/park-turbines/constants.ts b/front/src/components/pages/park-page/components/park-turbines/constants.ts new file mode 100644 index 0000000..0cc3ce5 --- /dev/null +++ b/front/src/components/pages/park-page/components/park-turbines/constants.ts @@ -0,0 +1,11 @@ +import { DataGridColumnConfig } from '@components/ui/data-grid/types'; +import { ParkTurbine } from 'src/api/wind'; + +export const columns: DataGridColumnConfig[] = [ + { 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' }, +]; diff --git a/front/src/components/ui/animation/slide/index.ts b/front/src/components/pages/park-page/components/park-turbines/index.ts similarity index 100% rename from front/src/components/ui/animation/slide/index.ts rename to front/src/components/pages/park-page/components/park-turbines/index.ts diff --git a/front/src/components/pages/park-page/components/park-turbines/types.ts b/front/src/components/pages/park-page/components/park-turbines/types.ts new file mode 100644 index 0000000..3004e17 --- /dev/null +++ b/front/src/components/pages/park-page/components/park-turbines/types.ts @@ -0,0 +1,6 @@ +import { ParkTurbine } from '@api/wind'; + +export type ParkTurbinesProps = { + value?: ParkTurbine[]; + onChange?: (value: ParkTurbine[]) => void; +}; diff --git a/front/src/components/ux/theme-select/index.tsx b/front/src/components/pages/park-page/index.ts similarity index 100% rename from front/src/components/ux/theme-select/index.tsx rename to front/src/components/pages/park-page/index.ts diff --git a/front/src/components/pages/park-page/styles.module.scss b/front/src/components/pages/park-page/styles.module.scss new file mode 100644 index 0000000..b204d88 --- /dev/null +++ b/front/src/components/pages/park-page/styles.module.scss @@ -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; +} diff --git a/front/src/components/pages/park-page/types.ts b/front/src/components/pages/park-page/types.ts new file mode 100644 index 0000000..13f5aa4 --- /dev/null +++ b/front/src/components/pages/park-page/types.ts @@ -0,0 +1,8 @@ +import { ParkTurbine } from '@api/wind'; + +export type ParkFormValues = { + name: string; + centerLatitude: string; + centerLongitude: string; + turbines: ParkTurbine[]; +}; diff --git a/front/src/components/ux/park-modal/utils.ts b/front/src/components/pages/park-page/utils.ts similarity index 55% rename from front/src/components/ux/park-modal/utils.ts rename to front/src/components/pages/park-page/utils.ts index 78490af..2f45d53 100644 --- a/front/src/components/ux/park-modal/utils.ts +++ b/front/src/components/pages/park-page/utils.ts @@ -1,11 +1,12 @@ -import { Park } from 'src/api/wind'; +import { ParkWithTurbines } from '@api/wind'; import { ParkFormValues } from './types'; -export const parkToFormValues = (park: Park): ParkFormValues => { +export const unpackPark = (park: ParkWithTurbines): ParkFormValues => { return { name: park.name, centerLatitude: String(park.centerLatitude), centerLongitude: String(park.centerLongitude), + turbines: park.turbines, }; }; diff --git a/front/src/components/pages/parks-page/component.tsx b/front/src/components/pages/parks-page/component.tsx index 7484ffd..e7dd82e 100644 --- a/front/src/components/pages/parks-page/component.tsx +++ b/front/src/components/pages/parks-page/component.tsx @@ -1,58 +1,52 @@ +import { getParks, Park } from '@api/wind'; import { Button, Heading } from '@components/ui'; import { DataGrid } from '@components/ui/data-grid'; -import { ParkModal } from '@components/ux'; -import { useRoute } from '@utils/route'; +import { ROUTES, useRoute } from '@utils/route'; import React, { useEffect, useState } from 'react'; -import { getParks, Park } from 'src/api/wind'; +import { Link } from 'react-router-dom'; import { columns } from './constants'; import styles from './styles.module.scss'; export function ParksPage() { - const [createModalOpen, setCreateModalOpen] = useState(false); - const [editModalOpen, setEditModalOpen] = useState(false); const [parks, setParks] = useState([]); const [selected, setSelected] = useState(null); const route = useRoute(); const fetchParks = async () => { const res = await getParks(); - setParks(res.data ?? []); + setParks(res.data); }; useEffect(() => { fetchParks(); }, []); - const handleCreateButtonClick = () => { - setCreateModalOpen(true); - }; - - const handleEditButtonClick = () => { - setEditModalOpen(true); - }; - const handleParkSelect = (items: Park[]) => { setSelected(items[0] ?? null); }; + const handleDeleteButtonClick = async () => { + // + }; + return (
- - {route.title} - + {route.title}
- - - + + + + {selected && ( + + + + )} + {selected && ( + + )}
- setCreateModalOpen(false)} - onSuccess={fetchParks} - /> - setEditModalOpen(false)} - onSuccess={fetchParks} - />
); } diff --git a/front/src/components/pages/parks-page/constants.ts b/front/src/components/pages/parks-page/constants.ts index ea8e9c1..17d6dea 100644 --- a/front/src/components/pages/parks-page/constants.ts +++ b/front/src/components/pages/parks-page/constants.ts @@ -3,6 +3,6 @@ import { Park } from 'src/api/wind'; export const columns: DataGridColumnConfig[] = [ { name: 'Name', getText: (t) => t.name, flex: '2' }, - { name: 'Center latitude', getText: (t) => String(t.centerLatitude) }, - { name: 'Center longitude', getText: (t) => String(t.centerLongitude) }, + { name: 'Center Latitude', getText: (t) => String(t.centerLatitude) }, + { name: 'Center Longitude', getText: (t) => String(t.centerLongitude) }, ]; diff --git a/front/src/components/pages/parks-page/styles.module.scss b/front/src/components/pages/parks-page/styles.module.scss index 5d18482..2f566e4 100644 --- a/front/src/components/pages/parks-page/styles.module.scss +++ b/front/src/components/pages/parks-page/styles.module.scss @@ -1,6 +1,6 @@ .page { display: grid; - padding: 40px 20px 20px; + padding: 40px 20px; gap: 20px; grid-template-rows: auto auto minmax(0, 1fr); } @@ -13,7 +13,7 @@ display: flex; padding: 10px; border: 1px solid var(--clr-border-100); - border-radius: 10px; + border-radius: 15px; background-color: var(--clr-layer-200); box-shadow: 0px 1px 2px var(--clr-shadow-100); gap: 10px; diff --git a/front/src/components/pages/turbine-type-page/component.tsx b/front/src/components/pages/turbine-type-page/component.tsx new file mode 100644 index 0000000..30696fe --- /dev/null +++ b/front/src/components/pages/turbine-type-page/component.tsx @@ -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(null); + const [pending, setPending] = useState(false); + const params = useParams(); + const navigate = useNavigate(); + const route = useRoute(); + + const { register, control, getValues, reset } = + useForm({}); + + 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 ( +
+ {route.title} +
+
+ {heading} +
+ +
+ ( + + )} + /> + ( + + )} + /> +
+
+ + +
+ +
+ ); +} diff --git a/front/src/components/pages/turbine-type-page/index.ts b/front/src/components/pages/turbine-type-page/index.ts new file mode 100644 index 0000000..bb82484 --- /dev/null +++ b/front/src/components/pages/turbine-type-page/index.ts @@ -0,0 +1 @@ +export * from './component'; diff --git a/front/src/components/pages/turbine-type-page/styles.module.scss b/front/src/components/pages/turbine-type-page/styles.module.scss new file mode 100644 index 0000000..b204d88 --- /dev/null +++ b/front/src/components/pages/turbine-type-page/styles.module.scss @@ -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; +} diff --git a/front/src/components/pages/turbine-type-page/types.ts b/front/src/components/pages/turbine-type-page/types.ts new file mode 100644 index 0000000..57aa074 --- /dev/null +++ b/front/src/components/pages/turbine-type-page/types.ts @@ -0,0 +1,5 @@ +export type TurbineTypeFormValues = { + name: string; + height: string; + bladeLength: string; +}; diff --git a/front/src/components/ux/turbine-type-modal/utils.ts b/front/src/components/pages/turbine-type-page/utils.ts similarity index 74% rename from front/src/components/ux/turbine-type-modal/utils.ts rename to front/src/components/pages/turbine-type-page/utils.ts index 4a2e0a1..48436a5 100644 --- a/front/src/components/ux/turbine-type-modal/utils.ts +++ b/front/src/components/pages/turbine-type-page/utils.ts @@ -1,8 +1,8 @@ -import { TurbineType } from 'src/api/wind'; +import { TurbineType } from '@api/wind'; import { TurbineTypeFormValues } from './types'; -export const turbineTypeToFormValues = ( +export const unpackTurbineType = ( turbineType: TurbineType, ): TurbineTypeFormValues => { return { diff --git a/front/src/components/pages/turbine-types-page/component.tsx b/front/src/components/pages/turbine-types-page/component.tsx index 40e84b4..628b19e 100644 --- a/front/src/components/pages/turbine-types-page/component.tsx +++ b/front/src/components/pages/turbine-types-page/component.tsx @@ -1,16 +1,14 @@ +import { deleteTurbineType, getTurbineTypes, TurbineType } from '@api/wind'; import { Button, Heading } from '@components/ui'; import { DataGrid } from '@components/ui/data-grid'; -import { TurbineTypeModal } from '@components/ux'; -import { useRoute } from '@utils/route'; +import { ROUTES, useRoute } from '@utils/route'; 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 styles from './styles.module.scss'; export function TurbineTypesPage() { - const [createModalOpen, setCreateModalOpen] = useState(false); - const [editModalOpen, setEditModalOpen] = useState(false); const [turbineTypes, setTurbineTypes] = useState([]); const [selected, setSelected] = useState(null); const route = useRoute(); @@ -24,44 +22,34 @@ export function TurbineTypesPage() { fetchTurbineTypes(); }, []); - const handleCreateButtonClick = () => { - setCreateModalOpen(true); - }; - - const handleEditButtonClick = () => { - setEditModalOpen(true); - }; - - const handleDeleteButtonClick = async () => { - await deleteTurbineTypes(selected.id); - fetchTurbineTypes(); - }; - const handleTurbineTypeSelect = (items: TurbineType[]) => { setSelected(items[0] ?? null); }; + const handleDeleteButtonClick = async () => { + await deleteTurbineType(selected.id); + fetchTurbineTypes(); + }; + return (
- - {route.title} - + {route.title}
- - - + + + + {selected && ( + + + + )} + {selected && ( + + )}
- setCreateModalOpen(false)} - onSuccess={fetchTurbineTypes} - /> - setEditModalOpen(false)} - onSuccess={fetchTurbineTypes} - />
); } diff --git a/front/src/components/pages/turbine-types-page/styles.module.scss b/front/src/components/pages/turbine-types-page/styles.module.scss index 5d18482..2f566e4 100644 --- a/front/src/components/pages/turbine-types-page/styles.module.scss +++ b/front/src/components/pages/turbine-types-page/styles.module.scss @@ -1,6 +1,6 @@ .page { display: grid; - padding: 40px 20px 20px; + padding: 40px 20px; gap: 20px; grid-template-rows: auto auto minmax(0, 1fr); } @@ -13,7 +13,7 @@ display: flex; padding: 10px; border: 1px solid var(--clr-border-100); - border-radius: 10px; + border-radius: 15px; background-color: var(--clr-layer-200); box-shadow: 0px 1px 2px var(--clr-shadow-100); gap: 10px; diff --git a/front/src/components/ui/animation/fade/component.tsx b/front/src/components/ui/animation/fade/component.tsx index 708b0d1..f747bf9 100644 --- a/front/src/components/ui/animation/fade/component.tsx +++ b/front/src/components/ui/animation/fade/component.tsx @@ -1,5 +1,11 @@ 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 { FadeProps } from './types'; @@ -14,7 +20,7 @@ export function FadeInner( }: Omit, ref: ForwardedRef, ) { - const [visibleInner, setVisibleInner] = useState(visible); + const [visibleInternal, setVisibleInternal] = useState(visible); const classNames = clsx( styles.fade, @@ -25,22 +31,21 @@ export function FadeInner( const inlineStyle = { ...style, '--animation-duration': `${duration}ms`, - } as React.CSSProperties; + } as CSSProperties; useEffect(() => { if (visible) { - setVisibleInner(true); - return; + setVisibleInternal(true); } }, [visible]); const handleAnimationEnd = (event: React.AnimationEvent) => { if (event.animationName === styles.fadeout) { - setVisibleInner(false); + setVisibleInternal(false); } }; - if (!visibleInner) { + if (!visibleInternal) { return null; } diff --git a/front/src/components/ui/animation/fade/styles.module.scss b/front/src/components/ui/animation/fade/styles.module.scss index 6a4275d..b26b897 100644 --- a/front/src/components/ui/animation/fade/styles.module.scss +++ b/front/src/components/ui/animation/fade/styles.module.scss @@ -1,5 +1,5 @@ .fade { - animation: fadein var(--animation-duration); + animation: fadein var(--animation-duration) ease-in-out; } .invisible { @@ -9,17 +9,21 @@ @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); } } diff --git a/front/src/components/ui/animation/fade/types.ts b/front/src/components/ui/animation/fade/types.ts index 10216fb..12a3f3d 100644 --- a/front/src/components/ui/animation/fade/types.ts +++ b/front/src/components/ui/animation/fade/types.ts @@ -1,4 +1,6 @@ +import { ComponentProps } from 'react'; + export type FadeProps = { visible: boolean; duration?: number; -} & React.ComponentProps<'div'>; +} & ComponentProps<'div'>; diff --git a/front/src/components/ui/animation/index.ts b/front/src/components/ui/animation/index.ts index e6ec28a..bfdbf75 100644 --- a/front/src/components/ui/animation/index.ts +++ b/front/src/components/ui/animation/index.ts @@ -1,3 +1,2 @@ export * from './fade'; export * from './ripple'; -export * from './slide'; diff --git a/front/src/components/ui/animation/ripple/component.tsx b/front/src/components/ui/animation/ripple/component.tsx index 00fa127..0faee3a 100644 --- a/front/src/components/ui/animation/ripple/component.tsx +++ b/front/src/components/ui/animation/ripple/component.tsx @@ -50,6 +50,7 @@ export function Ripple() { }; const handlePointerDown = (event: React.MouseEvent) => { + event.stopPropagation(); const { pageX, pageY } = event; addWave(pageX, pageY); }; diff --git a/front/src/components/ui/animation/ripple/styles.module.scss b/front/src/components/ui/animation/ripple/styles.module.scss index 800e368..3495443 100644 --- a/front/src/components/ui/animation/ripple/styles.module.scss +++ b/front/src/components/ui/animation/ripple/styles.module.scss @@ -10,14 +10,15 @@ position: absolute; border-radius: 100%; background-color: var(--clr-ripple); + pointer-events: none; } .visible { - animation: fadein 0.3s linear; + animation: fadein 0.25s linear; } .invisible { - animation: fadeout 0.3s linear forwards; + animation: fadeout 0.25s linear; } @keyframes fadein { diff --git a/front/src/components/ui/animation/slide/component.tsx b/front/src/components/ui/animation/slide/component.tsx deleted file mode 100644 index 7c07f49..0000000 --- a/front/src/components/ui/animation/slide/component.tsx +++ /dev/null @@ -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, - ref: ForwardedRef, -) { - const [visibleInner, setVisibleInner] = useState(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 ( -
- ); -} - -export const Slide = forwardRef(SlideInner); diff --git a/front/src/components/ui/animation/slide/styles.module.scss b/front/src/components/ui/animation/slide/styles.module.scss deleted file mode 100644 index c4d3186..0000000 --- a/front/src/components/ui/animation/slide/styles.module.scss +++ /dev/null @@ -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); - } -} diff --git a/front/src/components/ui/animation/slide/types.ts b/front/src/components/ui/animation/slide/types.ts deleted file mode 100644 index 9c869cd..0000000 --- a/front/src/components/ui/animation/slide/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type SlideProps = { - visible: boolean; - duration?: number; -} & React.ComponentProps<'div'>; diff --git a/front/src/components/ui/autocomplete/component.tsx b/front/src/components/ui/autocomplete/component.tsx index 7d8cc44..216c12d 100644 --- a/front/src/components/ui/autocomplete/component.tsx +++ b/front/src/components/ui/autocomplete/component.tsx @@ -26,8 +26,6 @@ function AutocompleteInner( label = {}, name, id, - className, - ...props }: Omit, 'ref'>, ref: ForwardedRef, ) { @@ -39,18 +37,15 @@ function AutocompleteInner( useImperativeHandle(ref, () => autocompleteRef.current, []); - useMissClick( - [autocompleteRef, menuRef], - () => setMenuVisible(false), - menuVisible, - ); + useMissClick({ + callback: () => setMenuVisible(false), + enabled: menuVisible, + whitelist: [autocompleteRef, menuRef], + }); - const autocompleteClassName = clsx( - styles.autocomplete, - styles[scale], - { [styles.menuVisible]: menuVisible }, - className, - ); + const autocompleteClassName = clsx(styles.autocomplete, styles[scale], { + [styles.menuVisible]: menuVisible, + }); const filteredOptions = options.filter((option) => { const label = getOptionLabel(option).toLocaleLowerCase(); @@ -80,7 +75,7 @@ function AutocompleteInner( }; return ( -
+
= { options: T[]; value?: T; - getOptionKey: (option: T) => React.Key; + getOptionKey: (option: T) => Key; getOptionLabel: (option: T) => string; onChange?: (option: T) => void; scale?: Scale; label?: LabelProps; name?: string; id?: string; -} & Omit, 'onChange'>; +} & Omit, 'onChange'>; diff --git a/front/src/components/ui/button/component.tsx b/front/src/components/ui/button/component.tsx index c67973c..eeb0af8 100644 --- a/front/src/components/ui/button/component.tsx +++ b/front/src/components/ui/button/component.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import React from 'react'; +import React, { ForwardedRef, forwardRef } from 'react'; import { Ripple } from '../animation/ripple/component'; import { Comet } from '../comet'; @@ -8,16 +8,18 @@ import { COMET_VARIANT_MAP } from './constants'; import styles from './styles.module.scss'; import { ButtonProps } from './types.js'; -export function Button({ - variant = 'primary', - scale = 'm', - pending = false, - className, - children, - disabled, - ...props -}: ButtonProps) { - const classNames = clsx( +function ButtonInner( + { + variant = 'primary', + scale = 'm', + pending = false, + className, + children, + ...props + }: ButtonProps, + ref: ForwardedRef, +) { + const buttonClassName = clsx( styles.button, styles[variant], styles[scale], @@ -25,11 +27,7 @@ export function Button({ className, ); return ( - + {pending && (
@@ -40,3 +38,5 @@ export function Button({ ); } + +export const Button = forwardRef(ButtonInner); diff --git a/front/src/components/ui/button/styles.module.scss b/front/src/components/ui/button/styles.module.scss index 64cd657..6830e1a 100644 --- a/front/src/components/ui/button/styles.module.scss +++ b/front/src/components/ui/button/styles.module.scss @@ -3,14 +3,9 @@ .button { position: relative; overflow: hidden; - box-shadow: 0px 2px 2px var(--clr-shadow-200); font-weight: 500; transition: all var(--td-100) ease-in-out; - &:disabled { - pointer-events: none; - } - &:not(:disabled) { cursor: pointer; } @@ -28,6 +23,8 @@ } .pending { + pointer-events: none; + .childrenWrapper { visibility: hidden; } @@ -37,27 +34,21 @@ background-color: var(--clr-primary); color: var(--clr-on-primary); - &:hover { - background-color: var(--clr-primary-hover); - } - - &:disabled, - &.pending { - background-color: var(--clr-primary-disabled); + @media (hover: hover) { + &:hover { + background-color: var(--clr-primary-hover); + } } } .secondary { background-color: var(--clr-secondary); color: var(--clr-on-secondary); - - &:hover { - background-color: var(--clr-secondary-hover); - } - - &:disabled, - &.pending { - background-color: var(--clr-secondary-disabled); + + @media (hover: hover) { + &:hover { + background-color: var(--clr-secondary-hover); + } } } diff --git a/front/src/components/ui/calendar/types.ts b/front/src/components/ui/calendar/types.ts index ea2b469..13481a1 100644 --- a/front/src/components/ui/calendar/types.ts +++ b/front/src/components/ui/calendar/types.ts @@ -1,6 +1,8 @@ +import { ComponentProps } from 'react'; + export type CalendarProps = { value?: string; onChange: (value: string) => void; min: Date | null; max: Date | null; -} & Omit, 'onChange'>; +} & Omit, 'onChange'>; diff --git a/front/src/components/ui/checkbox/styles.module.scss b/front/src/components/ui/checkbox/styles.module.scss index eb6bbf9..7a348d7 100644 --- a/front/src/components/ui/checkbox/styles.module.scss +++ b/front/src/components/ui/checkbox/styles.module.scss @@ -45,7 +45,7 @@ .checkbox { display: flex; justify-content: center; - border: 2px solid var(--clr-border-200); + border: 1px solid var(--clr-border-200); background-color: var(--clr-layer-300); box-shadow: 0px 2px 2px var(--clr-shadow-200); transition: all var(--td-100) ease-in-out; diff --git a/front/src/components/ui/comet/types.ts b/front/src/components/ui/comet/types.ts index 2508855..be329ec 100644 --- a/front/src/components/ui/comet/types.ts +++ b/front/src/components/ui/comet/types.ts @@ -1,6 +1,8 @@ +import { ComponentProps } from 'react'; + import { Scale } from '../types'; export type CometProps = { scale?: Scale; variant?: 'onPrimary' | 'onSecondary'; -} & React.ComponentProps<'div'>; +} & ComponentProps<'div'>; diff --git a/front/src/components/ui/data-grid/component.tsx b/front/src/components/ui/data-grid/component.tsx index 58a9417..c40c562 100644 --- a/front/src/components/ui/data-grid/component.tsx +++ b/front/src/components/ui/data-grid/component.tsx @@ -1,71 +1,67 @@ -import { arrayToObject } from '@utils/array'; -import clsx from 'clsx'; import React, { useMemo, useState } from 'react'; import { DataGridHeader, DataGridRow } from './components'; -import styles from './styles.module.scss'; import { DataGridProps } from './types'; export function DataGrid({ items, columns, getItemKey, - className, selectedItems, onItemsSelect, - multiselect = true, + multiselect, + className, ...props }: DataGridProps) { - const [allRowsSelected, setAllRowsSelected] = useState(false); + const [allItemsSelected, setAllItemsSelected] = useState(false); - const selectedItemsMap = useMemo( - () => arrayToObject(selectedItems, (i) => getItemKey(i)), - [selectedItems], - ); + const selectedItemsMap = useMemo(() => { + const map: Record = {}; + 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) { return; } - setAllRowsSelected(!allRowsSelected); - if (allRowsSelected) { - onItemsSelect([]); - } else { - onItemsSelect([...items]); - } + onItemsSelect?.(allItemsSelected ? [] : [...items]); + setAllItemsSelected(!allItemsSelected); }; - const handleRowSelect = (item: T) => { - setAllRowsSelected(false); - const key = getItemKey(item); - const selected = selectedItemsMap[key]; + const handleItemSelect = (key: string, item: T) => { + const selected = Boolean(selectedItemsMap[key]); if (!multiselect) { onItemsSelect(selected ? [] : [item]); - } else { - onItemsSelect( - selected - ? selectedItems.filter((i) => key !== getItemKey(i)) - : [...selectedItems, item], - ); + return; } + onItemsSelect?.( + selected + ? selectedItems.filter((i) => key !== getItemKey(i)) + : [...selectedItems, item], + ); + setAllItemsSelected(false); }; return ( -
+
{items.map((item) => { - const key = getItemKey(item); + const key = String(getItemKey(item)); return ( handleRowSelect(item)} - key={key} + selected={Boolean(selectedItemsMap[key])} + onSelect={() => handleItemSelect(key, item)} + key={getItemKey(item)} /> ); })} diff --git a/front/src/components/ui/data-grid/components/DataGridHeader/component.tsx b/front/src/components/ui/data-grid/components/DataGridHeader/component.tsx index c4df8d7..b5fdfd7 100644 --- a/front/src/components/ui/data-grid/components/DataGridHeader/component.tsx +++ b/front/src/components/ui/data-grid/components/DataGridHeader/component.tsx @@ -12,8 +12,8 @@ import { DataGridHeaderProps } from './types'; export function DataGridHeader({ columns, - allRowsSelected, - onSelectAllRows, + allItemsSelected, + onSelectAllItems, }: DataGridHeaderProps) { const [sort, setSort] = useState({ order: 'asc', column: '' }); @@ -32,8 +32,8 @@ export function DataGridHeader({ return (
{columns.map((column) => { diff --git a/front/src/components/ui/data-grid/components/DataGridHeader/styles.module.scss b/front/src/components/ui/data-grid/components/DataGridHeader/styles.module.scss index 16cb851..8c37034 100644 --- a/front/src/components/ui/data-grid/components/DataGridHeader/styles.module.scss +++ b/front/src/components/ui/data-grid/components/DataGridHeader/styles.module.scss @@ -6,6 +6,7 @@ padding: 10px; border: solid 1px var(--clr-border-100); background-color: var(--clr-layer-300); + border-top-left-radius: 10px; } .cell { @@ -18,12 +19,17 @@ border: solid 1px var(--clr-border-100); background-color: var(--clr-layer-300); cursor: pointer; - font-weight: 500; gap: 10px; transition: all var(--td-100) ease-in-out; - &:hover { - background-color: var(--clr-layer-300-hover); + &:last-of-type { + border-top-right-radius: 10px; + } + + @media (hover: hover) { + &:hover { + background-color: var(--clr-layer-300-hover); + } } } diff --git a/front/src/components/ui/data-grid/components/DataGridHeader/types.ts b/front/src/components/ui/data-grid/components/DataGridHeader/types.ts index dec95a3..590d30a 100644 --- a/front/src/components/ui/data-grid/components/DataGridHeader/types.ts +++ b/front/src/components/ui/data-grid/components/DataGridHeader/types.ts @@ -2,6 +2,6 @@ import { DataGridColumnConfig } from '../../types'; export type DataGridHeaderProps = { columns: DataGridColumnConfig[]; - allRowsSelected: boolean; - onSelectAllRows: () => void; + allItemsSelected: boolean; + onSelectAllItems: () => void; }; diff --git a/front/src/components/ui/data-grid/components/DataGridRow/component.tsx b/front/src/components/ui/data-grid/components/DataGridRow/component.tsx index 3bf6f7c..c55621e 100644 --- a/front/src/components/ui/data-grid/components/DataGridRow/component.tsx +++ b/front/src/components/ui/data-grid/components/DataGridRow/component.tsx @@ -24,7 +24,7 @@ export function DataGridRow({ style={{ flex: column.flex }} key={column.name} > - {column.getText(object)} + {column.getText(object)}
))}
diff --git a/front/src/components/ui/data-grid/preview.tsx b/front/src/components/ui/data-grid/preview.tsx index 4695146..99d8183 100644 --- a/front/src/components/ui/data-grid/preview.tsx +++ b/front/src/components/ui/data-grid/preview.tsx @@ -1,10 +1,19 @@ import { PreviewArticle } from '@components/ui/preview'; -import React from 'react'; +import React, { useState } from 'react'; 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() { + const [selectedItems, setSelectedItems] = useState([]); + const items: Cat[] = [ { name: 'Luna', breed: 'British Shorthair', color: 'Gray', age: '2' }, { name: 'Simba', breed: 'Siamese', color: 'Cream', age: '1' }, @@ -14,15 +23,24 @@ export function DataGridPreview() { ]; const columns: DataGridColumnConfig[] = [ - { name: 'Name', getText: (cat) => cat.name, flex: '2' }, - { name: 'Breed', getText: (cat) => cat.breed }, + { name: 'Name', getText: (cat) => cat.name }, + { name: 'Breed', getText: (cat) => cat.breed, scale: 2 }, { name: 'Age', getText: (cat) => cat.age }, { name: 'Color', getText: (cat) => cat.color }, ]; return ( - +
+ name} + selectedItems={selectedItems} + onItemsSelect={setSelectedItems} + /> +
); } diff --git a/front/src/components/ui/data-grid/styles.module.scss b/front/src/components/ui/data-grid/styles.module.scss deleted file mode 100644 index e67efd1..0000000 --- a/front/src/components/ui/data-grid/styles.module.scss +++ /dev/null @@ -1,4 +0,0 @@ -.dataGrid { - border-radius: 10px; - box-shadow: 0px 2px 2px var(--clr-shadow-200); -} diff --git a/front/src/components/ui/data-grid/types.ts b/front/src/components/ui/data-grid/types.ts index 45e0a5e..253debf 100644 --- a/front/src/components/ui/data-grid/types.ts +++ b/front/src/components/ui/data-grid/types.ts @@ -1,3 +1,5 @@ +import { ComponentPropsWithoutRef, Key } from 'react'; + export type DataGridColumnConfig = { name: string; getText: (object: T) => string; @@ -13,15 +15,8 @@ export type DataGridSort = { export type DataGridProps = { items: T[]; columns: DataGridColumnConfig[]; - getItemKey: (object: T) => string; - selectedItems: T[]; - onItemsSelect: (selectedItems: T[]) => void; + getItemKey: (item: T) => Key; + selectedItems?: T[]; + onItemsSelect?: (selectedItems: T[]) => void; multiselect?: boolean; -} & React.ComponentPropsWithoutRef<'div'>; - -export type Cat = { - name: string; - breed: string; - age: string; - color: string; -}; +} & ComponentPropsWithoutRef<'div'>; diff --git a/front/src/components/ui/date-input/component.tsx b/front/src/components/ui/date-input/component.tsx index c60ca7c..1a80cd3 100644 --- a/front/src/components/ui/date-input/component.tsx +++ b/front/src/components/ui/date-input/component.tsx @@ -37,11 +37,11 @@ export function DateInput({ setDirtyDate(valueToDirtyDate(value)); }, [value]); - useMissClick( - [wrapperRef, calendarWrapperRef], - () => setCalendarVisible(false), - calendarVisible, - ); + useMissClick({ + callback: () => setCalendarVisible(false), + enabled: calendarVisible, + whitelist: [wrapperRef, calendarWrapperRef], + }); const handleCalendarButtonClick = () => { setCalendarVisible(!calendarVisible); diff --git a/front/src/components/ui/dialog/component.tsx b/front/src/components/ui/dialog/component.tsx new file mode 100644 index 0000000..0fcfa10 --- /dev/null +++ b/front/src/components/ui/dialog/component.tsx @@ -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(null); + + useMissClick({ + callback: onClose, + enabled: open, + whitelist: [dialogRef, triggerElementRef], + }); + + const dialogClassName = clsx(styles.dialog, className); + + return ( + + +
+ {heading} + {message} + {children} +
+
+
+ ); +} diff --git a/front/src/components/ui/dialog/index.ts b/front/src/components/ui/dialog/index.ts new file mode 100644 index 0000000..fb2697a --- /dev/null +++ b/front/src/components/ui/dialog/index.ts @@ -0,0 +1,2 @@ +export * from './component'; +export * from './preview'; diff --git a/front/src/components/ui/dialog/preview.tsx b/front/src/components/ui/dialog/preview.tsx new file mode 100644 index 0000000..e3a9d44 --- /dev/null +++ b/front/src/components/ui/dialog/preview.tsx @@ -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(null); + const [open, setOpen] = useState(false); + + return ( + + + setOpen(false)} + heading="Dialog" + message="Short dialog message" + triggerElementRef={openButtonRef} + > +
+ + +
+
+
+ ); +} diff --git a/front/src/components/ui/dialog/styles.module.scss b/front/src/components/ui/dialog/styles.module.scss new file mode 100644 index 0000000..71b4e34 --- /dev/null +++ b/front/src/components/ui/dialog/styles.module.scss @@ -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; +} diff --git a/front/src/components/ui/dialog/types.ts b/front/src/components/ui/dialog/types.ts new file mode 100644 index 0000000..9478b35 --- /dev/null +++ b/front/src/components/ui/dialog/types.ts @@ -0,0 +1,9 @@ +import { ComponentPropsWithoutRef, MutableRefObject } from 'react'; + +export type DialogProps = { + heading: string; + message: string; + open: boolean; + onClose: () => void; + triggerElementRef?: MutableRefObject; +} & ComponentPropsWithoutRef<'div'>; diff --git a/front/src/components/ui/file-uploader/types.ts b/front/src/components/ui/file-uploader/types.ts index 95b5365..564bbdc 100644 --- a/front/src/components/ui/file-uploader/types.ts +++ b/front/src/components/ui/file-uploader/types.ts @@ -1,3 +1,5 @@ +import { ComponentPropsWithoutRef } from 'react'; + import { LabelProps } from '../label'; import { RawInputProps } from '../raw'; import { Scale } from '../types'; @@ -8,4 +10,4 @@ export type FileUploaderProps = { scale?: Scale; label?: LabelProps; input?: Omit; -} & Omit, 'onChange'>; +} & Omit, 'onChange'>; diff --git a/front/src/components/ui/heading/types.ts b/front/src/components/ui/heading/types.ts index 0b11346..d9e8965 100644 --- a/front/src/components/ui/heading/types.ts +++ b/front/src/components/ui/heading/types.ts @@ -1,3 +1,5 @@ +import { ComponentProps } from 'react'; + import { TextColor } from '../types'; 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 = { tag: T; color?: TextColor; -} & React.ComponentProps; +} & ComponentProps; diff --git a/front/src/components/ui/icon-button/component.tsx b/front/src/components/ui/icon-button/component.tsx index 1e8fcbd..1f30bb5 100644 --- a/front/src/components/ui/icon-button/component.tsx +++ b/front/src/components/ui/icon-button/component.tsx @@ -1,22 +1,33 @@ import clsx from 'clsx'; -import React from 'react'; +import React, { ForwardedRef, forwardRef } from 'react'; import { Ripple } from '../animation'; import { RawButton } from '../raw'; import styles from './styles.module.scss'; import { IconButtonProps } from './types'; -export function IconButton({ - scale = 'm', - className, - children, - ...props -}: IconButtonProps) { - const classes = clsx(styles.button, styles[scale], className); +function IconButtonInner( + { + variant = 'circle', + scale = 'm', + className, + children, + ...props + }: IconButtonProps, + ref: ForwardedRef, +) { + const buttonClassName = clsx( + styles.button, + styles[scale], + styles[variant], + className, + ); return ( - + {children} ); } + +export const IconButton = forwardRef(IconButtonInner); diff --git a/front/src/components/ui/icon-button/styles.module.scss b/front/src/components/ui/icon-button/styles.module.scss index 2044968..d0c39c1 100644 --- a/front/src/components/ui/icon-button/styles.module.scss +++ b/front/src/components/ui/icon-button/styles.module.scss @@ -3,44 +3,75 @@ .button { position: relative; overflow: hidden; - border-radius: 100%; background-color: transparent; cursor: pointer; transition: all var(--td-100) ease-in-out; - &:hover { - background-color: var(--clr-ripple); - - svg { - fill: var(--clr-text-300); - } - } - svg { width: 100%; height: 100%; fill: var(--clr-text-100); 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; -$padding: 4px; +$rect-padding: 6px; +$circle-padding: 4px; +$border-radius: 8px; .s { width: $size; height: $size; - padding: $padding; + + &.rect { + padding: $rect-padding; + border-radius: $border-radius; + } + + &.circle { + padding: $circle-padding; + } } .m { width: 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 { width: 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); + } } diff --git a/front/src/components/ui/icon-button/types.ts b/front/src/components/ui/icon-button/types.ts index 82de98a..bfaee29 100644 --- a/front/src/components/ui/icon-button/types.ts +++ b/front/src/components/ui/icon-button/types.ts @@ -3,5 +3,6 @@ import { Scale } from '@components/ui/types'; import { RawButtonProps } from '../raw'; export type IconButtonProps = { + variant?: 'circle' | 'rect'; scale?: Scale; } & RawButtonProps; diff --git a/front/src/components/ui/image-file-manager/types.ts b/front/src/components/ui/image-file-manager/types.ts index 9f59dab..9014ff8 100644 --- a/front/src/components/ui/image-file-manager/types.ts +++ b/front/src/components/ui/image-file-manager/types.ts @@ -6,4 +6,4 @@ export type ImageFileManagerProps = { onChange?: (value: File | null) => void; scale?: Scale; label?: LabelProps; -} & Omit, 'onChange'>; +} & Omit, 'onChange'>; diff --git a/front/src/components/ui/index.tsx b/front/src/components/ui/index.ts similarity index 86% rename from front/src/components/ui/index.tsx rename to front/src/components/ui/index.ts index 503045e..83cf09b 100644 --- a/front/src/components/ui/index.tsx +++ b/front/src/components/ui/index.ts @@ -7,9 +7,12 @@ export { FileUploader } from './file-uploader'; export { Heading } from './heading'; export { IconButton } from './icon-button'; export { ImageFileManager } from './image-file-manager'; +export { LinkButton } from './link-button'; export { Menu } from './menu'; +export { Dialog } from './dialog'; export { NumberInput } from './number-input'; export { Overlay } from './overlay'; +export { Pagination } from './pagination'; export { Paragraph } from './paragraph'; export { PasswordInput } from './password-input'; export { RadioGroup } from './radio-group'; diff --git a/front/src/components/ui/input/component.tsx b/front/src/components/ui/input/component.tsx index 97921b6..8eeb60c 100644 --- a/front/src/components/ui/input/component.tsx +++ b/front/src/components/ui/input/component.tsx @@ -11,6 +11,7 @@ function InputInner( wrapper = {}, leftNode, rightNode, + invalid, className, onFocus, onBlur, @@ -24,6 +25,7 @@ function InputInner( styles.wrapper, focus && styles.wrapperFocus, wrapper.className, + invalid && styles.invalid, ); const inputClassNames = clsx(styles.input, className); diff --git a/front/src/components/ui/input/styles.module.scss b/front/src/components/ui/input/styles.module.scss index 1a68299..30d43f3 100644 --- a/front/src/components/ui/input/styles.module.scss +++ b/front/src/components/ui/input/styles.module.scss @@ -14,6 +14,10 @@ } } +.invalid { + border-color: var(--clr-error); +} + .wrapperFocus { z-index: 1; border-color: var(--clr-primary); diff --git a/front/src/components/ui/input/types.ts b/front/src/components/ui/input/types.ts index 4b88da0..8a4a0cc 100644 --- a/front/src/components/ui/input/types.ts +++ b/front/src/components/ui/input/types.ts @@ -1,12 +1,14 @@ import { Scale } from '@components/ui/types'; +import { ComponentProps, ReactNode } from 'react'; import { RawInputProps } from '../raw'; type InputProps = { scale?: Scale; - wrapper?: React.ComponentProps<'div'>; - leftNode?: React.ReactNode; - rightNode?: React.ReactNode; + wrapper?: ComponentProps<'div'>; + leftNode?: ReactNode; + rightNode?: ReactNode; + invalid?: boolean; } & RawInputProps; export { InputProps }; diff --git a/front/src/components/ui/label/component.tsx b/front/src/components/ui/label/component.tsx index 4802220..8741eb8 100644 --- a/front/src/components/ui/label/component.tsx +++ b/front/src/components/ui/label/component.tsx @@ -11,6 +11,7 @@ function LabelInner( scale, position = 'top', required = {}, + error, className, children, ...props @@ -29,6 +30,11 @@ function LabelInner( )} {!reversed && children} + {error && ( + + {error} + + )} ); } diff --git a/front/src/components/ui/label/styles.module.scss b/front/src/components/ui/label/styles.module.scss index 184cd50..e8a5779 100644 --- a/front/src/components/ui/label/styles.module.scss +++ b/front/src/components/ui/label/styles.module.scss @@ -1,5 +1,9 @@ .label { display: flex; + + .error { + color: var(--clr-error); + } } .left, diff --git a/front/src/components/ui/label/types.ts b/front/src/components/ui/label/types.ts index a3113e4..4360229 100644 --- a/front/src/components/ui/label/types.ts +++ b/front/src/components/ui/label/types.ts @@ -1,3 +1,5 @@ +import { ComponentProps } from 'react'; + import { Required, Scale } from '../types'; export type LabelProps = { @@ -5,4 +7,5 @@ export type LabelProps = { scale?: Scale; position?: 'left' | 'top' | 'right' | 'bottom'; required?: Required; -} & React.ComponentProps<'label'>; + error?: string; +} & ComponentProps<'label'>; diff --git a/front/src/components/ui/link-button/component.tsx b/front/src/components/ui/link-button/component.tsx new file mode 100644 index 0000000..ce6207a --- /dev/null +++ b/front/src/components/ui/link-button/component.tsx @@ -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 ( + + {children} + + + ); +} diff --git a/front/src/components/ui/link-button/index.tsx b/front/src/components/ui/link-button/index.tsx new file mode 100644 index 0000000..bb82484 --- /dev/null +++ b/front/src/components/ui/link-button/index.tsx @@ -0,0 +1 @@ +export * from './component'; diff --git a/front/src/components/ui/link-button/styles.module.scss b/front/src/components/ui/link-button/styles.module.scss new file mode 100644 index 0000000..f477aea --- /dev/null +++ b/front/src/components/ui/link-button/styles.module.scss @@ -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); +} diff --git a/front/src/components/ui/link-button/types.ts b/front/src/components/ui/link-button/types.ts new file mode 100644 index 0000000..e9e0d95 --- /dev/null +++ b/front/src/components/ui/link-button/types.ts @@ -0,0 +1,7 @@ +import { ComponentPropsWithoutRef } from 'react'; + +import { Scale } from '../types'; + +export type LinkButtonProps = { + scale?: Scale; +} & ComponentPropsWithoutRef<'a'>; diff --git a/front/src/components/ui/menu/types.ts b/front/src/components/ui/menu/types.ts index bcc3787..5c22e09 100644 --- a/front/src/components/ui/menu/types.ts +++ b/front/src/components/ui/menu/types.ts @@ -1,7 +1,9 @@ +import { ComponentProps, Key } from 'react'; + export type MenuProps = { options: T[]; selected?: T; - getOptionKey: (option: T) => React.Key; + getOptionKey: (option: T) => Key; getOptionLabel: (option: T) => string; onSelect?: (option: T) => void; -} & Omit, 'onSelect'>; +} & Omit, 'onSelect'>; diff --git a/front/src/components/ui/overlay/component.tsx b/front/src/components/ui/overlay/component.tsx index f35c483..1d4952d 100644 --- a/front/src/components/ui/overlay/component.tsx +++ b/front/src/components/ui/overlay/component.tsx @@ -1,16 +1,47 @@ import clsx from 'clsx'; -import React from 'react'; +import React, { ForwardedRef, forwardRef, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; -import { Fade } from '../animation'; import styles from './styles.module.scss'; import { OverlayProps } from './types'; -export function Overlay({ open, children, variant = 'small' }: OverlayProps) { +function OverlayInner( + { open, className, ...props }: OverlayProps, + ref: ForwardedRef, +) { + const [openInternal, setOpenInternal] = useState(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( - -
{children}
-
, +
, document.body, ); } + +export const Overlay = forwardRef(OverlayInner); diff --git a/front/src/components/ui/overlay/styles.module.scss b/front/src/components/ui/overlay/styles.module.scss index a48431c..58ac456 100644 --- a/front/src/components/ui/overlay/styles.module.scss +++ b/front/src/components/ui/overlay/styles.module.scss @@ -1,27 +1,30 @@ -.backdrop { +.overlay { position: absolute; top: 0; left: 0; - display: grid; - width: 100%; - height: 100%; - padding: 20px; - backdrop-filter: blur(5px); - background-color: rgba(0 0 0 / 0.75); + width: 100vw; + height: 100dvh; + animation: fadein 0.25s forwards ease-in-out; } -.small { - grid-template: - '. . .' 1fr - '. form .' auto - '. . .' 5fr - / 1fr minmax(0, 400px) 1fr; +.closed { + animation: fadeout 0.25s forwards ease-in-out; } -.large { - grid-template: - '. . .' 1fr - '. form .' auto - '. . .' 5fr - / 1fr minmax(0, 600px) 1fr; +@keyframes fadein { + from { + background-color: rgba(0 0 0 / 0); + } + to { + 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); + } } diff --git a/front/src/components/ui/overlay/types.ts b/front/src/components/ui/overlay/types.ts index 9f40902..166871f 100644 --- a/front/src/components/ui/overlay/types.ts +++ b/front/src/components/ui/overlay/types.ts @@ -1,7 +1,5 @@ -import { ReactNode } from 'react'; +import { ComponentPropsWithoutRef } from 'react'; export type OverlayProps = { open: boolean; - children: ReactNode; - variant?: 'small' | 'large'; -}; +} & ComponentPropsWithoutRef<'div'>; diff --git a/front/src/components/ui/pagination/component.tsx b/front/src/components/ui/pagination/component.tsx new file mode 100644 index 0000000..2bec239 --- /dev/null +++ b/front/src/components/ui/pagination/component.tsx @@ -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 ( +
+ + + + {pageNumbers.map((number, index) => { + if (number === null) { + return ( + + ... + + ); + } + const isCurrent = number === value; + return ( + onChange?.(number)} + > + {number} + + ); + })} + + + +
+ ); +} diff --git a/front/src/components/ui/pagination/components/index.ts b/front/src/components/ui/pagination/components/index.ts new file mode 100644 index 0000000..ea4561b --- /dev/null +++ b/front/src/components/ui/pagination/components/index.ts @@ -0,0 +1 @@ +export * from './pagination-item'; diff --git a/front/src/components/ui/pagination/components/pagination-item/component.tsx b/front/src/components/ui/pagination/components/pagination-item/component.tsx new file mode 100644 index 0000000..8e27bf5 --- /dev/null +++ b/front/src/components/ui/pagination/components/pagination-item/component.tsx @@ -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 ( + + {children} + + + ); +} diff --git a/front/src/components/ui/pagination/components/pagination-item/index.ts b/front/src/components/ui/pagination/components/pagination-item/index.ts new file mode 100644 index 0000000..bb82484 --- /dev/null +++ b/front/src/components/ui/pagination/components/pagination-item/index.ts @@ -0,0 +1 @@ +export * from './component'; diff --git a/front/src/components/ui/pagination/components/pagination-item/styles.module.scss b/front/src/components/ui/pagination/components/pagination-item/styles.module.scss new file mode 100644 index 0000000..b78bdf0 --- /dev/null +++ b/front/src/components/ui/pagination/components/pagination-item/styles.module.scss @@ -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); + } +} diff --git a/front/src/components/ui/pagination/components/pagination-item/types.ts b/front/src/components/ui/pagination/components/pagination-item/types.ts new file mode 100644 index 0000000..a2ea2fa --- /dev/null +++ b/front/src/components/ui/pagination/components/pagination-item/types.ts @@ -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'>; diff --git a/front/src/components/ui/pagination/index.ts b/front/src/components/ui/pagination/index.ts new file mode 100644 index 0000000..1d89bd8 --- /dev/null +++ b/front/src/components/ui/pagination/index.ts @@ -0,0 +1,2 @@ +export { Pagination } from './component'; +export { PaginationPreview } from './preview'; diff --git a/front/src/components/ui/pagination/preview.tsx b/front/src/components/ui/pagination/preview.tsx new file mode 100644 index 0000000..1c57082 --- /dev/null +++ b/front/src/components/ui/pagination/preview.tsx @@ -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(1); + const [value2, setValue2] = useState(1); + const [value3, setValue3] = useState(1); + + return ( + + + + + + ); +} diff --git a/front/src/components/ui/pagination/styles.module.scss b/front/src/components/ui/pagination/styles.module.scss new file mode 100644 index 0000000..50ed4da --- /dev/null +++ b/front/src/components/ui/pagination/styles.module.scss @@ -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); +} diff --git a/front/src/components/ui/pagination/types.ts b/front/src/components/ui/pagination/types.ts new file mode 100644 index 0000000..fde3f63 --- /dev/null +++ b/front/src/components/ui/pagination/types.ts @@ -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; +}; diff --git a/front/src/components/ui/pagination/utils.ts b/front/src/components/ui/pagination/utils.ts new file mode 100644 index 0000000..e5a7b11 --- /dev/null +++ b/front/src/components/ui/pagination/utils.ts @@ -0,0 +1,30 @@ +export const getPageNumbers = ( + page: number, + total: number, + sibling: number, + boundary: number, +): number[] => { + const pages: number[] = []; + const visible = Math.min(total, (boundary + sibling + 1) * 2 + 1); + + const isOverflow = visible !== total; + const isLeftOverflow = isOverflow && page > boundary + sibling + 2; + const isRightOverflow = isOverflow && total - page > boundary + sibling + 1; + + let cursor = 1; + + for (let i = 1; i <= visible; i += 1) { + if (i === boundary + 1 && isLeftOverflow) { + pages.push(null); + cursor = Math.min(page - sibling, total - boundary - sibling * 2 - 1); + } else if (i === visible - boundary && isRightOverflow) { + pages.push(null); + cursor = total - boundary + 1; + } else { + pages.push(cursor); + cursor += 1; + } + } + + return pages; +}; diff --git a/front/src/components/ui/paragraph/types.ts b/front/src/components/ui/paragraph/types.ts index 8aa96c0..44071b1 100644 --- a/front/src/components/ui/paragraph/types.ts +++ b/front/src/components/ui/paragraph/types.ts @@ -1,8 +1,10 @@ +import { ComponentPropsWithoutRef } from 'react'; + import { Scale, TextColor } from '../types'; type ParagraphProps = { scale?: Scale; color?: TextColor; -} & React.ComponentPropsWithoutRef<'p'>; +} & ComponentPropsWithoutRef<'p'>; export { ParagraphProps }; diff --git a/front/src/components/ui/popover/component.tsx b/front/src/components/ui/popover/component.tsx index 6edcc39..351dfd6 100644 --- a/front/src/components/ui/popover/component.tsx +++ b/front/src/components/ui/popover/component.tsx @@ -7,7 +7,7 @@ import React, { } from 'react'; import { createPortal } from 'react-dom'; -import { Slide } from '../animation'; +import { Fade } from '../animation'; import styles from './styles.module.scss'; import { PopoverProps } from './types'; import { calcFadeStyles } from './utils'; @@ -58,7 +58,7 @@ export function Popover({ } return createPortal( - {element} - , + , document.body, ); } diff --git a/front/src/components/ui/popover/types.ts b/front/src/components/ui/popover/types.ts index dffba38..17c15a7 100644 --- a/front/src/components/ui/popover/types.ts +++ b/front/src/components/ui/popover/types.ts @@ -1,12 +1,14 @@ +import { MutableRefObject, ReactElement } from 'react'; + export type PopoverPosition = 'top' | 'bottom'; export type PopoverHorizontalAlign = 'left' | 'right' | 'center' | 'stretch'; export type PopoverProps = { visible: boolean; - anchorRef: React.MutableRefObject; + anchorRef: MutableRefObject; position: PopoverPosition; horizontalAlign: PopoverHorizontalAlign; - element: React.ReactElement; + element: ReactElement; flip?: boolean; }; diff --git a/front/src/components/ui/popover/utils.ts b/front/src/components/ui/popover/utils.ts index 460a50e..7ceb356 100644 --- a/front/src/components/ui/popover/utils.ts +++ b/front/src/components/ui/popover/utils.ts @@ -1,5 +1,5 @@ import { px } from '@utils/css'; -import { CSSProperties } from 'react'; +import { CSSProperties, MutableRefObject } from 'react'; import { PopoverHorizontalAlign, PopoverPosition } from './types'; @@ -77,7 +77,7 @@ const applyHorizontalAlign = ( export const calcFadeStyles = ( elementRect: DOMRect, - anchorRef: React.MutableRefObject, + anchorRef: MutableRefObject, position: PopoverPosition, horizontalAlign: PopoverHorizontalAlign, flip: boolean, diff --git a/front/src/components/ui/preview/preview-article/types.ts b/front/src/components/ui/preview/preview-article/types.ts index 5ed3e57..b7ab9f6 100644 --- a/front/src/components/ui/preview/preview-article/types.ts +++ b/front/src/components/ui/preview/preview-article/types.ts @@ -1,3 +1,5 @@ +import { ComponentProps } from 'react'; + export type PreviewArticleProps = { title: string; -} & React.ComponentProps<'div'>; +} & ComponentProps<'div'>; diff --git a/front/src/components/ui/preview/preview-box/types.ts b/front/src/components/ui/preview/preview-box/types.ts index 3663ae7..925bb31 100644 --- a/front/src/components/ui/preview/preview-box/types.ts +++ b/front/src/components/ui/preview/preview-box/types.ts @@ -1 +1,3 @@ -export type PreviewBoxProps = {} & React.ComponentProps<'div'>; +import { ComponentProps } from 'react'; + +export type PreviewBoxProps = {} & ComponentProps<'div'>; diff --git a/front/src/components/ui/radio-group/types.ts b/front/src/components/ui/radio-group/types.ts index 61939dd..416318a 100644 --- a/front/src/components/ui/radio-group/types.ts +++ b/front/src/components/ui/radio-group/types.ts @@ -1,3 +1,5 @@ +import { Key } from 'react'; + import { Scale } from '../types'; export type RadioGroupProps = { @@ -5,7 +7,7 @@ export type RadioGroupProps = { value: T; items: T[]; onChange: (value: T) => void; - getItemKey: (item: T) => React.Key; + getItemKey: (item: T) => Key; getItemLabel: (item: T) => string; scale?: Scale; label?: string; diff --git a/front/src/components/ui/radio/styles.module.scss b/front/src/components/ui/radio/styles.module.scss index 928e2e8..38845ea 100644 --- a/front/src/components/ui/radio/styles.module.scss +++ b/front/src/components/ui/radio/styles.module.scss @@ -26,7 +26,7 @@ .radio { display: flex; justify-content: center; - border: 2px solid var(--clr-border-200); + border: 1px solid var(--clr-border-200); border-radius: 100%; background-color: var(--clr-layer-300); box-shadow: 0px 2px 2px var(--clr-shadow-200); diff --git a/front/src/components/ui/raw/raw-button/styles.module.scss b/front/src/components/ui/raw/raw-button/styles.module.scss index 7c7e305..68d9190 100644 --- a/front/src/components/ui/raw/raw-button/styles.module.scss +++ b/front/src/components/ui/raw/raw-button/styles.module.scss @@ -3,4 +3,8 @@ border: none; background-color: transparent; font: inherit; + + &:disabled { + pointer-events: none; + } } diff --git a/front/src/components/ui/raw/raw-button/types.ts b/front/src/components/ui/raw/raw-button/types.ts index 5cfecd4..a418e27 100644 --- a/front/src/components/ui/raw/raw-button/types.ts +++ b/front/src/components/ui/raw/raw-button/types.ts @@ -1 +1,3 @@ -export type RawButtonProps = {} & React.ComponentProps<'button'>; +import { ComponentProps } from 'react'; + +export type RawButtonProps = {} & ComponentProps<'button'>; diff --git a/front/src/components/ui/raw/raw-input/types.ts b/front/src/components/ui/raw/raw-input/types.ts index c47878a..452da97 100644 --- a/front/src/components/ui/raw/raw-input/types.ts +++ b/front/src/components/ui/raw/raw-input/types.ts @@ -1 +1,3 @@ -export type RawInputProps = {} & React.ComponentProps<'input'>; +import { ComponentProps } from 'react'; + +export type RawInputProps = {} & ComponentProps<'input'>; diff --git a/front/src/components/ui/select/component.tsx b/front/src/components/ui/select/component.tsx index 58b0bc0..d6e271c 100644 --- a/front/src/components/ui/select/component.tsx +++ b/front/src/components/ui/select/component.tsx @@ -36,7 +36,11 @@ function SelectInner( useImperativeHandle(ref, () => selectRef.current, []); - useMissClick([selectRef, menuRef], () => setMenuVisible(false), menuVisible); + useMissClick({ + callback: () => setMenuVisible(false), + enabled: menuVisible, + whitelist: [selectRef, menuRef], + }); const selectClassName = clsx(styles.select, styles[scale], { [styles.menuVisible]: menuVisible, diff --git a/front/src/components/ui/select/types.ts b/front/src/components/ui/select/types.ts index fe454e2..2dc6e45 100644 --- a/front/src/components/ui/select/types.ts +++ b/front/src/components/ui/select/types.ts @@ -1,16 +1,16 @@ +import { ComponentProps, Key } from 'react'; + import { LabelProps } from '../label'; -import { TextInputProps } from '../text-input'; import { Scale } from '../types'; export type SelectProps = { options: T[]; value?: T; - getOptionKey: (option: T) => React.Key; + getOptionKey: (option: T) => Key; getOptionLabel: (option: T) => string; onChange?: (option: T) => void; scale?: Scale; label?: LabelProps; name?: string; id?: string; - input?: TextInputProps; -} & Omit, 'onChange'>; +} & Omit, 'onChange'>; diff --git a/front/src/components/ui/span/types.ts b/front/src/components/ui/span/types.ts index b86af5f..1c28afd 100644 --- a/front/src/components/ui/span/types.ts +++ b/front/src/components/ui/span/types.ts @@ -1,8 +1,10 @@ +import { ComponentPropsWithoutRef } from 'react'; + import { Scale, TextColor } from '../types'; type SpanProps = { scale?: Scale; color?: TextColor; -} & React.ComponentPropsWithoutRef<'span'>; +} & ComponentPropsWithoutRef<'span'>; export { SpanProps }; diff --git a/front/src/components/ui/text-area/types.ts b/front/src/components/ui/text-area/types.ts index d7adf08..e8e7cb2 100644 --- a/front/src/components/ui/text-area/types.ts +++ b/front/src/components/ui/text-area/types.ts @@ -1,7 +1,9 @@ +import { ComponentPropsWithoutRef } from 'react'; + import { LabelProps } from '../label'; import { Scale } from '../types'; export type TextAreaProps = { scale?: Scale; label?: LabelProps; -} & React.ComponentPropsWithoutRef<'textarea'>; +} & ComponentPropsWithoutRef<'textarea'>; diff --git a/front/src/components/ui/text-input/component.tsx b/front/src/components/ui/text-input/component.tsx index 5c148e9..24dadc9 100644 --- a/front/src/components/ui/text-input/component.tsx +++ b/front/src/components/ui/text-input/component.tsx @@ -14,7 +14,13 @@ function TextInputInner( }; return ( ); } diff --git a/front/src/components/ui/text-input/preview.tsx b/front/src/components/ui/text-input/preview.tsx index f9003d1..be1ac25 100644 --- a/front/src/components/ui/text-input/preview.tsx +++ b/front/src/components/ui/text-input/preview.tsx @@ -6,9 +6,28 @@ import { TextInput } from './component'; export function TextInputPreview() { return ( - - - + + + + ); } diff --git a/front/src/components/ui/types.ts b/front/src/components/ui/types.ts index 3c01c2b..2ed9971 100644 --- a/front/src/components/ui/types.ts +++ b/front/src/components/ui/types.ts @@ -1,4 +1,4 @@ -export type Scale = 'm' | 's' | 'l' | 'none'; +export type Scale = 'm' | 's' | 'l'; export type TextColor = 't100' | 't200' | 't300'; diff --git a/front/src/components/ux/header/component.tsx b/front/src/components/ux/header/component.tsx index 10ee181..f379ac2 100644 --- a/front/src/components/ux/header/component.tsx +++ b/front/src/components/ux/header/component.tsx @@ -1,17 +1,33 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; +import { Heading, IconButton } from '@components/ui'; +import MenuIcon from '@public/images/svg/menu.svg'; +import clsx from 'clsx'; +import React, { useRef, useState } from 'react'; -import { ThemeSelect } from '../theme-select'; +import { MobileSidebar } from '../mobile-sidebar'; import styles from './styles.module.scss'; +import { HeaderProps } from './types'; + +export function Header({ className, ...props }: HeaderProps) { + const openButtonRef = useRef(null); + const [open, setOpen] = useState(false); + + const headerClassName = clsx(styles.header, className); -export function Header() { return ( -
- -
- Home - Form -
+
+ setOpen(true)} + variant="rect" + ref={openButtonRef} + > + + + Wind + setOpen(false)} + openButtonRef={openButtonRef} + />
); } diff --git a/front/src/components/ux/header/styles.module.scss b/front/src/components/ux/header/styles.module.scss index 6a488f2..3ee0cc8 100644 --- a/front/src/components/ux/header/styles.module.scss +++ b/front/src/components/ux/header/styles.module.scss @@ -1,14 +1,13 @@ .header { display: flex; - flex-direction: column; + align-items: center; padding: 20px; border-bottom: 1px solid var(--clr-border-100); background-color: var(--clr-layer-200); box-shadow: 0px 1px 2px var(--clr-shadow-100); gap: 10px; -} -.linkBox { - display: flex; - gap: 10px; + .icon { + fill: var(--clr-text-300); + } } diff --git a/front/src/components/ux/header/types.ts b/front/src/components/ux/header/types.ts new file mode 100644 index 0000000..4c14b1b --- /dev/null +++ b/front/src/components/ux/header/types.ts @@ -0,0 +1,3 @@ +import { ComponentPropsWithoutRef } from 'react'; + +export type HeaderProps = ComponentPropsWithoutRef<'div'>; diff --git a/front/src/components/ux/index.ts b/front/src/components/ux/index.ts index dedded2..da680f3 100644 --- a/front/src/components/ux/index.ts +++ b/front/src/components/ux/index.ts @@ -1,4 +1,5 @@ -export * from './header'; -export * from './park-modal'; -export * from './sidebar'; -export * from './turbine-type-modal'; +export { Header } from './header'; +export { Sidebar } from './sidebar'; +export { SignInForm } from './sign-in-form'; +export { SignUpForm } from './sign-up-form'; +export { ThemeSelect } from './theme-select'; diff --git a/front/src/components/ux/mobile-sidebar/component.tsx b/front/src/components/ux/mobile-sidebar/component.tsx new file mode 100644 index 0000000..d9688b2 --- /dev/null +++ b/front/src/components/ux/mobile-sidebar/component.tsx @@ -0,0 +1,42 @@ +import { Heading, IconButton, Overlay } from '@components/ui'; +import CloseIcon from '@public/images/svg/close.svg'; +import { useMissClick } from '@utils/miss-click'; +import clsx from 'clsx'; +import React, { useRef } from 'react'; + +import { Navigation } from '../navigation'; +import { ThemeSelect } from '../theme-select'; +import styles from './styles.module.scss'; +import { MobileSidebarProps } from './types'; + +export function MobileSidebar({ + open, + onClose, + openButtonRef, +}: MobileSidebarProps) { + const overlayRef = useRef(null); + const sidebarRef = useRef(null); + const sidebarClassName = clsx(styles.sidebar, { [styles.closed]: !open }); + + useMissClick({ + callback: onClose, + enabled: open, + whitelist: [sidebarRef, openButtonRef], + blacklist: [overlayRef], + }); + + return ( + +
+
+ + + + Wind +
+ + +
+
+ ); +} diff --git a/front/src/components/ux/mobile-sidebar/index.ts b/front/src/components/ux/mobile-sidebar/index.ts new file mode 100644 index 0000000..bb82484 --- /dev/null +++ b/front/src/components/ux/mobile-sidebar/index.ts @@ -0,0 +1 @@ +export * from './component'; diff --git a/front/src/components/ux/mobile-sidebar/styles.module.scss b/front/src/components/ux/mobile-sidebar/styles.module.scss new file mode 100644 index 0000000..bf04c44 --- /dev/null +++ b/front/src/components/ux/mobile-sidebar/styles.module.scss @@ -0,0 +1,47 @@ +.overlay { + display: grid; + grid-template-columns: auto 1fr; +} + +.sidebar { + display: flex; + flex-direction: column; + padding: 20px; + border-right: 1px solid var(--clr-border-100); + animation: slidein 0.25s forwards ease-in-out; + background-color: var(--clr-layer-200); + box-shadow: 0px 1px 2px var(--clr-shadow-100); + gap: 20px; +} + +.closed { + animation: slideout 0.25s forwards ease-in-out; +} + +@keyframes slidein { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0px); + } +} + +@keyframes slideout { + from { + transform: translateX(0px); + } + to { + transform: translateX(-100%); + } +} + +.header { + display: flex; + align-items: center; + gap: 10px; + + .icon { + fill: var(--clr-text-300); + } +} diff --git a/front/src/components/ux/mobile-sidebar/types.ts b/front/src/components/ux/mobile-sidebar/types.ts new file mode 100644 index 0000000..a7fb17c --- /dev/null +++ b/front/src/components/ux/mobile-sidebar/types.ts @@ -0,0 +1,7 @@ +import { MutableRefObject } from 'react'; + +export type MobileSidebarProps = { + open: boolean; + onClose: () => void; + openButtonRef: MutableRefObject; +}; diff --git a/front/src/components/ux/navigation/component.tsx b/front/src/components/ux/navigation/component.tsx new file mode 100644 index 0000000..31a1caf --- /dev/null +++ b/front/src/components/ux/navigation/component.tsx @@ -0,0 +1,19 @@ +import { Ripple } from '@components/ui/animation'; +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { NAVIGATION_LINKS } from './consts'; +import styles from './styles.module.scss'; + +export function Navigation() { + return ( + + ); +} diff --git a/front/src/components/ux/navigation/consts.ts b/front/src/components/ux/navigation/consts.ts new file mode 100644 index 0000000..2083fee --- /dev/null +++ b/front/src/components/ux/navigation/consts.ts @@ -0,0 +1,6 @@ +import { ROUTES } from '@utils/route'; + +export const NAVIGATION_LINKS = [ + { path: ROUTES.turbineTypes.path, title: ROUTES.turbineTypes.title }, + { path: ROUTES.parks.path, title: ROUTES.parks.title }, +]; diff --git a/front/src/components/ux/navigation/index.tsx b/front/src/components/ux/navigation/index.tsx new file mode 100644 index 0000000..bb82484 --- /dev/null +++ b/front/src/components/ux/navigation/index.tsx @@ -0,0 +1 @@ +export * from './component'; diff --git a/front/src/components/ux/navigation/styles.module.scss b/front/src/components/ux/navigation/styles.module.scss new file mode 100644 index 0000000..bfba026 --- /dev/null +++ b/front/src/components/ux/navigation/styles.module.scss @@ -0,0 +1,13 @@ +.navigation { + display: flex; + flex-direction: column; +} + +.link { + position: relative; + overflow: hidden; + padding: 10px; + border-radius: 10px; + color: var(--clr-text-300); + text-decoration: none; +} diff --git a/front/src/components/ux/park-modal/component.tsx b/front/src/components/ux/park-modal/component.tsx deleted file mode 100644 index 261c75c..0000000 --- a/front/src/components/ux/park-modal/component.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { - Autocomplete, - Button, - Heading, - NumberInput, - Overlay, - TextInput, -} from '@components/ui'; -import { Controller, useForm } from '@utils/form'; -import React, { useEffect, useState } from 'react'; -import { - createPark, - editPark, - getParkTurines, - getTurbineTypes, - TurbineType, -} from 'src/api/wind'; -import { ParkTurbine } from 'src/api/wind/types'; - -import styles from './styles.module.scss'; -import { ParkFormValues, ParkModalProps } from './types'; -import { parkToFormValues } from './utils'; - -export function ParkModal({ park, open, onClose, onSuccess }: ParkModalProps) { - const [turbineTypes, setTurbineTypes] = useState([]); - const [parkTurbines, setParkTurbines] = useState([]); - const [pending, setPending] = useState(false); - const isEdit = park !== null; - - console.log(parkTurbines); - - const { register, getValues, reset, control } = useForm({ - initialValues: {}, - }); - - const fetchTurbineTypes = async () => { - const res = await getTurbineTypes(); - setTurbineTypes(res.data ?? []); - }; - - const fetchParkTurbines = async () => { - if (!park) { - return; - } - const res = await getParkTurines(park.id); - setParkTurbines(res.data ?? []); - }; - - useEffect(() => { - fetchTurbineTypes(); - }, []); - - useEffect(() => { - reset(park ? parkToFormValues(park) : {}); - fetchParkTurbines(); - }, [park]); - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - setPending(true); - if (isEdit) { - await editPark(getValues(), park.id); - } else { - await createPark(getValues()); - } - setPending(false); - onClose(); - onSuccess(); - }; - - return ( - -
- - {isEdit ? 'Edit' : 'Create new'} - -
- -
- ( - - )} - /> - ( - - )} - /> -
-
- id} - getOptionLabel={({ name }) => name} - className={styles.autocomplete} - label={{ text: 'Turbines' }} - /> - -
-
-
- - -
-
-
- ); -} diff --git a/front/src/components/ux/park-modal/index.ts b/front/src/components/ux/park-modal/index.ts deleted file mode 100644 index c01846b..0000000 --- a/front/src/components/ux/park-modal/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './component'; -export { type ParkFormValues } from './types'; diff --git a/front/src/components/ux/park-modal/styles.module.scss b/front/src/components/ux/park-modal/styles.module.scss deleted file mode 100644 index 7cb8e4f..0000000 --- a/front/src/components/ux/park-modal/styles.module.scss +++ /dev/null @@ -1,48 +0,0 @@ -.form { - display: grid; - padding: 40px 20px; - border-radius: 10px; - background-color: var(--clr-layer-200); - box-shadow: 0px 1px 2px var(--clr-shadow-100); - gap: 40px; - grid-area: form; - grid-template-rows: auto 1fr auto; - - & > * { - width: 100%; - } -} - -.heading { - text-align: center; -} - -.inputBox { - display: grid; - gap: 20px; -} - -.duo { - display: flex; - gap: 10px; - - & > * { - flex: 1; - } -} - -.autocompleteBox { - display: flex; - align-items: flex-end; - gap: 10px; -} - -.autocomplete { - flex: 1; -} - -.buttonBox { - display: flex; - justify-content: end; - gap: 10px; -} diff --git a/front/src/components/ux/park-modal/types.ts b/front/src/components/ux/park-modal/types.ts deleted file mode 100644 index 5893ed8..0000000 --- a/front/src/components/ux/park-modal/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Park } from 'src/api/wind'; - -export type ParkFormValues = { - name: string; - centerLatitude: string; - centerLongitude: string; -}; - -export type ParkModalProps = { - park: Park; - open: boolean; - onClose: () => void; - onSuccess: () => void; -}; diff --git a/front/src/components/ux/sidebar/component.tsx b/front/src/components/ux/sidebar/component.tsx index 77c7f25..bd3431d 100644 --- a/front/src/components/ux/sidebar/component.tsx +++ b/front/src/components/ux/sidebar/component.tsx @@ -1,27 +1,24 @@ import { Heading } from '@components/ui'; -import { useRoute } from '@utils/route'; -import React from 'react'; -import { Link } from 'react-router-dom'; +import clsx from 'clsx'; +import React, { ForwardedRef, forwardRef } from 'react'; +import { Navigation } from '../navigation'; import { ThemeSelect } from '../theme-select'; -import { links } from './constants'; import styles from './styles.module.scss'; +import { SidebarProps } from './types'; -export function Sidebar() { - const route = useRoute(); - console.log(route); - +function SidebarInner( + { className, ...props }: SidebarProps, + ref: ForwardedRef, +) { + const sidebarClassName = clsx(styles.sidebar, className); return ( -
- Wind App +
+ Wind -
- {links.map((link) => ( - - {link.title} - - ))} -
+
); } + +export const Sidebar = forwardRef(SidebarInner); diff --git a/front/src/components/ux/sidebar/constants.ts b/front/src/components/ux/sidebar/constants.ts deleted file mode 100644 index ee7c3c6..0000000 --- a/front/src/components/ux/sidebar/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ROUTES } from '@utils/route'; - -import { SidebarLink } from './types'; - -export const links: SidebarLink[] = [ - { url: ROUTES.turbineTypes.path, title: ROUTES.turbineTypes.title }, - { url: ROUTES.parks.path, title: ROUTES.parks.title }, -]; diff --git a/front/src/components/ux/sidebar/styles.module.scss b/front/src/components/ux/sidebar/styles.module.scss index 5d9099f..d2c10b4 100644 --- a/front/src/components/ux/sidebar/styles.module.scss +++ b/front/src/components/ux/sidebar/styles.module.scss @@ -1,20 +1,9 @@ .sidebar { display: flex; flex-direction: column; - padding: 20px; + padding: 30px 20px; border-right: 1px solid var(--clr-border-100); background-color: var(--clr-layer-200); box-shadow: 0px 1px 2px var(--clr-shadow-100); gap: 20px; } - -.links { - display: flex; - flex-direction: column; - gap: 10px; -} - -.link { - color: var(--clr-text-200); - text-decoration: none; -} diff --git a/front/src/components/ux/sidebar/types.ts b/front/src/components/ux/sidebar/types.ts index 92522e4..d89a14e 100644 --- a/front/src/components/ux/sidebar/types.ts +++ b/front/src/components/ux/sidebar/types.ts @@ -1,4 +1,3 @@ -export type SidebarLink = { - url: string; - title: string; -}; +import { ComponentPropsWithoutRef } from 'react'; + +export type SidebarProps = ComponentPropsWithoutRef<'div'>; diff --git a/front/src/components/ux/sign-in-form/component.tsx b/front/src/components/ux/sign-in-form/component.tsx new file mode 100644 index 0000000..5299ab1 --- /dev/null +++ b/front/src/components/ux/sign-in-form/component.tsx @@ -0,0 +1,40 @@ +import { + Button, + Heading, + LinkButton, + PasswordInput, + TextInput, +} from '@components/ui'; +import { useForm } from '@utils/form'; +import { ROUTES } from '@utils/route'; +import clsx from 'clsx'; +import React from 'react'; + +import styles from './styles.module.scss'; +import { SignInFormProps, SignInFormStore } from './types'; + +export function SignInForm({ className, ...props }: SignInFormProps) { + const { register, getValues } = useForm({}); + const classNames = clsx(className, styles.form); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + console.log(getValues()); + }; + + return ( +
+ + Sign in + +
+ + +
+
+ + Not a member? +
+
+ ); +} diff --git a/front/src/components/ux/sign-in-form/index.ts b/front/src/components/ux/sign-in-form/index.ts new file mode 100644 index 0000000..fba6544 --- /dev/null +++ b/front/src/components/ux/sign-in-form/index.ts @@ -0,0 +1 @@ +export { SignInForm } from './component'; diff --git a/front/src/components/ux/turbine-type-modal/styles.module.scss b/front/src/components/ux/sign-in-form/styles.module.scss similarity index 62% rename from front/src/components/ux/turbine-type-modal/styles.module.scss rename to front/src/components/ux/sign-in-form/styles.module.scss index 6c5db31..b996084 100644 --- a/front/src/components/ux/turbine-type-modal/styles.module.scss +++ b/front/src/components/ux/sign-in-form/styles.module.scss @@ -1,12 +1,10 @@ .form { display: grid; - padding: 40px 20px; - border-radius: 10px; + padding: 40px 20px 20px; + border-radius: 15px; background-color: var(--clr-layer-200); box-shadow: 0px 1px 2px var(--clr-shadow-100); - gap: 40px; - grid-area: form; - grid-template-rows: auto 1fr auto; + gap: 30px; & > * { width: 100%; @@ -23,7 +21,6 @@ } .buttonBox { - display: flex; - justify-content: end; + display: grid; gap: 10px; } diff --git a/front/src/components/ux/sign-in-form/types.ts b/front/src/components/ux/sign-in-form/types.ts new file mode 100644 index 0000000..caa4093 --- /dev/null +++ b/front/src/components/ux/sign-in-form/types.ts @@ -0,0 +1,8 @@ +import { ComponentProps } from 'react'; + +export type SignInFormStore = { + email: string; + password: string; +}; + +export type SignInFormProps = {} & ComponentProps<'form'>; diff --git a/front/src/components/ux/sign-up-form/component.tsx b/front/src/components/ux/sign-up-form/component.tsx new file mode 100644 index 0000000..caeb375 --- /dev/null +++ b/front/src/components/ux/sign-up-form/component.tsx @@ -0,0 +1,40 @@ +import { + Button, + Heading, + LinkButton, + PasswordInput, + TextInput, +} from '@components/ui'; +import { useForm } from '@utils/form'; +import { ROUTES } from '@utils/route'; +import clsx from 'clsx'; +import React from 'react'; + +import styles from './styles.module.scss'; +import { SignUpFormProps, SignUpFormStore } from './types'; + +export function SignUpForm({ className, ...props }: SignUpFormProps) { + const { register, getValues } = useForm({}); + const classNames = clsx(className, styles.form); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + console.log(getValues()); + }; + + return ( +
+ + Sign up + +
+ + +
+
+ + Already a member? +
+
+ ); +} diff --git a/front/src/components/ux/sign-up-form/index.ts b/front/src/components/ux/sign-up-form/index.ts new file mode 100644 index 0000000..455d405 --- /dev/null +++ b/front/src/components/ux/sign-up-form/index.ts @@ -0,0 +1 @@ +export { SignUpForm } from './component'; diff --git a/front/src/components/ux/sign-up-form/styles.module.scss b/front/src/components/ux/sign-up-form/styles.module.scss new file mode 100644 index 0000000..b996084 --- /dev/null +++ b/front/src/components/ux/sign-up-form/styles.module.scss @@ -0,0 +1,26 @@ +.form { + display: grid; + padding: 40px 20px 20px; + border-radius: 15px; + background-color: var(--clr-layer-200); + box-shadow: 0px 1px 2px var(--clr-shadow-100); + gap: 30px; + + & > * { + width: 100%; + } +} + +.heading { + text-align: center; +} + +.inputBox { + display: grid; + gap: 20px; +} + +.buttonBox { + display: grid; + gap: 10px; +} diff --git a/front/src/components/ux/sign-up-form/types.ts b/front/src/components/ux/sign-up-form/types.ts new file mode 100644 index 0000000..e089c14 --- /dev/null +++ b/front/src/components/ux/sign-up-form/types.ts @@ -0,0 +1,9 @@ +import { ComponentProps } from 'react'; + +export type SignUpFormStore = { + email: string; + password: string; + passwordRepeat: string; +}; + +export type SignUpFormProps = {} & ComponentProps<'form'>; diff --git a/front/src/components/ux/theme-select/component.tsx b/front/src/components/ux/theme-select/component.tsx index 8b34ebe..f656983 100644 --- a/front/src/components/ux/theme-select/component.tsx +++ b/front/src/components/ux/theme-select/component.tsx @@ -17,7 +17,7 @@ export function ThemeSelect() { getOptionKey={(t) => t.key} getOptionLabel={(t) => t.name} onChange={handleChange} - label={{ text: 'Theme', position: 'left' }} + label={{ text: 'Theme' }} id="theme" /> ); diff --git a/front/src/components/ux/theme-select/index.ts b/front/src/components/ux/theme-select/index.ts new file mode 100644 index 0000000..bb82484 --- /dev/null +++ b/front/src/components/ux/theme-select/index.ts @@ -0,0 +1 @@ +export * from './component'; diff --git a/front/src/components/ux/theme-select/styles.module.scss b/front/src/components/ux/theme-select/styles.module.scss deleted file mode 100644 index c9bbd3f..0000000 --- a/front/src/components/ux/theme-select/styles.module.scss +++ /dev/null @@ -1,4 +0,0 @@ -.select { - padding: 20px; - background-color: var(--clr-layer-200); -} diff --git a/front/src/components/ux/turbine-type-modal/component.tsx b/front/src/components/ux/turbine-type-modal/component.tsx deleted file mode 100644 index 2d246d6..0000000 --- a/front/src/components/ux/turbine-type-modal/component.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { - Button, - Heading, - NumberInput, - Overlay, - TextInput, -} from '@components/ui'; -import { Controller, useForm } from '@utils/form'; -import React, { useEffect, useState } from 'react'; -import { createTurbineTypes, editTurbineTypes } from 'src/api/wind'; - -import styles from './styles.module.scss'; -import { TurbineTypeFormValues, TurbineTypeModalProps } from './types'; -import { turbineTypeToFormValues } from './utils'; - -export function TurbineTypeModal({ - turbineType, - open, - onClose, - onSuccess, -}: TurbineTypeModalProps) { - const [pending, setPending] = useState(false); - const isEdit = turbineType !== null; - - const { register, control, getValues, reset } = - useForm({ - initialValues: {}, - }); - - useEffect(() => { - reset(turbineType ? turbineTypeToFormValues(turbineType) : {}); - }, [turbineType]); - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - setPending(true); - if (isEdit) { - await editTurbineTypes(getValues(), turbineType.id); - } else { - await createTurbineTypes(getValues()); - } - setPending(false); - onClose(); - onSuccess(); - }; - - return ( - -
- - {isEdit ? 'Edit' : 'Create new'} - -
- - ( - - )} - /> - ( - - )} - /> -
-
- - -
-
-
- ); -} diff --git a/front/src/components/ux/turbine-type-modal/index.ts b/front/src/components/ux/turbine-type-modal/index.ts deleted file mode 100644 index 0bfef12..0000000 --- a/front/src/components/ux/turbine-type-modal/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './component'; -export { type TurbineTypeFormValues } from './types'; diff --git a/front/src/components/ux/turbine-type-modal/types.ts b/front/src/components/ux/turbine-type-modal/types.ts deleted file mode 100644 index 82e4d6f..0000000 --- a/front/src/components/ux/turbine-type-modal/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TurbineType } from 'src/api/wind'; - -export type TurbineTypeFormValues = { - name: string; - height: string; - bladeLength: string; -}; - -export type TurbineTypeModalProps = { - turbineType: TurbineType; - open: boolean; - onClose: () => void; - onSuccess: () => void; -}; diff --git a/front/src/index.tsx b/front/src/index.tsx index 47025bc..64ba82b 100644 --- a/front/src/index.tsx +++ b/front/src/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import App from './components/app'; +import { App } from './components/app'; const rootElement = document.getElementById('root'); const root = createRoot(rootElement); diff --git a/front/src/utils/device/index.ts b/front/src/utils/device/index.ts new file mode 100644 index 0000000..04bca77 --- /dev/null +++ b/front/src/utils/device/index.ts @@ -0,0 +1 @@ +export * from './utils'; diff --git a/front/src/utils/device/types.ts b/front/src/utils/device/types.ts new file mode 100644 index 0000000..c4946dc --- /dev/null +++ b/front/src/utils/device/types.ts @@ -0,0 +1 @@ +export type DeviceType = 'desktop' | 'mobile'; diff --git a/front/src/utils/device/utils.ts b/front/src/utils/device/utils.ts new file mode 100644 index 0000000..91c4b93 --- /dev/null +++ b/front/src/utils/device/utils.ts @@ -0,0 +1,28 @@ +import { useLayoutEffect, useState } from 'react'; + +import { DeviceType } from './types'; + +const defineDeviceType = (): DeviceType => { + const width = window.innerWidth; + if (width <= 800) { + return 'mobile'; + } + return 'desktop'; +}; + +export const useDeviceType = () => { + const [deviceType, setDeviceType] = useState(defineDeviceType()); + + const handleResize = () => { + setDeviceType(defineDeviceType()); + }; + + useLayoutEffect(() => { + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return deviceType; +}; diff --git a/front/src/utils/form/controller/types.ts b/front/src/utils/form/controller/types.ts index cd1bcd5..b000f59 100644 --- a/front/src/utils/form/controller/types.ts +++ b/front/src/utils/form/controller/types.ts @@ -1,3 +1,5 @@ +import { ReactNode } from 'react'; + export type ControllerRenderParams = { name: string; value: T; @@ -9,5 +11,5 @@ export type ControllerProps = { hash: number; value: T; onChange: (value: T) => void; - render: (params: ControllerRenderParams) => React.ReactNode; + render: (params: ControllerRenderParams) => ReactNode; }; diff --git a/front/src/utils/form/types.ts b/front/src/utils/form/types.ts index 684b874..820a6f0 100644 --- a/front/src/utils/form/types.ts +++ b/front/src/utils/form/types.ts @@ -1,3 +1,5 @@ +import { MutableRefObject } from 'react'; + export type FormValues = Partial; export type FormStore = { @@ -5,4 +7,4 @@ export type FormStore = { hash: number; }; -export type FormStoreRef = React.MutableRefObject>; +export type FormStoreRef = MutableRefObject>; diff --git a/front/src/utils/miss-click/index.tsx b/front/src/utils/miss-click/index.tsx index 4b9f7d3..8d7294f 100644 --- a/front/src/utils/miss-click/index.tsx +++ b/front/src/utils/miss-click/index.tsx @@ -1,29 +1,41 @@ import { useEffect } from 'react'; -export const useMissClick = ( - ignore: React.MutableRefObject[], - callback: () => void, - visible: boolean, -) => { - useEffect(() => { - if (!visible) { +import { UseMissClickParams } from './types'; + +export const useMissClick = ({ + callback, + enabled, + blacklist, + whitelist, +}: UseMissClickParams) => { + const handleClick = (event: MouseEvent) => { + const { target } = event; + if (!(target instanceof Element)) { return; } - const listener = (event: MouseEvent) => { - const { target } = event; - if (!(target instanceof Element)) { + for (let i = 0; i < whitelist.length; i += 1) { + if (whitelist[i]?.current?.contains(target)) { return; } - for (let i = 0; i < ignore.length; i += 1) { - if (ignore[i].current?.contains(target)) { - return; - } - } + } + if (!blacklist) { callback(); - }; - window.addEventListener('click', listener); + return; + } + for (let i = 0; i < blacklist.length; i += 1) { + if (blacklist[i]?.current?.contains(target)) { + callback(); + } + } + }; + + useEffect(() => { + if (!enabled) { + return; + } + window.addEventListener('click', handleClick); return () => { - window.removeEventListener('click', listener); + window.removeEventListener('click', handleClick); }; - }, [visible]); + }, [enabled]); }; diff --git a/front/src/utils/miss-click/types.ts b/front/src/utils/miss-click/types.ts new file mode 100644 index 0000000..f2e7d3f --- /dev/null +++ b/front/src/utils/miss-click/types.ts @@ -0,0 +1,8 @@ +import { MutableRefObject } from 'react'; + +export type UseMissClickParams = { + callback: () => void; + enabled: boolean; + whitelist: MutableRefObject[]; + blacklist?: MutableRefObject[]; +}; diff --git a/front/src/utils/route/constants.ts b/front/src/utils/route/constants.ts index 7922590..c5b2deb 100644 --- a/front/src/utils/route/constants.ts +++ b/front/src/utils/route/constants.ts @@ -3,8 +3,10 @@ import { arrayToObject } from '@utils/array'; import { AppRoute, AppRouteName } from './types'; export const ROUTES: Record = { - turbineTypes: { path: 'turbine-types', title: 'Turbine types' }, - parks: { path: 'parks', title: 'Parks' }, + turbineTypes: { path: '/turbine-types', title: 'Turbine Types' }, + turbineType: { path: '/turbine-types/:id', title: 'Turbine Type' }, + parks: { path: '/parks', title: 'Parks' }, + park: { path: '/parks/:id', title: 'Park' }, }; export const routeArray = Object.values(ROUTES); diff --git a/front/src/utils/route/types.ts b/front/src/utils/route/types.ts index 9f3c9cb..d346794 100644 --- a/front/src/utils/route/types.ts +++ b/front/src/utils/route/types.ts @@ -1,4 +1,4 @@ -export type AppRouteName = 'turbineTypes' | 'parks'; +export type AppRouteName = 'turbineTypes' | 'turbineType' | 'parks' | 'park'; export type AppRoute = { path: string; diff --git a/front/tsconfig.json b/front/tsconfig.json index a57cb58..7805a34 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -12,6 +12,7 @@ "@storage/*": ["src/utils/storage/*"], "@store/*": ["src/store/*"], "@utils/*": ["src/utils/*"], + "@api/*": ["src/api/*"], }, "plugins": [ {