- Build complete Next.js CRM for commercial real estate - Add authentication with JWT sessions and role-based access - Add GoHighLevel API integration for contacts, conversations, opportunities - Add AI-powered Control Center with tool calling - Add Setup page with onboarding checklist (/setup) - Add sidebar navigation with Setup menu item - Fix type errors in onboarding API, GHL services, and control center tools - Add Prisma schema with SQLite for local development - Add UI components with clay morphism design system Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
583 lines
22 KiB
TypeScript
583 lines
22 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useMemo } from 'react';
|
|
import {
|
|
User,
|
|
Building,
|
|
Mail,
|
|
Lock,
|
|
Save,
|
|
Bell,
|
|
Camera,
|
|
RotateCcw,
|
|
Check,
|
|
X,
|
|
Eye,
|
|
EyeOff,
|
|
Shield,
|
|
AlertCircle
|
|
} from 'lucide-react';
|
|
|
|
type TabType = 'profile' | 'password' | 'notifications';
|
|
|
|
interface NotificationSettings {
|
|
emailNotifications: boolean;
|
|
dealUpdates: boolean;
|
|
weeklyDigest: boolean;
|
|
marketingEmails: boolean;
|
|
smsAlerts: boolean;
|
|
}
|
|
|
|
export default function SettingsPage() {
|
|
const [activeTab, setActiveTab] = useState<TabType>('profile');
|
|
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
|
const [showNewPassword, setShowNewPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [profileSaveStatus, setProfileSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
|
|
const [passwordSaveStatus, setPasswordSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
|
|
const [notificationSaveStatus, setNotificationSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
|
|
|
|
// Simulated current user data
|
|
const initialProfileData = {
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
email: 'john.doe@example.com',
|
|
brokerage: 'Premier Real Estate',
|
|
phone: '(555) 123-4567',
|
|
};
|
|
|
|
const [formData, setFormData] = useState({
|
|
...initialProfileData,
|
|
currentPassword: '',
|
|
newPassword: '',
|
|
confirmPassword: '',
|
|
});
|
|
|
|
const [notifications, setNotifications] = useState<NotificationSettings>({
|
|
emailNotifications: true,
|
|
dealUpdates: true,
|
|
weeklyDigest: false,
|
|
marketingEmails: false,
|
|
smsAlerts: true,
|
|
});
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const { name, value } = e.target;
|
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
|
};
|
|
|
|
const handleNotificationChange = (key: keyof NotificationSettings) => {
|
|
setNotifications((prev) => ({ ...prev, [key]: !prev[key] }));
|
|
};
|
|
|
|
const resetProfileForm = () => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
...initialProfileData,
|
|
}));
|
|
setProfileSaveStatus('idle');
|
|
};
|
|
|
|
const resetPasswordForm = () => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
currentPassword: '',
|
|
newPassword: '',
|
|
confirmPassword: '',
|
|
}));
|
|
setPasswordSaveStatus('idle');
|
|
};
|
|
|
|
const handleProfileSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setProfileSaveStatus('saving');
|
|
|
|
// Simulate API call
|
|
setTimeout(() => {
|
|
setProfileSaveStatus('success');
|
|
setTimeout(() => setProfileSaveStatus('idle'), 3000);
|
|
}, 1000);
|
|
};
|
|
|
|
const handlePasswordSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (formData.newPassword !== formData.confirmPassword) {
|
|
setPasswordSaveStatus('error');
|
|
return;
|
|
}
|
|
setPasswordSaveStatus('saving');
|
|
|
|
// Simulate API call
|
|
setTimeout(() => {
|
|
setPasswordSaveStatus('success');
|
|
resetPasswordForm();
|
|
setTimeout(() => setPasswordSaveStatus('idle'), 3000);
|
|
}, 1000);
|
|
};
|
|
|
|
const handleNotificationSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setNotificationSaveStatus('saving');
|
|
|
|
// Simulate API call
|
|
setTimeout(() => {
|
|
setNotificationSaveStatus('success');
|
|
setTimeout(() => setNotificationSaveStatus('idle'), 3000);
|
|
}, 1000);
|
|
};
|
|
|
|
// Password strength calculation
|
|
const passwordStrength = useMemo(() => {
|
|
const password = formData.newPassword;
|
|
if (!password) return { score: 0, label: '', color: '' };
|
|
|
|
let score = 0;
|
|
if (password.length >= 8) score++;
|
|
if (password.length >= 12) score++;
|
|
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++;
|
|
if (/\d/.test(password)) score++;
|
|
if (/[^a-zA-Z0-9]/.test(password)) score++;
|
|
|
|
if (score <= 1) return { score: 1, label: 'Weak', color: 'bg-red-500' };
|
|
if (score <= 2) return { score: 2, label: 'Fair', color: 'bg-orange-500' };
|
|
if (score <= 3) return { score: 3, label: 'Good', color: 'bg-yellow-500' };
|
|
if (score <= 4) return { score: 4, label: 'Strong', color: 'bg-green-500' };
|
|
return { score: 5, label: 'Very Strong', color: 'bg-emerald-500' };
|
|
}, [formData.newPassword]);
|
|
|
|
const tabs: { id: TabType; label: string; icon: React.ReactNode }[] = [
|
|
{ id: 'profile', label: 'Profile', icon: <User className="w-4 h-4" /> },
|
|
{ id: 'password', label: 'Password', icon: <Lock className="w-4 h-4" /> },
|
|
{ id: 'notifications', label: 'Notifications', icon: <Bell className="w-4 h-4" /> },
|
|
];
|
|
|
|
const SaveStatusBadge = ({ status }: { status: 'idle' | 'saving' | 'success' | 'error' }) => {
|
|
if (status === 'idle') return null;
|
|
|
|
return (
|
|
<div className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
|
status === 'saving' ? 'bg-blue-50 text-blue-700' :
|
|
status === 'success' ? 'bg-green-50 text-green-700' :
|
|
'bg-red-50 text-red-700'
|
|
}`}>
|
|
{status === 'saving' && (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-blue-700 border-t-transparent rounded-full animate-spin" />
|
|
Saving changes...
|
|
</>
|
|
)}
|
|
{status === 'success' && (
|
|
<>
|
|
<Check className="w-4 h-4" />
|
|
Changes saved successfully!
|
|
</>
|
|
)}
|
|
{status === 'error' && (
|
|
<>
|
|
<AlertCircle className="w-4 h-4" />
|
|
Failed to save. Please try again.
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8 max-w-4xl mx-auto">
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-800">Settings</h1>
|
|
<p className="text-gray-500 mt-2">Manage your account and preferences</p>
|
|
</div>
|
|
|
|
{/* Tab Navigation */}
|
|
<div className="flex flex-wrap gap-3 mb-8 p-1 bg-gray-100 rounded-xl w-fit">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`flex items-center gap-2 px-5 py-3 rounded-lg font-medium transition-all ${
|
|
activeTab === tab.id
|
|
? 'bg-white text-indigo-600 shadow-sm'
|
|
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
{tab.icon}
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Profile Tab */}
|
|
{activeTab === 'profile' && (
|
|
<div className="clay-card shadow-lg border border-border/50 p-8">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="clay-icon-btn bg-indigo-100">
|
|
<User className="w-5 h-5 text-indigo-600" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-800 mb-1">Profile Information</h2>
|
|
<p className="text-sm text-gray-500">Update your personal details and photo</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Profile Photo Upload */}
|
|
<div className="flex items-center gap-8 mb-8 pb-8 border-b border-gray-200">
|
|
<div className="relative p-2">
|
|
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-indigo-400 to-purple-500 flex items-center justify-center text-white text-2xl font-bold">
|
|
{formData.firstName.charAt(0)}{formData.lastName.charAt(0)}
|
|
</div>
|
|
<button className="absolute bottom-2 right-2 w-8 h-8 bg-white rounded-full shadow-lg flex items-center justify-center border border-gray-200 hover:bg-gray-50 transition-colors">
|
|
<Camera className="w-4 h-4 text-gray-600" />
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-gray-800">Profile Photo</h3>
|
|
<p className="text-sm text-gray-500 mb-3">JPG, PNG or GIF. Max size 2MB.</p>
|
|
<button className="text-sm text-indigo-600 hover:text-indigo-700 font-medium">
|
|
Upload new photo
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleProfileSubmit} className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<label htmlFor="firstName" className="block text-sm font-semibold text-gray-700 mb-2">
|
|
First Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="firstName"
|
|
name="firstName"
|
|
value={formData.firstName}
|
|
onChange={handleChange}
|
|
placeholder="Enter your first name"
|
|
className="clay-input h-14 py-4 border border-gray-200 w-full"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="lastName" className="block text-sm font-semibold text-gray-700 mb-2">
|
|
Last Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="lastName"
|
|
name="lastName"
|
|
value={formData.lastName}
|
|
onChange={handleChange}
|
|
placeholder="Enter your last name"
|
|
className="clay-input h-14 py-4 border border-gray-200 w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-semibold text-gray-700 mb-2">
|
|
<span className="flex items-center gap-2">
|
|
<Mail className="w-4 h-4 text-gray-400" />
|
|
Email Address
|
|
</span>
|
|
</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
name="email"
|
|
value={formData.email}
|
|
onChange={handleChange}
|
|
placeholder="Enter your email address"
|
|
className="clay-input h-14 py-4 border border-gray-200 w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="phone" className="block text-sm font-semibold text-gray-700 mb-2">
|
|
Phone Number
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
id="phone"
|
|
name="phone"
|
|
value={formData.phone}
|
|
onChange={handleChange}
|
|
placeholder="Enter your phone number"
|
|
className="clay-input h-14 py-4 border border-gray-200 w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="brokerage" className="block text-sm font-semibold text-gray-700 mb-2">
|
|
<span className="flex items-center gap-2">
|
|
<Building className="w-4 h-4 text-gray-400" />
|
|
Brokerage Name
|
|
</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="brokerage"
|
|
name="brokerage"
|
|
value={formData.brokerage}
|
|
onChange={handleChange}
|
|
placeholder="Enter your brokerage name"
|
|
className="clay-input h-14 py-4 border border-gray-200 w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 pt-6 mt-8 border-t border-gray-200">
|
|
<SaveStatusBadge status={profileSaveStatus} />
|
|
<div className="flex gap-4">
|
|
<button
|
|
type="button"
|
|
onClick={resetProfileForm}
|
|
className="clay-btn flex items-center gap-2 px-6 py-3 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-xl"
|
|
>
|
|
<RotateCcw className="w-4 h-4" />
|
|
Reset
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="clay-btn-primary flex items-center gap-2 px-6 py-3"
|
|
disabled={profileSaveStatus === 'saving'}
|
|
>
|
|
<Save className="w-4 h-4" />
|
|
Save Profile
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* Password Tab */}
|
|
{activeTab === 'password' && (
|
|
<div className="clay-card shadow-lg border border-border/50 p-8">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="clay-icon-btn bg-amber-100">
|
|
<Lock className="w-5 h-5 text-amber-600" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-800 mb-1">Change Password</h2>
|
|
<p className="text-sm text-gray-500">Update your security credentials</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Security Tips */}
|
|
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4 mb-8">
|
|
<div className="flex items-start gap-3">
|
|
<Shield className="w-5 h-5 text-blue-600 mt-0.5" />
|
|
<div>
|
|
<h4 className="font-semibold text-blue-800 text-sm">Password Security Tips</h4>
|
|
<ul className="text-sm text-blue-700 mt-1 space-y-1">
|
|
<li>Use at least 12 characters with uppercase, lowercase, numbers, and symbols</li>
|
|
<li>Avoid using personal information or common words</li>
|
|
<li>Do not reuse passwords from other accounts</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handlePasswordSubmit} className="space-y-6">
|
|
<div>
|
|
<label htmlFor="currentPassword" className="block text-sm font-semibold text-gray-700 mb-2">
|
|
Current Password
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showCurrentPassword ? 'text' : 'password'}
|
|
id="currentPassword"
|
|
name="currentPassword"
|
|
value={formData.currentPassword}
|
|
onChange={handleChange}
|
|
placeholder="Enter your current password"
|
|
className="clay-input h-14 py-4 border border-gray-200 w-full pr-12"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
>
|
|
{showCurrentPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="newPassword" className="block text-sm font-semibold text-gray-700 mb-2">
|
|
New Password
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showNewPassword ? 'text' : 'password'}
|
|
id="newPassword"
|
|
name="newPassword"
|
|
value={formData.newPassword}
|
|
onChange={handleChange}
|
|
placeholder="Enter new password"
|
|
className="clay-input h-14 py-4 border border-gray-200 w-full pr-12"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowNewPassword(!showNewPassword)}
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
>
|
|
{showNewPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Password Strength Indicator */}
|
|
{formData.newPassword && (
|
|
<div className="mt-3">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-xs text-gray-500">Password Strength</span>
|
|
<span className={`text-xs font-medium ${
|
|
passwordStrength.score <= 2 ? 'text-red-600' :
|
|
passwordStrength.score <= 3 ? 'text-yellow-600' : 'text-green-600'
|
|
}`}>
|
|
{passwordStrength.label}
|
|
</span>
|
|
</div>
|
|
<div className="flex gap-1">
|
|
{[1, 2, 3, 4, 5].map((level) => (
|
|
<div
|
|
key={level}
|
|
className={`h-1.5 flex-1 rounded-full transition-colors ${
|
|
level <= passwordStrength.score ? passwordStrength.color : 'bg-gray-200'
|
|
}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="confirmPassword" className="block text-sm font-semibold text-gray-700 mb-2">
|
|
Confirm New Password
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showConfirmPassword ? 'text' : 'password'}
|
|
id="confirmPassword"
|
|
name="confirmPassword"
|
|
value={formData.confirmPassword}
|
|
onChange={handleChange}
|
|
placeholder="Confirm new password"
|
|
className={`clay-input h-14 py-4 border w-full pr-12 ${
|
|
formData.confirmPassword && formData.newPassword !== formData.confirmPassword
|
|
? 'border-red-300 focus:border-red-500'
|
|
: 'border-gray-200'
|
|
}`}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
>
|
|
{showConfirmPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
|
</button>
|
|
</div>
|
|
{formData.confirmPassword && formData.newPassword !== formData.confirmPassword && (
|
|
<p className="text-red-500 text-sm mt-2 flex items-center gap-1">
|
|
<X className="w-4 h-4" />
|
|
Passwords do not match
|
|
</p>
|
|
)}
|
|
{formData.confirmPassword && formData.newPassword === formData.confirmPassword && formData.newPassword && (
|
|
<p className="text-green-500 text-sm mt-2 flex items-center gap-1">
|
|
<Check className="w-4 h-4" />
|
|
Passwords match
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 pt-6 mt-8 border-t border-gray-200">
|
|
<SaveStatusBadge status={passwordSaveStatus} />
|
|
<div className="flex gap-4">
|
|
<button
|
|
type="button"
|
|
onClick={resetPasswordForm}
|
|
className="clay-btn flex items-center gap-2 px-6 py-3 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-xl"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="clay-btn-primary flex items-center gap-2 px-6 py-3"
|
|
disabled={passwordSaveStatus === 'saving' || !formData.currentPassword || !formData.newPassword || formData.newPassword !== formData.confirmPassword}
|
|
>
|
|
<Lock className="w-4 h-4" />
|
|
Update Password
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* Notifications Tab */}
|
|
{activeTab === 'notifications' && (
|
|
<div className="clay-card shadow-lg border border-border/50 p-8">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="clay-icon-btn bg-purple-100">
|
|
<Bell className="w-5 h-5 text-purple-600" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-800 mb-1">Notification Preferences</h2>
|
|
<p className="text-sm text-gray-500">Choose how you want to be notified</p>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleNotificationSubmit} className="space-y-2">
|
|
{[
|
|
{ key: 'emailNotifications', label: 'Email Notifications', description: 'Receive important updates via email' },
|
|
{ key: 'dealUpdates', label: 'Deal Updates', description: 'Get notified when deals change status' },
|
|
{ key: 'weeklyDigest', label: 'Weekly Digest', description: 'Receive a summary of your activity each week' },
|
|
{ key: 'marketingEmails', label: 'Marketing Communications', description: 'Stay updated on new features and tips' },
|
|
{ key: 'smsAlerts', label: 'SMS Alerts', description: 'Receive text messages for urgent notifications' },
|
|
].map((item) => (
|
|
<div
|
|
key={item.key}
|
|
className="flex items-center justify-between py-5 px-4 gap-6 rounded-xl hover:bg-gray-50 transition-colors"
|
|
>
|
|
<div>
|
|
<h4 className="font-medium text-gray-800 mb-1">{item.label}</h4>
|
|
<p className="text-sm text-gray-500">{item.description}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleNotificationChange(item.key as keyof NotificationSettings)}
|
|
className={`relative w-12 h-7 rounded-full transition-colors flex-shrink-0 ${
|
|
notifications[item.key as keyof NotificationSettings]
|
|
? 'bg-indigo-600'
|
|
: 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`absolute top-1 w-5 h-5 bg-white rounded-full shadow transition-transform ${
|
|
notifications[item.key as keyof NotificationSettings]
|
|
? 'translate-x-6'
|
|
: 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
))}
|
|
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 pt-6 mt-8 border-t border-gray-200">
|
|
<SaveStatusBadge status={notificationSaveStatus} />
|
|
<button
|
|
type="submit"
|
|
className="clay-btn-primary flex items-center gap-2 px-6 py-3"
|
|
disabled={notificationSaveStatus === 'saving'}
|
|
>
|
|
<Save className="w-4 h-4" />
|
|
Save Preferences
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|