370 lines
14 KiB
TypeScript
370 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { usePathname, useRouter } from 'next/navigation';
|
|
import { motion } from 'framer-motion';
|
|
import { z } from 'zod';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import Link from 'next/link';
|
|
import {
|
|
Bell,
|
|
CreditCard,
|
|
Lock,
|
|
Mail,
|
|
Phone,
|
|
ShieldCheck,
|
|
SlidersHorizontal,
|
|
User,
|
|
} from 'lucide-react';
|
|
import { getAuth, sendPasswordResetEmail, updateProfile } from 'firebase/auth';
|
|
import { app } from '@/lib/firebase';
|
|
import { getPurchasesByUser, updateUserOnboardingCompleted } from '@/lib/firestore';
|
|
import toast from 'react-hot-toast';
|
|
|
|
const profileSchema = z.object({
|
|
displayName: z.string().trim().min(2, 'Display name must be at least 2 characters.'),
|
|
phone: z
|
|
.string()
|
|
.trim()
|
|
.optional()
|
|
.transform((value) => (value ? value : undefined))
|
|
.refine(
|
|
(value) => !value || /^\+?[0-9\s\-()]{7,20}$/.test(value),
|
|
'Enter a valid phone number.'
|
|
),
|
|
});
|
|
|
|
const containerVariants = {
|
|
hidden: { opacity: 0 },
|
|
show: { opacity: 1, transition: { staggerChildren: 0.08 } },
|
|
};
|
|
|
|
const cardVariants = {
|
|
hidden: { opacity: 0, y: 14 },
|
|
show: { opacity: 1, y: 0, transition: { duration: 0.4 } },
|
|
};
|
|
|
|
export default function SettingsPage() {
|
|
const { user, isLoading } = useAuth();
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const auth = getAuth(app);
|
|
const firebaseUser = auth.currentUser;
|
|
|
|
const [profileForm, setProfileForm] = useState({
|
|
displayName: user?.name || '',
|
|
phone: user?.phoneNumber || '',
|
|
});
|
|
const [profileErrors, setProfileErrors] = useState<{ displayName?: string; phone?: string }>(
|
|
{}
|
|
);
|
|
const [isSavingProfile, setIsSavingProfile] = useState(false);
|
|
const [resetStatus, setResetStatus] = useState<string | null>(null);
|
|
|
|
const [emailNotifications, setEmailNotifications] = useState(true);
|
|
const [callAlerts, setCallAlerts] = useState(true);
|
|
const [weeklyReports, setWeeklyReports] = useState(false);
|
|
|
|
const [totalCreditsPurchased, setTotalCreditsPurchased] = useState<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && !user) {
|
|
router.push('/auth');
|
|
}
|
|
}, [user, isLoading, router]);
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
getPurchasesByUser(user.uid)
|
|
.then((purchases) => {
|
|
const total = purchases.reduce((sum, purchase) => sum + (purchase.credits || 0), 0);
|
|
setTotalCreditsPurchased(total);
|
|
})
|
|
.catch((error) => {
|
|
console.error('Failed to load purchases', error);
|
|
toast.error('Unable to load purchases.');
|
|
setTotalCreditsPurchased(0);
|
|
});
|
|
}
|
|
}, [user]);
|
|
|
|
const creationDate = useMemo(() => {
|
|
const createdAt = firebaseUser?.metadata?.creationTime;
|
|
if (!createdAt) return 'Unknown';
|
|
return new Date(createdAt).toLocaleDateString();
|
|
}, [firebaseUser]);
|
|
|
|
const handleProfileChange = (key: 'displayName' | 'phone', value: string) => {
|
|
setProfileForm((prev) => ({ ...prev, [key]: value }));
|
|
setProfileErrors((prev) => ({ ...prev, [key]: undefined }));
|
|
};
|
|
|
|
const handleSaveProfile = async () => {
|
|
if (!firebaseUser) return;
|
|
const result = profileSchema.safeParse(profileForm);
|
|
if (!result.success) {
|
|
const errors: { displayName?: string; phone?: string } = {};
|
|
result.error.issues.forEach((issue) => {
|
|
const field = issue.path[0] as 'displayName' | 'phone';
|
|
errors[field] = issue.message;
|
|
});
|
|
setProfileErrors(errors);
|
|
return;
|
|
}
|
|
|
|
setIsSavingProfile(true);
|
|
try {
|
|
await updateProfile(firebaseUser, { displayName: result.data.displayName.trim() });
|
|
setProfileForm((prev) => ({ ...prev, displayName: result.data.displayName.trim() }));
|
|
toast.success('Profile updated.');
|
|
} catch (error) {
|
|
console.error('Error updating profile', error);
|
|
setProfileErrors({ displayName: 'Failed to update profile. Try again.' });
|
|
toast.error('Unable to update profile.');
|
|
} finally {
|
|
setIsSavingProfile(false);
|
|
}
|
|
};
|
|
|
|
const handlePasswordReset = async () => {
|
|
if (!firebaseUser?.email) return;
|
|
setResetStatus(null);
|
|
try {
|
|
await sendPasswordResetEmail(auth, firebaseUser.email);
|
|
setResetStatus('Password reset email sent.');
|
|
toast.success('Password reset email sent.');
|
|
} catch (error) {
|
|
console.error('Password reset failed', error);
|
|
setResetStatus('Unable to send reset email. Try again.');
|
|
toast.error('Unable to send reset email.');
|
|
}
|
|
};
|
|
|
|
const handleRestartTour = async () => {
|
|
if (!user) return;
|
|
try {
|
|
await updateUserOnboardingCompleted(user.uid, false);
|
|
toast.success('Tour restarted. Redirecting...');
|
|
router.push('/dashboard');
|
|
} catch (error) {
|
|
console.error('Failed to restart tour', error);
|
|
toast.error('Unable to restart tour.');
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#0f0b1a] via-[#1b1330] to-[#0f1b3d] text-white">
|
|
<div className="animate-pulse text-lg">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!user) return null;
|
|
|
|
const navLinks = [
|
|
{ label: 'Dashboard', href: '/dashboard' },
|
|
{ label: 'Scheduled Calls', href: '/dashboard/scheduled-calls' },
|
|
{ label: 'Voice Agent', href: '/dashboard/agent-settings' },
|
|
{ label: 'Credits', href: '/dashboard/credits' },
|
|
{ label: 'Notifications', href: '/dashboard/notifications' },
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-[#0f0b1a] via-[#1b1330] to-[#0f1b3d] text-white px-6 py-10">
|
|
<motion.div
|
|
className="max-w-5xl mx-auto space-y-6"
|
|
variants={containerVariants}
|
|
initial="hidden"
|
|
animate="show"
|
|
>
|
|
<motion.header
|
|
variants={cardVariants}
|
|
className="flex flex-wrap items-center justify-between gap-4"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="h-10 w-10 rounded-2xl bg-[#8b5cf6]/20 border border-[#8b5cf6]/40 flex items-center justify-center">
|
|
<SlidersHorizontal className="h-5 w-5 text-[#8b5cf6]" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-2xl font-semibold">Settings</h1>
|
|
<p className="text-xs text-[#9ca3af]">Manage preferences and account details</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{navLinks.map((link) => {
|
|
const isActive = pathname === link.href;
|
|
return (
|
|
<Link
|
|
key={link.href}
|
|
href={link.href}
|
|
className={`px-3 py-1.5 rounded-full border text-xs transition ${
|
|
isActive
|
|
? 'bg-[#8b5cf6]/20 border-[#8b5cf6]/40 text-white'
|
|
: 'bg-white/5 border-white/10 text-[#9ca3af] hover:text-white'
|
|
}`}
|
|
>
|
|
{link.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</motion.header>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<motion.div variants={cardVariants} className="lg:col-span-2 space-y-6">
|
|
<div className="bg-[#1a1625] border border-white/10 rounded-3xl p-6 shadow-2xl space-y-5">
|
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
<User className="h-5 w-5 text-[#8b5cf6]" />
|
|
User Profile
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="bg-[#0f0b1a] border border-white/10 rounded-2xl p-4">
|
|
<p className="text-xs uppercase text-[#9ca3af] flex items-center gap-2">
|
|
<Mail className="h-4 w-4" />
|
|
Email
|
|
</p>
|
|
<p className="text-sm mt-2">{user.email}</p>
|
|
</div>
|
|
<div className="bg-[#0f0b1a] border border-white/10 rounded-2xl p-4">
|
|
<p className="text-xs uppercase text-[#9ca3af] flex items-center gap-2">
|
|
<User className="h-4 w-4" />
|
|
Display Name
|
|
</p>
|
|
<input
|
|
value={profileForm.displayName}
|
|
onChange={(event) => handleProfileChange('displayName', event.target.value)}
|
|
className="mt-2 w-full rounded-xl bg-transparent border border-white/10 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]"
|
|
placeholder="Your name"
|
|
/>
|
|
{profileErrors.displayName && (
|
|
<p className="text-xs text-red-300 mt-2">{profileErrors.displayName}</p>
|
|
)}
|
|
</div>
|
|
<div className="bg-[#0f0b1a] border border-white/10 rounded-2xl p-4 md:col-span-2">
|
|
<p className="text-xs uppercase text-[#9ca3af] flex items-center gap-2">
|
|
<Phone className="h-4 w-4" />
|
|
Phone
|
|
</p>
|
|
<input
|
|
value={profileForm.phone}
|
|
onChange={(event) => handleProfileChange('phone', event.target.value)}
|
|
className="mt-2 w-full rounded-xl bg-transparent border border-white/10 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]"
|
|
placeholder="+34 600 000 000"
|
|
/>
|
|
{profileErrors.phone && (
|
|
<p className="text-xs text-red-300 mt-2">{profileErrors.phone}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={handleSaveProfile}
|
|
disabled={isSavingProfile}
|
|
className="px-4 py-2 rounded-2xl bg-[#8b5cf6] text-white font-semibold shadow-2xl hover:bg-[#7c3aed] transition disabled:opacity-60"
|
|
>
|
|
{isSavingProfile ? 'Saving...' : 'Save Profile'}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-[#1a1625] border border-white/10 rounded-3xl p-6 shadow-2xl space-y-5">
|
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
<ShieldCheck className="h-5 w-5 text-[#8b5cf6]" />
|
|
Security
|
|
</h2>
|
|
<p className="text-sm text-[#9ca3af]">
|
|
Reset your password to keep your account secure.
|
|
</p>
|
|
<button
|
|
onClick={handlePasswordReset}
|
|
className="w-full md:w-auto px-4 py-2 rounded-2xl bg-white/5 border border-white/10 text-white hover:bg-white/10 transition flex items-center gap-2"
|
|
>
|
|
<Lock className="h-4 w-4" />
|
|
Change Password
|
|
</button>
|
|
{resetStatus && <p className="text-xs text-[#9ca3af]">{resetStatus}</p>}
|
|
</div>
|
|
|
|
<div className="bg-[#1a1625] border border-white/10 rounded-3xl p-6 shadow-2xl space-y-5">
|
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
<Bell className="h-5 w-5 text-[#8b5cf6]" />
|
|
Notification Preferences
|
|
</h2>
|
|
{[
|
|
{
|
|
key: 'email',
|
|
label: 'Email notifications',
|
|
value: emailNotifications,
|
|
setter: setEmailNotifications,
|
|
},
|
|
{
|
|
key: 'calls',
|
|
label: 'Call alerts',
|
|
value: callAlerts,
|
|
setter: setCallAlerts,
|
|
},
|
|
{
|
|
key: 'reports',
|
|
label: 'Weekly usage reports',
|
|
value: weeklyReports,
|
|
setter: setWeeklyReports,
|
|
},
|
|
].map((item) => (
|
|
<div key={item.key} className="flex items-center justify-between">
|
|
<p className="text-sm">{item.label}</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => item.setter(!item.value)}
|
|
className={`h-6 w-12 rounded-full transition flex items-center ${
|
|
item.value ? 'bg-[#8b5cf6]' : 'bg-white/10'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`h-5 w-5 rounded-full bg-white shadow transform transition ${
|
|
item.value ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
))}
|
|
<p className="text-xs text-[#9ca3af]">
|
|
Preferences save locally for now. TODO: persist in Firestore.
|
|
</p>
|
|
</div>
|
|
</motion.div>
|
|
|
|
<motion.div variants={cardVariants} className="space-y-6">
|
|
<div className="bg-[#1a1625] border border-white/10 rounded-3xl p-6 shadow-2xl space-y-4">
|
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
<CreditCard className="h-5 w-5 text-[#8b5cf6]" />
|
|
Account Info
|
|
</h2>
|
|
<div className="bg-[#0f0b1a] border border-white/10 rounded-2xl p-4 space-y-2">
|
|
<p className="text-xs uppercase text-[#9ca3af]">Member Since</p>
|
|
<p className="text-sm">{creationDate}</p>
|
|
</div>
|
|
<div className="bg-[#0f0b1a] border border-white/10 rounded-2xl p-4 space-y-2">
|
|
<p className="text-xs uppercase text-[#9ca3af]">Total Credits Purchased</p>
|
|
<p className="text-sm">
|
|
{totalCreditsPurchased === null ? 'Loading…' : totalCreditsPurchased}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => router.push('/dashboard/credits')}
|
|
className="w-full px-4 py-2 rounded-2xl bg-[#8b5cf6] text-white font-semibold shadow-2xl hover:bg-[#7c3aed] transition"
|
|
>
|
|
Go to Billing & Credits
|
|
</button>
|
|
<button
|
|
onClick={handleRestartTour}
|
|
className="w-full px-4 py-2 rounded-2xl bg-white/5 border border-white/10 text-white hover:bg-white/10 transition"
|
|
>
|
|
Restart Tour
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|