Files
smoothschedule/frontend/src/pages/Settings.tsx
poduck b10426fbdb feat: Add photo galleries to services, resource types management, and UI improvements
Major features:
- Add drag-and-drop photo gallery to Service create/edit modals
- Add Resource Types management section to Settings (CRUD for custom types)
- Add edit icon consistency to Resources table (pencil icon in actions)
- Improve Services page with drag-to-reorder and customer preview mockup

Backend changes:
- Add photos JSONField to Service model with migration
- Add ResourceType model with category (STAFF/OTHER), description fields
- Add ResourceTypeViewSet with CRUD operations
- Add service reorder endpoint for display order

Frontend changes:
- Services page: two-column layout, drag-reorder, photo upload
- Settings page: Resource Types tab with full CRUD modal
- Resources page: Edit icon in actions column instead of row click
- Sidebar: Payments link visibility based on role and paymentsEnabled
- Update types.ts with Service.photos and ResourceTypeDefinition

Note: Removed photos from ResourceType (kept only for Service)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 01:11:53 -05:00

1905 lines
95 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useOutletContext } from 'react-router-dom';
import { Business, User, CustomDomain } from '../types';
import { Save, Globe, Palette, BookKey, Check, Sparkles, CheckCircle, Link2, AlertCircle, ExternalLink, Copy, Crown, ShieldCheck, Trash2, RefreshCw, Star, Eye, EyeOff, Key, ShoppingCart, Building2, Users, Lock, Wallet, X, Plus, Layers, Pencil } from 'lucide-react';
import DomainPurchase from '../components/DomainPurchase';
import { useBusinessOAuthSettings, useUpdateBusinessOAuthSettings } from '../hooks/useBusinessOAuth';
import { useCustomDomains, useAddCustomDomain, useDeleteCustomDomain, useVerifyCustomDomain, useSetPrimaryDomain } from '../hooks/useCustomDomains';
import { useBusinessOAuthCredentials, useUpdateBusinessOAuthCredentials } from '../hooks/useBusinessOAuthCredentials';
import { useResourceTypes, useCreateResourceType, useUpdateResourceType, useDeleteResourceType } from '../hooks/useResourceTypes';
import OnboardingWizard from '../components/OnboardingWizard';
// Curated color palettes with complementary primary and secondary colors
const colorPalettes = [
{
name: 'Ocean Blue',
description: 'Professional & trustworthy',
primary: '#2563eb',
secondary: '#0ea5e9',
preview: 'bg-gradient-to-br from-blue-600 to-sky-500',
},
{
name: 'Sky Blue',
description: 'Light & airy',
primary: '#0ea5e9',
secondary: '#38bdf8',
preview: 'bg-gradient-to-br from-sky-500 to-sky-400',
},
{
name: 'Cyan Splash',
description: 'Modern & vibrant',
primary: '#06b6d4',
secondary: '#22d3ee',
preview: 'bg-gradient-to-br from-cyan-500 to-cyan-400',
},
{
name: 'Aqua Fresh',
description: 'Clean & refreshing',
primary: '#14b8a6',
secondary: '#2dd4bf',
preview: 'bg-gradient-to-br from-teal-500 to-teal-400',
},
{
name: 'Mint Green',
description: 'Soft & welcoming',
primary: '#10b981',
secondary: '#34d399',
preview: 'bg-gradient-to-br from-emerald-500 to-emerald-400',
},
{
name: 'Coral Reef',
description: 'Warm & inviting',
primary: '#f97316',
secondary: '#fb923c',
preview: 'bg-gradient-to-br from-orange-500 to-orange-400',
},
{
name: 'Lavender Dream',
description: 'Gentle & elegant',
primary: '#a78bfa',
secondary: '#c4b5fd',
preview: 'bg-gradient-to-br from-violet-400 to-violet-300',
},
{
name: 'Rose Pink',
description: 'Friendly & modern',
primary: '#ec4899',
secondary: '#f472b6',
preview: 'bg-gradient-to-br from-pink-500 to-pink-400',
},
{
name: 'Forest Green',
description: 'Natural & calming',
primary: '#059669',
secondary: '#10b981',
preview: 'bg-gradient-to-br from-emerald-600 to-emerald-400',
},
{
name: 'Royal Purple',
description: 'Creative & luxurious',
primary: '#7c3aed',
secondary: '#a78bfa',
preview: 'bg-gradient-to-br from-violet-600 to-purple-400',
},
{
name: 'Slate Gray',
description: 'Minimal & sophisticated',
primary: '#475569',
secondary: '#64748b',
preview: 'bg-gradient-to-br from-slate-600 to-slate-400',
},
{
name: 'Crimson Red',
description: 'Bold & dynamic',
primary: '#dc2626',
secondary: '#ef4444',
preview: 'bg-gradient-to-br from-red-600 to-red-400',
},
];
type SettingsTab = 'general' | 'domains' | 'authentication' | 'resources';
// Resource Types Management Section Component
const ResourceTypesSection: React.FC = () => {
const { t } = useTranslation();
const { data: resourceTypes = [], isLoading } = useResourceTypes();
const createResourceType = useCreateResourceType();
const updateResourceType = useUpdateResourceType();
const deleteResourceType = useDeleteResourceType();
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingType, setEditingType] = useState<any>(null);
const [formData, setFormData] = useState({
name: '',
description: '',
category: 'OTHER' as 'STAFF' | 'OTHER',
iconName: '',
});
const openCreateModal = () => {
setEditingType(null);
setFormData({ name: '', description: '', category: 'OTHER', iconName: '' });
setIsModalOpen(true);
};
const openEditModal = (type: any) => {
setEditingType(type);
setFormData({
name: type.name,
description: type.description || '',
category: type.category,
iconName: type.icon_name || type.iconName || '',
});
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
setEditingType(null);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (editingType) {
await updateResourceType.mutateAsync({
id: editingType.id,
updates: formData,
});
} else {
await createResourceType.mutateAsync(formData);
}
closeModal();
} catch (error) {
console.error('Failed to save resource type:', error);
}
};
const handleDelete = async (id: string, name: string) => {
if (window.confirm(`Are you sure you want to delete the "${name}" resource type?`)) {
try {
await deleteResourceType.mutateAsync(id);
} catch (error: any) {
alert(error.response?.data?.error || 'Failed to delete resource type');
}
}
};
return (
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Layers size={20} className="text-indigo-500" />
{t('settings.resourceTypes', 'Resource Types')}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{t('settings.resourceTypesDescription', 'Define custom types for your resources (e.g., Stylist, Treatment Room, Equipment)')}
</p>
</div>
<button
onClick={openCreateModal}
className="flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors font-medium text-sm"
>
<Plus size={16} />
{t('settings.addResourceType', 'Add Type')}
</button>
</div>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-600"></div>
</div>
) : resourceTypes.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
<Layers size={40} className="mx-auto mb-2 opacity-30" />
<p>{t('settings.noResourceTypes', 'No custom resource types yet.')}</p>
<p className="text-sm mt-1">{t('settings.addFirstResourceType', 'Add your first resource type to categorize your resources.')}</p>
</div>
) : (
<div className="space-y-3">
{resourceTypes.map((type: any) => {
const isDefault = type.is_default || type.isDefault;
return (
<div
key={type.id}
className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700"
>
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center shrink-0 ${
type.category === 'STAFF' ? 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400' : 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
}`}>
{type.category === 'STAFF' ? <Users size={20} /> : <Layers size={20} />}
</div>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-gray-900 dark:text-white flex items-center gap-2">
{type.name}
{isDefault && (
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded">
Default
</span>
)}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
{type.category === 'STAFF' ? 'Requires staff assignment' : 'General resource'}
</p>
{type.description && (
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1 line-clamp-2">
{type.description}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2 shrink-0 ml-2">
<button
onClick={() => openEditModal(type)}
className="p-2 text-gray-400 hover:text-brand-600 dark:hover:text-brand-400 transition-colors"
title={t('common.edit', 'Edit')}
>
<Pencil size={16} />
</button>
{!isDefault && (
<button
onClick={() => handleDelete(type.id, type.name)}
disabled={deleteResourceType.isPending}
className="p-2 text-gray-400 hover:text-red-600 dark:hover:text-red-400 transition-colors disabled:opacity-50"
title={t('common.delete', 'Delete')}
>
<Trash2 size={16} />
</button>
)}
</div>
</div>
</div>
);
})}
</div>
)}
{/* Modal for Create/Edit */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-md w-full max-h-[90vh] overflow-hidden flex flex-col">
<div className="flex items-center justify-between p-6 border-b border-gray-100 dark:border-gray-700 shrink-0">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{editingType
? t('settings.editResourceType', 'Edit Resource Type')
: t('settings.addResourceType', 'Add Resource Type')}
</h3>
<button
onClick={closeModal}
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X className="h-5 w-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto">
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('settings.resourceTypeName', 'Name')} *
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
placeholder={t('settings.resourceTypeNamePlaceholder', 'e.g., Stylist, Treatment Room, Camera')}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('settings.resourceTypeDescription', 'Description')}
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500 resize-none"
placeholder={t('settings.resourceTypeDescriptionPlaceholder', 'Describe this type of resource...')}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('settings.resourceTypeCategory', 'Category')} *
</label>
<select
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value as 'STAFF' | 'OTHER' })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
>
<option value="STAFF">{t('settings.categoryStaff', 'Staff (requires staff assignment)')}</option>
<option value="OTHER">{t('settings.categoryOther', 'Other (general resource)')}</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{formData.category === 'STAFF'
? t('settings.staffCategoryHint', 'Staff resources must be assigned to a team member')
: t('settings.otherCategoryHint', 'General resources like rooms, equipment, or vehicles')}
</p>
</div>
</div>
<div className="flex justify-end gap-3 p-6 border-t border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50 shrink-0">
<button
type="button"
onClick={closeModal}
className="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
{t('common.cancel', 'Cancel')}
</button>
<button
type="submit"
disabled={createResourceType.isPending || updateResourceType.isPending}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{editingType ? t('common.save', 'Save') : t('common.create', 'Create')}
</button>
</div>
</form>
</div>
</div>
)}
</section>
);
};
const SettingsPage: React.FC = () => {
const { t } = useTranslation();
const { business, updateBusiness, user } = useOutletContext<{ business: Business; updateBusiness: (updates: Partial<Business>) => void; user: User}>();
const [formState, setFormState] = useState(business);
const isOwner = user.role === 'owner';
const [showCustomColors, setShowCustomColors] = useState(false);
const [showToast, setShowToast] = useState(false);
const [activeTab, setActiveTab] = useState<SettingsTab>('general');
// OAuth settings
const { data: oauthData, isLoading: oauthLoading } = useBusinessOAuthSettings();
const updateOAuthMutation = useUpdateBusinessOAuthSettings();
const [oauthSettings, setOAuthSettings] = useState({
enabledProviders: [] as string[],
allowRegistration: false,
autoLinkByEmail: true,
});
// Custom Domains
const { data: customDomains = [], isLoading: domainsLoading } = useCustomDomains();
const addDomainMutation = useAddCustomDomain();
const deleteDomainMutation = useDeleteCustomDomain();
const verifyDomainMutation = useVerifyCustomDomain();
const setPrimaryMutation = useSetPrimaryDomain();
const [newDomain, setNewDomain] = useState('');
const [verifyingDomainId, setVerifyingDomainId] = useState<number | null>(null);
const [verifyError, setVerifyError] = useState<string | null>(null);
// OAuth Credentials
const { data: oauthCredentials, isLoading: credentialsLoading } = useBusinessOAuthCredentials();
const updateCredentialsMutation = useUpdateBusinessOAuthCredentials();
const [useCustomCredentials, setUseCustomCredentials] = useState(false);
const [credentials, setCredentials] = useState<any>({
google: { client_id: '', client_secret: '' },
apple: { client_id: '', client_secret: '', team_id: '', key_id: '' },
facebook: { client_id: '', client_secret: '' },
linkedin: { client_id: '', client_secret: '' },
microsoft: { client_id: '', client_secret: '', tenant_id: '' },
twitter: { client_id: '', client_secret: '' },
twitch: { client_id: '', client_secret: '' },
});
const [showSecrets, setShowSecrets] = useState<{ [key: string]: boolean }>({});
const [showOnboarding, setShowOnboarding] = useState(false);
// Drag and drop state for logo uploads
const [isDraggingLogo, setIsDraggingLogo] = useState(false);
const [isDraggingEmailLogo, setIsDraggingEmailLogo] = useState(false);
// Lightbox state for viewing logos
const [lightboxImage, setLightboxImage] = useState<{ url: string; title: string } | null>(null);
// Email preview modal state
const [showEmailPreview, setShowEmailPreview] = useState(false);
// Update OAuth settings when data loads
useEffect(() => {
if (oauthData?.businessSettings) {
setOAuthSettings(oauthData.businessSettings);
}
}, [oauthData]);
// Update OAuth credentials when data loads
useEffect(() => {
if (oauthCredentials) {
setUseCustomCredentials(oauthCredentials.use_custom_credentials || false);
if (oauthCredentials.google || oauthCredentials.apple || oauthCredentials.facebook ||
oauthCredentials.linkedin || oauthCredentials.microsoft || oauthCredentials.twitter ||
oauthCredentials.twitch) {
setCredentials({
google: oauthCredentials.google || { client_id: '', client_secret: '' },
apple: oauthCredentials.apple || { client_id: '', client_secret: '', team_id: '', key_id: '' },
facebook: oauthCredentials.facebook || { client_id: '', client_secret: '' },
linkedin: oauthCredentials.linkedin || { client_id: '', client_secret: '' },
microsoft: oauthCredentials.microsoft || { client_id: '', client_secret: '', tenant_id: '' },
twitter: oauthCredentials.twitter || { client_id: '', client_secret: '' },
twitch: oauthCredentials.twitch || { client_id: '', client_secret: '' },
});
}
}
}, [oauthCredentials]);
// Auto-hide toast after 3 seconds
useEffect(() => {
if (showToast) {
const timer = setTimeout(() => setShowToast(false), 3000);
return () => clearTimeout(timer);
}
}, [showToast]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
if (type === 'checkbox') {
setFormState(prev => ({...prev, [name]: checked }));
} else {
setFormState(prev => ({ ...prev, [name]: value }));
}
};
// Drag and drop handlers for logo upload
const handleLogoDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingLogo(true);
};
const handleLogoDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingLogo(false);
};
const handleLogoDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingLogo(false);
const files = e.dataTransfer.files;
if (files && files.length > 0) {
const file = files[0];
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onloadend = () => {
setFormState(prev => ({ ...prev, logoUrl: reader.result as string }));
};
reader.readAsDataURL(file);
}
}
};
// Drag and drop handlers for email logo upload
const handleEmailLogoDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingEmailLogo(true);
};
const handleEmailLogoDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingEmailLogo(false);
};
const handleEmailLogoDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingEmailLogo(false);
const files = e.dataTransfer.files;
if (files && files.length > 0) {
const file = files[0];
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onloadend = () => {
setFormState(prev => ({ ...prev, emailLogoUrl: reader.result as string }));
};
reader.readAsDataURL(file);
}
}
};
const handleSave = () => {
updateBusiness(formState);
setShowToast(true);
};
const handleCancel = () => {
setFormState(business);
};
const handleOAuthSave = () => {
updateOAuthMutation.mutate(oauthSettings, {
onSuccess: () => {
setShowToast(true);
},
});
};
const toggleProvider = (provider: string) => {
setOAuthSettings((prev) => {
const isEnabled = prev.enabledProviders.includes(provider);
return {
...prev,
enabledProviders: isEnabled
? prev.enabledProviders.filter((p) => p !== provider)
: [...prev.enabledProviders, provider],
};
});
};
// Custom Domain handlers
const handleAddDomain = () => {
if (!newDomain.trim()) return;
addDomainMutation.mutate(newDomain, {
onSuccess: () => {
setNewDomain('');
setShowToast(true);
},
onError: (error: any) => {
alert(error.response?.data?.error || 'Failed to add domain');
},
});
};
const handleDeleteDomain = (domainId: number) => {
if (!confirm('Are you sure you want to delete this custom domain?')) return;
deleteDomainMutation.mutate(domainId, {
onSuccess: () => {
setShowToast(true);
},
});
};
const handleVerifyDomain = (domainId: number) => {
setVerifyingDomainId(domainId);
setVerifyError(null);
verifyDomainMutation.mutate(domainId, {
onSuccess: (data) => {
setVerifyingDomainId(null);
if (data.verified) {
setShowToast(true);
} else {
setVerifyError(data.message);
}
},
onError: (error: any) => {
setVerifyingDomainId(null);
setVerifyError(error.response?.data?.message || 'Verification failed');
},
});
};
const handleSetPrimary = (domainId: number) => {
setPrimaryMutation.mutate(domainId, {
onSuccess: () => {
setShowToast(true);
},
onError: (error: any) => {
alert(error.response?.data?.error || 'Failed to set primary domain');
},
});
};
// OAuth Credentials handlers
const handleCredentialsSave = () => {
const updateData: any = {
use_custom_credentials: useCustomCredentials,
};
if (useCustomCredentials) {
Object.entries(credentials).forEach(([provider, creds]: [string, any]) => {
if (creds.client_id || creds.client_secret) {
updateData[provider] = creds;
}
});
}
updateCredentialsMutation.mutate(updateData, {
onSuccess: () => {
setShowToast(true);
},
});
};
const updateCredential = (provider: string, field: string, value: string) => {
setCredentials((prev: any) => ({
...prev,
[provider]: {
...prev[provider],
[field]: value,
},
}));
};
const toggleShowSecret = (key: string) => {
setShowSecrets((prev) => ({ ...prev, [key]: !prev[key] }));
};
// Provider display names and icons
const providerInfo: Record<string, { name: string; icon: string }> = {
google: { name: 'Google', icon: '🔍' },
apple: { name: 'Apple', icon: '🍎' },
facebook: { name: 'Facebook', icon: '📘' },
linkedin: { name: 'LinkedIn', icon: '💼' },
microsoft: { name: 'Microsoft', icon: '🪟' },
twitter: { name: 'X (Twitter)', icon: '🐦' },
twitch: { name: 'Twitch', icon: '🎮' },
};
// Tab configuration
const tabs = [
{ id: 'general' as const, label: 'General', icon: Building2 },
{ id: 'resources' as const, label: 'Resource Types', icon: Layers },
{ id: 'domains' as const, label: 'Domains', icon: Globe },
{ id: 'authentication' as const, label: 'Authentication', icon: Lock },
];
return (
<div className="p-8 max-w-4xl mx-auto pb-24">
{/* Header */}
<div className="mb-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('settings.businessSettings')}</h2>
<p className="text-gray-500 dark:text-gray-400">{t('settings.businessSettingsDescription')}</p>
</div>
{/* Tab Navigation */}
<div className="border-b border-gray-200 dark:border-gray-700 mb-6">
<nav className="flex gap-1" aria-label="Settings tabs">
{tabs.map((tab) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
isActive
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<Icon size={18} />
{tab.label}
</button>
);
})}
</nav>
</div>
{/* Tab Content */}
<div className="space-y-6">
{/* GENERAL TAB */}
{activeTab === 'general' && (
<>
{/* Business Identity */}
{isOwner && (
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Building2 size={20} className="text-brand-500"/> Business Identity
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t('settings.businessName')}</label>
<input type="text" name="name" value={formState.name} onChange={handleChange} className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-brand-500"/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t('settings.subdomain')}</label>
<div className="flex">
<input type="text" name="subdomain" value={formState.subdomain} onChange={handleChange} className="flex-1 min-w-0 px-4 py-2 border border-r-0 border-gray-300 dark:border-gray-600 rounded-l-lg bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400 cursor-not-allowed" readOnly/>
<span className="inline-flex items-center px-4 py-2 border border-l-0 border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-500 dark:text-gray-400 text-sm rounded-r-lg">.smoothschedule.com</span>
</div>
</div>
</div>
</section>
)}
{/* Branding */}
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
<Palette size={20} className="text-purple-500"/> {t('settings.branding')}
</h3>
{/* Logo Upload */}
<div className="mb-6 pb-6 border-b border-gray-200 dark:border-gray-700">
<h4 className="font-medium text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Image size={16} className="text-blue-500" />
Brand Logos
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
Upload your logos for different purposes. PNG with transparent background recommended.
</p>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Website Logo Upload/Display */}
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h5 className="font-medium text-gray-900 dark:text-white mb-3">Website Logo</h5>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
Used in sidebar and customer-facing pages
</p>
<div
onDragOver={handleLogoDragOver}
onDragLeave={handleLogoDragLeave}
onDrop={handleLogoDrop}
className={`transition-all ${isDraggingLogo ? 'scale-105' : ''}`}
>
{formState.logoUrl ? (
<div className="space-y-3">
<div className="relative inline-block group">
<img
src={formState.logoUrl}
alt="Business logo"
onClick={() => setLightboxImage({ url: formState.logoUrl!, title: 'Website Logo' })}
className="w-32 h-32 object-contain border-2 border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 p-2 cursor-pointer hover:border-blue-400 transition-colors"
/>
<button
type="button"
onClick={() => {
setFormState(prev => ({
...prev,
logoUrl: undefined,
logoDisplayMode: 'logo-and-text' // Reset to show icon with text
}));
}}
className="absolute -top-2 -right-2 bg-red-500 hover:bg-red-600 text-white rounded-full p-1.5 shadow-lg transition-colors z-10"
title="Remove logo"
>
<X size={14} />
</button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Click to view full size Click × to remove Drag and drop to replace
</p>
</div>
) : (
<div className={`w-32 h-32 border-2 border-dashed rounded-lg flex items-center justify-center transition-colors ${
isDraggingLogo
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-500'
: 'border-gray-300 dark:border-gray-600 text-gray-400'
}`}>
<div className="text-center">
<Image size={32} className="mx-auto mb-2" />
<p className="text-xs">Drop image here</p>
</div>
</div>
)}
</div>
<div className="mt-3">
<input
type="file"
id="logo-upload"
className="hidden"
accept="image/png,image/jpeg,image/svg+xml"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
// TODO: Upload to backend
const reader = new FileReader();
reader.onloadend = () => {
setFormState(prev => ({ ...prev, logoUrl: reader.result as string }));
};
reader.readAsDataURL(file);
}
}}
/>
<label
htmlFor="logo-upload"
className="inline-flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 cursor-pointer transition-colors text-sm font-medium"
>
<Upload size={16} />
{formState.logoUrl ? 'Change Logo' : 'Upload Logo'}
</label>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
PNG, JPG, or SVG. Recommended: 500x500px
</p>
</div>
{/* Logo Display Mode */}
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Display Mode
</label>
<select
name="logoDisplayMode"
value={formState.logoDisplayMode || 'text-only'}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg text-sm"
>
<option value="text-only">Text Only</option>
<option value="logo-only">Logo Only</option>
<option value="logo-and-text">Logo and Text</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
How your branding appears in the sidebar
</p>
</div>
</div>
{/* Email Logo Upload/Display */}
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h5 className="font-medium text-gray-900 dark:text-white mb-3">Email Logo</h5>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
Used in email notifications and receipts
</p>
<div
onDragOver={handleEmailLogoDragOver}
onDragLeave={handleEmailLogoDragLeave}
onDrop={handleEmailLogoDrop}
className={`transition-all ${isDraggingEmailLogo ? 'scale-105' : ''}`}
>
{formState.emailLogoUrl ? (
<div className="space-y-3">
<div className="relative inline-block group">
<img
src={formState.emailLogoUrl}
alt="Email logo"
onClick={() => setLightboxImage({ url: formState.emailLogoUrl!, title: 'Email Logo' })}
className="w-48 h-16 object-contain border-2 border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 p-2 cursor-pointer hover:border-blue-400 transition-colors"
/>
<button
type="button"
onClick={() => {
setFormState(prev => ({ ...prev, emailLogoUrl: undefined }));
}}
className="absolute -top-2 -right-2 bg-red-500 hover:bg-red-600 text-white rounded-full p-1.5 shadow-lg transition-colors z-10"
title="Remove email logo"
>
<X size={14} />
</button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Click to view full size Click × to remove Drag and drop to replace
</p>
</div>
) : (
<div className={`w-48 h-16 border-2 border-dashed rounded-lg flex items-center justify-center transition-colors ${
isDraggingEmailLogo
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-500'
: 'border-gray-300 dark:border-gray-600 text-gray-400'
}`}>
<div className="text-center">
<Image size={24} className="mx-auto mb-1" />
<p className="text-xs">Drop image here</p>
</div>
</div>
)}
</div>
<div className="mt-3">
<input
type="file"
id="email-logo-upload"
className="hidden"
accept="image/png,image/jpeg,image/svg+xml"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
// TODO: Upload to backend
const reader = new FileReader();
reader.onloadend = () => {
setFormState(prev => ({ ...prev, emailLogoUrl: reader.result as string }));
};
reader.readAsDataURL(file);
}
}}
/>
<label
htmlFor="email-logo-upload"
className="inline-flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 cursor-pointer transition-colors text-sm font-medium"
>
<Upload size={16} />
{formState.emailLogoUrl ? 'Change Email Logo' : 'Upload Email Logo'}
</label>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
PNG with transparent background. Recommended: 600x200px
</p>
<button
type="button"
onClick={() => setShowEmailPreview(true)}
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium mt-3"
>
<Eye size={16} />
Preview Email
</button>
</div>
</div>
</div>
{/* Sidebar Preview */}
<div className="mt-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Sidebar Preview
</label>
<div
className="w-full max-w-xs p-6 rounded-xl"
style={{ backgroundColor: formState.primaryColor }}
>
<div className="flex items-center gap-3">
{/* Logo-only mode: full width */}
{formState.logoDisplayMode === 'logo-only' && formState.logoUrl ? (
<div className="flex items-center justify-center w-full">
<img
src={formState.logoUrl}
alt={formState.name}
className="max-w-full max-h-16 object-contain"
/>
</div>
) : (
<>
{/* Logo/Icon display - only show if NOT text-only mode */}
{formState.logoDisplayMode !== 'text-only' && (
formState.logoUrl ? (
<div className="flex items-center justify-center w-10 h-10 shrink-0">
<img
src={formState.logoUrl}
alt={formState.name}
className="w-full h-full object-contain"
/>
</div>
) : (
<div
className="flex items-center justify-center w-10 h-10 bg-white rounded-lg text-brand-600 font-bold text-xl shrink-0"
style={{ color: formState.primaryColor }}
>
{formState.name.substring(0, 2).toUpperCase()}
</div>
)
)}
{/* Text display - only show if NOT logo-only mode */}
{formState.logoDisplayMode !== 'logo-only' && (
<div className="overflow-hidden">
<h1 className="font-bold leading-tight truncate text-white">{formState.name}</h1>
<p className="text-xs text-white/60 truncate">{business.subdomain}.smoothschedule.com</p>
</div>
)}
</>
)}
</div>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
This is how your branding will appear in the navigation sidebar
</p>
</div>
</div>
{/* Color Palette Selection */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div>
<h4 className="font-medium text-gray-900 dark:text-white flex items-center gap-2">
<Sparkles size={16} className="text-amber-500" />
Recommended Palettes
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Choose a professionally designed color scheme</p>
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{colorPalettes.map((palette) => {
const isSelected = formState.primaryColor.toLowerCase() === palette.primary.toLowerCase() &&
formState.secondaryColor.toLowerCase() === palette.secondary.toLowerCase();
return (
<button
key={palette.name}
type="button"
onClick={() => {
setFormState(prev => ({
...prev,
primaryColor: palette.primary,
secondaryColor: palette.secondary,
}));
setShowCustomColors(false);
}}
className={`relative p-3 rounded-xl border-2 transition-all duration-200 text-left group hover:scale-[1.02] ${
isSelected
? 'border-brand-500 ring-2 ring-brand-500/20 bg-brand-50 dark:bg-brand-900/20'
: 'border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500'
}`}
>
{isSelected && (
<div className="absolute -top-2 -right-2 w-5 h-5 bg-brand-500 rounded-full flex items-center justify-center shadow-sm">
<Check size={12} className="text-white" />
</div>
)}
<div className={`h-12 rounded-lg mb-2 ${palette.preview}`} />
<div className="flex gap-1.5 mb-2">
<div
className="w-5 h-5 rounded-full border-2 border-white shadow-sm"
style={{ backgroundColor: palette.primary }}
title="Primary"
/>
<div
className="w-5 h-5 rounded-full border-2 border-white shadow-sm"
style={{ backgroundColor: palette.secondary }}
title="Secondary"
/>
</div>
<p className="text-xs font-semibold text-gray-900 dark:text-white truncate">{palette.name}</p>
<p className="text-[10px] text-gray-500 dark:text-gray-400 truncate">{palette.description}</p>
</button>
);
})}
</div>
</div>
{/* Custom Colors Toggle */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<button
type="button"
onClick={() => setShowCustomColors(!showCustomColors)}
className="flex items-center gap-2 text-sm font-medium text-brand-600 dark:text-brand-400 hover:text-brand-700 dark:hover:text-brand-300"
>
<Palette size={16} />
{showCustomColors ? 'Hide custom colors' : 'Use custom colors'}
</button>
{showCustomColors && (
<div className="mt-4 p-4 bg-gray-50 dark:bg-gray-900/50 rounded-xl space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Primary Color</label>
<div className="flex items-center gap-3">
<input
type="color"
name="primaryColor"
value={formState.primaryColor}
onChange={handleChange}
className="h-10 w-10 p-1 rounded-lg cursor-pointer border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700"
/>
<input
type="text"
name="primaryColor"
value={formState.primaryColor}
onChange={handleChange}
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg font-mono text-sm uppercase"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Secondary Color</label>
<div className="flex items-center gap-3">
<input
type="color"
name="secondaryColor"
value={formState.secondaryColor}
onChange={handleChange}
className="h-10 w-10 p-1 rounded-lg cursor-pointer border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700"
/>
<input
type="text"
name="secondaryColor"
value={formState.secondaryColor}
onChange={handleChange}
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg font-mono text-sm uppercase"
/>
</div>
</div>
</div>
</div>
)}
</div>
{/* Live Preview */}
<div className="mt-6 border-t border-gray-200 dark:border-gray-700 pt-4">
<h4 className="font-medium text-gray-900 dark:text-white mb-3 text-sm">Live Preview</h4>
<div className="bg-gray-100 dark:bg-gray-900 rounded-xl p-3 overflow-hidden">
<div
className="rounded-t-lg p-2.5 flex items-center justify-between"
style={{ backgroundColor: formState.primaryColor }}
>
<div className="flex items-center gap-2">
<div className="w-5 h-5 bg-white rounded font-bold text-[10px] flex items-center justify-center" style={{ color: formState.primaryColor }}>
{formState.name.charAt(0)}
</div>
<span className="text-white text-xs font-semibold">{formState.name}</span>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-b-lg p-3 space-y-2">
<div className="flex items-center gap-2">
<button
className="px-2 py-1 rounded text-[10px] text-white font-medium"
style={{ backgroundColor: formState.primaryColor }}
>
Primary
</button>
<button
className="px-2 py-1 rounded text-[10px] font-medium border"
style={{ color: formState.secondaryColor, borderColor: formState.secondaryColor }}
>
Secondary
</button>
</div>
</div>
</div>
</div>
</section>
{/* Payments */}
{isOwner && (
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Wallet size={20} className="text-emerald-500"/> {t('settings.payments')}
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-gray-900 dark:text-white">{t('settings.acceptPayments')}</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">{t('settings.acceptPaymentsDescription')}</p>
</div>
<button
type="button"
className={`${formState.paymentsEnabled ? 'bg-brand-600' : 'bg-gray-200 dark:bg-gray-600'} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500`}
role="switch"
onClick={() => {
const newEnabled = !formState.paymentsEnabled;
setFormState(prev => ({ ...prev, paymentsEnabled: newEnabled }));
// If enabling payments and Stripe isn't connected, show onboarding wizard
if (newEnabled && !business.stripeConnectAccountId) {
setShowOnboarding(true);
}
}}
>
<span className={`${formState.paymentsEnabled ? 'translate-x-5' : 'translate-x-0'} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`}/>
</button>
</div>
{formState.paymentsEnabled && !business.stripeConnectAccountId && (
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<div className="flex items-start gap-2">
<AlertCircle size={16} className="text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
<div className="text-sm text-amber-700 dark:text-amber-400">
<p className="font-medium">{t('settings.stripeSetupRequired')}</p>
<p className="text-xs mt-1">{t('settings.stripeSetupDescription')}</p>
</div>
</div>
</div>
)}
</div>
</section>
)}
{/* Booking Policy */}
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<BookKey size={20} className="text-green-500"/> {t('settings.bookingPolicy')}
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Require Payment to Book</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Customers must have a card on file to book.</p>
</div>
<button type="button" className={`${formState.requirePaymentMethodToBook ? 'bg-brand-600' : 'bg-gray-200 dark:bg-gray-600'} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500`} role="switch" onClick={() => setFormState(prev => ({ ...prev, requirePaymentMethodToBook: !prev.requirePaymentMethodToBook }))} >
<span className={`${formState.requirePaymentMethodToBook ? 'translate-x-5' : 'translate-x-0'} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`}/>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Cancellation Window (hours)</label>
<input type="number" name="cancellationWindowHours" value={formState.cancellationWindowHours} onChange={handleChange} className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-brand-500" placeholder="e.g., 24"/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Late Fee (%)</label>
<input type="number" name="lateCancellationFeePercent" value={formState.lateCancellationFeePercent} onChange={handleChange} min="0" max="100" className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-brand-500" placeholder="e.g., 50"/>
</div>
</div>
</div>
</section>
</>
)}
{/* RESOURCES TAB */}
{activeTab === 'resources' && isOwner && (
<ResourceTypesSection />
)}
{/* DOMAINS TAB */}
{activeTab === 'domains' && (
<>
{/* Quick Domain Setup */}
{isOwner && (
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Link2 size={20} className="text-brand-500"/> Your Booking URL
</h3>
<div className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<code className="flex-1 text-sm font-mono text-gray-900 dark:text-white">
{formState.subdomain}.smoothschedule.com
</code>
<button
onClick={() => navigator.clipboard.writeText(`${formState.subdomain}.smoothschedule.com`)}
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
title="Copy to clipboard"
>
<Copy size={16} />
</button>
</div>
</section>
)}
{/* Custom Domains Management */}
{isOwner && business.plan !== 'Free' && (
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Globe size={20} className="text-indigo-500" />
Custom Domains
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Use your own domains for your booking pages
</p>
</div>
</div>
{/* Add New Domain Form */}
<div className="mb-4 p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="flex gap-2">
<input
type="text"
value={newDomain}
onChange={(e) => setNewDomain(e.target.value)}
placeholder="booking.yourdomain.com"
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-brand-500 font-mono text-sm"
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleAddDomain();
}
}}
/>
<button
onClick={handleAddDomain}
disabled={addDomainMutation.isPending || !newDomain.trim()}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium text-sm"
>
{addDomainMutation.isPending ? 'Adding...' : 'Add'}
</button>
</div>
</div>
{/* Custom Domains List */}
{domainsLoading ? (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
Loading domains...
</div>
) : customDomains.length === 0 ? (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
<Globe size={40} className="mx-auto mb-2 opacity-30" />
<p className="text-sm">No custom domains yet. Add one above.</p>
</div>
) : (
<div className="space-y-3">
{customDomains.map((domain) => (
<div
key={domain.id}
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-800"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-mono text-sm font-semibold text-gray-900 dark:text-white">
{domain.domain}
</h4>
{domain.is_primary && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300 rounded">
<Star size={10} className="fill-current" /> Primary
</span>
)}
{domain.is_verified ? (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded">
<CheckCircle size={10} /> Verified
</span>
) : (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 rounded">
<AlertCircle size={10} /> Pending
</span>
)}
</div>
{!domain.is_verified && (
<div className="mt-2 p-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded text-xs">
<p className="font-medium text-amber-800 dark:text-amber-300 mb-1">Add DNS TXT record:</p>
<div className="space-y-1">
<div className="flex items-center gap-1">
<span className="text-amber-700 dark:text-amber-400">Name:</span>
<code className="px-1 bg-white dark:bg-gray-800 rounded text-gray-900 dark:text-white">{domain.dns_txt_record_name}</code>
<button onClick={() => navigator.clipboard.writeText(domain.dns_txt_record_name)} className="text-amber-600 hover:text-amber-700"><Copy size={12} /></button>
</div>
<div className="flex items-center gap-1">
<span className="text-amber-700 dark:text-amber-400">Value:</span>
<code className="px-1 bg-white dark:bg-gray-800 rounded text-gray-900 dark:text-white truncate max-w-[200px]">{domain.dns_txt_record}</code>
<button onClick={() => navigator.clipboard.writeText(domain.dns_txt_record)} className="text-amber-600 hover:text-amber-700"><Copy size={12} /></button>
</div>
</div>
{verifyError && verifyingDomainId === domain.id && (
<p className="mt-1 text-red-600 dark:text-red-400">{verifyError}</p>
)}
</div>
)}
</div>
<div className="flex items-center gap-1 ml-3">
{!domain.is_verified && (
<button
onClick={() => handleVerifyDomain(domain.id)}
disabled={verifyingDomainId === domain.id}
className="p-1.5 text-brand-600 dark:text-brand-400 hover:bg-brand-50 dark:hover:bg-brand-900/30 rounded transition-colors disabled:opacity-50"
title="Verify"
>
<RefreshCw size={16} className={verifyingDomainId === domain.id ? 'animate-spin' : ''} />
</button>
)}
{domain.is_verified && !domain.is_primary && (
<button
onClick={() => handleSetPrimary(domain.id)}
disabled={setPrimaryMutation.isPending}
className="p-1.5 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 rounded transition-colors"
title="Set as Primary"
>
<Star size={16} />
</button>
)}
<button
onClick={() => handleDeleteDomain(domain.id)}
disabled={deleteDomainMutation.isPending}
className="p-1.5 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors"
title="Delete"
>
<Trash2 size={16} />
</button>
</div>
</div>
</div>
))}
</div>
)}
</section>
)}
{/* Domain Purchase */}
{isOwner && business.plan !== 'Free' && (
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<ShoppingCart size={20} className="text-green-500" />
Purchase a Domain
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Search and register a new domain name
</p>
</div>
</div>
<DomainPurchase />
</section>
)}
{/* Upgrade prompt for free plans */}
{isOwner && business.plan === 'Free' && (
<section className="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-900/20 dark:to-orange-900/20 p-6 rounded-xl border border-amber-200 dark:border-amber-800">
<div className="flex items-start gap-4">
<div className="p-3 bg-amber-100 dark:bg-amber-900/40 rounded-lg">
<Crown size={24} className="text-amber-600 dark:text-amber-400" />
</div>
<div>
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Unlock Custom Domains</h4>
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
Upgrade to use your own domain (e.g., <span className="font-mono">book.yourbusiness.com</span>) or purchase a new one.
</p>
<button className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-indigo-500 to-purple-500 rounded-lg hover:from-indigo-600 hover:to-purple-600 transition-all">
<Crown size={16} /> View Plans
</button>
</div>
</div>
</section>
)}
</>
)}
{/* AUTHENTICATION TAB */}
{activeTab === 'authentication' && isOwner && (
<>
{/* OAuth & Social Login */}
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Users size={20} className="text-indigo-500" /> Social Login
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">Choose which providers customers can use to sign in</p>
</div>
<button
onClick={handleOAuthSave}
disabled={oauthLoading || updateOAuthMutation.isPending}
className="flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors shadow-sm font-medium text-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save size={16} />
{updateOAuthMutation.isPending ? 'Saving...' : 'Save'}
</button>
</div>
{oauthLoading ? (
<div className="flex items-center justify-center py-6">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-600"></div>
</div>
) : oauthData?.availableProviders && oauthData.availableProviders.length > 0 ? (
<div className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{oauthData.availableProviders.map((provider) => {
const isEnabled = oauthSettings.enabledProviders.includes(provider);
const info = providerInfo[provider] || { name: provider, icon: '🔐' };
return (
<button
key={provider}
type="button"
onClick={() => toggleProvider(provider)}
className={`relative p-3 rounded-lg border-2 transition-all text-left ${
isEnabled
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20'
: 'border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500'
}`}
>
{isEnabled && (
<div className="absolute top-1.5 right-1.5 w-4 h-4 bg-brand-500 rounded-full flex items-center justify-center">
<Check size={10} className="text-white" />
</div>
)}
<div className="flex items-center gap-2">
<span className="text-lg">{info.icon}</span>
<span className="text-sm font-medium text-gray-900 dark:text-white">{info.name}</span>
</div>
</button>
);
})}
</div>
<div className="pt-4 border-t border-gray-200 dark:border-gray-700 space-y-3">
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium text-gray-900 dark:text-white">Allow OAuth Registration</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">New customers can create accounts via OAuth</p>
</div>
<button
type="button"
className={`${oauthSettings.allowRegistration ? 'bg-brand-600' : 'bg-gray-200 dark:bg-gray-600'} relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500`}
role="switch"
onClick={() => setOAuthSettings((prev) => ({ ...prev, allowRegistration: !prev.allowRegistration }))}
>
<span className={`${oauthSettings.allowRegistration ? 'translate-x-4' : 'translate-x-0'} pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`} />
</button>
</div>
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium text-gray-900 dark:text-white">Auto-link by Email</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">Link OAuth accounts to existing accounts by email</p>
</div>
<button
type="button"
className={`${oauthSettings.autoLinkByEmail ? 'bg-brand-600' : 'bg-gray-200 dark:bg-gray-600'} relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500`}
role="switch"
onClick={() => setOAuthSettings((prev) => ({ ...prev, autoLinkByEmail: !prev.autoLinkByEmail }))}
>
<span className={`${oauthSettings.autoLinkByEmail ? 'translate-x-4' : 'translate-x-0'} pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`} />
</button>
</div>
</div>
</div>
) : (
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
<div className="flex items-start gap-3">
<AlertCircle size={18} className="text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-amber-800 dark:text-amber-300 text-sm">No OAuth Providers Available</p>
<p className="text-xs text-amber-700 dark:text-amber-400 mt-1">
Contact your platform administrator to enable OAuth providers.
</p>
</div>
</div>
</div>
)}
</section>
{/* Custom OAuth Credentials */}
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Key size={20} className="text-purple-500" />
Custom OAuth Credentials
{business.plan === 'Free' && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 rounded-full">
<Crown size={12} /> Pro
</span>
)}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Use your own OAuth app credentials for complete branding control
</p>
</div>
{business.plan !== 'Free' && (
<button
onClick={handleCredentialsSave}
disabled={credentialsLoading || updateCredentialsMutation.isPending}
className="flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors shadow-sm font-medium text-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save size={16} />
{updateCredentialsMutation.isPending ? 'Saving...' : 'Save'}
</button>
)}
</div>
{business.plan === 'Free' ? (
<div className="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-900/20 dark:to-orange-900/20 rounded-lg p-4 border border-amber-200 dark:border-amber-800">
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
Upgrade to use your own OAuth credentials for custom branding and higher rate limits.
</p>
<button className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-gradient-to-r from-indigo-500 to-purple-500 rounded-lg hover:from-indigo-600 hover:to-purple-600 transition-all">
<Crown size={14} /> View Plans
</button>
</div>
) : credentialsLoading ? (
<div className="flex items-center justify-center py-6">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-600"></div>
</div>
) : (
<div className="space-y-4">
{/* Toggle Custom Credentials */}
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<div>
<h4 className="text-sm font-medium text-gray-900 dark:text-white">Use Custom Credentials</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">
{useCustomCredentials ? 'Using your custom OAuth credentials' : 'Using platform shared credentials'}
</p>
</div>
<button
type="button"
className={`${useCustomCredentials ? 'bg-brand-600' : 'bg-gray-200 dark:bg-gray-600'} relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500`}
role="switch"
onClick={() => setUseCustomCredentials(!useCustomCredentials)}
>
<span className={`${useCustomCredentials ? 'translate-x-4' : 'translate-x-0'} pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`} />
</button>
</div>
{useCustomCredentials && (
<div className="space-y-3">
{/* Provider credentials - collapsible */}
{(['google', 'apple', 'facebook', 'linkedin', 'microsoft', 'twitter', 'twitch'] as const).map((provider) => {
const info = providerInfo[provider];
const providerCreds = credentials[provider];
const hasCredentials = providerCreds.client_id || providerCreds.client_secret;
return (
<details key={provider} className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<summary className="flex items-center justify-between p-3 cursor-pointer bg-gray-50 dark:bg-gray-900/50 hover:bg-gray-100 dark:hover:bg-gray-900">
<div className="flex items-center gap-2">
<span className="text-lg">{info.icon}</span>
<span className="font-medium text-gray-900 dark:text-white text-sm">{info.name}</span>
{hasCredentials && (
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded">
Configured
</span>
)}
</div>
</summary>
<div className="p-3 space-y-2 border-t border-gray-200 dark:border-gray-700">
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Client ID</label>
<input
type="text"
value={providerCreds.client_id}
onChange={(e) => updateCredential(provider, 'client_id', e.target.value)}
placeholder={`Enter ${info.name} Client ID`}
className="w-full px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 text-sm"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Client Secret</label>
<div className="relative">
<input
type={showSecrets[`${provider}_secret`] ? 'text' : 'password'}
value={providerCreds.client_secret}
onChange={(e) => updateCredential(provider, 'client_secret', e.target.value)}
placeholder={`Enter ${info.name} Client Secret`}
className="w-full px-3 py-1.5 pr-8 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 text-sm"
/>
<button
type="button"
onClick={() => toggleShowSecret(`${provider}_secret`)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{showSecrets[`${provider}_secret`] ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
</div>
</div>
{provider === 'apple' && (
<>
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Team ID</label>
<input
type="text"
value={providerCreds.team_id || ''}
onChange={(e) => updateCredential(provider, 'team_id', e.target.value)}
placeholder="Enter Apple Team ID"
className="w-full px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 text-sm"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Key ID</label>
<input
type="text"
value={providerCreds.key_id || ''}
onChange={(e) => updateCredential(provider, 'key_id', e.target.value)}
placeholder="Enter Apple Key ID"
className="w-full px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 text-sm"
/>
</div>
</>
)}
{provider === 'microsoft' && (
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Tenant ID <span className="text-gray-400">(optional)</span></label>
<input
type="text"
value={providerCreds.tenant_id || ''}
onChange={(e) => updateCredential(provider, 'tenant_id', e.target.value)}
placeholder="common"
className="w-full px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 text-sm"
/>
</div>
)}
</div>
</details>
);
})}
</div>
)}
{!useCustomCredentials && (
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<div className="flex items-start gap-2">
<ShieldCheck size={16} className="text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<p className="text-xs text-blue-700 dark:text-blue-400">
Using platform credentials. Enable custom credentials above to use your own OAuth apps with your branding.
</p>
</div>
</div>
)}
</div>
)}
</section>
</>
)}
</div>
{/* Toast Notification */}
<div
className={`fixed bottom-6 right-6 transition-all duration-300 ease-in-out ${
showToast
? 'opacity-100 translate-y-0'
: 'opacity-0 translate-y-4 pointer-events-none'
}`}
>
<div className="flex items-center gap-3 px-4 py-3 bg-green-600 text-white rounded-lg shadow-lg">
<CheckCircle size={20} />
<span className="font-medium">{t('settings.savedSuccessfully')}</span>
</div>
</div>
{/* Onboarding Wizard for Stripe Connect */}
{showOnboarding && (
<OnboardingWizard
business={business}
onComplete={() => {
setShowOnboarding(false);
updateBusiness({ initialSetupComplete: true });
}}
onSkip={() => {
setShowOnboarding(false);
// If they skip, disable payments
setFormState(prev => ({ ...prev, paymentsEnabled: false }));
updateBusiness({ paymentsEnabled: false });
}}
/>
)}
{/* Lightbox Modal for Logo Preview */}
{lightboxImage && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4"
onClick={() => setLightboxImage(null)}
>
<div className="relative max-w-4xl max-h-[90vh] flex flex-col">
<div className="flex items-center justify-between mb-4 text-white">
<h3 className="text-lg font-semibold">{lightboxImage.title}</h3>
<button
onClick={() => setLightboxImage(null)}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
title="Close"
>
<X size={24} />
</button>
</div>
<div
className="bg-white dark:bg-gray-800 rounded-lg p-8 overflow-auto"
onClick={(e) => e.stopPropagation()}
>
<img
src={lightboxImage.url}
alt={lightboxImage.title}
className="max-w-full max-h-[70vh] object-contain mx-auto"
/>
</div>
<p className="text-white text-sm mt-4 text-center">
Click anywhere outside to close
</p>
</div>
</div>
)}
{/* Email Preview Modal */}
{showEmailPreview && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4"
onClick={() => setShowEmailPreview(false)}
>
<div
className="relative max-w-2xl w-full bg-white dark:bg-gray-800 rounded-lg shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">Email Preview</h3>
<button
onClick={() => setShowEmailPreview(false)}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
title="Close"
>
<X size={24} />
</button>
</div>
<div className="p-6 overflow-auto max-h-[70vh]">
{/* Email Template Preview */}
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-8" style={{ fontFamily: 'Arial, sans-serif' }}>
{/* Email Header with Logo */}
<div className="bg-white dark:bg-gray-800 rounded-t-lg p-6 text-center border-b-4" style={{ borderBottomColor: formState.primaryColor }}>
{formState.emailLogoUrl ? (
<img
src={formState.emailLogoUrl}
alt={formState.name}
className="mx-auto max-h-20 object-contain"
/>
) : (
<div className="flex items-center justify-center">
<div
className="inline-flex items-center justify-center w-16 h-16 rounded-full text-white font-bold text-2xl"
style={{ backgroundColor: formState.primaryColor }}
>
{formState.name.substring(0, 2).toUpperCase()}
</div>
</div>
)}
</div>
{/* Email Body */}
<div className="bg-white dark:bg-gray-800 p-8">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Appointment Confirmation
</h2>
<p className="text-gray-700 dark:text-gray-300 mb-4">
Hi John Doe,
</p>
<p className="text-gray-700 dark:text-gray-300 mb-6">
Your appointment has been confirmed. Here are the details:
</p>
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 mb-6">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-gray-500 dark:text-gray-400 font-medium">Service</p>
<p className="text-gray-900 dark:text-white">Haircut & Style</p>
</div>
<div>
<p className="text-gray-500 dark:text-gray-400 font-medium">Date & Time</p>
<p className="text-gray-900 dark:text-white">Dec 15, 2025 at 2:00 PM</p>
</div>
<div>
<p className="text-gray-500 dark:text-gray-400 font-medium">Duration</p>
<p className="text-gray-900 dark:text-white">60 minutes</p>
</div>
<div>
<p className="text-gray-500 dark:text-gray-400 font-medium">Price</p>
<p className="text-gray-900 dark:text-white">$45.00</p>
</div>
</div>
</div>
<button
className="w-full text-white font-semibold py-3 px-6 rounded-lg transition-colors"
style={{ backgroundColor: formState.primaryColor }}
>
View Appointment Details
</button>
<p className="text-gray-600 dark:text-gray-400 text-sm mt-6">
Need to make changes? You can reschedule or cancel up to 24 hours before your appointment.
</p>
</div>
{/* Email Footer */}
<div className="bg-gray-100 dark:bg-gray-900 rounded-b-lg p-6 text-center">
<p className="text-gray-600 dark:text-gray-400 text-sm mb-2">
{formState.name}
</p>
<p className="text-gray-500 dark:text-gray-500 text-xs">
{business.subdomain}.smoothschedule.com
</p>
<p className="text-gray-400 dark:text-gray-600 text-xs mt-4">
© 2025 {formState.name}. All rights reserved.
</p>
</div>
</div>
</div>
<div className="p-6 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 rounded-b-lg">
<p className="text-sm text-gray-600 dark:text-gray-400 text-center">
This is a preview of how your appointment confirmation emails will appear to customers.
</p>
</div>
</div>
</div>
)}
{/* Floating Action Buttons */}
<div className="fixed bottom-0 left-64 right-0 p-4 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 shadow-lg z-40 md:left-64">
<div className="max-w-4xl mx-auto flex items-center justify-between">
<div className="text-sm text-gray-600 dark:text-gray-400">
{showToast && (
<span className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircle size={16} />
Changes saved successfully
</span>
)}
</div>
<div className="flex items-center gap-3">
<button
onClick={handleCancel}
className="flex items-center gap-2 px-6 py-3 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors font-medium"
>
<X size={18} />
Cancel Changes
</button>
<button
onClick={handleSave}
className="flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors shadow-md font-medium"
>
<Save size={18} />
Save Changes
</button>
</div>
</div>
</div>
</div>
);
};
export default SettingsPage;