- {starredLangs?.slice(0, 6).map((l, i) => {
+ {starredLangs?.slice(0, 6).map((l) => {
const max = starredLangs[0]?.count || 1;
return (
diff --git a/frontend/src/components/shared/Navbar.jsx b/frontend/src/components/shared/Navbar.jsx
index aa10e55..6f12806 100644
--- a/frontend/src/components/shared/Navbar.jsx
+++ b/frontend/src/components/shared/Navbar.jsx
@@ -265,8 +265,10 @@ export default function Navbar() {
// ── Close mobile menu on route change ───────────────────────────────────
useEffect(() => {
- setIsMenuOpen(false);
- setMobileMegaOpen(false);
+ setTimeout(() => {
+ setIsMenuOpen(false);
+ setMobileMegaOpen(false);
+ }, 0);
}, [location.pathname]);
useEffect(() => {
diff --git a/frontend/src/components/shared/ProtectedRoute.jsx b/frontend/src/components/shared/ProtectedRoute.jsx
index 6a542c0..44d008a 100644
--- a/frontend/src/components/shared/ProtectedRoute.jsx
+++ b/frontend/src/components/shared/ProtectedRoute.jsx
@@ -1,11 +1,11 @@
import { Navigate } from "react-router-dom";
import { useAuth } from "../../context/AuthContext";
-import Loader from "./loaders/LoaderSwitcher";
+import { GuardSkeleton } from "./skeletons/PageSkeletons";
const ProtectedRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
- if (loading) return
;
+ if (loading) return
;
if (!isAuthenticated) return
;
return children;
diff --git a/frontend/src/components/shared/PublicRoute.jsx b/frontend/src/components/shared/PublicRoute.jsx
index 1c590f8..f75a89c 100644
--- a/frontend/src/components/shared/PublicRoute.jsx
+++ b/frontend/src/components/shared/PublicRoute.jsx
@@ -1,11 +1,11 @@
import { Navigate } from "react-router-dom";
import { useAuth } from "../../context/AuthContext";
-import Loader from "./loaders/LoaderSwitcher";
+import { GuardSkeleton } from "./skeletons/PageSkeletons";
const PublicRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
- if (loading) return
;
+ if (loading) return
;
if (isAuthenticated) return
;
return children;
diff --git a/frontend/src/components/shared/skeletons/PageSkeletons.jsx b/frontend/src/components/shared/skeletons/PageSkeletons.jsx
new file mode 100644
index 0000000..0e7081b
--- /dev/null
+++ b/frontend/src/components/shared/skeletons/PageSkeletons.jsx
@@ -0,0 +1,73 @@
+function SkeletonBlock({ className = "" }) {
+ return
;
+}
+
+/**
+ * Generic auth-guard loading placeholder — shown by ProtectedRoute / PublicRoute
+ * while authentication state is being resolved. Page-agnostic by design.
+ */
+export function GuardSkeleton() {
+ return (
+
+ );
+}
+
+export function DashboardPageSkeleton() {
+ return (
+
+ );
+}
+
+export function AccountCenterSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx
index dcdc4ed..8814cc6 100644
--- a/frontend/src/context/AuthContext.jsx
+++ b/frontend/src/context/AuthContext.jsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useState, useEffect } from "react";
import { getProfile } from "../services/userService";
@@ -24,7 +25,7 @@ export const AuthProvider = ({ children }) => {
setToken(storedToken);
const response = await getProfile();
setUser(response.data);
- } catch (error) {
+ } catch {
// Token expired or invalid
localStorage.removeItem("token");
localStorage.removeItem("user");
diff --git a/frontend/src/index.css b/frontend/src/index.css
index f1d8c73..f3fa2de 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1 +1,16 @@
@import "tailwindcss";
+
+@keyframes page-fade-in {
+ from {
+ opacity: 0;
+ transform: translateY(6px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.page-fade-in {
+ animation: page-fade-in 260ms ease-out;
+}
diff --git a/frontend/src/pages/AccountCenterPage.jsx b/frontend/src/pages/AccountCenterPage.jsx
index b206356..c4944b8 100644
--- a/frontend/src/pages/AccountCenterPage.jsx
+++ b/frontend/src/pages/AccountCenterPage.jsx
@@ -2,28 +2,12 @@ import { useAuth } from "../context/AuthContext";
import { useState, useEffect } from "react";
import { Link, useSearchParams } from "react-router-dom";
import { getProfile, deleteAccount } from "../services/userService";
+import { AccountCenterSkeleton } from "../components/shared/skeletons/PageSkeletons";
const API_BASE = import.meta.env.VITE_API_BASE_URL;
// ── Helpers ──────────────────────────────────────────────────────────────────
-const RANK_COLORS = {
- "legendary grandmaster": "text-red-600",
- "international grandmaster": "text-red-500",
- grandmaster: "text-red-400",
- "international master": "text-orange-500",
- master: "text-orange-400",
- "candidate master": "text-purple-600",
- expert: "text-blue-600",
- specialist: "text-cyan-600",
- pupil: "text-green-600",
- newbie: "text-gray-500",
- unrated: "text-gray-400",
-};
-
-const rankColor = (rank = "") =>
- RANK_COLORS[(rank || "").toLowerCase()] || "text-black";
-
// ── Sub-components ────────────────────────────────────────────────────────────
function SectionLabel({ text }) {
@@ -273,7 +257,7 @@ function DangerZone({ onLogout }) {
try {
await deleteAccount();
onLogout(); // Logs the user out and redirects to login
- } catch (err) {
+ } catch {
alert("Failed to delete account. Please try again later.");
setLoading(false);
setConfirm(false);
@@ -327,7 +311,7 @@ function DangerZone({ onLogout }) {
// ── Main Page ─────────────────────────────────────────────────────────────────
export default function AccountCenterPage() {
- const { user, setUser, logout } = useAuth();
+ const { user, setUser, logout, loading: authLoading } = useAuth();
const [searchParams] = useSearchParams();
const [banner, setBanner] = useState("");
@@ -337,15 +321,21 @@ export default function AccountCenterPage() {
const username = searchParams.get("githubUsername");
if (status === "connected") {
- setBanner(`GitHub account @${username || "connected"} linked successfully!`);
- // Refresh user profile so the card shows the new GitHub identity
- getProfile()
- .then((res) => setUser(res.data))
- .catch(() => {});
+ setTimeout(() => {
+ setBanner(`GitHub account @${username || "connected"} linked successfully!`);
+ // Refresh user profile so the card shows the new GitHub identity
+ getProfile()
+ .then((res) => setUser(res.data))
+ .catch(() => {});
+ }, 0);
// Clean URL
window.history.replaceState({}, "", "/account-center");
}
- }, [searchParams]);
+ }, [searchParams, setUser]);
+
+ if (authLoading || !user) {
+ return
;
+ }
return (
diff --git a/frontend/src/pages/CodeforcesPage.jsx b/frontend/src/pages/CodeforcesPage.jsx
index 2dfed82..d073839 100644
--- a/frontend/src/pages/CodeforcesPage.jsx
+++ b/frontend/src/pages/CodeforcesPage.jsx
@@ -57,8 +57,6 @@ function ActivityHeatmap({ dailyActivity = {} }) {
return "bg-black border-black";
};
- const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
-
return (
@@ -106,16 +104,7 @@ function RatingChart({ history = [] }) {
const points = history.map((h, i) => `${toX(i)},${toY(h.newRating)}`).join(" ");
- // Draw coloured band zones
- const zones = [
- { maxR: 1200, color: "#e5e5e5", label: "Newbie" },
- { maxR: 1400, color: "#d4edda", label: "Pupil" },
- { maxR: 1600, color: "#cce5ff", label: "Specialist" },
- { maxR: 1900, color: "#d6d6ff", label: "Expert" },
- { maxR: 2100, color: "#ffe8cc", label: "CM" },
- { maxR: 2400, color: "#ffd6d6", label: "Master" },
- { maxR: Infinity, color: "#ffb3b3", label: "GM+" },
- ];
+ // Rating history chart configuration
return (
@@ -304,7 +293,6 @@ export default function CodeforcesPage() {
ratingHistory,
submissions,
isConnected,
- isPending,
loading,
syncing,
error,
diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx
index 59d59d5..e6354d0 100644
--- a/frontend/src/pages/DashboardPage.jsx
+++ b/frontend/src/pages/DashboardPage.jsx
@@ -5,7 +5,7 @@ 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 { DashboardPageSkeleton } from "../components/shared/skeletons/PageSkeletons";
export default function DashboardPage() {
const { user, loading, logout } = useAuth();
@@ -29,7 +29,7 @@ export default function DashboardPage() {
};
if (loading) {
- return
;
+ return
;
}
return (
@@ -74,8 +74,11 @@ export default function DashboardPage() {
{/* Codeforces Widget */}
{cfLoading ? (
-
-
+
) : cfConnected && cfData ? (
-
);
}
diff --git a/frontend/src/pages/GitHubCallbackPage.jsx b/frontend/src/pages/GitHubCallbackPage.jsx
index 3615b3b..9c5f8b4 100644
--- a/frontend/src/pages/GitHubCallbackPage.jsx
+++ b/frontend/src/pages/GitHubCallbackPage.jsx
@@ -23,8 +23,10 @@ export default function GitHubCallbackPage() {
useEffect(() => {
const error = searchParams.get("githubAuthError");
if (error) {
- setStatus("error");
- setErrorMsg(decodeURIComponent(error));
+ setTimeout(() => {
+ setStatus("error");
+ setErrorMsg(decodeURIComponent(error));
+ }, 0);
setTimeout(() => navigate(`/login?error=${encodeURIComponent(error)}`), 2000);
return;
}
@@ -35,8 +37,10 @@ export default function GitHubCallbackPage() {
const token = params.get("token");
if (!token) {
- setStatus("error");
- setErrorMsg("No token received. Please try again.");
+ setTimeout(() => {
+ setStatus("error");
+ setErrorMsg("No token received. Please try again.");
+ }, 0);
setTimeout(() => navigate("/login"), 2500);
return;
}
@@ -54,7 +58,7 @@ export default function GitHubCallbackPage() {
}
navigate("/dashboard", { replace: true });
- }, []);
+ }, [login, navigate, searchParams]);
return (
diff --git a/frontend/src/pages/GitHubIntelligencePage.jsx b/frontend/src/pages/GitHubIntelligencePage.jsx
index f013d15..9d842cc 100644
--- a/frontend/src/pages/GitHubIntelligencePage.jsx
+++ b/frontend/src/pages/GitHubIntelligencePage.jsx
@@ -4,8 +4,9 @@ import { getGitHubDashboard } from "../services/githubService";
import {
StatCard, ScoreMeter, LangBytesChart, ActivityChart,
ContribHeatmap, TopReposTable, ActivityFeed,
- GistsPanel, StarredInsights, OrgsPanel, PRFootprint, fmt,
+ GistsPanel, StarredInsights, OrgsPanel, PRFootprint,
} from "../components/github/GitHubComponents";
+import { fmt } from "../utils/githubHelpers";
// ── Loading ───────────────────────────────────────────────────────────────────
function Skeleton() {
@@ -42,7 +43,7 @@ function NotConnected() {
}
// ── Profile Hero ──────────────────────────────────────────────────────────────
-function ProfileHero({ profile, orgs, totalStars, totalForks, ownedRepos, forkedRepos }) {
+function ProfileHero({ profile, totalStars, totalForks, ownedRepos, forkedRepos }) {
return (
@@ -175,7 +176,7 @@ export default function GitHubIntelligencePage() {
if (!data) return
;
const {
- profile, orgs, repos, ownedRepos, forkedRepos, topByStars,
+ profile, orgs, ownedRepos, forkedRepos, topByStars,
totalStars, totalForks, langByBytes, contributions, events,
gists, starred, starredTopics, starredLangs,
prs, issues, metrics,
@@ -186,7 +187,7 @@ export default function GitHubIntelligencePage() {
{/* Hero */}
-
diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx
index 5ca7599..58dd0e2 100644
--- a/frontend/src/pages/LoginPage.jsx
+++ b/frontend/src/pages/LoginPage.jsx
@@ -42,7 +42,7 @@ export default function LoginPage() {
navigate('/dashboard', { replace: true });
}
}
- }, [searchParams]);
+ }, [searchParams, auth, navigate]);
const handleSubmit = async (e) => {
e.preventDefault();
diff --git a/frontend/src/pages/SignupPage.jsx b/frontend/src/pages/SignupPage.jsx
index 9ab95b2..16bcc2b 100644
--- a/frontend/src/pages/SignupPage.jsx
+++ b/frontend/src/pages/SignupPage.jsx
@@ -12,7 +12,6 @@ export default function SignupPage() {
const [password, setPassword] = useState("");
const [otp, setOtp] = useState("");
const [error, setError] = useState("");
- const [githubMessage, setGithubMessage] = useState("");
const [loading, setLoading] = useState(false);
const [cooldown, setCooldown] = useState(0);
const [isEmailValid, setIsEmailValid] = useState(true);
diff --git a/frontend/src/utils/githubHelpers.js b/frontend/src/utils/githubHelpers.js
new file mode 100644
index 0000000..8cb0b20
--- /dev/null
+++ b/frontend/src/utils/githubHelpers.js
@@ -0,0 +1,11 @@
+export const COLORS = ["#000", "#222", "#444", "#666", "#888", "#aaa", "#ccc"];
+
+export const fmt = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n ?? 0));
+
+export const timeAgo = (d) => {
+ const s = (Date.now() - new Date(d)) / 1000;
+ if (s < 60) return `${Math.floor(s)}s ago`;
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
+ return `${Math.floor(s / 86400)}d ago`;
+};
diff --git a/server/package-lock.json b/server/package-lock.json
index f315bd4..5cb2039 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -624,7 +624,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -2127,7 +2126,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/server/server.js b/server/server.js
index 812011b..48922f0 100644
--- a/server/server.js
+++ b/server/server.js
@@ -5,10 +5,15 @@ import './config/env.js';
const PORT = process.env.PORT || 5000;
const startServer = async () => {
- await connectDB();
- app.listen(PORT, () => {
- console.log(`Server running on port ${PORT}`);
- });
+ try {
+ await connectDB();
+ app.listen(PORT, () => {
+ console.log(`Server running on port ${PORT}`);
+ });
+ } catch (err) {
+ console.error('Failed to connect to database:', err);
+ process.exit(1);
+ }
};
startServer();