[test-entity]: front pt.2

This commit is contained in:
it-is-not-alright 2024-11-20 03:08:12 +04:00
parent a60304ca0f
commit 658a351d28
55 changed files with 498 additions and 240 deletions

View File

@ -3,4 +3,5 @@ export const WIND_ENDPOINTS = {
turbineType: 'api/wind/turbine_type',
parks: 'api/wind/parks',
park: 'api/wind/park',
parkTurbine: 'api/wind/park_turbine',
};

View File

@ -1,10 +1,11 @@
import { ApiResponse } from '@api/types';
import { ParkFormValues } from '@components/pages/park-page/types';
import { TurbineTypeFormValues } from '@components/pages/turbine-type-page/types';
import { api } from '../api';
import { WIND_ENDPOINTS } from './constants';
import { Park, ParkTurbine, ParkWithTurbines, TurbineType } from './types';
import { packTurbineTypes } from './utils';
import { packPark, packParkTurbine, packTurbineTypes } from './utils';
export const getTurbineTypes = () => {
return api.get<TurbineType[]>(WIND_ENDPOINTS.turbines);
@ -63,3 +64,47 @@ export const getParkWithTurbines = async (
error: parkPesponse.error || turbinesResponse.error || null,
};
};
export const createPark = async (formValues: Partial<ParkFormValues>) => {
const parkPesponse = await api.post<Park>(
WIND_ENDPOINTS.park,
packPark(formValues),
);
await Promise.all(
formValues.turbines?.map((t) => {
return api.post(
WIND_ENDPOINTS.parkTurbine,
packParkTurbine(t, parkPesponse.data.id),
);
}),
);
return getParkWithTurbines(String(parkPesponse.data.id));
};
export const updatePark = async (
formValues: Partial<ParkFormValues>,
id: string,
) => {
const parkPesponse = await api.put<Park>(
`${WIND_ENDPOINTS.park}/${id}`,
packPark(formValues),
);
await Promise.all(
formValues.turbines?.map((t) => {
if (t.new) {
return api.post(
WIND_ENDPOINTS.parkTurbine,
packParkTurbine(t, parkPesponse.data.id),
);
}
if (t.delete) {
return api.delete(`${WIND_ENDPOINTS.parkTurbine}/${t.id}`);
}
return api.put(
`${WIND_ENDPOINTS.parkTurbine}/${parkPesponse.data.id}/${t.id}`,
packParkTurbine(t, parkPesponse.data.id),
);
}),
);
return getParkWithTurbines(id);
};

View File

@ -1,9 +1,32 @@
import {
ParkFormTurbine,
ParkFormValues,
} from '@components/pages/park-page/types';
import { TurbineTypeFormValues } from '@components/pages/turbine-type-page/types';
export const packTurbineTypes = (values: Partial<TurbineTypeFormValues>) => {
return {
Name: values.name ?? '',
Height: parseInt(values.height ?? '0'),
BladeLength: parseInt(values.bladeLength ?? '0'),
Height: parseInt(values.height || '0'),
BladeLength: parseInt(values.bladeLength || '0'),
};
};
export const packPark = (values: Partial<ParkFormValues>) => {
return {
Name: values.name ?? '',
CenterLatitude: parseInt(values.centerLatitude || '0'),
CenterLongitude: parseInt(values.centerLongitude || '0'),
};
};
export const packParkTurbine = (turbine: ParkFormTurbine, parkId: number) => {
return {
wind_park_id: parkId,
turbine_id: turbine.id,
x_offset: parseInt(turbine.xOffset || '0'),
y_offset: parseInt(turbine.yOffset || '0'),
angle: parseInt(turbine.angle || '0'),
comment: turbine.comment ?? '',
};
};

View File

