mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-09-23 04:20:16 -04:00
feat: add initial seed data migration and form builder components
- Created migration 0001_seed_data.sql to insert minimal seed data for users, accounts, and roles. - Added meta journal for migration tracking. - Implemented FormBuilder component for dynamic form field creation and management. - Developed FormFieldRenderer component to render various types of form fields based on user input. - Introduced constants for trust levels and status configurations. - Defined types for form fields and trial data structures to enhance type safety and clarity.
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
"use client";
|
||||
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/ui/select";
|
||||
import type { FormField } from "~/lib/types/forms";
|
||||
import { FORM_FIELD_TYPES } from "~/lib/types/forms";
|
||||
|
||||
interface FormFieldRendererProps {
|
||||
field: FormField;
|
||||
value: unknown;
|
||||
onChange: (value: unknown) => void;
|
||||
mode: "preview" | "data-entry" | "participant";
|
||||
index: number;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function FormFieldRenderer({
|
||||
field,
|
||||
value,
|
||||
onChange,
|
||||
mode,
|
||||
index,
|
||||
error,
|
||||
disabled = false,
|
||||
}: FormFieldRendererProps) {
|
||||
const handleChange = (val: unknown) => {
|
||||
if (!disabled) {
|
||||
onChange(val);
|
||||
}
|
||||
};
|
||||
|
||||
const commonProps = {
|
||||
disabled,
|
||||
className: error ? "border-destructive" : "",
|
||||
};
|
||||
|
||||
const scale = (field.settings?.scale as number) || 5;
|
||||
|
||||
switch (field.type) {
|
||||
case "text":
|
||||
return (
|
||||
<Input
|
||||
{...commonProps}
|
||||
value={String(value ?? "")}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder="Enter your response..."
|
||||
/>
|
||||
);
|
||||
|
||||
case "textarea":
|
||||
return (
|
||||
<Textarea
|
||||
{...commonProps}
|
||||
value={String(value ?? "")}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder="Enter your response..."
|
||||
/>
|
||||
);
|
||||
|
||||
case "multiple_choice": {
|
||||
const containerClass =
|
||||
mode === "participant"
|
||||
? `mt-2 space-y-2 ${error ? "border-destructive rounded-md border p-2" : ""}`
|
||||
: "space-y-2";
|
||||
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
{field.options?.map((opt, i) => (
|
||||
<label key={i} className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name={field.id}
|
||||
value={opt}
|
||||
checked={value === opt}
|
||||
onChange={() => handleChange(opt)}
|
||||
disabled={disabled}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span className="text-sm">{opt}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case "checkbox":
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(value)}
|
||||
onChange={(e) => handleChange(e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
{mode === "participant" && (
|
||||
<Label className="cursor-pointer font-normal">Yes, I agree</Label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case "yes_no":
|
||||
if (mode === "data-entry") {
|
||||
return (
|
||||
<Select
|
||||
value={String(value ?? "")}
|
||||
onValueChange={(val) => handleChange(val)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="yes">Yes</SelectItem>
|
||||
<SelectItem value="no">No</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name={field.id}
|
||||
value="yes"
|
||||
checked={value === "yes"}
|
||||
onChange={() => handleChange("yes")}
|
||||
disabled={disabled}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span className="text-sm">Yes</span>
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name={field.id}
|
||||
value="no"
|
||||
checked={value === "no"}
|
||||
onChange={() => handleChange("no")}
|
||||
disabled={disabled}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span className="text-sm">No</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "rating": {
|
||||
const scale = field.settings?.scale || 5;
|
||||
if (mode === "data-entry") {
|
||||
return (
|
||||
<Select
|
||||
value={String(value ?? "")}
|
||||
onValueChange={(val) => handleChange(parseInt(val))}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select rating..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Array.from({ length: scale }, (_, i) => (
|
||||
<SelectItem key={i} value={String(i + 1)}>
|
||||
{i + 1}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
if (mode === "participant") {
|
||||
return (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{Array.from({ length: scale }, (_, i) => (
|
||||
<label key={i} className="cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={field.id}
|
||||
value={String(i + 1)}
|
||||
checked={value === i + 1}
|
||||
onChange={() => handleChange(i + 1)}
|
||||
disabled={disabled}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<span className="hover:bg-muted peer-checked:bg-primary peer-checked:text-primary-foreground flex h-10 w-10 items-center justify-center rounded-full border text-sm font-medium transition-colors">
|
||||
{i + 1}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{Array.from({ length: scale }, (_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
className="disabled h-8 w-8 rounded border"
|
||||
disabled
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case "date":
|
||||
return (
|
||||
<Input
|
||||
type="date"
|
||||
{...commonProps}
|
||||
value={String(value ?? "")}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
/>
|
||||
);
|
||||
|
||||
case "signature":
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
{...commonProps}
|
||||
value={String(value ?? "")}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder={
|
||||
mode === "participant"
|
||||
? "Type your full name as signature"
|
||||
: "Type name as signature..."
|
||||
}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
By entering your name above, you confirm that the information
|
||||
provided is accurate.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface FormFieldLabelProps {
|
||||
field: FormField;
|
||||
index: number;
|
||||
showIndex?: boolean;
|
||||
}
|
||||
|
||||
export function FormFieldLabel({
|
||||
field,
|
||||
index,
|
||||
showIndex = true,
|
||||
}: FormFieldLabelProps) {
|
||||
const fieldType = FORM_FIELD_TYPES.find((f) => f.value === field.type);
|
||||
return (
|
||||
<Label>
|
||||
{showIndex && `${index + 1}. `}
|
||||
{field.label}
|
||||
{field.required && <span className="text-destructive"> *</span>}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user