initial setup, formik, fields, layout
This commit is contained in:
parent
765cc0c7cf
commit
b23d706180
16 changed files with 578 additions and 53 deletions
38
components/Form/ClientField.tsx
Normal file
38
components/Form/ClientField.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React from "react"
|
||||
import SelectInput from "./SelectInput"
|
||||
import { Utils } from "./Utils"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faClock } from "@fortawesome/free-regular-svg-icons"
|
||||
|
||||
// Define a type for your props to improve type-checking
|
||||
type ClientFieldProps = {
|
||||
testingCenters: string[]
|
||||
clients: string[]
|
||||
}
|
||||
|
||||
// Destructure the props in the function parameter
|
||||
const ClientField: React.FC<ClientFieldProps> = ({ testingCenters, clients }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{testingCenters.map((testingCenter) => (
|
||||
<div key={Utils.camelize(testingCenter)} className="flex gap-14 items-center">
|
||||
<span>{testingCenter}</span>
|
||||
<div className="flex gap-2 items-center">
|
||||
<SelectInput name={Utils.camelize(testingCenter)}>
|
||||
<option value="">Select Client</option>
|
||||
{clients.map((option: string) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
<FontAwesomeIcon icon={faClock}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ClientField
|
||||
10
components/Form/ErrorMessage.tsx
Normal file
10
components/Form/ErrorMessage.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from "react"
|
||||
|
||||
const ErrorMessage = ({ touched, error }: { touched: boolean; error?: string }) => {
|
||||
if (touched && error) {
|
||||
return <p className="mt-1">{error}</p>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default ErrorMessage
|
||||
67
components/Form/FileInput.tsx
Normal file
67
components/Form/FileInput.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { useField, useFormikContext } from "formik";
|
||||
import React, { useRef } from "react";
|
||||
import ErrorMessage from "./ErrorMessage";
|
||||
import RequiredAsterisk from "./RequiredAsterisk";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFile } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
|
||||
interface FileInputProps {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
name:string;
|
||||
}
|
||||
|
||||
const FileInput: React.FC<FileInputProps> = ({ label, ...props }) => {
|
||||
const { required } = props;
|
||||
const [field, meta] = useField(props);
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { files } = event.target;
|
||||
const file = files ? files[0] : null;
|
||||
setFieldValue(props.name, file); // Directly use setFieldValue to update Formik's state
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const files = event.dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
setFieldValue(props.name, file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<label className="text-md" htmlFor={props.name}>
|
||||
{label}
|
||||
<RequiredAsterisk required={required} />
|
||||
</label>
|
||||
<div className="drop-area" onClick={handleClick} onDrop={handleDrop} onDragOver={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faFile} />
|
||||
<h4>Drag & Drop Here or Browse</h4>
|
||||
</div>
|
||||
<button type="button">Upload Manifest</button>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
ref={fileInputRef}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<ErrorMessage touched={meta.touched} error={meta.error} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileInput;
|
||||
57
components/Form/RadioGroup.tsx
Normal file
57
components/Form/RadioGroup.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { useField } from "formik"
|
||||
import React from "react"
|
||||
import ErrorMessage from "./ErrorMessage"
|
||||
import RequiredAsterisk from "./RequiredAsterisk"
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group"
|
||||
|
||||
const RadioGroupField = ({
|
||||
name,
|
||||
label,
|
||||
options,
|
||||
...props
|
||||
}: {
|
||||
name: string
|
||||
label: string
|
||||
options: string[]
|
||||
[x: string]: any // Allow any other props
|
||||
}) => {
|
||||
const { required, ...restProps } = props
|
||||
const [field, meta, helpers] = useField({ ...restProps, name })
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
helpers.setValue(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<label className="text-md" htmlFor={label}>
|
||||
{label}
|
||||
</label>
|
||||
<RequiredAsterisk required={required} />
|
||||
<RadioGroup.Root
|
||||
className="flex gap-3"
|
||||
value={field.value}
|
||||
onValueChange={handleChange}
|
||||
aria-label={label}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div className="flex items-center" key={option}>
|
||||
<RadioGroup.Item
|
||||
className="hover:bg-violet3 h-[25px] w-[25px] cursor-default rounded-full border bg-white outline-none focus:shadow-[0_0_0_2px]"
|
||||
value={option}
|
||||
id={`${name}-${option}`}
|
||||
>
|
||||
<RadioGroup.Indicator className="relative flex h-full w-full items-center justify-center after:block after:h-[11px] after:w-[11px] after:rounded-[50%] after:bg-violet-400 after:content-['']" />
|
||||
</RadioGroup.Item>
|
||||
<label className="pl-[15px] text-[15px] leading-none" htmlFor={`${name}-${option}`}>
|
||||
{option}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup.Root>
|
||||
<ErrorMessage touched={meta.touched} error={meta.error} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RadioGroupField
|
||||
7
components/Form/RequiredAsterisk.tsx
Normal file
7
components/Form/RequiredAsterisk.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import React from "react"
|
||||
|
||||
const RequiredAsterisk = ({ required }: { required?: boolean }) => {
|
||||
return required ? <span className="text-sm"> *</span> : null
|
||||
}
|
||||
|
||||
export default RequiredAsterisk
|
||||
24
components/Form/SelectInput.tsx
Normal file
24
components/Form/SelectInput.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useField } from "formik"
|
||||
import React from "react"
|
||||
import ErrorMessage from "./ErrorMessage"
|
||||
import RequiredAsterisk from "./RequiredAsterisk"
|
||||
|
||||
const SelectInput = ({ label, ...props }: { label?: string; name: string; [x: string]: any }) => {
|
||||
const { required, ...restProps } = props
|
||||
const [field, meta] = useField(restProps)
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
{label && (
|
||||
<label className="text-md" htmlFor={props.id || props.name}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<RequiredAsterisk required={required} />
|
||||
<select className="h-10 w-full rounded-lg border px-3 text-base shadow-sm" {...field} {...restProps} />
|
||||
<ErrorMessage touched={meta.touched} error={meta.error} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectInput
|
||||
20
components/Form/StatusField.tsx
Normal file
20
components/Form/StatusField.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from "react"
|
||||
|
||||
const StatusField = ({
|
||||
label,
|
||||
statusLabel,
|
||||
statusColor,
|
||||
}: {
|
||||
label: string
|
||||
statusLabel: string
|
||||
statusColor: string
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<h4>{label}</h4>
|
||||
<span className={statusColor}>{statusLabel}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusField
|
||||
38
components/Form/ToggleField.tsx
Normal file
38
components/Form/ToggleField.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useField } from "formik";
|
||||
import React, { useState } from "react";
|
||||
import ErrorMessage from "./ErrorMessage";
|
||||
import RequiredAsterisk from "./RequiredAsterisk";
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
|
||||
const ToggleField = ({
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>) => {
|
||||
const { required, ...restProps } = props;
|
||||
const [field, meta] = useField({ ...(restProps as { name: string }), type: "checkbox" });
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<label className="flex items-center gap-2">
|
||||
{children}
|
||||
<RequiredAsterisk required={required} />
|
||||
</label>
|
||||
<SwitchPrimitive.Root
|
||||
className="relative h-[25px] w-[42px] cursor-default rounded-full bg-black shadow-[0_2px_10px] outline-none focus:shadow-[0_0_0_2px] focus:shadow-black"
|
||||
checked={checked}
|
||||
onCheckedChange={(isChecked) => {
|
||||
setChecked(isChecked);
|
||||
field.onChange({ target: { name: field.name, value: isChecked } });
|
||||
}}
|
||||
>
|
||||
<SwitchPrimitive.Thumb className="block h-[21px] w-[21px] translate-x-0.5 rounded-full bg-white shadow-[0_2px_2px] shadow-black transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||
</SwitchPrimitive.Root>
|
||||
<ErrorMessage touched={meta.touched} error={meta.error} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleField;
|
||||
10
components/Form/Utils.ts
Normal file
10
components/Form/Utils.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export class Utils {
|
||||
//Function to convert strings to camel case
|
||||
static camelize = (str: string) => {
|
||||
return str
|
||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
|
||||
return index === 0 ? word.toLowerCase() : word.toUpperCase()
|
||||
})
|
||||
.replace(/\s+/g, "")
|
||||
}
|
||||
}
|
||||
0
components/Form/data.tsx
Normal file
0
components/Form/data.tsx
Normal file
172
components/Form/index.tsx
Normal file
172
components/Form/index.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
"use client"
|
||||
|
||||
import React, { useState } from "react";
|
||||
|
||||
//Dependencies
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import axios from "axios";
|
||||
|
||||
//Input fields
|
||||
import SelectInput from "./SelectInput";
|
||||
import RadioGroup from "./RadioGroup";
|
||||
import FileInput from "./FileInput";
|
||||
import ToggleField from "./ToggleField";
|
||||
import StatusField from "./StatusField";
|
||||
import ClientField from "./ClientField";
|
||||
|
||||
interface FormValues {
|
||||
importName: string;
|
||||
manifestFile: File | null;
|
||||
splitSchedule: string;
|
||||
client: string;
|
||||
testingCenter: string;
|
||||
toleranceWindow: boolean;
|
||||
}
|
||||
|
||||
const initialValues: FormValues = {
|
||||
importName: '',
|
||||
manifestFile: null,
|
||||
splitSchedule: '',
|
||||
client: '',
|
||||
testingCenter: '',
|
||||
toleranceWindow: false,
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
importName: Yup.string()
|
||||
.required('Import name is required')
|
||||
.oneOf(["Import ABC", "Import DEF", "Import GHI"], "Invalid Import Name"),
|
||||
manifestFile: Yup.mixed()
|
||||
.required('A file is required')
|
||||
.nullable(), // Allows for null values, adjust based on your logic for file handling
|
||||
splitSchedule: Yup.string()
|
||||
.required('This field is required')
|
||||
.oneOf(["Yes", "No"], "Invalid Option"),
|
||||
client: Yup.string()
|
||||
.required('Client selection is required')
|
||||
.oneOf(["Single", "Multiple"], "Invalid Client Type"),
|
||||
testingCenter: Yup.string()
|
||||
.required('Testing center selection is required'), // Assuming a selection is required
|
||||
toleranceWindow: Yup.boolean()
|
||||
.required('Tolerance window selection is required'), // Assuming this is a boolean represented by a toggle
|
||||
});
|
||||
|
||||
|
||||
const data = {
|
||||
importNames: ["Import ABC", "Import DEF", "Import GHI"],
|
||||
splitScheduleOptions: ["Yes", "No"],
|
||||
clientOptions: ["Single", "Multiple"],
|
||||
testingCenterOptions: ["testing center 1", "testing center 2", "testing center 3", "testing center 4"],
|
||||
clientsOptions: ["client 1", "client 2", "client 3"],
|
||||
};
|
||||
|
||||
const FormComponent: React.FC = () => {
|
||||
const [formSubmitted, setFormSubmitted] = useState(false);
|
||||
|
||||
const submitForm = (values: FormValues) => {
|
||||
return axios.post(
|
||||
`/api/submitForm`,
|
||||
{
|
||||
submissionData: values,
|
||||
},
|
||||
{
|
||||
// options
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={async (values, { setSubmitting }) => {
|
||||
try {
|
||||
console.log(values);
|
||||
await submitForm(values);
|
||||
setFormSubmitted(true);
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{formik => (
|
||||
<Form>
|
||||
<div className="flex flex-col lg:flex-row gap-4 lg:gap-16">
|
||||
<div className="flex flex-col grow lg:max-w-[500px]">
|
||||
<SelectInput
|
||||
key="importName"
|
||||
name="importName"
|
||||
label="Select Import Name:"
|
||||
required
|
||||
>
|
||||
<option value="">Select Import Name:</option>
|
||||
{data.importNames.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
<FileInput
|
||||
label="Select a manifest that you'd like to import"
|
||||
name="manifestFile"
|
||||
required
|
||||
/>
|
||||
<StatusField
|
||||
label="Elapse Data Checking:"
|
||||
statusLabel="No Elapsed Dates!"
|
||||
statusColor="text-green-500"
|
||||
/>
|
||||
<ToggleField
|
||||
key="toleranceWindow"
|
||||
name="toleranceWindow"
|
||||
required={true}
|
||||
>Tolerance Window:</ToggleField>
|
||||
</div>
|
||||
<div className="flex flex-col grow lg:max-w-[350px]">
|
||||
<RadioGroup
|
||||
key="splitSchedule"
|
||||
name="splitSchedule"
|
||||
label="Split schedule using social distancing?"
|
||||
options={data.splitScheduleOptions}
|
||||
required={true}
|
||||
/>
|
||||
<StatusField
|
||||
label="Location Checking:"
|
||||
statusLabel="All Available!"
|
||||
statusColor="text-green-500"
|
||||
/>
|
||||
<RadioGroup
|
||||
key="client"
|
||||
name="client"
|
||||
label="Client"
|
||||
options={data.clientOptions}
|
||||
required={true}
|
||||
/>
|
||||
<ClientField
|
||||
testingCenters={data.testingCenterOptions}
|
||||
clients={data.clientsOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="mx-auto mt-4"
|
||||
type="submit"
|
||||
disabled={formik.isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default FormComponent;
|
||||
|
||||
0
components/Form/types.ts
Normal file
0
components/Form/types.ts
Normal file
Loading…
Add table
Add a link
Reference in a new issue