Files
holacompi/app/dashboard/settings/page.tsx
T
Haitham Khalifa b538d84e17 Initial commit
2026-02-16 12:18:06 +01:00

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>
);
}