8 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
kayela
21338f1541 feat: restruction of admin sidebar, button slightly adjusted, member tiers header added, routing for sidbar adjusted 2026-02-01 16:44:55 -06:00
kayela
da366272b4 fix: fixed total pending display 2026-02-01 15:36:43 -06:00
kayela
af27190e29 Phone formatting works, start card moved, registration styling changed 2026-02-01 15:16:12 -06:00
kayela
235156a9ee Merge branch 'features' into dev 2026-02-01 10:44:12 -06:00
kayela
01a3c38085 fix: button text now visable 2026-01-30 09:50:33 -06:00
9 changed files with 441 additions and 448 deletions

View File

@@ -239,6 +239,20 @@ function App() {
</AdminLayout> </AdminLayout>
</PrivateRoute> </PrivateRoute>
} /> } />
<Route path="/admin/registration" element={
<PrivateRoute adminOnly>
<AdminLayout>
<AdminRegistrationBuilder />
</AdminLayout>
</PrivateRoute>
} />
<Route path="/admin/member-tiers" element={
<PrivateRoute adminOnly>
<AdminLayout>
<AdminMemberTiers />
</AdminLayout>
</PrivateRoute>
} />
<Route path="/admin/plans" element={ <Route path="/admin/plans" element={
<PrivateRoute adminOnly> <PrivateRoute adminOnly>
<AdminLayout> <AdminLayout>
@@ -293,6 +307,7 @@ function App() {
<Navigate to="/admin/settings/permissions" replace /> <Navigate to="/admin/settings/permissions" replace />
</PrivateRoute> </PrivateRoute>
} /> } />
<Route path="/admin/settings" element={ <Route path="/admin/settings" element={
<PrivateRoute adminOnly> <PrivateRoute adminOnly>
<AdminLayout> <AdminLayout>
@@ -303,9 +318,7 @@ function App() {
<Route index element={<Navigate to="stripe" replace />} /> <Route index element={<Navigate to="stripe" replace />} />
<Route path="stripe" element={<AdminSettings />} /> <Route path="stripe" element={<AdminSettings />} />
<Route path="permissions" element={<AdminRoles />} /> <Route path="permissions" element={<AdminRoles />} />
<Route path="member-tiers" element={<AdminMemberTiers />} />
<Route path="theme" element={<AdminTheme />} /> <Route path="theme" element={<AdminTheme />} />
<Route path="registration" element={<AdminRegistrationBuilder />} />
</Route> </Route>
{/* 404 - Catch all undefined routes */} {/* 404 - Catch all undefined routes */}

View File

@@ -27,6 +27,8 @@ import {
Heart, Heart,
Sun, Sun,
Moon, Moon,
Star,
FileEdit
} from 'lucide-react'; } from 'lucide-react';
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
@@ -104,18 +106,31 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
path: '/admin', path: '/admin',
disabled: false disabled: false
}, },
{ {
name: 'Staff', name: 'Staff & Admins',
icon: UserCog, icon: UserCog,
path: '/admin/staff', path: '/admin/staff',
disabled: false disabled: false
}, },
{ {
name: 'Members', name: 'Member Roster',
icon: Users, icon: Users,
path: '/admin/members', path: '/admin/members',
disabled: false disabled: false
}, },
{
name: 'Member Tiers',
icon: Star,
path: '/admin/member-tiers',
disabled: false
},
{
name: 'Registration',
icon: FileEdit,
path: '/admin/registration',
disabled: false
},
{ {
name: 'Validations', name: 'Validations',
icon: CheckCircle, icon: CheckCircle,
@@ -316,6 +331,18 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
{/* Dashboard - Standalone */} {/* Dashboard - Standalone */}
{renderNavItem(filteredNavItems.find(item => item.name === 'Dashboard'))} {renderNavItem(filteredNavItems.find(item => item.name === 'Dashboard'))}
{/* Onboarding Section */}
{isOpen && (
<div className="px-4 py-2 mt-6">
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Onboarding
</h3>
</div>
)}
<div className="space-y-1">
{renderNavItem(filteredNavItems.find(item => item.name === 'Registration'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Validations'))}
</div>
{/* MEMBERSHIP Section */} {/* MEMBERSHIP Section */}
{isOpen && ( {isOpen && (
<div className="px-4 py-2 mt-6"> <div className="px-4 py-2 mt-6">
@@ -325,9 +352,9 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
</div> </div>
)} )}
<div className="space-y-1"> <div className="space-y-1">
{renderNavItem(filteredNavItems.find(item => item.name === 'Staff'))} {renderNavItem(filteredNavItems.find(item => item.name === 'Member Roster'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Members'))} {renderNavItem(filteredNavItems.find(item => item.name === 'Member Tiers'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Validations'))} {renderNavItem(filteredNavItems.find(item => item.name === 'Staff & Admins'))}
</div> </div>
{/* FINANCIALS Section */} {/* FINANCIALS Section */}

View File

@@ -128,7 +128,7 @@ const ChangePasswordDialog = ({ open, onOpenChange }) => {
<Button <Button
type="button" type="button"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="btn-outline mr-33" className="btn-outline mr-33 text-white"
> >
Cancel Cancel
</Button> </Button>

View File

@@ -5,9 +5,8 @@ import { CreditCard, Shield, Star, Palette, FileEdit } from 'lucide-react';
const settingsItems = [ const settingsItems = [
{ label: 'Stripe', path: '/admin/settings/stripe', icon: CreditCard }, { label: 'Stripe', path: '/admin/settings/stripe', icon: CreditCard },
{ label: 'Permissions', path: '/admin/settings/permissions', icon: Shield }, { label: 'Permissions', path: '/admin/settings/permissions', icon: Shield },
{ label: 'Member Tiers', path: '/admin/settings/member-tiers', icon: Star },
{ label: 'Theme', path: '/admin/settings/theme', icon: Palette }, { label: 'Theme', path: '/admin/settings/theme', icon: Palette },
{ label: 'Registration Form', path: '/admin/settings/registration', icon: FileEdit },
]; ];
const SettingsTabs = () => { const SettingsTabs = () => {

View File

@@ -47,6 +47,15 @@ const DynamicFormField = ({
const hasError = errors.length > 0; const hasError = errors.length > 0;
const errorMessage = errors[0]; const errorMessage = errors[0];
const formatPhoneNumber = (rawValue) => {
const digits = String(rawValue || '').replace(/\D/g, '').slice(0, 10);
if (digits.length <= 3) return digits;
if (digits.length <= 6) {
return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
}
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
};
// Common input className // Common input className
const inputClassName = `h-14 rounded-xl border-2 ${ const inputClassName = `h-14 rounded-xl border-2 ${
hasError hasError
@@ -59,6 +68,11 @@ const DynamicFormField = ({
const { value: newValue, type: inputType, checked } = e.target; const { value: newValue, type: inputType, checked } = e.target;
if (inputType === 'checkbox') { if (inputType === 'checkbox') {
onChange(id, checked); onChange(id, checked);
return;
}
if (type === 'phone') {
onChange(id, formatPhoneNumber(newValue));
return;
} else { } else {
onChange(id, newValue); onChange(id, newValue);
} }
@@ -111,6 +125,8 @@ const DynamicFormField = ({
value={value || ''} value={value || ''}
onChange={handleInputChange} onChange={handleInputChange}
placeholder={placeholder} placeholder={placeholder}
inputMode={type === 'phone' ? 'numeric' : undefined}
maxLength={type === 'phone' ? 14 : undefined}
className={inputClassName} className={inputClassName}
data-testid={`field-${id}`} data-testid={`field-${id}`}
/> />

View File

@@ -71,7 +71,7 @@ const AdminDashboard = () => {
</div> </div>
<Link to={'/'} className=''> <Link to={'/'} className=''>
<Button <Button
className="btn-lavender mb-8 md:mb-0 " className="btn-lavender mb-8 md:mb-0 mr-4 "
> >
<Globe /> <Globe />
View Public Site View Public Site

View File

@@ -150,9 +150,15 @@ const AdminMemberTiers = () => {
<div className="space-y-6"> <div className="space-y-6">
{/* Header and Actions */} {/* Header and Actions */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Members Tiers
</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Configure tier names, time ranges, and badges displayed in the members directory. Configure tier names, time ranges, and badges displayed in the members directory.
</p> </p>
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{hasChanges && ( {hasChanges && (
<Button variant="outline" onClick={handleDiscardChanges}> <Button variant="outline" onClick={handleDiscardChanges}>

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,12 +39,9 @@ import {
AlignLeft, AlignLeft,
Upload, Upload,
Lock, Lock,
ChevronUp,
ChevronDown,
Settings, Settings,
Zap, Zap,
Copy, X
X,
} from 'lucide-react'; } from 'lucide-react';
// Field type icons // Field type icons
@@ -191,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) || [];
@@ -334,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 = {
@@ -363,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
), ),
@@ -372,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) {
@@ -397,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
@@ -417,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) =>
@@ -425,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) =>
@@ -440,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,24 +483,23 @@ const AdminRegistrationBuilder = () => {
{/* Main Builder Layout */} {/* Main Builder Layout */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* Left Sidebar - Steps & Sections */} <div className="lg:col-span-9">
<div className="lg:col-span-3">
<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="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-lg border cursor-pointer transition-colors ${ className={`px-3 py-2 rounded-t-lg border cursor-pointer transition-colors ${selectedStep === step.id
selectedStep === step.id ? 'bg-white border-gray-300 border-b-white'
? 'border-brand-purple bg-brand-lavender/10' : 'bg-gray-50 border-transparent hover:bg-gray-100'
: 'border-gray-200 hover:border-gray-300'
}`} }`}
onClick={() => { onClick={() => {
setSelectedStep(step.id); setSelectedStep(step.id);
@@ -518,69 +507,59 @@ 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">
<GripVertical className="h-4 w-4 text-gray-400" /> <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>
<div className="flex items-center gap-1">
<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, 'up');
}}
disabled={index === 0}
>
<ChevronUp className="h-3 w-3" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
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-red-700"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDeleteStep(step.id); handleDeleteStep(step.id);
}} }}
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="size-3" />
</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 ${ className={`p-6 rounded-lg border cursor-pointer transition-colors ${selectedSection === section.id
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'
}`} }`}
@@ -589,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);
@@ -603,61 +587,16 @@ const AdminRegistrationBuilder = () => {
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
</div> </div>
</div>
))}
</div>
</>
)}
</Card>
</div>
{/* Center - Form Canvas */}
<div className="lg:col-span-6">
<Card className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold">
{currentStep?.title || 'Select a step'}
</h2>
{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}
className={`p-3 rounded-lg border cursor-pointer transition-all ${ className={`p-3 rounded-lg border cursor-pointer transition-all ${selectedField === field.id
selectedField === field.id
? 'border-brand-purple bg-brand-lavender/5 ring-2 ring-brand-purple/20' ? 'border-brand-purple bg-brand-lavender/5 ring-2 ring-brand-purple/20'
: 'border-gray-200 hover:border-gray-300' : 'border-gray-200 hover:border-gray-300'
} ${field.is_fixed ? 'bg-gray-50' : ''}`} } ${field.is_fixed ? 'bg-gray-50' : ''}`}
@@ -688,40 +627,209 @@ 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>
{selectedField === field.id && (
<div className="mt-4 border-t pt-4 space-y-4">
<div>
<Label className="text-xs text-muted-foreground">Field ID</Label>
<div className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
{field.id}
</div>
</div>
<div>
<Label htmlFor={`field-label-${field.id}`}>Label</Label>
<Input
id={`field-label-${field.id}`}
value={field.label}
onChange={(e) => handleUpdateField(section.id, field.id, { label: e.target.value })}
disabled={field.is_fixed}
/>
</div>
<div>
<Label>Type</Label>
<Select
value={field.type}
onValueChange={(value) => handleUpdateField(section.id, field.id, { type: value })}
disabled={field.is_fixed}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(FIELD_TYPE_LABELS).map(([type, label]) => (
<SelectItem key={type} value={type}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Checkbox
id={`field-required-${field.id}`}
checked={field.required}
onCheckedChange={(checked) =>
handleUpdateField(section.id, field.id, { required: checked })
}
disabled={field.is_fixed}
/>
<Label htmlFor={`field-required-${field.id}`}>Required</Label>
</div>
<div>
<Label>Width</Label>
<Select
value={field.width || 'full'}
onValueChange={(value) => handleUpdateField(section.id, field.id, { width: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="full">Full Width</SelectItem>
<SelectItem value="half">Half Width</SelectItem>
<SelectItem value="third">Third Width</SelectItem>
</SelectContent>
</Select>
</div>
{['text', 'email', 'phone', 'textarea'].includes(field.type) && (
<div>
<Label htmlFor={`field-placeholder-${field.id}`}>Placeholder</Label>
<Input
id={`field-placeholder-${field.id}`}
value={field.placeholder || ''}
onChange={(e) =>
handleUpdateField(section.id, field.id, { placeholder: e.target.value })
}
/>
</div>
)}
{['dropdown', 'radio', 'multiselect'].includes(field.type) && (
<div>
<Label>Options</Label>
<div className="space-y-2 mt-2">
{options.map((option, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input
value={option.label}
onChange={(e) => {
const newOptions = [...options];
newOptions[idx] = {
...newOptions[idx],
label: e.target.value,
value: e.target.value.toLowerCase().replace(/\s+/g, '_'),
};
handleUpdateField(section.id, field.id, { options: newOptions });
}}
placeholder="Option label"
/>
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => {
const newOptions = options.filter((_, i) => i !== idx);
handleUpdateField(section.id, field.id, { options: newOptions });
}}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
<Button
size="sm"
variant="outline"
onClick={() => {
const newOptions = [
...options,
{ value: `option_${Date.now()}`, label: 'New Option' },
];
handleUpdateField(section.id, field.id, { options: newOptions });
}}
>
<Plus className="h-4 w-4 mr-2" />
Add Option
</Button>
</div>
</div>
)}
<div>
<Label htmlFor={`field-mapping-${field.id}`}>Database Mapping</Label>
<Input
id={`field-mapping-${field.id}`}
value={field.mapping || ''}
onChange={(e) =>
handleUpdateField(section.id, field.id, { mapping: e.target.value })
}
placeholder="Leave empty for custom data"
disabled={field.is_fixed}
/>
<p className="text-xs text-muted-foreground mt-1">
Maps to User model field. Empty = stored in custom_registration_data
</p>
</div>
{field.is_fixed && (
<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.
</div>
)}
</div>
)}
</div> </div>
); );
})} })}
{sortedFields.length === 0 && ( {sortedFields.length === 0 && (
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-6 text-muted-foreground">
No fields in this section. Click "Add Field" to add one. No fields in this section. Click "Add Field" to add one.
</div> </div>
)} )}
</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> </div>
); );
})} })}
{sortedSections.length === 0 && ( {sortedSections.length === 0 && (
<div className="text-center py-12 text-muted-foreground"> <div className="text-center py-6 text-muted-foreground">
No sections in this step. Add a section from the left sidebar. No sections yet. Click "Add Section" to get started.
</div>
)}
</div>
</div> </div>
)} )}
</Card> </Card>
</div>
{/* Conditional Rules */} <div className="lg:col-span-3">
<Card className="p-6 mt-6"> <Card className="p-6">
<div className="flex 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" />
<h2 className="text-lg font-semibold">Conditional Rules</h2> <h2 className="text-lg font-semibold">Conditional Rules</h2>
@@ -737,185 +845,10 @@ const AdminRegistrationBuilder = () => {
</div> </div>
</Card> </Card>
</div> </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>
<Label className="text-xs text-muted-foreground">Field ID</Label>
<div className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
{selectedFieldData.id}
</div>
</div>
{/* Label */}
<div>
<Label htmlFor="field-label">Label</Label>
<Input
id="field-label"
value={selectedFieldData.label}
onChange={(e) => handleUpdateField(selectedField, { label: e.target.value })}
disabled={selectedFieldData.is_fixed}
/>
</div>
{/* Type */}
<div>
<Label>Type</Label>
<Select
value={selectedFieldData.type}
onValueChange={(value) => handleUpdateField(selectedField, { type: value })}
disabled={selectedFieldData.is_fixed}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(FIELD_TYPE_LABELS).map(([type, label]) => (
<SelectItem key={type} value={type}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Required */}
<div className="flex items-center gap-2">
<Checkbox
id="field-required"
checked={selectedFieldData.required}
onCheckedChange={(checked) =>
handleUpdateField(selectedField, { required: checked })
}
disabled={selectedFieldData.is_fixed}
/>
<Label htmlFor="field-required">Required</Label>
</div>
{/* Width */}
<div>
<Label>Width</Label>
<Select
value={selectedFieldData.width || 'full'}
onValueChange={(value) => handleUpdateField(selectedField, { width: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="full">Full Width</SelectItem>
<SelectItem value="half">Half Width</SelectItem>
<SelectItem value="third">Third Width</SelectItem>
</SelectContent>
</Select>
</div>
{/* Placeholder */}
{['text', 'email', 'phone', 'textarea'].includes(selectedFieldData.type) && (
<div>
<Label htmlFor="field-placeholder">Placeholder</Label>
<Input
id="field-placeholder"
value={selectedFieldData.placeholder || ''}
onChange={(e) =>
handleUpdateField(selectedField, { placeholder: e.target.value })
}
/>
</div>
)}
{/* Options for dropdown/radio/multiselect */}
{['dropdown', 'radio', 'multiselect'].includes(selectedFieldData.type) && (
<div>
<Label>Options</Label>
<div className="space-y-2 mt-2">
{(selectedFieldData.options || []).map((option, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input
value={option.label}
onChange={(e) => {
const newOptions = [...selectedFieldData.options];
newOptions[idx] = {
...newOptions[idx],
label: e.target.value,
value: e.target.value.toLowerCase().replace(/\s+/g, '_'),
};
handleUpdateField(selectedField, { options: newOptions });
}}
placeholder="Option label"
/>
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => {
const newOptions = selectedFieldData.options.filter(
(_, i) => i !== idx
);
handleUpdateField(selectedField, { options: newOptions });
}}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
<Button
size="sm"
variant="outline"
onClick={() => {
const newOptions = [
...(selectedFieldData.options || []),
{ value: `option_${Date.now()}`, label: 'New Option' },
];
handleUpdateField(selectedField, { options: newOptions });
}}
>
<Plus className="h-4 w-4 mr-2" />
Add Option
</Button>
</div>
</div>
)}
{/* Mapping */}
<div>
<Label htmlFor="field-mapping">Database Mapping</Label>
<Input
id="field-mapping"
value={selectedFieldData.mapping || ''}
onChange={(e) =>
handleUpdateField(selectedField, { mapping: e.target.value })
}
placeholder="Leave empty for custom data"
disabled={selectedFieldData.is_fixed}
/>
<p className="text-xs text-muted-foreground mt-1">
Maps to User model field. Empty = stored in custom_registration_data
</p>
</div>
{selectedFieldData.is_fixed && (
<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.
</div>
)}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
Select a field to edit its properties
</div>
)}
</Card>
</div>
</div> </div>
{/* Add Step Dialog */} {/* Add Step Dialog */}
<Dialog open={addStepDialogOpen} onOpenChange={setAddStepDialogOpen}> <Dialog open={addStepDialogOpen} onOpenChange={setAddStepDialogOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
@@ -1033,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,
@@ -1120,8 +1053,7 @@ const AdminRegistrationBuilder = () => {
.map((field) => ( .map((field) => (
<div <div
key={field.id} key={field.id}
className={`${ className={`${field.width === 'full'
field.width === 'full'
? 'col-span-2' ? 'col-span-2'
: field.width === 'third' : field.width === 'third'
? 'col-span-1' ? 'col-span-1'

View File

@@ -354,13 +354,7 @@ const AdminValidations = () => {
Quick Overview Quick Overview
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4"> <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<StatCard
title="Total Pending"
value={loading ? '-' : pendingUsers.length}
icon={Users}
iconBgClass="text-brand-purple"
dataTestId="stat-total-users"
/>
<StatCard <StatCard
title="Awaiting Email" title="Awaiting Email"
@@ -394,7 +388,13 @@ const AdminValidations = () => {
dataTestId="stat-rejected" dataTestId="stat-rejected"
/> />
<StatCard
title="Total Pending"
value={loading ? '-' : pendingUsers.filter(user => ['pending_email', 'pending_validation', 'pre_validated', 'payment_pending',].includes(user.status)).length}
icon={Users}
iconBgClass="text-brand-purple"
dataTestId="stat-total-users"
/>
</div> </div>
</Card> </Card>