Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions frontend/src/components/ui/SkeletonCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const SkeletonCard = () => (
<div
aria-hidden="true"
className="border-4 border-black bg-white p-6 sm:p-8 shadow-[8px_8px_0_0_rgba(0,0,0,1)]"
>
{/* Header skeleton */}
<div className="h-8 sm:h-10 bg-gray-200 w-3/4 mb-6 animate-pulse motion-reduce:animate-none"></div>

{/* Content skeleton - 3 rows */}
<div className="space-y-4">
<div className="h-6 bg-gray-200 w-full animate-pulse motion-reduce:animate-none"></div>
<div className="h-6 bg-gray-200 w-5/6 animate-pulse motion-reduce:animate-none"></div>
<div className="h-6 bg-gray-200 w-4/6 animate-pulse motion-reduce:animate-none"></div>
</div>
</div>
);
Comment thread
grishabhatia marked this conversation as resolved.

export default SkeletonCard;
176 changes: 98 additions & 78 deletions frontend/src/pages/DashboardPage.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useAuth } from "../context/AuthContext";
import { useNavigate, Link } from "react-router-dom";
import { useCodeforces } from "../hooks/useCodeforces";
import ConnectBanner from "../components/codeforces/ConnectBanner";
import VerifyModal from "../components/codeforces/VerifyModal";
import DashboardExecutiveSummary from "../components/dashboard/DashboardExecutiveSummary";
import LoaderSwitcher from "../components/shared/loaders/LoaderSwitcher";
import SkeletonCard from "../components/ui/SkeletonCard";