@ -1,11 +1,16 @@
import { getParkWithTurbines, ParkWithTurbines } from '@api/wind';
import { Button, Heading, NumberInput, TextInput } from '@components/ui';
import {
createPark,
getParkWithTurbines,
ParkWithTurbines,
updatePark,
} from '@api/wind';
import { Button, Heading, NumberField, TextInput } from '@components/ui';
import { ParkTurbines } from '@components/ux';
import { Controller, useForm } from '@utils/form';
import { useRoute } from '@utils/route';
import { ROUTES, useRoute } from '@utils/route';
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { ParkTurbines } from './components';
import styles from './styles.module.scss';
import { ParkFormValues } from './types';
import { unpackPark } from './utils';
@ -14,6 +19,7 @@ export function ParkPage() {
const [park, setPark] = useState<ParkWithTurbines>(null);
const [pending, setPending] = useState<boolean>(false);
const params = useParams();
const navigate = useNavigate();
const route = useRoute();
const { register, control, getValues, reset } = useForm<ParkFormValues>({});
@ -35,6 +41,20 @@ export function ParkPage() {
fetchPark();
}, [id]);
const handleFormSubmit = async (event: React.FormEvent) => {
event.preventDefault();
setPending(true);
if (isEdit) {
const response = await updatePark(getValues(), id);
setPark(response.data);
reset(unpackPark(response.data));
} else {
const response = await createPark(getValues());
navigate(ROUTES.park.path.replace(':id', String(response.data.id)));
}
setPending(false);
};
const handleReset = () => {
if (isEdit) {
reset(unpackPark(park));
@ -44,7 +64,7 @@ export function ParkPage() {
};
return (
<div className={styles.page}>
<div className={styles.page} onSubmit={handleFormSubmit}>
<Heading tag="h1">{route.title}</Heading>
<form className={styles.form}>
<header>
@ -55,13 +75,13 @@ export function ParkPage() {
<Controller
{...control('centerLatitude')}
render={(props) => (
<NumberInput label={{ text: 'Center Latitude' }} {...props} />
<NumberField label={{ text: 'Center Latitude' }} {...props} />
)}
/>
<Controller
{...control('centerLongitude')}
render={(props) => (
<NumberInput label={{ text: 'Center Longitude' }} {...props} />
<NumberField label={{ text: 'Center Longitude' }} {...props} />
)}
/>
</div>
@ -76,7 +96,9 @@ export function ParkPage() {
</form>
<Controller
{...control('turbines')}
render={(props) => <ParkTurbines {...props} />}
render={(props) => (
<ParkTurbines savedTurbines={park?.turbines ?? []} {...props} />
)}
/>
</div>
);

View File

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

View File

@ -1,19 +0,0 @@
import { DataGrid } from '@components/ui/data-grid';
import React from 'react';
import { columns } from './constants';
import { ParkTurbinesProps } from './types';
export function ParkTurbines({ value = [] }: ParkTurbinesProps) {
return (
<div>
<div></div>
<DataGrid
items={value}
columns={columns}
getItemKey={({ id }) => String(id)}
selectedItems={[]}
/>
</div>
);
}

View File

@ -1,11 +0,0 @@
import { DataGridColumnConfig } from '@components/ui/data-grid/types';
import { ParkTurbine } from 'src/api/wind';
export const columns: DataGridColumnConfig<ParkTurbine>[] = [
{ name: 'Id', getText: (t) => String(t.id) },
{ name: 'Name', getText: (t) => t.name },
{ name: 'X', getText: (t) => String(t.xOffset) },
{ name: 'Y', getText: (t) => String(t.yOffset) },
{ name: 'Angle', getText: (t) => String(t.angle) },
{ name: 'Comment', getText: (t) => String(t.comment), flex: '2' },
];

View File

@ -1,6 +0,0 @@
import { ParkTurbine } from '@api/wind';
export type ParkTurbinesProps = {
value?: ParkTurbine[];
onChange?: (value: ParkTurbine[]) => void;
};

View File

@ -1,8 +1,17 @@
import { ParkTurbine } from '@api/wind';
export type ParkFormTurbine = {
id: number;
name: string;
xOffset: string;
yOffset: string;
angle: string;
comment: string;
new?: boolean;
delete?: boolean;
};
export type ParkFormValues = {
name: string;
centerLatitude: string;
centerLongitude: string;
turbines: ParkTurbine[];
turbines: ParkFormTurbine[];
};

View File

@ -7,6 +7,13 @@ export const unpackPark = (park: ParkWithTurbines): ParkFormValues => {
name: park.name,
centerLatitude: String(park.centerLatitude),
centerLongitude: String(park.centerLongitude),
turbines: park.turbines,
turbines: park.turbines.map((t) => ({
id: t.id,
name: t.name,
xOffset: String(t.xOffset),
yOffset: String(t.yOffset),
angle: String(t.angle),
comment: t.comment,
})),
};
};

View File

@ -2,7 +2,7 @@ import { DataGridColumnConfig } from '@components/ui/data-grid/types';
import { Park } from 'src/api/wind';
export const columns: DataGridColumnConfig<Park>[] = [
{ name: 'Name', getText: (t) => t.name, flex: '2' },
{ name: 'Name', getText: (t) => t.name, width: 2 },
{ name: 'Center Latitude', getText: (t) => String(t.centerLatitude) },
{ name: 'Center Longitude', getText: (t) => String(t.centerLongitude) },
];

View File

@ -12,7 +12,6 @@
.actions {
display: flex;
padding: 10px;
border: 1px solid var(--clr-border-100);
border-radius: 15px;
background-color: var(--clr-layer-200);
box-shadow: 0px 1px 2px var(--clr-shadow-100);

View File

@ -4,7 +4,7 @@ import {
getTurbineType,
TurbineType,
} from '@api/wind';
import { Button, Heading, NumberInput, TextInput } from '@components/ui';
import { Button, Heading, NumberField, TextInput } from '@components/ui';
import { Controller, useForm } from '@utils/form';
import { ROUTES, useRoute } from '@utils/route';
import React, { useEffect, useState } from 'react';
@ -47,6 +47,7 @@ export function TurbineTypePage() {
if (isEdit) {
const response = await editTurbineTypes(getValues(), id);
setTurbineType(response.data);
reset(unpackTurbineType(response.data));
} else {
const response = await createTurbineTypes(getValues());
navigate(
@ -76,13 +77,13 @@ export function TurbineTypePage() {
<Controller
{...control('height')}
render={(props) => (
<NumberInput label={{ text: 'Height' }} {...props} />
<NumberField label={{ text: 'Height' }} {...props} />
)}
/>
<Controller
{...control('bladeLength')}
render={(props) => (
<NumberInput label={{ text: 'Blade length' }} {...props} />
<NumberField label={{ text: 'Blade length' }} {...props} />
)}
/>
</div>

View File

@ -2,7 +2,7 @@ import { DataGridColumnConfig } from '@components/ui/data-grid/types';
import { TurbineType } from 'src/api/wind';
export const columns: DataGridColumnConfig<TurbineType>[] = [
{ name: 'Name', getText: (t) => t.name, flex: '2' },
{ name: 'Name', getText: (t) => t.name, width: 2 },
{ name: 'Height', getText: (t) => String(t.height) },
{ name: 'Blade length', getText: (t) => String(t.bladeLength) },
];

View File

@ -12,7 +12,6 @@
.actions {
display: flex;
padding: 10px;
border: 1px solid var(--clr-border-100);
border-radius: 15px;
background-color: var(--clr-layer-200);
box-shadow: 0px 1px 2px var(--clr-shadow-100);

View File

@ -15,6 +15,11 @@ export function DataGrid<T>({
}: DataGridProps<T>) {
const [allItemsSelected, setAllItemsSelected] = useState<boolean>(false);
const columnsTemplate = useMemo(() => {
const main = columns.map((c) => `${c.width ?? 1}fr`).join(' ');
return `auto ${main}`;
}, []);
const selectedItemsMap = useMemo(() => {
const map: Record<string, T> = {};
for (let i = 0; i < selectedItems.length; i += 1) {
@ -35,7 +40,7 @@ export function DataGrid<T>({
const handleItemSelect = (key: string, item: T) => {
const selected = Boolean(selectedItemsMap[key]);
if (!multiselect) {
onItemsSelect(selected ? [] : [item]);
onItemsSelect?.(selected ? [] : [item]);
return;
}
onItemsSelect?.(
@ -52,6 +57,7 @@ export function DataGrid<T>({
columns={columns}
allItemsSelected={allItemsSelected}
onSelectAllItems={handleSelectAllItems}
columnsTemplate={columnsTemplate}
/>
{items.map((item) => {
const key = String(getItemKey(item));
@ -62,6 +68,7 @@ export function DataGrid<T>({
selected={Boolean(selectedItemsMap[key])}
onSelect={() => handleItemSelect(key, item)}
key={getItemKey(item)}
columnsTemplate={columnsTemplate}
/>
);
})}

View File

@ -14,6 +14,7 @@ export function DataGridHeader<T>({
columns,
allItemsSelected,
onSelectAllItems,
columnsTemplate,
}: DataGridHeaderProps<T>) {
const [sort, setSort] = useState<DataGridSort>({ order: 'asc', column: '' });
@ -30,7 +31,10 @@ export function DataGridHeader<T>({
};
return (
<header className={styles.header}>
<header
className={styles.header}
style={{ gridTemplateColumns: columnsTemplate }}
>
<Checkbox
checked={allItemsSelected}
onChange={onSelectAllItems}
@ -44,7 +48,6 @@ export function DataGridHeader<T>({
});
return (
<RawButton
style={{ flex: column.flex }}
className={cellClassName}
key={column.name}
onClick={() => handleSortButtonClick(column.name)}

View File

@ -1,5 +1,5 @@
.header {
display: flex;
display: grid;
}
.checkboxLabel {
@ -13,7 +13,6 @@
position: relative;
display: flex;
overflow: hidden;
flex: 1;
align-items: center;
padding: 10px;
border: solid 1px var(--clr-border-100);

View File

@ -4,4 +4,5 @@ export type DataGridHeaderProps<T> = {
columns: DataGridColumnConfig<T>[];
allItemsSelected: boolean;
onSelectAllItems: () => void;
columnsTemplate: string;
};

View File

@ -10,20 +10,20 @@ export function DataGridRow<T>({
columns,
selected,
onSelect,
columnsTemplate,
}: DataGridRowProps<T>) {
return (
<div className={styles.row}>
<div
className={styles.row}
style={{ gridTemplateColumns: columnsTemplate }}
>
<Checkbox
checked={selected}
label={{ className: styles.checkboxLabel }}
onChange={onSelect}
/>
{columns.map((column) => (
<div
className={styles.cell}
style={{ flex: column.flex }}
key={column.name}
>
<div className={styles.cell} key={column.name}>
<Span>{column.getText(object)}</Span>
</div>
))}

View File

@ -1,5 +1,5 @@
.row {
display: flex;
display: grid;
}
.checkboxLabel {
@ -11,7 +11,6 @@
.cell {
display: flex;
overflow: hidden;
flex: 1 0 0;
align-items: center;
padding: 10px;
border: solid 1px var(--clr-border-100);

View File

@ -5,4 +5,5 @@ export type DataGridRowProps<T> = {
columns: DataGridColumnConfig<T>[];
selected: boolean;
onSelect: () => void;
columnsTemplate: string;
};

View File

@ -24,7 +24,7 @@ export function DataGridPreview() {
const columns: DataGridColumnConfig<Cat>[] = [
{ name: 'Name', getText: (cat) => cat.name },
{ name: 'Breed', getText: (cat) => cat.breed, scale: 2 },
{ name: 'Breed', getText: (cat) => cat.breed, width: 2 },
{ name: 'Age', getText: (cat) => cat.age },
{ name: 'Color', getText: (cat) => cat.color },
];

View File

@ -4,7 +4,7 @@ export type DataGridColumnConfig<T> = {
name: string;
getText: (object: T) => string;
sortable?: boolean;
flex?: string;
width?: number;
};
export type DataGridSort = {

View File

@ -3,13 +3,14 @@ export { Button } from './button';
export { Checkbox } from './checkbox';
export { CheckboxGroup } from './checkbox-group';
export { DateInput } from './date-input';
export { Dialog } from './dialog';
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 { NumberField } from './number-field';
export { NumberInput } from './number-input';
export { Overlay } from './overlay';
export { Pagination } from './pagination';

View File

@ -0,0 +1,28 @@
import React, { ForwardedRef, forwardRef } from 'react';
import { Label, LabelProps } from '../label';
import { NumberInput } from '../number-input';
import { NumberFieldProps } from './types';
function NumberFieldInner(
{ scale, label = {}, required, ...props }: Omit<NumberFieldProps, 'ref'>,
ref: ForwardedRef<HTMLInputElement>,
) {
const labelProps: LabelProps = {
...label,
required: { value: required, ...label.required },
};
return (
<Label scale={scale} {...labelProps}>
<NumberInput
scale={scale}
ref={ref}
required={required}
{...props}
invalid={label.error !== undefined}
/>
</Label>
);
}
export const NumberField = forwardRef(NumberFieldInner);

View File

@ -0,0 +1,6 @@
import { LabelProps } from '../label/types';
import { NumberInputProps } from '../number-input/types';
export type NumberFieldProps = {
label?: LabelProps;
} & NumberInputProps;

View File

@ -1,6 +1,6 @@
import React from 'react';
import { TextInput } from '../text-input';
import { Input } from '../input';
import { NumberInputProps } from './types';
export function NumberInput({
@ -34,5 +34,5 @@ export function NumberInput({
onChange?.(num);
};
return <TextInput value={value ?? ''} onChange={handleChange} {...props} />;
return <Input value={value ?? ''} onChange={handleChange} {...props} />;
}

View File

@ -1,5 +1,5 @@
export { Header } from './header';
export { ParkTurbineTable } from './park-turbine-table';
export { ParkTurbines } from './park-turbines';
export { Sidebar } from './sidebar';
export { SignInForm } from './sign-in-form';
export { SignUpForm } from './sign-up-form';
export { ThemeSelect } from './theme-select';

View File

@ -0,0 +1,35 @@
import { ParkFormTurbine } from '@components/pages/park-page/types';
import React from 'react';
import { ParkTurbineTableHeader, ParkTurbineTableRow } from './components';
import { ParkTurbineTableProps } from './types';
export function ParkTurbineTable({
turbines,
onChange,
selectedIDs,
onSelect,
}: ParkTurbineTableProps) {
const handleRowChange = (turbine: ParkFormTurbine, index: number) => {
onChange(turbines.with(index, turbine));
};
const handleSelect = (turbineId: number) => {
onSelect({ ...selectedIDs, [turbineId]: !selectedIDs[turbineId] });
};
return (
<div>
<ParkTurbineTableHeader />
{turbines.map((turbine, index) => (
<ParkTurbineTableRow
turbine={turbine}
onChange={(t) => handleRowChange(t, index)}
selected={selectedIDs[turbine.id] ?? false}
onSelect={() => handleSelect(turbine.id)}
key={turbine.id}
/>
))}
</div>
);
}

View File

@ -0,0 +1,2 @@
export * from './park-turbine-table-header';
export * from './park-turbine-table-row';

View File

@ -0,0 +1,35 @@
import { Checkbox } from '@components/ui/checkbox';
import { Span } from '@components/ui/span';
import React from 'react';
import styles from './styles.module.scss';
export function ParkTurbineTableHeader() {
return (
<header className={styles.header}>
<Checkbox
checked={false}
onChange={() => {}}
label={{ className: styles.checkboxLabel }}
/>
<Span color="t300" className={styles.cell}>
Id
</Span>
<Span color="t300" className={styles.cell}>
Name
</Span>
<Span color="t300" className={styles.cell}>
X
</Span>
<Span color="t300" className={styles.cell}>
Y
</Span>
<Span color="t300" className={styles.cell}>
Angle
</Span>
<Span color="t300" className={styles.cell}>
Comment
</Span>
</header>
);
}

View File

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

View File

@ -0,0 +1,26 @@
.header {
display: grid;
grid-template-columns: auto 1fr 1fr 1fr 1fr 1fr 3fr;
}
.checkboxLabel {
padding: 10px;
border: solid 1px var(--clr-border-100);
background-color: var(--clr-layer-300);
border-top-left-radius: 10px;
}
.cell {
position: relative;
display: flex;
overflow: hidden;
align-items: center;
padding: 10px;
border: solid 1px var(--clr-border-100);
background-color: var(--clr-layer-300);
transition: all var(--td-100) ease-in-out;
&:last-of-type {
border-top-right-radius: 10px;
}
}

View File

@ -0,0 +1,52 @@
import { NumberInput } from '@components/ui';
import { Checkbox } from '@components/ui/checkbox';
import { Input } from '@components/ui/input';
import React from 'react';
import styles from './styles.module.scss';
import { ParkTurbineTableRowProps } from './types';
export function ParkTurbineTableRow({
turbine,
onChange,
selected,
onSelect,
}: ParkTurbineTableRowProps) {
return (
<div className={styles.row}>
<Checkbox
checked={selected}
onChange={onSelect}
label={{ className: styles.checkboxLabel }}
/>
<Input value={turbine.id} wrapper={{ className: styles.cell }} disabled />
<Input
value={turbine.name}
wrapper={{ className: styles.cell }}
disabled
/>
<NumberInput
value={String(turbine.xOffset)}
wrapper={{ className: styles.cell }}
onChange={(value) => onChange({ ...turbine, xOffset: value })}
/>
<NumberInput
value={String(turbine.yOffset)}
wrapper={{ className: styles.cell }}
onChange={(value) => onChange({ ...turbine, yOffset: value })}
/>
<NumberInput
value={String(turbine.angle)}
wrapper={{ className: styles.cell }}
onChange={(value) => onChange({ ...turbine, angle: value })}
/>
<Input
value={turbine.comment}
wrapper={{ className: styles.cell }}
onChange={(event) =>
onChange({ ...turbine, comment: event.target.value })
}
/>
</div>
);
}

View File

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

View File

@ -0,0 +1,16 @@
.row {
display: grid;
grid-template-columns: auto 1fr 1fr 1fr 1fr 1fr 3fr;
.cell {
border: solid 1px var(--clr-border-100);
border-radius: 0;
background-color: var(--clr-layer-200);
}
}
.checkboxLabel {
padding: 10px;
border: solid 1px var(--clr-border-100);
background-color: var(--clr-layer-200);
}

View File

@ -0,0 +1,8 @@
import { ParkFormTurbine } from '@components/pages/park-page/types';
export type ParkTurbineTableRowProps = {
turbine: ParkFormTurbine;
onChange: (turbine: ParkFormTurbine) => void;
selected: boolean;
onSelect: () => void;
};

View File

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

View File

@ -0,0 +1,8 @@
import { ParkFormTurbine } from '@components/pages/park-page/types';
export type ParkTurbineTableProps = {
turbines: ParkFormTurbine[];
onChange: (turbines: ParkFormTurbine[]) => void;
selectedIDs: Record<number, boolean>;
onSelect: (selected: Record<number, boolean>) => void;
};

View File

@ -0,0 +1,87 @@
import { getTurbineTypes, TurbineType } from '@api/wind';
import { ParkFormTurbine } from '@components/pages/park-page/types';
import { Autocomplete, Button } from '@components/ui';
import React, { useEffect, useState } from 'react';
import { ParkTurbineTable } from '../park-turbine-table';
import styles from './styles.module.scss';
import { ParkTurbinesProps } from './types';
export function ParkTurbines({
savedTurbines,
value = [],
onChange,
}: ParkTurbinesProps) {
const [turbineTypes, setTurbineTypes] = useState<TurbineType[]>([]);
const [turbineType, setTurbineType] = useState<TurbineType>(null);
const [selectedIDs, setSelectedIDs] = useState<Record<number, boolean>>({});
const fetchTurbineTypes = async () => {
const res = await getTurbineTypes();
setTurbineTypes(res.data ?? []);
};
useEffect(() => {
fetchTurbineTypes();
}, []);
const handleAddButtonClick = () => {
if (
savedTurbines.find((t) => t.id === turbineType.id) ||
value.find((t) => t.id === turbineType.id)
) {
return;
}
onChange([
...value,
{
id: turbineType.id,
name: turbineType.name,
xOffset: '',
yOffset: '',
angle: '',
comment: '',
new: true,
},
]);
};
const handleDeleteButtonClick = () => {
const newValue: ParkFormTurbine[] = [];
value.forEach((turbine) => {
if (!selectedIDs[turbine.id]) {
newValue.push(turbine);
return;
}
if (savedTurbines.find((t) => t.id === turbine.id)) {
newValue.push({ ...turbine, delete: true });
}
});
onChange(newValue);
setSelectedIDs({});
};
return (
<div className={styles.parkTurbines}>
<div className={styles.actions}>
<Autocomplete
options={turbineTypes ?? []}
getOptionKey={({ id }) => id}
getOptionLabel={({ name }) => name}
onChange={(t) => setTurbineType(t)}
value={turbineType}
/>
<Button onClick={handleAddButtonClick}>Add</Button>
<Button variant="secondary" onClick={handleDeleteButtonClick}>
Delete
</Button>
</div>
<ParkTurbineTable
turbines={value.filter((t) => !t.delete)}
onChange={onChange}
selectedIDs={selectedIDs}
onSelect={setSelectedIDs}
/>
</div>
);
}

View File

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

View File

@ -0,0 +1,14 @@
.parkTurbines {
display: grid;
gap: 10px;
grid-template-rows: auto 1fr;
}
.actions {
display: flex;
padding: 10px;
border-radius: 15px;
background-color: var(--clr-layer-200);
box-shadow: 0px 1px 2px var(--clr-shadow-100);
gap: 10px;
}

View File

@ -0,0 +1,8 @@
import { ParkTurbine } from '@api/wind';
import { ParkFormTurbine } from '@components/pages/park-page/types';
export type ParkTurbinesProps = {
savedTurbines: ParkTurbine[];
value?: ParkFormTurbine[];
onChange?: (value: ParkFormTurbine[]) => void;
};

View File

@ -1,40 +0,0 @@
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<SignInFormStore>({});
const classNames = clsx(className, styles.form);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
console.log(getValues());
};
return (
<form onSubmit={handleSubmit} className={classNames} {...props}>
<Heading tag="h1" className={styles.heading}>
Sign in
</Heading>
<div className={styles.inputBox}>
<TextInput {...register('email')} label={{ text: 'Email' }} />
<PasswordInput {...register('password')} label={{ text: 'Password' }} />
</div>
<div className={styles.buttonBox}>
<Button type="submit">Sign in</Button>
<LinkButton href={ROUTES.signUp.path}>Not a member?</LinkButton>
</div>
</form>
);
}

View File

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

View File

@ -1,26 +0,0 @@
.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;
}

View File

@ -1,8 +0,0 @@
import { ComponentProps } from 'react';
export type SignInFormStore = {
email: string;
password: string;
};
export type SignInFormProps = {} & ComponentProps<'form'>;

View File

@ -1,40 +0,0 @@
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<SignUpFormStore>({});
const classNames = clsx(className, styles.form);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
console.log(getValues());
};
return (
<form onSubmit={handleSubmit} className={classNames} {...props}>
<Heading tag="h1" className={styles.heading}>
Sign up
</Heading>
<div className={styles.inputBox}>
<TextInput {...register('email')} label={{ text: 'Email' }} />
<PasswordInput {...register('password')} label={{ text: 'Password' }} />
</div>
<div className={styles.buttonBox}>
<Button type="submit">Sign up</Button>
<LinkButton href={ROUTES.signIn.path}>Already a member?</LinkButton>
</div>
</form>
);
}

View File

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

View File

@ -1,26 +0,0 @@
.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;
}

View File

@ -1,9 +0,0 @@
import { ComponentProps } from 'react';
export type SignUpFormStore = {
email: string;
password: string;
passwordRepeat: string;
};
export type SignUpFormProps = {} & ComponentProps<'form'>;

View File

@ -143,12 +143,12 @@ class WindParkTurbineRepository:
return db_park_turbine
@staticmethod
def get(db: Session, park_turbine_id: int):
return db.query(WindParkTurbine).filter(WindParkTurbine.turbine_id == park_turbine_id).first()
def get(db: Session, park_id: int, turbine_id: int):
return db.query(WindParkTurbine).filter(WindParkTurbine.wind_park_id == park_id, WindParkTurbine.turbine_id == turbine_id).first()
@staticmethod
def update(db: Session, park_turbine_id: int, park_turbine: WindParkTurbineCreate):
db_park_turbine = db.query(WindParkTurbine).filter(WindParkTurbine.turbine_id == park_turbine_id).first()
def update(db: Session, park_id: int, turbine_id: int, park_turbine: WindParkTurbineCreate):
db_park_turbine = db.query(WindParkTurbine).filter(WindParkTurbine.wind_park_id == park_id, WindParkTurbine.turbine_id == turbine_id).first()
if db_park_turbine:
for key, value in park_turbine.dict().items():
setattr(db_park_turbine, key, value)

View File

@ -77,17 +77,17 @@ async def create_park_turbine(park_turbine: WindParkTurbineCreate, db: Session =
return WindParkTurbineRepository.create(db=db, park_turbine=park_turbine)
@router.get("/park_turbine/{park_turbine_id}", response_model=WindParkTurbineResponse)
async def read_park_turbine(park_turbine_id: int, db: Session = Depends(get_db)):
park_turbine = WindParkTurbineRepository.get(db=db, park_turbine_id=park_turbine_id)
@router.get("/park_turbine/{park_id}/{turbine_id}", response_model=WindParkTurbineResponse)
async def read_park_turbine(park_id: int, turbine_id: int, db: Session = Depends(get_db)):
park_turbine = WindParkTurbineRepository.get(db=db, park_id=park_id, turbine_id=turbine_id)
if park_turbine is None:
raise HTTPException(status_code=404, detail="Park Turbine not found")
return park_turbine
@router.put("/park_turbine/{park_turbine_id}", response_model=WindParkTurbineResponse)
async def update_park_turbine(park_turbine_id: int, park_turbine: WindParkTurbineCreate, db: Session = Depends(get_db)):
updated_park_turbine = WindParkTurbineRepository.update(db=db, park_turbine_id=park_turbine_id,
@router.put("/park_turbine/{park_id}/{turbine_id}", response_model=WindParkTurbineResponse)
async def update_park_turbine(park_id: int, turbine_id: int, park_turbine: WindParkTurbineCreate, db: Session = Depends(get_db)):
updated_park_turbine = WindParkTurbineRepository.update(db=db, park_id=park_id, turbine_id=turbine_id,
park_turbine=park_turbine)
if updated_park_turbine is None:
raise HTTPException(status_code=404, detail="Park Turbine not found")