3 Commits

Author SHA1 Message Date
kayela
96e4b711a8 styling 2026-02-04 11:44:50 -06:00
kayela
ced8d75bcc updated registration screen 2026-02-04 10:42:07 -06:00
kayela
f0ee505339 restructured layout 2026-02-02 16:36:52 -06:00

View File

@@ -19,12 +19,6 @@ import {
DialogTitle, DialogTitle,
DialogFooter, DialogFooter,
} from '../../components/ui/dialog'; } from '../../components/ui/dialog';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '../../components/ui/accordion';
import { toast } from 'sonner'; import { toast } from 'sonner';
import api from '../../utils/api'; import api from '../../utils/api';
import { import {
@@ -45,13 +39,9 @@ import {
AlignLeft, AlignLeft,
Upload, Upload,
Lock, Lock,
ChevronUp,
ChevronDown,
Settings, Settings,
Zap, Zap,
Copy, X
X,
Grip
} from 'lucide-react'; } from 'lucide-react';
// Field type icons // Field type icons
@@ -192,6 +182,7 @@ const AdminRegistrationBuilder = () => {
// Get current step // Get current step
const currentStep = sortedSteps.find((s) => s.id === selectedStep); const currentStep = sortedSteps.find((s) => s.id === selectedStep);
const currentStepIndex = sortedSteps.findIndex((s) => s.id === selectedStep);
// Get sections for current step // Get sections for current step
const sortedSections = currentStep?.sections?.sort((a, b) => a.order - b.order) || []; const sortedSections = currentStep?.sections?.sort((a, b) => a.order - b.order) || [];
@@ -335,13 +326,13 @@ const AdminRegistrationBuilder = () => {
}; };
// Add field // Add field
const handleAddField = () => { const handleAddField = (sectionId = selectedSection) => {
if (!selectedSection) { if (!sectionId) {
toast.error('Please select a section first'); toast.error('Please select a section first');
return; return;
} }
const section = currentStep?.sections?.find((s) => s.id === selectedSection); const section = currentStep?.sections?.find((s) => s.id === sectionId);
const fieldCount = section?.fields?.length || 0; const fieldCount = section?.fields?.length || 0;
const newField = { const newField = {
@@ -364,7 +355,7 @@ const AdminRegistrationBuilder = () => {
? { ? {
...s, ...s,
sections: s.sections.map((sec) => sections: s.sections.map((sec) =>
sec.id === selectedSection sec.id === sectionId
? { ...sec, fields: [...(sec.fields || []), newField] } ? { ...sec, fields: [...(sec.fields || []), newField] }
: sec : sec
), ),
@@ -373,13 +364,14 @@ const AdminRegistrationBuilder = () => {
), ),
})); }));
setSelectedSection(sectionId);
setSelectedField(newField.id); setSelectedField(newField.id);
setAddFieldDialogOpen(false); setAddFieldDialogOpen(false);
}; };
// Delete field // Delete field
const handleDeleteField = (fieldId) => { const handleDeleteField = (sectionId, fieldId) => {
const section = currentStep?.sections?.find((s) => s.id === selectedSection); const section = currentStep?.sections?.find((s) => s.id === sectionId);
const field = section?.fields?.find((f) => f.id === fieldId); const field = section?.fields?.find((f) => f.id === fieldId);
if (field?.is_fixed) { if (field?.is_fixed) {
@@ -398,7 +390,7 @@ const AdminRegistrationBuilder = () => {
? { ? {
...s, ...s,
sections: s.sections.map((sec) => sections: s.sections.map((sec) =>
sec.id === selectedSection sec.id === sectionId
? { ? {
...sec, ...sec,
fields: sec.fields fields: sec.fields
@@ -418,7 +410,7 @@ const AdminRegistrationBuilder = () => {
}; };
// Update field // Update field
const handleUpdateField = (fieldId, updates) => { const handleUpdateField = (sectionId, fieldId, updates) => {
updateSchema((prev) => ({ updateSchema((prev) => ({
...prev, ...prev,
steps: prev.steps.map((s) => steps: prev.steps.map((s) =>
@@ -426,7 +418,7 @@ const AdminRegistrationBuilder = () => {
? { ? {
...s, ...s,
sections: s.sections.map((sec) => sections: s.sections.map((sec) =>
sec.id === selectedSection sec.id === sectionId
? { ? {
...sec, ...sec,
fields: sec.fields.map((f) => fields: sec.fields.map((f) =>
@@ -441,10 +433,7 @@ const AdminRegistrationBuilder = () => {
})); }));
}; };
// Get selected field data
const selectedFieldData = currentStep?.sections
?.find((s) => s.id === selectedSection)
?.fields?.find((f) => f.id === selectedField);
if (loading) { if (loading) {
return ( return (
@@ -493,23 +482,24 @@ const AdminRegistrationBuilder = () => {
)} )}
{/* Main Builder Layout */} {/* Main Builder Layout */}
{/* Left Sidebar - Steps & Sections */} <div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
<div className="lg:col-span-3"> <div className="lg:col-span-9">
<Card className="p-4"> <Card className="p-4">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Steps</h2> <h2 className="text-lg font-semibold">Steps</h2>
<Button size="sm" variant="ghost" onClick={() => setAddStepDialogOpen(true)}> <Button size="sm" variant="ghost" className="w-32" onClick={() => setAddStepDialogOpen(true)}>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
Add Step
</Button> </Button>
</div> </div>
<div className="flex space-y-2"> <div className="flex flex-wrap items-end gap-2 border-b border-gray-200">
{sortedSteps.map((step, index) => ( {sortedSteps.map((step, index) => (
<div <div
key={step.id} key={step.id}
className={`p-3 rounded-t-lg border cursor-pointer transition-colors ${selectedStep === step.id className={`px-3 py-2 rounded-t-lg border cursor-pointer transition-colors ${selectedStep === step.id
? ' bg-brand-lavender/10 border-b-4 border-b-brand-dark-lavender' ? 'bg-white border-gray-300 border-b-white'
: '' : 'bg-gray-50 border-transparent hover:bg-gray-100'
}`} }`}
onClick={() => { onClick={() => {
setSelectedStep(step.id); setSelectedStep(step.id);
@@ -517,41 +507,15 @@ const AdminRegistrationBuilder = () => {
setSelectedField(null); setSelectedField(null);
}} }}
> >
<div className="flex items-center justify-between"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="font-medium">Step: </div> <span className="font-medium">Step {index + 1}</span>
<span className="font-medium text-sm">{step.title}</span> {/* <span className="text-sm text-muted-foreground">{step.title || 'Untitled Step'}</span> */}
</div> </div>
{/* Mod Buttons */}
<div className="flex items-center gap-1">
{/* <Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={(e) => {
e.stopPropagation();
handleMoveStep(step.id, 'up');
}}
disabled={index === 0}
>
<ChevronUp className="h-3 w-3" />
</Button>
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
className="h-6 w-6" className="h-6 w-6 text-red-500 hover:text-background"
onClick={(e) => {
e.stopPropagation();
handleMoveStep(step.id, 'down');
}}
disabled={index === sortedSteps.length - 1}
>
<ChevronDown className="h-3 w-3" />
</Button> */}
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-red-500 hover:text-white ml-2"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDeleteStep(step.id); handleDeleteStep(step.id);
@@ -561,25 +525,41 @@ const AdminRegistrationBuilder = () => {
</Button> </Button>
</div> </div>
</div> </div>
</div>
))} ))}
{sortedSteps.length === 0 && (
<div className="text-center py-6 text-muted-foreground">
No steps yet. Click "Add Step" to get started.
</div>
)}
</div> </div>
{/* Sections for selected step */} {/* Sections for selected step */}
{currentStep && ( {currentStep && (
<> <div className="mt-6">
<div className="flex justify-between items-center mt-6 mb-4"> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mb-4">
<h2 className="text-lg font-semibold">Sections</h2> <div>
<h2 className="text-xl font-semibold">
Step {currentStepIndex + 1}: {currentStep?.title || 'Untitled Step'}
</h2>
{currentStep.description && (
<p className="text-sm text-muted-foreground mt-1">{currentStep.description}</p>
)}
</div>
<Button size="sm" variant="ghost" onClick={() => setAddSectionDialogOpen(true)}> <Button size="sm" variant="ghost" onClick={() => setAddSectionDialogOpen(true)}>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
Add Section
</Button> </Button>
</div> </div>
<div className="space-y-2"> <div className="space-y-4">
{sortedSections.map((section) => ( {sortedSections.map((section) => {
const sortedFields = section.fields?.sort((a, b) => a.order - b.order) || [];
return (
<div <div
key={section.id} key={section.id}
className={`p-3 rounded-lg border cursor-pointer transition-colors ${selectedSection === section.id className={`p-6 rounded-lg border cursor-pointer transition-colors ${selectedSection === section.id
? 'border-brand-purple bg-brand-lavender/10' ? 'border-brand-purple bg-brand-lavender/10'
: 'border-gray-200 hover:border-gray-300' : 'border-gray-200 hover:border-gray-300'
}`} }`}
@@ -588,12 +568,17 @@ const AdminRegistrationBuilder = () => {
setSelectedField(null); setSelectedField(null);
}} }}
> >
<div className="flex items-center justify-between"> <div className="flex items-start justify-between gap-3">
<span className="text-sm">{section.title}</span> <div>
<h3 className="text-lg font-medium">{section.title || 'Untitled Section'}</h3>
{section.description && (
<p className="text-sm text-muted-foreground mt-1">{section.description}</p>
)}
</div>
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
className="h-6 w-6 text-red-500 hover:text-red-700" className="h-6 w-6 text-red-500 hover:text-background"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDeleteSection(section.id); handleDeleteSection(section.id);
@@ -602,62 +587,12 @@ const AdminRegistrationBuilder = () => {
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
</div> </div>
</div>
))}
</div>
</>
)}
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* Center - Form Canvas */}
<div className="lg:col-span-9">
<Card className="p-6">
<div className="flex justify-between items-center mb-6">
<div className='relative -mx-11 -my-2 flex gap-2 items-center'>
<Grip className="size-10 text-gray-400 py-2 bg-background" />
<h2 className="text-xl font-semibold">
{currentStep?.title || 'Select a step'}
</h2>
</div>
{selectedSection && (
<Button size="sm" onClick={() => setAddFieldDialogOpen(true)}>
<Plus className="h-4 w-4 mr-2" />
Add Field
</Button>
)}
</div>
{currentStep?.description && (
<p className="text-muted-foreground mb-6">{currentStep.description}</p>
)}
{/* Sections and Fields */}
{sortedSections.map((section) => {
const sortedFields = section.fields?.sort((a, b) => a.order - b.order) || [];
return (
<div
key={section.id}
className={`mb-6 p-4 rounded-lg border-2 ${selectedSection === section.id
? 'border-brand-purple'
: 'border-dashed border-gray-200'
}`}
onClick={() => setSelectedSection(section.id)}
>
<h3 className="text-lg font-medium mb-4">{section.title}</h3>
{section.description && (
<p className="text-sm text-muted-foreground mb-4">{section.description}</p>
)}
{/* Fields */} {/* Fields */}
<div className="space-y-3"> <div className="mt-4 space-y-3">
{sortedFields.map((field) => { {sortedFields.map((field) => {
const IconComponent = FIELD_TYPE_ICONS[field.type] || Type; const IconComponent = FIELD_TYPE_ICONS[field.type] || Type;
const options = field.options || [];
return ( return (
<div <div
key={field.id} key={field.id}
@@ -692,73 +627,40 @@ const AdminRegistrationBuilder = () => {
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
className="h-6 w-6 text-red-500 hover:text-red-700" className="h-6 w-6 text-red-500 hover:text-background"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDeleteField(field.id); handleDeleteField(section.id, field.id);
}} }}
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
)} )}
</div> </div>
</div>
);
})}
{sortedFields.length === 0 && ( {selectedField === field.id && (
<div className="text-center py-8 text-muted-foreground"> <div className="mt-4 border-t pt-4 space-y-4">
No fields in this section. Click "Add Field" to add one.
</div>
)}
</div>
</div>
);
})}
{sortedSections.length === 0 && (
<div className="text-center py-12 text-muted-foreground">
No sections in this step. Add a section from the left sidebar.
</div>
)}
</Card>
</div>
{/* Right Sidebar - Field Properties */}
<div className="lg:col-span-3">
<Card className="p-4">
<h2 className="text-lg font-semibold mb-4">Field Properties</h2>
{selectedFieldData ? (
<div className="space-y-4">
{/* Field ID */}
<div> <div>
<Label className="text-xs text-muted-foreground">Field ID</Label> <Label className="text-xs text-muted-foreground">Field ID</Label>
<div className="text-sm font-mono bg-gray-100 px-2 py-1 rounded"> <div className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
{selectedFieldData.id} {field.id}
</div> </div>
</div> </div>
{/* Label */}
<div> <div>
<Label htmlFor="field-label">Label</Label> <Label htmlFor={`field-label-${field.id}`}>Label</Label>
<Input <Input
id="field-label" id={`field-label-${field.id}`}
value={selectedFieldData.label} value={field.label}
onChange={(e) => handleUpdateField(selectedField, { label: e.target.value })} onChange={(e) => handleUpdateField(section.id, field.id, { label: e.target.value })}
disabled={selectedFieldData.is_fixed} disabled={field.is_fixed}
/> />
</div> </div>
{/* Type */}
<div> <div>
<Label>Type</Label> <Label>Type</Label>
<Select <Select
value={selectedFieldData.type} value={field.type}
onValueChange={(value) => handleUpdateField(selectedField, { type: value })} onValueChange={(value) => handleUpdateField(section.id, field.id, { type: value })}
disabled={selectedFieldData.is_fixed} disabled={field.is_fixed}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -772,26 +674,22 @@ const AdminRegistrationBuilder = () => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* Required */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox
id="field-required" id={`field-required-${field.id}`}
checked={selectedFieldData.required} checked={field.required}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
handleUpdateField(selectedField, { required: checked }) handleUpdateField(section.id, field.id, { required: checked })
} }
disabled={selectedFieldData.is_fixed} disabled={field.is_fixed}
/> />
<Label htmlFor="field-required">Required</Label> <Label htmlFor={`field-required-${field.id}`}>Required</Label>
</div> </div>
{/* Width */}
<div> <div>
<Label>Width</Label> <Label>Width</Label>
<Select <Select
value={selectedFieldData.width || 'full'} value={field.width || 'full'}
onValueChange={(value) => handleUpdateField(selectedField, { width: value })} onValueChange={(value) => handleUpdateField(section.id, field.id, { width: value })}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -804,37 +702,35 @@ const AdminRegistrationBuilder = () => {
</Select> </Select>
</div> </div>
{/* Placeholder */} {['text', 'email', 'phone', 'textarea'].includes(field.type) && (
{['text', 'email', 'phone', 'textarea'].includes(selectedFieldData.type) && (
<div> <div>
<Label htmlFor="field-placeholder">Placeholder</Label> <Label htmlFor={`field-placeholder-${field.id}`}>Placeholder</Label>
<Input <Input
id="field-placeholder" id={`field-placeholder-${field.id}`}
value={selectedFieldData.placeholder || ''} value={field.placeholder || ''}
onChange={(e) => onChange={(e) =>
handleUpdateField(selectedField, { placeholder: e.target.value }) handleUpdateField(section.id, field.id, { placeholder: e.target.value })
} }
/> />
</div> </div>
)} )}
{/* Options for dropdown/radio/multiselect */} {['dropdown', 'radio', 'multiselect'].includes(field.type) && (
{['dropdown', 'radio', 'multiselect'].includes(selectedFieldData.type) && (
<div> <div>
<Label>Options</Label> <Label>Options</Label>
<div className="space-y-2 mt-2"> <div className="space-y-2 mt-2">
{(selectedFieldData.options || []).map((option, idx) => ( {options.map((option, idx) => (
<div key={idx} className="flex items-center gap-2"> <div key={idx} className="flex items-center gap-2">
<Input <Input
value={option.label} value={option.label}
onChange={(e) => { onChange={(e) => {
const newOptions = [...selectedFieldData.options]; const newOptions = [...options];
newOptions[idx] = { newOptions[idx] = {
...newOptions[idx], ...newOptions[idx],
label: e.target.value, label: e.target.value,
value: e.target.value.toLowerCase().replace(/\s+/g, '_'), value: e.target.value.toLowerCase().replace(/\s+/g, '_'),
}; };
handleUpdateField(selectedField, { options: newOptions }); handleUpdateField(section.id, field.id, { options: newOptions });
}} }}
placeholder="Option label" placeholder="Option label"
/> />
@@ -843,10 +739,8 @@ const AdminRegistrationBuilder = () => {
variant="ghost" variant="ghost"
className="h-8 w-8" className="h-8 w-8"
onClick={() => { onClick={() => {
const newOptions = selectedFieldData.options.filter( const newOptions = options.filter((_, i) => i !== idx);
(_, i) => i !== idx handleUpdateField(section.id, field.id, { options: newOptions });
);
handleUpdateField(selectedField, { options: newOptions });
}} }}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
@@ -858,10 +752,10 @@ const AdminRegistrationBuilder = () => {
variant="outline" variant="outline"
onClick={() => { onClick={() => {
const newOptions = [ const newOptions = [
...(selectedFieldData.options || []), ...options,
{ value: `option_${Date.now()}`, label: 'New Option' }, { value: `option_${Date.now()}`, label: 'New Option' },
]; ];
handleUpdateField(selectedField, { options: newOptions }); handleUpdateField(section.id, field.id, { options: newOptions });
}} }}
> >
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
@@ -871,37 +765,70 @@ const AdminRegistrationBuilder = () => {
</div> </div>
)} )}
{/* Mapping */}
<div> <div>
<Label htmlFor="field-mapping">Database Mapping</Label> <Label htmlFor={`field-mapping-${field.id}`}>Database Mapping</Label>
<Input <Input
id="field-mapping" id={`field-mapping-${field.id}`}
value={selectedFieldData.mapping || ''} value={field.mapping || ''}
onChange={(e) => onChange={(e) =>
handleUpdateField(selectedField, { mapping: e.target.value }) handleUpdateField(section.id, field.id, { mapping: e.target.value })
} }
placeholder="Leave empty for custom data" placeholder="Leave empty for custom data"
disabled={selectedFieldData.is_fixed} disabled={field.is_fixed}
/> />
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
Maps to User model field. Empty = stored in custom_registration_data Maps to User model field. Empty = stored in custom_registration_data
</p> </p>
</div> </div>
{selectedFieldData.is_fixed && ( {field.is_fixed && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-yellow-800 text-sm"> <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-yellow-800 text-sm">
This is a fixed field and cannot be removed or have its core properties changed. This is a fixed field and cannot be removed or have its core properties changed.
</div> </div>
)} )}
</div> </div>
) : ( )}
<div className="text-center py-8 text-muted-foreground"> </div>
Select a field to edit its properties );
})}
{sortedFields.length === 0 && (
<div className="text-center py-6 text-muted-foreground">
No fields in this section. Click "Add Field" to add one.
</div>
)}
</div>
<Button
size="sm"
className="w-full mt-4"
onClick={(e) => {
e.stopPropagation();
setSelectedSection(section.id);
setSelectedField(null);
setAddFieldDialogOpen(true);
}}
>
<Plus className="size-4 mr-2" />
Add Field
</Button>
</div>
);
})}
{sortedSections.length === 0 && (
<div className="text-center py-6 text-muted-foreground">
No sections yet. Click "Add Section" to get started.
</div>
)}
</div>
</div> </div>
)} )}
</Card> </Card>
{/* Conditional Rules */} </div>
<Card className="p-6 mt-6">
<div className="lg:col-span-3">
<Card className="p-6">
<div className="flex flex-col justify-between items-center mb-4"> <div className="flex flex-col justify-between items-center mb-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Zap className="h-5 w-5 text-yellow-500" /> <Zap className="h-5 w-5 text-yellow-500" />
@@ -921,6 +848,7 @@ const AdminRegistrationBuilder = () => {
</div> </div>
{/* Add Step Dialog */} {/* Add Step Dialog */}
<Dialog open={addStepDialogOpen} onOpenChange={setAddStepDialogOpen}> <Dialog open={addStepDialogOpen} onOpenChange={setAddStepDialogOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
@@ -1038,7 +966,7 @@ const AdminRegistrationBuilder = () => {
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
className="h-6 w-6 text-red-500" className="h-6 w-6 text-red-500 hover-text-background"
onClick={() => { onClick={() => {
updateSchema((prev) => ({ updateSchema((prev) => ({
...prev, ...prev,