export default function DashboardPage() {
const { user, loading, logout } = useAuth();
const navigate = useNavigate();
const [modalOpen, setModalOpen] = useState(false);

// Skeleton loading state
const [isSkeletonLoading, setIsSkeletonLoading] = useState(true);

const {
dashboardSummary: cfData,
Expand All @@ -24,10 +28,18 @@ export default function DashboardPage() {
} = useCodeforces(true);

const handleLogout = async () => {
await logout(); // clears HttpOnly cookies server-side
await logout();
navigate("/");
};

// Simulate loading for skeleton demo (3 seconds)
useEffect(() => {
const timer = setTimeout(() => {
setIsSkeletonLoading(false);
}, 3000);
return () => clearTimeout(timer);
}, []);
Comment thread
grishabhatia marked this conversation as resolved.

if (loading) {
return <LoaderSwitcher />;
}
Expand All @@ -52,98 +64,106 @@ export default function DashboardPage() {
</button>
</header>

{/* AI Executive Summary - The Hero of the Dashboard */}
{/* AI Executive Summary */}
<DashboardExecutiveSummary />

{/* Metrics Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 sm:gap-10 mb-12 sm:mb-16">
{/* GitHub Stats */}
<div className="border-4 border-black p-6 sm:p-8 bg-white shadow-[8px_8px_0_0_rgba(0,0,0,1)] hover:-translate-y-1 transition-transform duration-300">
<h3 className="text-2xl sm:text-3xl font-black uppercase tracking-tighter text-black mb-6 border-b-4 border-black pb-4">GitHub</h3>
<div className="flex flex-col space-y-6">
<div className="flex justify-between items-center">
<span className="font-black text-black uppercase tracking-widest text-sm">Commits</span>
<span className="font-black text-3xl">β€”</span>
</div>
<div className="flex justify-between items-center">
<span className="font-black text-black uppercase tracking-widest text-sm">Active PRs</span>
<span className="font-black text-3xl">β€”</span>
</div>
</div>
{/* Metrics Grid - Skeleton OR Actual Content */}
{isSkeletonLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-10 mb-12 sm:mb-16">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>

{/* Codeforces Widget */}
{cfLoading ? (
<div className="border-4 border-black p-6 sm:p-8 bg-white flex items-center justify-center shadow-[8px_8px_0_0_rgba(0,0,0,1)]">
<div className="w-10 h-10 border-[4px] border-black border-t-transparent animate-spin" />
</div>
) : cfConnected && cfData ? (
<Link
to="/codeforces"
className="border-4 border-black p-6 sm:p-8 bg-white shadow-[8px_8px_0_0_rgba(0,0,0,1)] hover:-translate-y-1 transition-transform duration-300 block"
>
<h3 className="text-2xl sm:text-3xl font-black uppercase tracking-tighter text-black mb-6 border-b-4 border-black pb-4">Codeforces</h3>
<div className="flex flex-col space-y-4">
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-10 mb-12 sm:mb-16">
{/* GitHub Stats */}
<div className="border-4 border-black p-6 sm:p-8 bg-white shadow-[8px_8px_0_0_rgba(0,0,0,1)] hover:-translate-y-1 transition-transform duration-300">
<h3 className="text-2xl sm:text-3xl font-black uppercase tracking-tighter text-black mb-6 border-b-4 border-black pb-4">GitHub</h3>
<div className="flex flex-col space-y-6">
<div className="flex justify-between items-center">
<span className="font-black text-black uppercase tracking-widest text-sm">Rating</span>
<span className="font-black text-3xl">{cfData.rating ?? "β€”"}</span>
<span className="font-black text-black uppercase tracking-widest text-sm">Commits</span>
<span className="font-black text-3xl">β€”</span>
</div>
<div className="flex justify-between items-center">
<span className="font-black text-black uppercase tracking-widest text-sm">Solved</span>
<span className="font-black text-3xl">{cfData.totalSolved ?? "β€”"}</span>
</div>
<div className="flex justify-between items-end mt-2 pt-4 border-t-2 border-dashed border-gray-300">
<span className="font-black text-black uppercase tracking-widest text-xs">Streak</span>
<span className="font-black text-lg text-red-600">{cfData.currentStreak ?? 0} Days πŸ”₯</span>
<span className="font-black text-black uppercase tracking-widest text-sm">Active PRs</span>
<span className="font-black text-3xl">β€”</span>
</div>
</div>
</Link>
) : (
<div
className="border-4 border-black p-6 sm:p-8 bg-white shadow-[8px_8px_0_0_rgba(0,0,0,1)] hover:-translate-y-1 transition-transform duration-300 flex flex-col justify-between cursor-pointer"
onClick={() => setModalOpen(true)}
>
<h3 className="text-2xl sm:text-3xl font-black uppercase tracking-tighter text-black mb-4 border-b-4 border-black pb-4">Codeforces</h3>
<p className="font-bold uppercase tracking-widest text-xs text-gray-500 mb-6 flex-1">
No CF account connected. Click to link your handle.
</p>
<button className="w-full py-3 bg-black text-white font-black uppercase tracking-widest text-sm hover:bg-gray-800 transition-colors border-2 border-black">
Connect β†’
</button>
</div>
)}

{/* Focus Quality */}
<div className="border-4 border-black p-6 sm:p-8 bg-black text-white shadow-[8px_8px_0_0_rgba(200,200,200,1)] hover:-translate-y-1 transition-transform duration-300">
<h3 className="text-2xl sm:text-3xl font-black uppercase tracking-tighter text-white mb-6 border-b-4 border-white pb-4">Focus Quality</h3>
<div className="flex flex-col space-y-6">
<div className="flex justify-between items-end">
<span className="font-black uppercase tracking-widest text-gray-300 text-sm">CF Streak</span>
<span className="font-black text-5xl text-white">
{cfConnected && cfData ? `${cfData.currentStreak}D` : "β€”"}
</span>
{/* Codeforces Widget */}
{cfLoading ? (
<div className="border-4 border-black p-6 sm:p-8 bg-white flex items-center justify-center shadow-[8px_8px_0_0_rgba(0,0,0,1)]">
<div className="w-10 h-10 border-[4px] border-black border-t-transparent animate-spin" />
</div>
<div>
<div className="w-full bg-gray-800 h-3 border border-white overflow-hidden">
<div
className="bg-white h-full transition-all duration-700"
style={{
width: cfConnected && cfData
? `${Math.min((cfData.currentStreak / Math.max(cfData.longestStreak, 1)) * 100, 100)}%`
: "0%"
}}
/>
) : cfConnected && cfData ? (
<Link
to="/codeforces"
className="border-4 border-black p-6 sm:p-8 bg-white shadow-[8px_8px_0_0_rgba(0,0,0,1)] hover:-translate-y-1 transition-transform duration-300 block"
>
<h3 className="text-2xl sm:text-3xl font-black uppercase tracking-tighter text-black mb-6 border-b-4 border-black pb-4">Codeforces</h3>
<div className="flex flex-col space-y-4">
<div className="flex justify-between items-center">
<span className="font-black text-black uppercase tracking-widest text-sm">Rating</span>
<span className="font-black text-3xl">{cfData.rating ?? "β€”"}</span>
</div>
<div className="flex justify-between items-center">
<span className="font-black text-black uppercase tracking-widest text-sm">Solved</span>
<span className="font-black text-3xl">{cfData.totalSolved ?? "β€”"}</span>
</div>
<div className="flex justify-between items-end mt-2 pt-4 border-t-2 border-dashed border-gray-300">
<span className="font-black text-black uppercase tracking-widest text-xs">Streak</span>
<span className="font-black text-lg text-red-600">{cfData.currentStreak ?? 0} Days πŸ”₯</span>
</div>
</div>
<div className="flex justify-between items-center mt-2">
<span className="text-[10px] font-black uppercase tracking-widest text-gray-400">Current</span>
<span className="text-[10px] font-black uppercase tracking-widest text-gray-400">
Best: {cfConnected && cfData ? `${cfData.longestStreak}D` : "β€”"}
</Link>
) : (
<div
className="border-4 border-black p-6 sm:p-8 bg-white shadow-[8px_8px_0_0_rgba(0,0,0,1)] hover:-translate-y-1 transition-transform duration-300 flex flex-col justify-between cursor-pointer"
onClick={() => setModalOpen(true)}
>
<h3 className="text-2xl sm:text-3xl font-black uppercase tracking-tighter text-black mb-4 border-b-4 border-black pb-4">Codeforces</h3>
<p className="font-bold uppercase tracking-widest text-xs text-gray-500 mb-6 flex-1">
No CF account connected. Click to link your handle.
</p>
<button className="w-full py-3 bg-black text-white font-black uppercase tracking-widest text-sm hover:bg-gray-800 transition-colors border-2 border-black">
Connect β†’
</button>
</div>
)}

{/* Focus Quality */}
<div className="border-4 border-black p-6 sm:p-8 bg-black text-white shadow-[8px_8px_0_0_rgba(200,200,200,1)] hover:-translate-y-1 transition-transform duration-300">
<h3 className="text-2xl sm:text-3xl font-black uppercase tracking-tighter text-white mb-6 border-b-4 border-white pb-4">Focus Quality</h3>
<div className="flex flex-col space-y-6">
<div className="flex justify-between items-end">
<span className="font-black uppercase tracking-widest text-gray-300 text-sm">CF Streak</span>
<span className="font-black text-5xl text-white">
{cfConnected && cfData ? `${cfData.currentStreak}D` : "β€”"}
</span>
</div>
<div>
<div className="w-full bg-gray-800 h-3 border border-white overflow-hidden">
<div
className="bg-white h-full transition-all duration-700"
style={{
width: cfConnected && cfData
? `${Math.min((cfData.currentStreak / Math.max(cfData.longestStreak, 1)) * 100, 100)}%`
: "0%"
}}
/>
</div>
<div className="flex justify-between items-center mt-2">
<span className="text-[10px] font-black uppercase tracking-widest text-gray-400">Current</span>
<span className="text-[10px] font-black uppercase tracking-widest text-gray-400">
Best: {cfConnected && cfData ? `${cfData.longestStreak}D` : "β€”"}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
)}

{/* Connect Banner β€” only shown if CF is not connected */}
{!cfLoading && !cfConnected && (
Expand All @@ -165,4 +185,4 @@ export default function DashboardPage() {
/>
</div>
);
}
}