[front]: form

This commit is contained in:
it-is-not-alright 2024-10-21 01:02:12 +04:00
parent d435cde55c
commit fd5c724342
33 changed files with 229 additions and 187 deletions

View File

@ -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",

View 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

View 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

View File

View 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>

View File

@ -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>
);
}

View File

@ -1 +0,0 @@
export { FormPage } from './component';

View File

@ -1,11 +0,0 @@
.about {
display: grid;
padding: 20px;
grid-template:
'. form .' auto
/ auto minmax(0, 380px) auto;
}
.form {
grid-area: form;
}

View File

@ -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>
); );

View File

@ -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;
} }

View File

@ -1,2 +1 @@
export { FormPage } from './form-page';
export { HomePage } from './home-page'; export { HomePage } from './home-page';

View File

@ -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'>;

View File

@ -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}>

View File

@ -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;

View File

@ -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);
}; };

View File

@ -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'>;

View File

@ -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;

View File

@ -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>
); );
} }

View File

@ -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';

View File

@ -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>
);
}

View File

@ -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],
};

View File

@ -1 +0,0 @@
export { LoginForm } from './component';

View File

@ -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'>;

View 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>
);
}

View File

@ -0,0 +1,5 @@
import { FormValues } from '@utils/form';
import { WindmillFormStore } from './types';
export const initialValues: FormValues<WindmillFormStore> = {};

View File

@ -0,0 +1,2 @@
export { WindmillForm } from './component';
export { type WindmillConfig } from './types';

View File

@ -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;
}

View 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'>;

View 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>
);
}

View File

@ -0,0 +1 @@
export * from './component';

View 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);
}

View File

@ -0,0 +1,6 @@
import { WindmillConfig } from '../windmill-form';
export type WindmillTableProps = {
value?: WindmillConfig[];
onChange?: (value: WindmillConfig[]) => void;
};

View File

@ -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