[front]: error handling
@ -1,4 +1,4 @@
|
||||
import { Heading } from '@components/ui';
|
||||
import { Heading, Paragraph } from '@components/ui';
|
||||
import { WindmillForm } from '@components/ux';
|
||||
import { WindmillFormResponse } from '@components/ux/windmill-form';
|
||||
import React, { useState } from 'react';
|
||||
@ -6,26 +6,35 @@ import React, { useState } from 'react';
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
export function HomePage() {
|
||||
const [formResponse, setFormResponse] = useState<WindmillFormResponse>({
|
||||
power: [],
|
||||
image: '',
|
||||
});
|
||||
const [result, setResult] = useState<WindmillFormResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleFormResponse = (response: WindmillFormResponse) => {
|
||||
setFormResponse(response);
|
||||
const handleFormSuccess = (response: WindmillFormResponse) => {
|
||||
setResult(response);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleFormFail = (message: string) => {
|
||||
setError(message);
|
||||
setResult(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.wrapperForm}>
|
||||
<WindmillForm onResponse={handleFormResponse} />
|
||||
<WindmillForm onSuccess={handleFormSuccess} onFail={handleFormFail} />
|
||||
</div>
|
||||
<div className={styles.result}>
|
||||
<Heading tag="h3">Result</Heading>
|
||||
<div className={styles.power}>{formResponse.power.join(' ')}</div>
|
||||
<div className={styles.image}>
|
||||
{formResponse.image && <img src={formResponse.image} alt="Image" />}
|
||||
</div>
|
||||
{result && (
|
||||
<>
|
||||
<div className={styles.power}>{result.power.join(' ')}</div>
|
||||
<div className={styles.image}>
|
||||
{result.image && <img src={result.image} alt="Image" />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{error && <Paragraph>{error}</Paragraph>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -12,14 +12,14 @@
|
||||
}
|
||||
|
||||
.result {
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--clr-layer-200);
|
||||
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||
gap: 20px;
|
||||
grid-area: result;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.image {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Button, DateInput, Heading } from '@components/ui';
|
||||
import { dateToInputString } from '@utils/date';
|
||||
import { Controller, useForm } from '@utils/form';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { downloadImage, getWindmillData } from 'src/api';
|
||||
|
||||
import { WindmillTable } from '../windmill-table';
|
||||
@ -10,20 +11,53 @@ import styles from './styles.module.scss';
|
||||
import { WindmillFormProps, WindmillFormStore } from './types';
|
||||
|
||||
export function WindmillForm({
|
||||
onResponse,
|
||||
onSuccess,
|
||||
onFail,
|
||||
className,
|
||||
...props
|
||||
}: WindmillFormProps) {
|
||||
const [pending, setPending] = useState<boolean>(false);
|
||||
const { control, reset, getValues } = useForm<WindmillFormStore>({
|
||||
initialValues,
|
||||
});
|
||||
const classNames = clsx(className, styles.form);
|
||||
const date = dateToInputString(new Date());
|
||||
|
||||
const validate = (values: Partial<WindmillFormStore>) => {
|
||||
if (!values.dateFrom) {
|
||||
onFail('Field "from" is required');
|
||||
return false;
|
||||
}
|
||||
if (!values.dateTo) {
|
||||
onFail('Field "to" is required');
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!values.windmills?.length ||
|
||||
values.windmills.some((r) => !r.x || !r.y)
|
||||
) {
|
||||
onFail('The table is filled incorrectly');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
const res = await getWindmillData(getValues());
|
||||
const image = await downloadImage(res.file_name);
|
||||
onResponse({ power: res.data, image: URL.createObjectURL(image) });
|
||||
const values = getValues();
|
||||
if (!validate(values)) {
|
||||
return;
|
||||
}
|
||||
setPending(true);
|
||||
try {
|
||||
const res = await getWindmillData(values);
|
||||
const image = await downloadImage(res.file_name);
|
||||
onSuccess({ power: res.data, image: URL.createObjectURL(image) });
|
||||
} catch {
|
||||
onFail('Fetch error');
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetButtonClick = () => {
|
||||
@ -36,11 +70,15 @@ export function WindmillForm({
|
||||
<div className={styles.dateRangeBox}>
|
||||
<Controller
|
||||
{...control('dateFrom')}
|
||||
render={(params) => <DateInput placeholder="from" {...params} />}
|
||||
render={(params) => (
|
||||
<DateInput placeholder="from" max={date} {...params} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
{...control('dateTo')}
|
||||
render={(params) => <DateInput placeholder="to" {...params} />}
|
||||
render={(params) => (
|
||||
<DateInput placeholder="to" max={date} {...params} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
@ -48,7 +86,9 @@ export function WindmillForm({
|
||||
render={(params) => <WindmillTable {...params} />}
|
||||
/>
|
||||
<div className={styles.buttonBox}>
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="submit" pending={pending}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={handleResetButtonClick}>
|
||||
Reset
|
||||
</Button>
|
||||
|
@ -16,5 +16,6 @@ export type WindmillFormResponse = {
|
||||
};
|
||||
|
||||
export type WindmillFormProps = {
|
||||
onResponse: (response: WindmillFormResponse) => void;
|
||||
onSuccess: (response: WindmillFormResponse) => void;
|
||||
onFail: (message: string) => void;
|
||||
} & React.ComponentProps<'form'>;
|
||||
|
1
front/src/utils/date/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './input';
|
8
front/src/utils/date/input.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const dateToInputString = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
};
|
BIN
server/public/floris/035a179e-5d97-4adc-9c04-ab029eac2c41.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
server/public/floris/06af7a24-8b78-4960-ab06-38d48a3f2ca3.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
server/public/floris/0bc8cac0-7d33-4e81-8987-e83462d315d3.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
server/public/floris/16525ebd-7740-4d48-a8cf-8623b83735b6.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
server/public/floris/1a216bab-847a-47e7-b199-8ed533b8913d.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
server/public/floris/1ee71613-fc85-4268-9255-749c807eb019.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
server/public/floris/227aa2c8-0fe0-4c97-a76c-3b48084720d4.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/234b2ea7-23a5-4a8d-ae77-3d1ff6c9fbfe.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
server/public/floris/25135ffe-fff1-43d0-ab21-9a106f66a9c8.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
server/public/floris/29b9f425-d835-4349-98fe-e8b8623e0882.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/31314a1a-5f60-476a-9f16-89319faac097.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
server/public/floris/355d5282-47d5-4b0b-93e0-827a079f3f5c.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
server/public/floris/4a493968-6e81-4c49-afc4-22912fd8113c.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
server/public/floris/5b1ea13d-614f-4c4a-8eb8-f65f5966c9a6.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
server/public/floris/5d907710-3ac4-4807-9643-35d203c6c865.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
server/public/floris/5fa9d11b-6b0a-4446-9fee-b4e0712ed4bf.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
server/public/floris/676bf294-4b74-426b-b68a-9a00f17d0933.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/7034891f-c964-4d31-9226-7da51dbad516.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
server/public/floris/72acf344-a507-42fd-b4a1-5bb645894b03.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
server/public/floris/75093389-2ac3-429a-90cf-83fe44891742.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
server/public/floris/7d5a9133-93b5-4edd-9119-519f66937c66.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
server/public/floris/7d9ba71a-ad1b-4b6d-9596-cf4cd0a99299.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
server/public/floris/7f4e6ca1-4067-4fdd-94dc-40f6057f63e3.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/827bca1b-ad20-4dc6-8c57-42210e47543f.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
server/public/floris/89f8b3a8-3765-48fe-9cb3-ad9daf803190.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/90d8abed-8155-4eeb-a427-ba23798ade1b.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
server/public/floris/935e2143-0706-46d4-9246-5e0bf92633f2.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
server/public/floris/9880c167-2063-4cd5-b7cf-f777fe66c7dc.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
server/public/floris/9f4c97bb-5283-48b1-b63a-0edf4dd6719b.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
server/public/floris/a570d3f5-8211-4f45-b41a-813cddc0ae75.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
server/public/floris/a6e854d5-c47b-4544-a4a0-5b72f647a935.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
server/public/floris/b79e2789-464a-4800-8dfd-57d05d61d5a1.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/bea5009e-fcd8-412d-ae18-8a8207e715e2.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
server/public/floris/bee4effc-e476-455b-95e1-ff4375402fbb.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
server/public/floris/c4a606de-669f-45a2-b8a8-1d5f044103a4.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
server/public/floris/cf3a0493-c8a8-4967-87d4-7c3016f7a887.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
server/public/floris/cf9e1696-1a96-427f-82fe-2ac042fd3178.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
server/public/floris/d3630984-05d1-4732-8830-c93a27e05782.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
server/public/floris/d7ab3f3c-2bf9-4f16-81c9-4946aadc0648.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
server/public/floris/de88c5da-c7e5-4bcc-b848-8581cacbc309.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
server/public/floris/e0edd7fd-8dca-46ce-9b10-6f33e73cd1b8.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/e15a6bf8-e08f-426b-96b0-efe3f95cdab1.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
server/public/floris/e35293fd-043f-4866-b3e9-6ff75c92eab8.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
server/public/floris/f0650bc4-1301-4440-be56-89a223b80018.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
server/public/floris/f401df27-2e48-49aa-b455-d84a7cb025e4.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/fb3ec95d-ef0b-4214-92fc-60fdbff20a69.png
Normal file
After Width: | Height: | Size: 14 KiB |