[front]: form
This commit is contained in:
parent
d435cde55c
commit
fd5c724342
6
front/package-lock.json
generated
6
front/package-lock.json
generated
@ -2811,7 +2811,7 @@
|
|||||||
"version": "15.7.13",
|
"version": "15.7.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||||
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
|
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.16",
|
"version": "6.9.16",
|
||||||
@ -2829,7 +2829,7 @@
|
|||||||
"version": "18.3.8",
|
"version": "18.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
|
||||||
"integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
|
"integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@ -4262,7 +4262,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
|
12
front/public/images/svg/delete.svg
Normal file
12
front/public/images/svg/delete.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 13 13" style="enable-background:new 0 0 13 13;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M11.5,3H11H9V2.5C9,1.67,8.33,1,7.5,1h-2C4.67,1,4,1.67,4,2.5V3H2H1.5C1.22,3,1,3.22,1,3.5S1.22,4,1.5,4H2v5.5
|
||||||
|
C2,10.88,3.12,12,4.5,12h4c1.38,0,2.5-1.12,2.5-2.5V4h0.5C11.78,4,12,3.78,12,3.5S11.78,3,11.5,3z M5,2.5C5,2.22,5.22,2,5.5,2h2
|
||||||
|
C7.78,2,8,2.22,8,2.5V3H5V2.5z M10,9.5c0,0.83-0.67,1.5-1.5,1.5h-4C3.67,11,3,10.33,3,9.5V4h7V9.5z"/>
|
||||||
|
<path d="M5.25,9.5c0.28,0,0.5-0.22,0.5-0.5V6c0-0.28-0.22-0.5-0.5-0.5S4.75,5.72,4.75,6v3C4.75,9.28,4.97,9.5,5.25,9.5z"/>
|
||||||
|
<path d="M7.75,9.5c0.28,0,0.5-0.22,0.5-0.5V6c0-0.28-0.22-0.5-0.5-0.5S7.25,5.72,7.25,6v3C7.25,9.28,7.47,9.5,7.75,9.5z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 959 B |
7
front/public/images/svg/plus.svg
Normal file
7
front/public/images/svg/plus.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 13 13" style="enable-background:new 0 0 13 13;" xml:space="preserve">
|
||||||
|
<path d="M9.5,6H7V3.5C7,3.22,6.78,3,6.5,3S6,3.22,6,3.5V6H3.5C3.22,6,3,6.22,3,6.5S3.22,7,3.5,7H6v2.5C6,9.78,6.22,10,6.5,10
|
||||||
|
S7,9.78,7,9.5V7h2.5C9.78,7,10,6.78,10,6.5S9.78,6,9.5,6z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 541 B |
0
front/src/api/floris/index.ts
Normal file
0
front/src/api/floris/index.ts
Normal file
@ -2,7 +2,7 @@ import './styles.scss';
|
|||||||
import '@public/fonts/styles.css';
|
import '@public/fonts/styles.css';
|
||||||
|
|
||||||
import { MainLayout } from '@components/layouts';
|
import { MainLayout } from '@components/layouts';
|
||||||
import { FormPage, HomePage } from '@components/pages';
|
import { HomePage } from '@components/pages';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
@ -12,7 +12,6 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
<Route path={'/'} element={<HomePage />} />
|
<Route path={'/'} element={<HomePage />} />
|
||||||
<Route path={'/form'} element={<FormPage />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { LoginForm } from '@components/ux';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
export function FormPage() {
|
|
||||||
return (
|
|
||||||
<div className={styles.about}>
|
|
||||||
<LoginForm className={styles.form} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export { FormPage } from './component';
|
|
@ -1,11 +0,0 @@
|
|||||||
.about {
|
|
||||||
display: grid;
|
|
||||||
padding: 20px;
|
|
||||||
grid-template:
|
|
||||||
'. form .' auto
|
|
||||||
/ auto minmax(0, 380px) auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
grid-area: form;
|
|
||||||
}
|
|
@ -1,10 +1,4 @@
|
|||||||
import { ButtonPreview } from '@components/ui/button';
|
import { WindmillForm } from '@components/ux';
|
||||||
import { CheckboxGroupPreview } from '@components/ui/checkbox-group';
|
|
||||||
import { DateInputPreview } from '@components/ui/date-input';
|
|
||||||
import { PasswordInputPreview } from '@components/ui/password-input';
|
|
||||||
import { RadioGroupPreview } from '@components/ui/radio-group';
|
|
||||||
import { SelectPreview } from '@components/ui/select';
|
|
||||||
import { TextInputPreview } from '@components/ui/text-input';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
@ -12,14 +6,8 @@ import styles from './styles.module.scss';
|
|||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.home}>
|
<div className={styles.home}>
|
||||||
<div className={styles.content}>
|
<div className={styles.about}>
|
||||||
<ButtonPreview />
|
<WindmillForm className={styles.form} />
|
||||||
<TextInputPreview />
|
|
||||||
<PasswordInputPreview />
|
|
||||||
<SelectPreview />
|
|
||||||
<DateInputPreview />
|
|
||||||
<CheckboxGroupPreview />
|
|
||||||
<RadioGroupPreview />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
.home {
|
.about {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
padding: 20px;
|
||||||
grid-template:
|
grid-template:
|
||||||
'. content .' auto
|
'. form .' auto
|
||||||
/ auto minmax(0, 1000px) auto;
|
/ auto minmax(0, 380px) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.form {
|
||||||
display: flex;
|
grid-area: form;
|
||||||
flex-direction: column;
|
|
||||||
padding: 20px 20px 60px 20px;
|
|
||||||
gap: 30px;
|
|
||||||
grid-area: content;
|
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1 @@
|
|||||||
export { FormPage } from './form-page';
|
|
||||||
export { HomePage } from './home-page';
|
export { HomePage } from './home-page';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export type CalendarProps = {
|
export type CalendarProps = {
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
min: Date | null;
|
min: Date | null;
|
||||||
max: Date | null;
|
max: Date | null;
|
||||||
} & Omit<React.ComponentProps<'div'>, 'onChange'>;
|
} & Omit<React.ComponentProps<'div'>, 'onChange'>;
|
||||||
|
@ -9,18 +9,10 @@ import styles from './styles.module.scss';
|
|||||||
import { CheckboxProps } from './types';
|
import { CheckboxProps } from './types';
|
||||||
|
|
||||||
function CheckboxInner(
|
function CheckboxInner(
|
||||||
{
|
{ scale = 'm', label = {}, required, ...props }: Omit<CheckboxProps, 'ref'>,
|
||||||
scale = 'm',
|
|
||||||
label = {},
|
|
||||||
required,
|
|
||||||
checked,
|
|
||||||
...props
|
|
||||||
}: Omit<CheckboxProps, 'ref'>,
|
|
||||||
ref: ForwardedRef<HTMLInputElement>,
|
ref: ForwardedRef<HTMLInputElement>,
|
||||||
) {
|
) {
|
||||||
const wrapperClassName = clsx(styles.wrapper, styles[scale], {
|
const wrapperClassName = clsx(styles.wrapper, styles[scale]);
|
||||||
[styles.checked]: checked,
|
|
||||||
});
|
|
||||||
|
|
||||||
const labelProps: LabelProps = {
|
const labelProps: LabelProps = {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
@ -37,7 +29,6 @@ function CheckboxInner(
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
required={required}
|
required={required}
|
||||||
checked={checked}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<div className={styles.checkbox}>
|
<div className={styles.checkbox}>
|
||||||
|
@ -19,6 +19,24 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
& ~ .checkbox {
|
||||||
|
border-width: 0;
|
||||||
|
background-color: var(--clr-primary);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 100%;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
& ~ .checkbox {
|
||||||
|
background-color: var(--clr-primary-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
@ -36,24 +54,6 @@
|
|||||||
transition: all var(--td-100) ease-in-out;
|
transition: all var(--td-100) ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checked {
|
|
||||||
.checkbox {
|
|
||||||
border-width: 0;
|
|
||||||
background-color: var(--clr-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.checkbox {
|
|
||||||
background-color: var(--clr-primary-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 100%;
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.s {
|
.s {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
|
||||||
|
@ -58,16 +58,16 @@ export function DateInput({
|
|||||||
(!minDate || date >= minDate) &&
|
(!minDate || date >= minDate) &&
|
||||||
(!maxDate || date <= maxDate)
|
(!maxDate || date <= maxDate)
|
||||||
) {
|
) {
|
||||||
onChange(newValue);
|
onChange?.(newValue);
|
||||||
} else {
|
} else {
|
||||||
onChange('');
|
onChange?.('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setDirtyDate(newDirtyDate);
|
setDirtyDate(newDirtyDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCalendarChange = (newValue: string) => {
|
const handleCalendarChange = (newValue: string) => {
|
||||||
onChange(newValue);
|
onChange?.(newValue);
|
||||||
setCalendarVisible(false);
|
setCalendarVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { TextInputProps } from '../text-input';
|
|||||||
|
|
||||||
export type DateInputProps = {
|
export type DateInputProps = {
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
max?: string;
|
max?: string;
|
||||||
min?: string;
|
min?: string;
|
||||||
} & Omit<TextInputProps, 'type' | 'value' | 'onChange'>;
|
} & Omit<TextInputProps, 'type' | 'value' | 'onChange'>;
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wrapperFocus {
|
.wrapperFocus {
|
||||||
|
z-index: 1;
|
||||||
border-color: var(--clr-primary);
|
border-color: var(--clr-primary);
|
||||||
background-color: var(--clr-layer-200);
|
background-color: var(--clr-layer-200);
|
||||||
outline-width: 3px;
|
outline-width: 3px;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { ThemeSelect } from '../theme-select';
|
import { ThemeSelect } from '../theme-select';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
@ -8,10 +7,6 @@ export function Header() {
|
|||||||
return (
|
return (
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<ThemeSelect />
|
<ThemeSelect />
|
||||||
<div className={styles.linkBox}>
|
|
||||||
<Link to="/">Home</Link>
|
|
||||||
<Link to="/form">Form</Link>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export { Header } from './header';
|
export { Header } from './header';
|
||||||
export { LoginForm } from './login-form';
|
|
||||||
export { ThemeSelect } from './theme-select';
|
export { ThemeSelect } from './theme-select';
|
||||||
|
export { WindmillForm } from './windmill-form';
|
||||||
|
export { WindmillTable } from './windmill-table';
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Paragraph,
|
|
||||||
PasswordInput,
|
|
||||||
Select,
|
|
||||||
TextInput,
|
|
||||||
} from '@components/ui';
|
|
||||||
import { Controller, useForm } from '@utils/form';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import { fruits, initialValues } from './constants';
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import { LoginFormProps, LoginFormStore } from './types';
|
|
||||||
|
|
||||||
export function LoginForm({ className, ...props }: LoginFormProps) {
|
|
||||||
const [result, setResult] = useState<string>('');
|
|
||||||
const { register, control, getValues, reset } = useForm<LoginFormStore>({
|
|
||||||
initialValues,
|
|
||||||
});
|
|
||||||
const classNames = clsx(className, styles.form);
|
|
||||||
|
|
||||||
const handleSubmit = (event: React.FormEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setResult(JSON.stringify(getValues(), null, 4));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetButtonClick = () => {
|
|
||||||
reset({ email: 'haha' });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className={classNames} {...props}>
|
|
||||||
<TextInput {...register('email')} label={{ text: 'Email' }} />
|
|
||||||
<PasswordInput {...register('password')} label={{ text: 'Password' }} />
|
|
||||||
<Controller
|
|
||||||
{...control('fruit')}
|
|
||||||
render={(params) => (
|
|
||||||
<Select
|
|
||||||
options={fruits}
|
|
||||||
getOptionKey={(o) => o.id}
|
|
||||||
getOptionLabel={(o) => o.name}
|
|
||||||
label={{ text: 'Fruit' }}
|
|
||||||
{...params}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className={styles.buttonBox}>
|
|
||||||
<Button type="submit">Login</Button>
|
|
||||||
<Button variant="secondary" onClick={handleResetButtonClick}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{result && <Paragraph className={styles.result}>{result}</Paragraph>}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
import { FormValues } from '@utils/form';
|
|
||||||
|
|
||||||
import { Fruit, LoginFormStore } from './types';
|
|
||||||
|
|
||||||
export const fruits: Fruit[] = [
|
|
||||||
{ id: 1, name: 'banana' },
|
|
||||||
{ id: 2, name: 'apple' },
|
|
||||||
{ id: 3, name: 'orange' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const initialValues: FormValues<LoginFormStore> = {
|
|
||||||
email: 'aaa',
|
|
||||||
fruit: fruits[1],
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export { LoginForm } from './component';
|
|
@ -1,12 +0,0 @@
|
|||||||
export type Fruit = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LoginFormStore = {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
fruit: Fruit;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LoginFormProps = {} & React.ComponentProps<'form'>;
|
|
50
front/src/components/ux/windmill-form/component.tsx
Normal file
50
front/src/components/ux/windmill-form/component.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Button, DateInput, Heading } from '@components/ui';
|
||||||
|
import { Controller, useForm } from '@utils/form';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { WindmillTable } from '../windmill-table';
|
||||||
|
import { initialValues } from './constants';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { WindmillFormProps, WindmillFormStore } from './types';
|
||||||
|
|
||||||
|
export function WindmillForm({ className, ...props }: WindmillFormProps) {
|
||||||
|
const { control, reset } = useForm<WindmillFormStore>({
|
||||||
|
initialValues,
|
||||||
|
});
|
||||||
|
const classNames = clsx(className, styles.form);
|
||||||
|
|
||||||
|
const handleSubmit = (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetButtonClick = () => {
|
||||||
|
reset({});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className={classNames} {...props}>
|
||||||
|
<Heading tag="h3">Windmill power</Heading>
|
||||||
|
<div className={styles.dateRangeBox}>
|
||||||
|
<Controller
|
||||||
|
{...control('dateFrom')}
|
||||||
|
render={(params) => <DateInput placeholder="from" {...params} />}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
{...control('dateTo')}
|
||||||
|
render={(params) => <DateInput placeholder="to" {...params} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
{...control('windmills')}
|
||||||
|
render={(params) => <WindmillTable {...params} />}
|
||||||
|
/>
|
||||||
|
<div className={styles.buttonBox}>
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
<Button variant="secondary" onClick={handleResetButtonClick}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
5
front/src/components/ux/windmill-form/constants.ts
Normal file
5
front/src/components/ux/windmill-form/constants.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { FormValues } from '@utils/form';
|
||||||
|
|
||||||
|
import { WindmillFormStore } from './types';
|
||||||
|
|
||||||
|
export const initialValues: FormValues<WindmillFormStore> = {};
|
2
front/src/components/ux/windmill-form/index.tsx
Normal file
2
front/src/components/ux/windmill-form/index.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { WindmillForm } from './component';
|
||||||
|
export { type WindmillConfig } from './types';
|
@ -11,16 +11,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dateRangeBox {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.buttonBox {
|
.buttonBox {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result {
|
|
||||||
overflow: auto;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 7px;
|
|
||||||
background-color: var(--clr-layer-100);
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
13
front/src/components/ux/windmill-form/types.ts
Normal file
13
front/src/components/ux/windmill-form/types.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export type WindmillConfig = {
|
||||||
|
x: string;
|
||||||
|
y: string;
|
||||||
|
angle: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WindmillFormStore = {
|
||||||
|
dateFrom: string;
|
||||||
|
dateTo: string;
|
||||||
|
windmills: WindmillConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WindmillFormProps = {} & React.ComponentProps<'form'>;
|
60
front/src/components/ux/windmill-table/component.tsx
Normal file
60
front/src/components/ux/windmill-table/component.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { IconButton, Span } from '@components/ui';
|
||||||
|
import DeleteIcon from '@public/images/svg/delete.svg';
|
||||||
|
import PlusIcon from '@public/images/svg/plus.svg';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { WindmillConfig } from '../windmill-form';
|
||||||
|
import { WindmillRableRow } from './parts/windmill-table-row';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { WindmillTableProps } from './types';
|
||||||
|
|
||||||
|
export function WindmillTable({ value, onChange }: WindmillTableProps) {
|
||||||
|
const [selectedRows, setSelectedRows] = useState<Record<string, boolean>>({});
|
||||||
|
const localValue = value ?? [];
|
||||||
|
|
||||||
|
const handleDeleteButtonClick = () => {
|
||||||
|
onChange?.(localValue.filter((_, i) => !selectedRows[i]));
|
||||||
|
setSelectedRows({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlusButtonClick = () => {
|
||||||
|
onChange?.([...localValue, { x: '', y: '', angle: '' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowChange = (index: number, windmill: WindmillConfig) => {
|
||||||
|
onChange?.(localValue.with(index, windmill));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowSelect = (index: number) => {
|
||||||
|
const checked = !selectedRows[index];
|
||||||
|
setSelectedRows({ ...selectedRows, [index]: checked });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<Span className={styles.span} />
|
||||||
|
<Span className={styles.span}>x</Span>
|
||||||
|
<Span className={styles.span}>y</Span>
|
||||||
|
<Span className={styles.span}>angle</Span>
|
||||||
|
</header>
|
||||||
|
{localValue.map((v, i) => (
|
||||||
|
<WindmillRableRow
|
||||||
|
key={i}
|
||||||
|
value={v}
|
||||||
|
onChange={(windmill) => handleRowChange(i, windmill)}
|
||||||
|
onSelect={() => handleRowSelect(i)}
|
||||||
|
selected={selectedRows[i] ?? false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<IconButton onClick={handleDeleteButtonClick}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={handlePlusButtonClick}>
|
||||||
|
<PlusIcon />
|
||||||
|
</IconButton>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
front/src/components/ux/windmill-table/index.tsx
Normal file
1
front/src/components/ux/windmill-table/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './component';
|
26
front/src/components/ux/windmill-table/styles.module.scss
Normal file
26
front/src/components/ux/windmill-table/styles.module.scss
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
.header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 46px 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.span {
|
||||||
|
padding: 13px;
|
||||||
|
border: 1px solid var(--clr-border-200);
|
||||||
|
background-color: var(--clr-layer-300);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid var(--clr-border-200);
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
background-color: var(--clr-layer-300);
|
||||||
|
}
|
6
front/src/components/ux/windmill-table/types.ts
Normal file
6
front/src/components/ux/windmill-table/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { WindmillConfig } from '../windmill-form';
|
||||||
|
|
||||||
|
export type WindmillTableProps = {
|
||||||
|
value?: WindmillConfig[];
|
||||||
|
onChange?: (value: WindmillConfig[]) => void;
|
||||||
|
};
|
@ -31,4 +31,4 @@ typing_extensions==4.12.2
|
|||||||
uvicorn==0.30.6
|
uvicorn==0.30.6
|
||||||
watchfiles==0.24.0
|
watchfiles==0.24.0
|
||||||
websockets==13.1
|
websockets==13.1
|
||||||
PyMySQL=1.1.1
|
PyMySQL==1.1.1
|
||||||
|
Loading…
Reference in New Issue
Block a user