307 lines
12 KiB
TypeScript
307 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { usePathname, useRouter } from 'next/navigation';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import {
|
|
ChevronDown,
|
|
LogOut,
|
|
Settings,
|
|
UserCircle,
|
|
CreditCard,
|
|
Twitter,
|
|
Linkedin,
|
|
Github,
|
|
X,
|
|
} from 'lucide-react';
|
|
import { getAuth, signOut } from 'firebase/auth';
|
|
import { app, db } from '@/lib/firebase';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import toast from 'react-hot-toast';
|
|
import { doc, onSnapshot } from 'firebase/firestore';
|
|
|
|
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
const { user } = useAuth();
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const auth = getAuth(app);
|
|
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [credits, setCredits] = useState<number | null>(null);
|
|
const [showCreditsPulse, setShowCreditsPulse] = useState(false);
|
|
const [isQuickBuyOpen, setIsQuickBuyOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!user) {
|
|
setCredits(null);
|
|
return;
|
|
}
|
|
const docRef = doc(db, 'users', user.uid);
|
|
const unsubscribe = onSnapshot(
|
|
docRef,
|
|
(snapshot) => {
|
|
const balance = snapshot.data()?.creditBalance ?? 0;
|
|
setCredits((prev) => {
|
|
if (prev !== null && prev !== balance) {
|
|
setShowCreditsPulse(true);
|
|
window.setTimeout(() => setShowCreditsPulse(false), 900);
|
|
}
|
|
return balance;
|
|
});
|
|
},
|
|
(error) => {
|
|
console.error('Failed to subscribe to credits', error);
|
|
setCredits(0);
|
|
}
|
|
);
|
|
return () => unsubscribe();
|
|
}, [user]);
|
|
|
|
const handleSignOut = async () => {
|
|
try {
|
|
await signOut(auth);
|
|
toast.success('Signed out.');
|
|
router.push('/auth');
|
|
} catch (error) {
|
|
console.error('Sign out failed', error);
|
|
toast.error('Unable to sign out. Please try again.');
|
|
}
|
|
};
|
|
|
|
const initial = user?.email?.[0]?.toUpperCase() ?? '?';
|
|
|
|
const pricingTiers = [
|
|
{ label: '10 Credits', price: '€10', value: 10 },
|
|
{ label: '50 Credits', price: '€45', value: 50 },
|
|
{ label: '100 Credits', price: '€85', value: 100 },
|
|
];
|
|
|
|
const handleQuickBuy = async (value: number, price: string) => {
|
|
if (!user) {
|
|
toast.error('Please sign in to purchase credits.');
|
|
return;
|
|
}
|
|
try {
|
|
const response = await fetch('/api/stripe/checkout', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
credits: value,
|
|
amount: Number(price.replace('€', '')),
|
|
userId: user.uid,
|
|
}),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error('Checkout failed');
|
|
}
|
|
const data = await response.json();
|
|
if (data.url) {
|
|
window.location.href = data.url;
|
|
} else {
|
|
throw new Error('Missing checkout URL');
|
|
}
|
|
} catch (error) {
|
|
console.error('Quick buy failed', error);
|
|
toast.error('Unable to start checkout.');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-[#0f0b1a] via-[#1b1330] to-[#0f1b3d] text-white flex flex-col">
|
|
<header className="border-b border-white/10 bg-white/5 backdrop-blur-sm">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
|
|
<div className="text-sm text-[#9ca3af]">Dashboard</div>
|
|
<div className="flex items-center gap-3">
|
|
<motion.button
|
|
onClick={() => setIsQuickBuyOpen(true)}
|
|
className="relative rounded-full bg-[#8b5cf6]/20 border border-[#8b5cf6]/40 px-3 py-1.5 text-xs font-semibold text-white hover:bg-[#8b5cf6]/30 transition"
|
|
animate={showCreditsPulse ? { scale: [1, 1.08, 1] } : { scale: 1 }}
|
|
transition={{ duration: 0.6 }}
|
|
>
|
|
💎 {credits ?? '—'} Credits
|
|
{showCreditsPulse && (
|
|
<span className="absolute inset-0 rounded-full bg-[#8b5cf6]/20 blur-md" />
|
|
)}
|
|
</motion.button>
|
|
<div className="relative" ref={dropdownRef}>
|
|
<button
|
|
onClick={() => setIsOpen((prev) => !prev)}
|
|
className="flex items-center gap-3 rounded-full border border-white/10 bg-white/5 px-3 py-1.5 hover:bg-white/10 transition"
|
|
>
|
|
<span className="h-9 w-9 rounded-full bg-[#8b5cf6] text-white flex items-center justify-center font-semibold">
|
|
{initial}
|
|
</span>
|
|
<ChevronDown className={`h-4 w-4 text-[#9ca3af] transition ${isOpen ? 'rotate-180' : ''}`} />
|
|
</button>
|
|
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10, scale: 0.98 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: 6, scale: 0.98 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="absolute right-0 mt-3 w-64 rounded-2xl border border-[#8b5cf6]/30 bg-[#1a1625] shadow-2xl overflow-hidden"
|
|
>
|
|
<div className="px-4 py-4 border-b border-white/10">
|
|
<div className="flex items-center gap-3">
|
|
<span className="h-10 w-10 rounded-full bg-[#8b5cf6]/30 border border-[#8b5cf6]/50 flex items-center justify-center font-semibold">
|
|
{initial}
|
|
</span>
|
|
<div>
|
|
<p className="text-sm font-semibold">Account</p>
|
|
<p className="text-xs text-[#9ca3af]">{user?.email ?? 'Unknown user'}</p>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 text-xs text-[#9ca3af]">
|
|
Credits: <span className="text-white">{credits ?? '—'}</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-2 space-y-1">
|
|
<button
|
|
onClick={() => {
|
|
setIsOpen(false);
|
|
router.push('/dashboard/settings');
|
|
}}
|
|
className="w-full flex items-center gap-2 px-3 py-2 rounded-xl text-sm text-[#9ca3af] hover:text-white hover:bg-white/5 transition"
|
|
>
|
|
<UserCircle className="h-4 w-4" />
|
|
Profile
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setIsOpen(false);
|
|
router.push('/dashboard/settings');
|
|
}}
|
|
className="w-full flex items-center gap-2 px-3 py-2 rounded-xl text-sm text-[#9ca3af] hover:text-white hover:bg-white/5 transition"
|
|
>
|
|
<Settings className="h-4 w-4" />
|
|
Settings
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setIsOpen(false);
|
|
router.push('/dashboard/credits');
|
|
}}
|
|
className="w-full flex items-center gap-2 px-3 py-2 rounded-xl text-sm text-[#9ca3af] hover:text-white hover:bg-white/5 transition"
|
|
>
|
|
<CreditCard className="h-4 w-4" />
|
|
Billing
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setIsOpen(false);
|
|
handleSignOut();
|
|
}}
|
|
className="w-full flex items-center gap-2 px-3 py-2 rounded-xl text-sm text-red-300 hover:text-red-200 hover:bg-red-500/10 transition"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
Sign Out
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="flex-1">{children}</div>
|
|
|
|
<AnimatePresence>
|
|
{isQuickBuyOpen && (
|
|
<motion.div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
onClick={() => setIsQuickBuyOpen(false)}
|
|
>
|
|
<motion.div
|
|
initial={{ scale: 0.96, opacity: 0, y: 20 }}
|
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
|
exit={{ scale: 0.96, opacity: 0, y: 10 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="w-full max-w-md rounded-3xl border border-[#8b5cf6]/30 bg-[#1a1625] p-6 shadow-2xl"
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<h3 className="text-xl font-semibold">Quick Buy Credits</h3>
|
|
<p className="text-xs text-[#9ca3af] mt-1">Choose a package to top up.</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsQuickBuyOpen(false)}
|
|
className="text-[#9ca3af] hover:text-white transition"
|
|
aria-label="Close quick buy"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
<div className="mt-5 space-y-3">
|
|
{pricingTiers.map((tier) => (
|
|
<button
|
|
key={tier.value}
|
|
onClick={() => handleQuickBuy(tier.value, tier.price)}
|
|
className="w-full flex items-center justify-between rounded-2xl border border-white/10 bg-[#0f0b1a] px-4 py-3 text-sm text-white hover:border-[#8b5cf6]/40 hover:bg-[#8b5cf6]/10 transition"
|
|
>
|
|
<span>{tier.label}</span>
|
|
<span className="text-[#9ca3af]">{tier.price}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
setIsQuickBuyOpen(false);
|
|
router.push('/dashboard/credits');
|
|
}}
|
|
className="mt-4 w-full px-4 py-2 rounded-2xl bg-[#8b5cf6] text-white font-semibold hover:bg-[#7c3aed] transition"
|
|
>
|
|
Go to Billing & Credits
|
|
</button>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
<footer className="mt-auto border-t border-white/10 bg-[#120f1f]/80 backdrop-blur-sm">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 text-center space-y-4">
|
|
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-[#9ca3af]">
|
|
<a href="#" className="hover:text-white transition">Privacy</a>
|
|
<span className="text-[#2f2544]">|</span>
|
|
<a href="#" className="hover:text-white transition">Terms</a>
|
|
<span className="text-[#2f2544]">|</span>
|
|
<a href="#" className="hover:text-white transition">Support</a>
|
|
<span className="text-[#2f2544]">|</span>
|
|
<a href="#" className="hover:text-white transition">Docs</a>
|
|
</div>
|
|
<div className="flex items-center justify-center gap-4 text-[#9ca3af]">
|
|
<a href="#" aria-label="Twitter" className="hover:text-white transition">
|
|
<Twitter className="h-4 w-4" />
|
|
</a>
|
|
<a href="#" aria-label="LinkedIn" className="hover:text-white transition">
|
|
<Linkedin className="h-4 w-4" />
|
|
</a>
|
|
<a href="#" aria-label="GitHub" className="hover:text-white transition">
|
|
<Github className="h-4 w-4" />
|
|
</a>
|
|
</div>
|
|
<p className="text-xs text-[#9ca3af]">© 2026 HolaCompi. All rights reserved.</p>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|