car-crushers-portal/components/FormGenerator.tsx

265 lines
6.5 KiB
TypeScript

import {
Checkbox,
CheckboxGroup,
FormControl,
FormErrorMessage,
Heading,
HStack,
Input,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Radio,
RadioGroup,
Select,
Stack,
Textarea,
} from "@chakra-ui/react";
import { type Dispatch, type SetStateAction, useState } from "react";
interface component {
id: string;
max_length?: number;
options?: { default?: boolean; value: string }[];
required: boolean;
title: string;
type: string;
value?: number | string | string[];
}
export default function ({
components,
read_only = true,
}: {
components: { [k: number]: component[] };
read_only: boolean;
}) {
function isNumberElemInvalid(e: HTMLInputElement): boolean {
return !(
e.value ||
isNaN(e.valueAsNumber) ||
e.valueAsNumber <= Number.MAX_SAFE_INTEGER ||
e.valueAsNumber >= Number.MIN_SAFE_INTEGER
);
}
function updateState(
state: { [k: string]: string | string[] },
setState: Dispatch<SetStateAction<{}>>,
id: string,
value: string
) {
const newState = { ...state };
newState[id] = value;
setState(newState);
}
function renderCheckboxOptions(
c: component,
state: { [k: string]: string | string[] },
setState: Dispatch<SetStateAction<{}>>
) {
if (!c.options) throw new Error("Options for checkbox are undefined");
const boxes = [];
const checkedBoxes = [];
for (const option of c.options) {
if (
option.default ||
(read_only && Array.isArray(c.value) && c.value.includes(option.value))
)
checkedBoxes.push(option.value);
boxes.push(
<Checkbox
isReadOnly={read_only}
onChange={(e) => {
const newState = { ...state };
const groupValues = newState[c.id] ?? [];
if (!Array.isArray(groupValues))
throw new Error("Expected CheckboxGroup values to be an array");
e.target.checked
? groupValues.push(e.target.value)
: groupValues.splice(
groupValues.findIndex((v) => v === e.target.value),
1
);
newState[c.id] = groupValues;
setState(newState);
}}
value={option.value}
>
{option.value}
</Checkbox>
);
}
return (
<CheckboxGroup defaultValue={checkedBoxes}>
<HStack spacing={5}>{boxes}</HStack>
</CheckboxGroup>
);
}
function renderRadioElements(
c: component,
state: { [k: string]: string | string[] },
setState: Dispatch<SetStateAction<{}>>
) {
if (!c.options) throw new Error("Options for radio buttons are undefined!");
const buttons = [];
for (const option of c.options) {
buttons.push(
<Radio checked={option.default} value={option.value}>
{option.value}
</Radio>
);
}
return (
<RadioGroup
id={c.id}
onChange={(e) => {
const newState = { ...state };
newState[c.id] = e;
setState(newState);
}}
>
<Stack direction="row">{buttons}</Stack>
</RadioGroup>
);
}
function renderSelectElements(
c: component,
state: { [k: string]: string | string[] },
setState: Dispatch<SetStateAction<{}>>
) {
if (!c.options) throw new Error("Options for select are undefined!");
const selectOptions = [];
for (const option of c.options) {
selectOptions.push(<option value={option.value}>{option.value}</option>);
}
return (
<Select
onChange={(e) => {
const newState = { ...state };
newState[c.id] = e.target.value;
setState(newState);
}}
placeholder="Select option"
>
{selectOptions}
</Select>
);
}
function generateReactComponents(
components: component[],
state: { [k: string]: string | string[] },
setState: Dispatch<SetStateAction<{}>>
): JSX.Element[] {
const fragmentsList = [];
for (const component of components) {
fragmentsList.push(
<Heading size="md">{component.title}</Heading>,
<br />
);
switch (component.type) {
case "checkbox":
fragmentsList.push(renderCheckboxOptions(component, state, setState));
break;
case "input":
fragmentsList.push(
<FormControl
isInvalid={
!(document.getElementById(component.id) as HTMLInputElement)
.value.length
}
isReadOnly={read_only}
>
<Input
id={component.id}
maxLength={component.max_length}
onChange={(e) =>
updateState(state, setState, component.id, e.target.value)
}
placeholder="Your response"
value={component.value}
/>
<FormErrorMessage>Field is required</FormErrorMessage>
</FormControl>
);
break;
case "number":
fragmentsList.push(
<NumberInput
isInvalid={isNumberElemInvalid(
document.getElementById(component.id) as HTMLInputElement
)}
isReadOnly={read_only}
>
<NumberInputField
id={component.id}
onChange={(e) =>
updateState(state, setState, component.id, e.target.value)
}
value={component.value}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
break;
case "radio":
fragmentsList.push(renderRadioElements(component, state, setState));
break;
case "select":
fragmentsList.push(renderSelectElements(component, state, setState));
break;
}
fragmentsList.push(<br />, <br />, <br />);
}
return fragmentsList;
}
const pages = [];
const [responses, setResponses] = useState({});
for (const [page, componentList] of Object.entries(components)) {
pages.push(
<div
id={`form-page-${page}`}
style={{ display: page ? "none" : undefined }}
>
{generateReactComponents(componentList, responses, setResponses)}
</div>
);
}
return <></>;
}