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