4 Commits

8 changed files with 330 additions and 289 deletions

View File

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

View File

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

View File

@@ -5,9 +5,8 @@ import { CreditCard, Shield, Star, Palette, FileEdit } from 'lucide-react';
const settingsItems = [
{ label: 'Stripe', path: '/admin/settings/stripe', icon: CreditCard },
{ 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: 'Registration Form', path: '/admin/settings/registration', icon: FileEdit },
];
const SettingsTabs = () => {

View File

@@ -47,6 +47,15 @@ const DynamicFormField = ({
const hasError = errors.length > 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
const inputClassName = `h-14 rounded-xl border-2 ${
hasError
@@ -59,6 +68,11 @@ const DynamicFormField = ({
const { value: newValue, type: inputType, checked } = e.target;
if (inputType === 'checkbox') {
onChange(id, checked);
return;
}
if (type === 'phone') {
onChange(id, formatPhoneNumber(newValue));
return;
} else {
onChange(id, newValue);
}
@@ -111,6 +125,8 @@ const DynamicFormField = ({
value={value || ''}
onChange={handleInputChange}
placeholder={placeholder}
inputMode={type === 'phone' ? 'numeric' : undefined}
maxLength={type === 'phone' ? 14 : undefined}
className={inputClassName}
data-testid={`field-${id}`}
/>

View File

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

View File

@@ -150,9 +150,15 @@ const AdminMemberTiers = () => {
<div className="space-y-6">
{/* Header and Actions */}
<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">
Configure tier names, time ranges, and badges displayed in the members directory.
</p>
</div>
<div className="flex items-center gap-2">
{hasChanges && (
<Button variant="outline" onClick={handleDiscardChanges}>

View File

@@ -51,6 +51,7 @@ import {
Zap,
Copy,
X,
Grip
} from 'lucide-react';
// Field type icons
@@ -492,25 +493,24 @@ const AdminRegistrationBuilder = () => {
)}
{/* Main Builder Layout */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* Left Sidebar - Steps & Sections */}
<div className="lg:col-span-3">
<Card className="p-4">
<div className="flex justify-between items-center mb-4">
<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" />
<p>Add Step</p>
</Button>
</div>
<div className="space-y-2">
<div className="flex">
{sortedSteps.map((step, index) => (
<div
key={step.id}
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
selectedStep === step.id
? 'border-brand-purple bg-brand-lavender/10'
: 'border-gray-200 hover:border-gray-300'
className={`p-3 rounded-t-lg border cursor-pointer transition-colors ${selectedStep === step.id
? ' bg-brand-lavender/10 border-b-4 border-b-brand-dark-lavender'
: ''
}`}
onClick={() => {
setSelectedStep(step.id);
@@ -520,11 +520,11 @@ const AdminRegistrationBuilder = () => {
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-gray-400" />
<span className="font-medium text-sm">{step.title}</span>
<div className="font-medium">Step: {index + 1} </div>
</div>
{/* Mod Buttons */}
<div className="flex items-center gap-1">
<Button
{/* <Button
size="icon"
variant="ghost"
className="h-6 w-6"
@@ -547,17 +547,18 @@ const AdminRegistrationBuilder = () => {
disabled={index === sortedSteps.length - 1}
>
<ChevronDown className="h-3 w-3" />
</Button>
</Button> */}
<Button
size="icon"
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 ml-2"
onClick={(e) => {
e.stopPropagation();
handleDeleteStep(step.id);
}}
>
<Trash2 className="h-3 w-3" />
<Trash2 className="size-3" />
</Button>
</div>
</div>
@@ -568,86 +569,66 @@ const AdminRegistrationBuilder = () => {
{/* Sections for selected step */}
{currentStep && (
<>
<div className="flex justify-between items-center mt-6 mb-4">
<h2 className="text-lg font-semibold">Sections</h2>
<Button size="sm" variant="ghost" onClick={() => setAddSectionDialogOpen(true)}>
<div className="flex justify-between flex-col items-center mt-6 mb-4">
<h2 className="text-lg font-semibold self-start">Sections</h2>
<Button size="sm" className='w-full' variant="ghost" onClick={() => setAddSectionDialogOpen(true)}>
<Plus className="h-4 w-4" />
<p>Add Section</p>
</Button>
</div>
<div className="space-y-2">
{sortedSections.map((section) => (
<div
<div className="space-y-2 ">
{sortedSections.map((section) => {
const sortedFields = section.fields?.sort((a, b) => a.order - b.order) || [];
return (<div
key={section.id}
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
selectedSection === section.id
? 'border-brand-purple bg-brand-lavender/10'
: 'border-gray-200 hover:border-gray-300'
}`}
className='p-3 rounded-lg bg-background'
onClick={() => {
setSelectedSection(section.id);
setSelectedField(null);
}}
>
<div className="flex items-center justify-between">
<span className="text-sm">{section.title}</span>
<div className="flex items-center justify-between ">
<div className='flex flex-col'>
<span className="text-xl font-semibold">{section.title}</span>
{section.description && (
<p className="text-sm text-muted-foreground mb-4">{section.description}</p>
)}
</div>
<Button
size="icon"
size="sm"
variant="ghost"
className="h-6 w-6 text-red-500 hover:text-red-700"
className=" text-red-500 self-start hover-text-background"
onClick={(e) => {
e.stopPropagation();
handleDeleteSection(section.id);
}}
>
<p>Delete Section</p>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
))}
</div>
</>
)}
</Card>
</div>
<div className="p-6 mt-4 border-brand-purple rounded-xl bg-brand-lavender/10 ">
<div className="flex justify-between items-center mb-6 ">
{/* 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)}>
<div>test</div>
<Button size="sm" className='' 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 (
{/* Fields */}
<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)}
className='mb-6 p-4 rounded-lg border-2 border-dashed bg-background border-gray-200'
>
<h3 className="text-lg font-medium mb-4">{section.title}</h3>
{section.description && (
<h3 className="text-lg font-medium mb-4">title</h3>
<p className="text-sm text-muted-foreground mb-4">{section.description}</p>
)}
{/* Fields */}
<div className="space-y-3">
@@ -656,8 +637,7 @@ const AdminRegistrationBuilder = () => {
return (
<div
key={field.id}
className={`p-3 rounded-lg border cursor-pointer transition-all ${
selectedField === field.id
className={`p-3 rounded-lg border cursor-pointer transition-all ${selectedField === field.id
? 'border-brand-purple bg-brand-lavender/5 ring-2 ring-brand-purple/20'
: 'border-gray-200 hover:border-gray-300'
} ${field.is_fixed ? 'bg-gray-50' : ''}`}
@@ -688,7 +668,7 @@ const AdminRegistrationBuilder = () => {
<Button
size="icon"
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) => {
e.stopPropagation();
handleDeleteField(field.id);
@@ -698,10 +678,12 @@ const AdminRegistrationBuilder = () => {
</Button>
)}
</div>
</div>
);
})}
{sortedFields.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No fields in this section. Click "Add Field" to add one.
@@ -709,39 +691,21 @@ const AdminRegistrationBuilder = () => {
)}
</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>
</div>)
}
)}
</div>
</>
)}
</Card>
{/* Conditional Rules */}
<Card className="p-6 mt-6">
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-2">
<Zap className="h-5 w-5 text-yellow-500" />
<h2 className="text-lg font-semibold">Conditional Rules</h2>
</div>
<Button size="sm" variant="outline" onClick={() => setConditionalDialogOpen(true)}>
<Settings className="h-4 w-4 mr-2" />
Manage Rules
</Button>
</div>
<div className="text-sm text-muted-foreground">
{schema?.conditional_rules?.length || 0} conditional rule(s) configured
</div>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* 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>
<h2 className="text-lg font-semibold mb-4">Edit Options</h2>
{selectedFieldData ? (
<div className="space-y-4">
@@ -912,6 +876,23 @@ const AdminRegistrationBuilder = () => {
</div>
)}
</Card>
{/* Conditional Rules */}
<Card className="p-6 mt-6">
<div className="flex flex-col justify-between items-center mb-4">
<div className="flex items-center gap-2">
<Zap className="h-5 w-5 text-yellow-500" />
<h2 className="text-lg font-semibold">Conditional Rules</h2>
</div>
<Button size="sm" variant="outline" onClick={() => setConditionalDialogOpen(true)}>
<Settings className="h-4 w-4 mr-2" />
Manage Rules
</Button>
</div>
<div className="text-sm text-muted-foreground">
{schema?.conditional_rules?.length || 0} conditional rule(s) configured
</div>
</Card>
</div>
</div>
@@ -1033,7 +1014,7 @@ const AdminRegistrationBuilder = () => {
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-red-500"
className="h-6 w-6 text-red-500 hover-text-background"
onClick={() => {
updateSchema((prev) => ({
...prev,
@@ -1120,8 +1101,7 @@ const AdminRegistrationBuilder = () => {
.map((field) => (
<div
key={field.id}
className={`${
field.width === 'full'
className={`${field.width === 'full'
? 'col-span-2'
: field.width === 'third'
? 'col-span-1'

View File

@@ -354,13 +354,7 @@ const AdminValidations = () => {
Quick Overview
</div>
<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
title="Awaiting Email"
@@ -394,7 +388,13 @@ const AdminValidations = () => {
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>
</Card>