Files
Haitham Khalifa b538d84e17 Initial commit
2026-02-16 12:18:06 +01:00

980 lines
38 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useAuth } from '@/contexts/AuthContext';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import Joyride, { CallBackProps, STATUS, EVENTS } from 'react-joyride';
import {
Bell,
CalendarClock,
CreditCard,
Gauge,
MessageSquare,
Phone,
Clock,
TrendingUp,
Coins,
Settings,
Sparkles,
Timer,
X,
Search,
Download,
ChevronLeft,
ChevronRight,
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import {
ResponsiveContainer,
LineChart,
Line,
XAxis,
YAxis,
Tooltip,
CartesianGrid,
} from 'recharts';
import toast from 'react-hot-toast';
import { getUser, updateUserOnboardingCompleted } from '@/lib/firestore';
interface Call {
id: number;
contactName: string;
date: string;
duration: string;
status: 'Completed' | 'Failed';
credits: number;
}
interface AnalyticsData {
totalCalls: number;
totalMinutes: number;
avgDuration: number;
creditsRemaining: number;
}
interface CallAnalyticsPoint {
day: string;
calls: number;
minutes: number;
}
export default function DashboardPage() {
const { user, isLoading, logout } = useAuth();
const router = useRouter();
const pathname = usePathname();
const [creditsRemaining, setCreditsRemaining] = useState<number | null>(null);
const [isStatsLoading, setIsStatsLoading] = useState(true);
const [isLowCreditsDismissed, setIsLowCreditsDismissed] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [dateRange, setDateRange] = useState<'7' | '30' | 'custom'>('7');
const [customStart, setCustomStart] = useState('');
const [customEnd, setCustomEnd] = useState('');
const [sortKey, setSortKey] = useState<'date' | 'duration' | 'status'>('date');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [currentPage, setCurrentPage] = useState(1);
const [selectedCall, setSelectedCall] = useState<Call | null>(null);
const [runTour, setRunTour] = useState(false);
const [tourStepIndex, setTourStepIndex] = useState(0);
const tourSteps = useMemo(
() => [
{
target: '[data-tour="welcome"]',
content: 'Welcome to HolaCompi! Here is your dashboard overview.',
placement: 'bottom',
},
{
target: '[data-tour="buy-credits"]',
content: 'Buy credits to power your calls whenever you need.',
placement: 'left',
},
{
target: '[data-tour="make-call"]',
content: 'Schedule a call to reach clients instantly.',
placement: 'top',
},
{
target: '[data-tour="view-settings"]',
content: 'Adjust preferences and manage your account settings here.',
placement: 'bottom',
},
],
[]
);
const mockChartData: CallAnalyticsPoint[] = [
{ day: 'Mon', calls: 8, minutes: 24 },
{ day: 'Tue', calls: 12, minutes: 38 },
{ day: 'Wed', calls: 6, minutes: 19 },
{ day: 'Thu', calls: 10, minutes: 31 },
{ day: 'Fri', calls: 7, minutes: 22 },
{ day: 'Sat', calls: 2, minutes: 12 },
{ day: 'Sun', calls: 2, minutes: 10 },
];
const mockRecentCalls: Call[] = [
{
id: 1,
contactName: 'Lucia Morales',
date: '2026-01-21 18:30',
duration: '4m 15s',
status: 'Completed',
credits: 4,
},
{
id: 2,
contactName: 'Clinic Valencia',
date: '2026-01-21 14:20',
duration: '2m 45s',
status: 'Completed',
credits: 3,
},
{
id: 3,
contactName: 'Miguel Ramos',
date: '2026-01-20 16:10',
duration: '5m 32s',
status: 'Completed',
credits: 6,
},
{
id: 4,
contactName: 'Barcelona Dental',
date: '2026-01-20 11:45',
duration: '1m 23s',
status: 'Failed',
credits: 0,
},
{
id: 5,
contactName: 'Sofia Ortega',
date: '2026-01-19 09:15',
duration: '3m 58s',
status: 'Completed',
credits: 4,
},
];
const analyticsData: AnalyticsData = useMemo(
() => ({
totalCalls: 47,
totalMinutes: 156,
avgDuration: 3.3,
creditsRemaining: creditsRemaining ?? 0,
}),
[creditsRemaining]
);
const fadeInUp = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0 },
};
const slideUp = {
hidden: { opacity: 0, y: 24 },
visible: { opacity: 1, y: 0 },
};
const parseCallDate = (value: string) => new Date(value.replace(' ', 'T'));
const durationToSeconds = (value: string) => {
const match = value.match(/(\d+)m\s*(\d+)s/);
if (!match) return 0;
return Number(match[1]) * 60 + Number(match[2]);
};
useEffect(() => {
if (!isLoading && !user) {
router.push('/auth');
}
}, [user, isLoading, router]);
useEffect(() => {
const fetchCredits = async () => {
setIsStatsLoading(true);
try {
const response = await fetch('/api/credits');
if (!response.ok) {
toast.error('Unable to load credits. Please try again.');
setCreditsRemaining(0);
return;
}
const data = await response.json();
setCreditsRemaining(data.balance ?? 0);
} catch (error) {
console.error('Failed to load credits', error);
toast.error('Unable to load credits. Please check your connection.');
setCreditsRemaining(0);
} finally {
setIsStatsLoading(false);
}
};
if (user) {
fetchCredits();
}
}, [user]);
useEffect(() => {
if (typeof window === 'undefined') return;
const dismissed = window.localStorage.getItem('lowCreditsBannerDismissed');
setIsLowCreditsDismissed(dismissed === 'true');
}, []);
useEffect(() => {
let isActive = true;
const loadOnboardingStatus = async () => {
if (!user) return;
try {
const userDoc = await getUser(user.uid);
const completed = userDoc?.onboardingCompleted ?? false;
if (!completed && isActive) {
setRunTour(true);
setTourStepIndex(0);
}
} catch (error) {
console.error('Failed to load onboarding status', error);
}
};
loadOnboardingStatus();
return () => {
isActive = false;
};
}, [user]);
const handleDismissLowCredits = () => {
setIsLowCreditsDismissed(true);
if (typeof window !== 'undefined') {
window.localStorage.setItem('lowCreditsBannerDismissed', 'true');
}
};
useEffect(() => {
setCurrentPage(1);
}, [searchTerm, dateRange, customStart, customEnd, sortKey, sortDirection]);
const filteredCalls = useMemo(() => {
const normalizedSearch = searchTerm.trim().toLowerCase();
const now = new Date();
let startDate: Date | null = null;
let endDate: Date | null = null;
if (dateRange === '7') {
startDate = new Date(now);
startDate.setDate(now.getDate() - 7);
} else if (dateRange === '30') {
startDate = new Date(now);
startDate.setDate(now.getDate() - 30);
} else if (dateRange === 'custom') {
startDate = customStart ? new Date(customStart) : null;
endDate = customEnd ? new Date(customEnd) : null;
if (endDate) {
endDate.setHours(23, 59, 59, 999);
}
}
return mockRecentCalls.filter((call) => {
const matchesSearch =
!normalizedSearch ||
call.contactName.toLowerCase().includes(normalizedSearch);
const callDate = parseCallDate(call.date);
const afterStart = !startDate || callDate >= startDate;
const beforeEnd = !endDate || callDate <= endDate;
return matchesSearch && afterStart && beforeEnd;
});
}, [searchTerm, dateRange, customStart, customEnd, mockRecentCalls]);
const sortedCalls = useMemo(() => {
const sorted = [...filteredCalls];
sorted.sort((a, b) => {
let comparison = 0;
if (sortKey === 'date') {
comparison = parseCallDate(a.date).getTime() - parseCallDate(b.date).getTime();
} else if (sortKey === 'duration') {
comparison = durationToSeconds(a.duration) - durationToSeconds(b.duration);
} else {
comparison = a.status.localeCompare(b.status);
}
return sortDirection === 'asc' ? comparison : -comparison;
});
return sorted;
}, [filteredCalls, sortKey, sortDirection]);
const callsPerPage = 10;
const totalPages = Math.max(1, Math.ceil(sortedCalls.length / callsPerPage));
const paginatedCalls = sortedCalls.slice(
(currentPage - 1) * callsPerPage,
currentPage * callsPerPage
);
const handleSort = (key: 'date' | 'duration' | 'status') => {
if (sortKey === key) {
setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc'));
return;
}
setSortKey(key);
setSortDirection('desc');
};
const handleExportCsv = () => {
if (sortedCalls.length === 0) {
toast.error('No calls available to export.');
return;
}
const rows = [
['Contact', 'Date', 'Duration', 'Status', 'Credits'],
...sortedCalls.map((call) => [
call.contactName,
call.date,
call.duration,
call.status,
call.credits.toString(),
]),
];
const csvContent = rows
.map((row) => row.map((value) => `"${value.replace(/"/g, '""')}"`).join(','))
.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'recent-calls.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const handleJoyride = async (data: CallBackProps) => {
const { status, index, type } = data;
if (type === EVENTS.STEP_AFTER) {
setTourStepIndex(index + 1);
}
if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
setRunTour(false);
setTourStepIndex(0);
if (user) {
try {
await updateUserOnboardingCompleted(user.uid, true);
} catch (error) {
console.error('Failed to save onboarding status', error);
}
}
}
};
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="text-center">
<div className="animate-pulse text-2xl">Loading...</div>
</div>
</div>
);
}
if (!user) {
return null;
}
const handleLogout = async () => {
try {
await logout();
router.push('/auth');
toast.success('Signed out successfully.');
} catch (error) {
console.error('Logout failed', error);
toast.error('Unable to log out. Please try again.');
}
};
const navLinks = [
{ label: 'Scheduled Calls', href: '/dashboard/scheduled-calls', icon: CalendarClock },
{ label: 'Voice Agent', href: '/dashboard/agent-settings', icon: Sparkles },
{ label: 'Credits & Limits', href: '/dashboard/credits', icon: CreditCard },
{ label: 'Notifications', href: '/dashboard/notifications', icon: Bell },
{ label: 'Chat', href: '/dashboard/chat', icon: MessageSquare },
{ label: 'Settings', href: '/dashboard/settings', icon: Settings },
];
return (
<div className="min-h-screen bg-gradient-to-br from-[#0f0b1a] via-[#1b1330] to-[#0f1b3d] text-white">
<Joyride
steps={tourSteps}
run={runTour}
stepIndex={tourStepIndex}
continuous
showSkipButton
scrollToFirstStep
disableOverlayClose
callback={handleJoyride}
locale={{ skip: 'Skip Tour', next: 'Next', last: 'Done' }}
styles={{
options: {
primaryColor: '#8b5cf6',
backgroundColor: '#1a1625',
textColor: '#ffffff',
overlayColor: 'rgba(15, 11, 26, 0.65)',
arrowColor: '#1a1625',
zIndex: 60,
},
tooltip: {
borderRadius: 16,
border: '1px solid rgba(139,92,246,0.35)',
},
buttonNext: {
backgroundColor: '#8b5cf6',
color: '#fff',
},
buttonSkip: {
color: '#c7c5d1',
},
}}
/>
<nav 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">
<div className="flex flex-wrap items-center justify-between gap-4 py-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">
<Sparkles className="h-5 w-5 text-[#8b5cf6]" />
</div>
<div>
<h1 className="text-2xl font-bold">HolaCompi</h1>
<p className="text-xs text-[#9ca3af]">
Your AI voice assistant dashboard
</p>
</div>
</div>
<button
onClick={handleLogout}
className="px-4 py-2 rounded-xl bg-red-500/10 hover:bg-red-500/20 text-red-300 border border-red-500/20 transition-colors"
>
Logout
</button>
</div>
<div className="flex flex-wrap gap-3 pb-4">
{navLinks.map((item) => {
const isActive = pathname === item.href;
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-2 px-4 py-2 rounded-full border text-sm transition ${
isActive
? 'bg-[#8b5cf6]/20 border-[#8b5cf6]/50 text-white shadow-lg'
: 'bg-white/5 border-white/10 text-[#9ca3af] hover:text-white hover:bg-white/10'
}`}
data-tour={item.label === 'Settings' ? 'view-settings' : undefined}
>
<Icon className="h-4 w-4" />
{item.label}
</Link>
);
})}
</div>
</div>
</nav>
{!isLowCreditsDismissed && creditsRemaining !== null && creditsRemaining < 10 && (
<motion.div
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.3 }}
className="sticky top-[72px] z-40"
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mt-4 rounded-2xl border border-yellow-400/40 bg-yellow-500/10 px-4 py-3 text-sm text-yellow-100 shadow-lg backdrop-blur-xl flex flex-wrap items-center justify-between gap-3">
<div>
Running low on credits! You have {creditsRemaining} credits remaining.
</div>
<div className="flex items-center gap-3">
<button
onClick={() => router.push('/dashboard/credits')}
className="px-3 py-1.5 rounded-full bg-[#8b5cf6] text-white text-xs font-semibold hover:bg-[#7c3aed] transition"
>
Buy More Credits
</button>
<button
onClick={handleDismissLowCredits}
className="text-yellow-100 hover:text-white transition"
aria-label="Dismiss low credits warning"
>
<X className="h-4 w-4" />
</button>
</div>
</div>
</div>
</motion.div>
)}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="space-y-10">
<motion.div
className="bg-[#1a1625] rounded-3xl border border-[#8b5cf6]/20 p-8 shadow-lg transition-transform hover:scale-[1.01]"
variants={fadeInUp}
initial="hidden"
animate="visible"
data-tour="welcome"
>
<h2 className="text-3xl font-semibold mb-3">
Welcome back, {user.name || user.email}! 👋
</h2>
<p className="text-[#9ca3af]">
Schedule calls, monitor credits, and tune your voice agent.
</p>
</motion.div>
<section className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-xl font-semibold flex items-center gap-2">
<Gauge className="h-5 w-5 text-[#8b5cf6]" />
Usage Overview
</h3>
<button
onClick={() => router.push('/dashboard/credits')}
className="text-xs text-[#9ca3af] hover:text-white transition animate-pulse"
data-tour="buy-credits"
>
View credits
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{(isStatsLoading ? new Array(4).fill(null) : [
{
label: 'Total Calls Made',
value: `${analyticsData.totalCalls} calls`,
icon: Phone,
gradient: 'from-[#8b5cf6]/30 to-[#4f46e5]/30',
},
{
label: 'Total Minutes Used',
value: `${analyticsData.totalMinutes} minutes`,
icon: Clock,
gradient: 'from-[#4f46e5]/30 to-[#3b82f6]/30',
},
{
label: 'Average Call Duration',
value: `${analyticsData.avgDuration} minutes`,
icon: TrendingUp,
gradient: 'from-[#8b5cf6]/30 to-[#ec4899]/30',
},
{
label: 'Credits Remaining',
value:
creditsRemaining === null
? 'Loading...'
: `${analyticsData.creditsRemaining} credits`,
icon: Coins,
gradient: 'from-[#10b981]/20 to-[#8b5cf6]/30',
},
]).map((card, index) => {
if (isStatsLoading) {
return (
<div
key={`skeleton-${index}`}
className="bg-white/5 border border-white/10 rounded-2xl p-5 shadow-lg backdrop-blur-xl animate-pulse"
>
<div className="h-12 w-12 rounded-2xl bg-white/10 border border-white/10" />
<div className="mt-4 h-3 w-24 rounded-full bg-white/10" />
<div className="mt-3 h-6 w-32 rounded-full bg-white/10" />
</div>
);
}
const Icon = card.icon;
const action =
card.label === 'Total Calls Made'
? () => {
const section = document.getElementById('call-history');
section?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
: card.label === 'Credits Remaining'
? () => router.push('/dashboard/credits')
: null;
const isClickable = Boolean(action);
return (
<motion.div
key={card.label}
className={`group relative bg-white/5 border border-white/10 rounded-2xl p-5 shadow-lg backdrop-blur-xl transition hover:shadow-2xl ${
isClickable ? 'cursor-pointer' : ''
}`}
variants={fadeInUp}
initial="hidden"
animate="visible"
transition={{ delay: index * 0.08, duration: 0.4, ease: 'easeOut' }}
whileHover={{ scale: 1.05 }}
onClick={action ?? undefined}
>
<div className="pointer-events-none absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition">
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-[#8b5cf6]/40 to-[#4f46e5]/30 blur-lg" />
<div className="absolute inset-0 rounded-2xl border border-[#8b5cf6]/50" />
</div>
<div
className={`relative rounded-2xl p-3 bg-gradient-to-r ${card.gradient} border border-white/10`}
>
<Icon className="h-5 w-5 text-white" />
</div>
<p className="text-xs uppercase text-[#9ca3af] mt-4">{card.label}</p>
<p className="text-2xl font-semibold mt-2">{card.value}</p>
<button
type="button"
onClick={(event) => {
event.stopPropagation();
action?.();
}}
disabled={!isClickable}
className={`mt-4 text-xs font-semibold text-[#c4b5fd] transition-all ${
isClickable ? 'group-hover:text-white' : 'text-[#6b7280]'
} opacity-0 group-hover:opacity-100 translate-y-1 group-hover:translate-y-0`}
>
View Details
</button>
</motion.div>
);
})}
</div>
</section>
<section className="grid grid-cols-1 lg:grid-cols-5 gap-6">
<motion.div
className="lg:col-span-3 bg-[#1a1625] border border-white/10 rounded-3xl p-6 shadow-lg backdrop-blur-xl transition-transform hover:scale-[1.01]"
variants={slideUp}
initial="hidden"
animate="visible"
transition={{ duration: 0.45, ease: 'easeOut' }}
>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold">Weekly Usage</h3>
<p className="text-xs text-[#9ca3af]">Last 7 days</p>
</div>
<div className="flex items-center gap-2 text-xs text-[#9ca3af]">
<Phone className="h-4 w-4 text-[#8b5cf6]" />
Calls & Minutes
</div>
</div>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={mockChartData}>
<CartesianGrid stroke="rgba(255,255,255,0.08)" strokeDasharray="4 4" />
<XAxis dataKey="day" stroke="#9ca3af" />
<YAxis stroke="#9ca3af" />
<Tooltip
contentStyle={{
background: '#1a1625',
border: '1px solid rgba(139,92,246,0.3)',
borderRadius: '12px',
color: '#fff',
}}
formatter={(value: number, name: string) => [
value,
name === 'calls' ? 'Calls' : 'Minutes',
]}
labelFormatter={(label) => `Day: ${label}`}
/>
<Line type="monotone" dataKey="calls" stroke="#8b5cf6" strokeWidth={3} />
<Line type="monotone" dataKey="minutes" stroke="#4f46e5" strokeWidth={3} />
</LineChart>
</ResponsiveContainer>
</div>
</motion.div>
<motion.div
id="call-history"
className="lg:col-span-2 bg-[#1a1625] border border-white/10 rounded-3xl p-6 shadow-lg backdrop-blur-xl transition-transform hover:scale-[1.01]"
variants={fadeInUp}
initial="hidden"
animate="visible"
transition={{ duration: 0.4, delay: 0.1, ease: 'easeOut' }}
>
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold">Recent Activity</h3>
<Timer className="h-5 w-5 text-[#8b5cf6]" />
</div>
<button
onClick={handleExportCsv}
className="text-xs px-3 py-1.5 rounded-full bg-white/5 border border-white/10 text-[#9ca3af] hover:text-white hover:bg-white/10 transition flex items-center gap-2"
>
<Download className="h-4 w-4" />
Export CSV
</button>
</div>
<div className="grid grid-cols-1 gap-3 mb-4">
<div className="flex items-center gap-2 rounded-2xl bg-[#0f0b1a] border border-white/10 px-4 py-2">
<Search className="h-4 w-4 text-[#9ca3af]" />
<input
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
placeholder="Search by contact name"
className="w-full bg-transparent text-sm text-white placeholder:text-[#9ca3af] focus:outline-none"
/>
</div>
<div className="flex flex-wrap items-center gap-3">
<select
value={dateRange}
onChange={(event) => setDateRange(event.target.value as '7' | '30' | 'custom')}
className="rounded-full bg-[#0f0b1a] border border-white/10 px-4 py-2 text-xs text-white focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]"
>
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="custom">Custom range</option>
</select>
{dateRange === 'custom' && (
<div className="flex flex-wrap items-center gap-2">
<input
type="date"
value={customStart}
onChange={(event) => setCustomStart(event.target.value)}
className="rounded-full bg-[#0f0b1a] border border-white/10 px-4 py-2 text-xs text-white focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]"
/>
<span className="text-xs text-[#9ca3af]">to</span>
<input
type="date"
value={customEnd}
onChange={(event) => setCustomEnd(event.target.value)}
className="rounded-full bg-[#0f0b1a] border border-white/10 px-4 py-2 text-xs text-white focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]"
/>
</div>
)}
</div>
</div>
{sortedCalls.length === 0 ? (
<div className="flex flex-col items-center justify-center text-center py-12">
<svg
width="160"
height="120"
viewBox="0 0 160 120"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="12" y="18" width="136" height="84" rx="16" fill="#1f1a2e" />
<rect x="28" y="34" width="104" height="12" rx="6" fill="#2f2544" />
<rect x="28" y="54" width="72" height="10" rx="5" fill="#2f2544" />
<rect x="28" y="72" width="88" height="10" rx="5" fill="#2f2544" />
<circle cx="126" cy="72" r="18" fill="#2f2544" />
<path
d="M120 72l6 6 10-12"
stroke="#8b5cf6"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<p className="text-sm text-[#9ca3af] mt-4">
No calls match your filters yet.
</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="text-[#9ca3af]">
<tr>
<th className="text-left pb-2 font-medium">Contact</th>
<th className="text-left pb-2 font-medium">
<button
onClick={() => handleSort('date')}
className="text-[#9ca3af] hover:text-white transition"
>
Date/Time {sortKey === 'date' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
</button>
</th>
<th className="text-left pb-2 font-medium">
<button
onClick={() => handleSort('duration')}
className="text-[#9ca3af] hover:text-white transition"
>
Duration {sortKey === 'duration' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
</button>
</th>
<th className="text-left pb-2 font-medium">
<button
onClick={() => handleSort('status')}
className="text-[#9ca3af] hover:text-white transition"
>
Status {sortKey === 'status' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
</button>
</th>
<th className="text-right pb-2 font-medium">Credits</th>
</tr>
</thead>
<motion.tbody
className="text-white/90"
initial="hidden"
animate="visible"
variants={{ hidden: {}, visible: {} }}
>
{paginatedCalls.map((call, index) => (
<motion.tr
key={call.id}
className="border-t border-white/5 cursor-pointer hover:bg-white/5 transition"
variants={fadeInUp}
transition={{ delay: 0.1 + index * 0.08, duration: 0.35 }}
onClick={() => setSelectedCall(call)}
>
<td className="py-3">{call.contactName}</td>
<td className="py-3">{call.date}</td>
<td className="py-3">{call.duration}</td>
<td className="py-3">
<span
className={`px-2.5 py-1 rounded-full text-xs border ${
call.status === 'Completed'
? 'bg-emerald-500/20 text-emerald-200 border-emerald-400/30'
: 'bg-red-500/20 text-red-200 border-red-400/30'
}`}
>
{call.status}
</span>
</td>
<td className="py-3 text-right">{call.credits}</td>
</motion.tr>
))}
</motion.tbody>
</table>
<div className="flex items-center justify-between pt-4 text-xs text-[#9ca3af]">
<span>
Page {currentPage} of {totalPages}
</span>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="px-2 py-1 rounded-full border border-white/10 text-[#9ca3af] hover:text-white disabled:opacity-40"
>
<ChevronLeft className="h-4 w-4" />
</button>
<button
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className="px-2 py-1 rounded-full border border-white/10 text-[#9ca3af] hover:text-white disabled:opacity-40"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
</div>
</div>
)}
</motion.div>
</section>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<button
onClick={() => router.push('/dashboard/chat')}
className="bg-[#1a1625] rounded-2xl border border-white/10 p-6 shadow-lg hover:-translate-y-1 hover:border-[#8b5cf6]/40 hover:shadow-2xl transition text-left w-full hover:scale-[1.01]"
>
<MessageSquare className="h-10 w-10 text-[#8b5cf6] mb-4" />
<h3 className="text-xl font-semibold mb-2">Chat</h3>
<p className="text-[#9ca3af] text-sm">
Talk to your AI companion in real-time.
</p>
</button>
<button
onClick={() => router.push('/dashboard/scheduled-calls')}
className="bg-[#1a1625] rounded-2xl border border-white/10 p-6 shadow-lg hover:-translate-y-1 hover:border-[#8b5cf6]/40 hover:shadow-2xl transition text-left w-full hover:scale-[1.01]"
data-tour="make-call"
>
<CalendarClock className="h-10 w-10 text-[#8b5cf6] mb-4" />
<h3 className="text-xl font-semibold mb-2">Scheduled Calls</h3>
<p className="text-[#9ca3af] text-sm">
Plan new calls and manage your queue.
</p>
</button>
<button
onClick={() => router.push('/dashboard/credits')}
className="bg-[#1a1625] rounded-2xl border border-white/10 p-6 shadow-lg hover:-translate-y-1 hover:border-[#8b5cf6]/40 hover:shadow-2xl transition text-left w-full hover:scale-[1.01]"
>
<CreditCard className="h-10 w-10 text-[#8b5cf6] mb-4" />
<h3 className="text-xl font-semibold mb-2">Credits & Limits</h3>
<p className="text-[#9ca3af] text-sm">
Track balance and adjust default call limits.
</p>
</button>
</div>
<div className="bg-gradient-to-r from-[#8b5cf6]/10 to-[#4f46e5]/10 rounded-2xl border border-[#8b5cf6]/20 p-6 shadow-lg transition-transform hover:scale-[1.01]">
<h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
<Sparkles className="h-5 w-5 text-[#8b5cf6]" />
Account Status
</h3>
<p className="text-sm text-[#9ca3af] mb-3">
Email: <span className="text-white">{user.email}</span>
</p>
<p className="text-sm text-[#9ca3af]">
UID: <span className="font-mono text-xs text-white/80">{user.uid}</span>
</p>
</div>
</div>
</main>
<AnimatePresence>
{selectedCall && (
<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={() => setSelectedCall(null)}
>
<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">{selectedCall.contactName}</h3>
<p className="text-xs text-[#9ca3af] mt-1">{selectedCall.date}</p>
</div>
<button
onClick={() => setSelectedCall(null)}
className="text-[#9ca3af] hover:text-white transition"
aria-label="Close call details"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="mt-5 space-y-3 text-sm">
<div className="flex items-center justify-between">
<span className="text-[#9ca3af]">Duration</span>
<span className="text-white">{selectedCall.duration}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[#9ca3af]">Status</span>
<span
className={`px-2.5 py-1 rounded-full text-xs border ${
selectedCall.status === 'Completed'
? 'bg-emerald-500/20 text-emerald-200 border-emerald-400/30'
: 'bg-red-500/20 text-red-200 border-red-400/30'
}`}
>
{selectedCall.status}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[#9ca3af]">Credits Used</span>
<span className="text-white">{selectedCall.credits}</span>
</div>
</div>
<div className="mt-6 flex justify-end">
<button
onClick={() => setSelectedCall(null)}
className="px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-white hover:bg-white/10 transition"
>
Close
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}