- {/* ── Row 4: Top repos + Language breakdown + Goal tracker ── */}
+ {/* -- Row 4: Top repos + Language breakdown + Goal tracker -- */}
}>
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 80e33f0e9..a2f1c7720 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,7 @@
import CustomCursor from "@/components/CustomCursor";
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
+import AppNavbar from "@/components/AppNavbar";
import Footer from "@/components/Footer";
import DeferredVercelMetrics from "@/components/DeferredVercelMetrics";
import Providers from "./providers";
@@ -74,12 +75,15 @@ export default async function RootLayout({
-
+
-
{children}
+
+
+ {children}
+
diff --git a/src/components/AppNavbar.tsx b/src/components/AppNavbar.tsx
new file mode 100644
index 000000000..22be3dde6
--- /dev/null
+++ b/src/components/AppNavbar.tsx
@@ -0,0 +1,178 @@
+"use client";
+
+import Link from "next/link";
+import { Menu, X } from "lucide-react";
+import { signOut, useSession } from "next-auth/react";
+import { usePathname } from "next/navigation";
+import { useEffect, useMemo, useState } from "react";
+
+type NavItem = {
+ href: string;
+ label: string;
+};
+
+function isActivePath(pathname: string, href: string) {
+ if (href === "/") {
+ return pathname === "/";
+ }
+
+ if (href.includes("#")) {
+ const [base] = href.split("#");
+ return pathname === base;
+ }
+
+ return pathname === href || pathname.startsWith(`${href}/`);
+}
+export default function AppNavbar() {
+ const pathname = usePathname();
+ const { data: session, status } = useSession();
+ const [mobileOpen, setMobileOpen] = useState(false);
+ const isPublicProfileRoute = pathname.startsWith("/u/");
+
+ useEffect(() => {
+ setMobileOpen(false);
+ }, [pathname]);
+
+ const isAuthenticated = status === "authenticated" && Boolean(session);
+ const identityLabel =
+ session?.user?.name ?? session?.githubLogin ?? session?.user?.email ?? "GitHub user";
+
+ const navItems = useMemo
(() => {
+ if (isAuthenticated) {
+ return [
+ { href: "/dashboard", label: "Dashboard" },
+ { href: "/dashboard#streaks", label: "Streaks" },
+ { href: "/dashboard#pull-requests", label: "Pull Requests" },
+ { href: "/dashboard#goals", label: "Goals" },
+ { href: "/leaderboard", label: "Leaderboard" },
+ { href: "/dashboard/settings", label: "Settings" },
+ ];
+ }
+
+ return [
+ { href: "/", label: "Home" },
+ { href: "/#features", label: "Features" },
+ { href: "/#open-source", label: "Open Source" },
+ { href: "/leaderboard", label: "Leaderboard" },
+ ];
+ }, [isAuthenticated]);
+
+ return (
+
+
+
+
{">"}
+
DEVTRACK
+
+
+
+
+
+ {isAuthenticated ? (
+ <>
+
+ {identityLabel}
+
+
+ >
+ ) : !isPublicProfileRoute ? (
+
+ Sign in with GitHub
+
+ ) : null}
+
+
+
+
+
+ {mobileOpen ? (
+
+
+ {navItems.map((item) => {
+ const active = isActivePath(pathname, item.href);
+
+ return (
+
+ {item.label}
+
+ );
+ })}
+
+ {isAuthenticated ? (
+ <>
+
+ {identityLabel}
+
+
+ >
+ ) : !isPublicProfileRoute ? (
+
+ Sign in with GitHub
+
+ ) : null}
+
+
+ ) : null}
+
+ );
+}
diff --git a/src/components/WeeklySummaryCard.tsx b/src/components/WeeklySummaryCard.tsx
index af03d0c71..bb1547c2e 100644
--- a/src/components/WeeklySummaryCard.tsx
+++ b/src/components/WeeklySummaryCard.tsx
@@ -19,6 +19,31 @@ interface WeeklySummaryData {
topRepo: string | null;
}
+function isWeeklySummaryData(value: unknown): value is WeeklySummaryData {
+ if (!value || typeof value !== "object") return false;
+
+ const summary = value as Partial;
+
+ return Boolean(
+ summary.commits &&
+ typeof summary.commits.current === "number" &&
+ typeof summary.commits.previous === "number" &&
+ typeof summary.commits.delta === "number" &&
+ typeof summary.commits.trend === "string" &&
+ summary.prs?.thisWeek &&
+ typeof summary.prs.thisWeek.opened === "number" &&
+ typeof summary.prs.thisWeek.merged === "number" &&
+ summary.prs?.lastWeek &&
+ typeof summary.prs.lastWeek.opened === "number" &&
+ typeof summary.prs.lastWeek.merged === "number" &&
+ summary.activeDays &&
+ typeof summary.activeDays.thisWeek === "number" &&
+ typeof summary.activeDays.lastWeek === "number" &&
+ typeof summary.streak === "number" &&
+ (typeof summary.topRepo === "string" || summary.topRepo === null)
+ );
+}
+
export default function WeeklySummaryCard() {
const { selectedAccount } = useAccount();
const [summary, setSummary] = useState(null);
@@ -43,7 +68,13 @@ export default function WeeklySummaryCard() {
if (!r.ok) throw new Error("API error");
return r.json();
})
- .then((data: WeeklySummaryData) => setSummary(data))
+ .then((data: unknown) => {
+ if (!isWeeklySummaryData(data)) {
+ throw new Error("Invalid weekly summary payload");
+ }
+
+ setSummary(data);
+ })
.catch(() =>
setError(
"We couldn't load your weekly summary right now. Please try again in a moment."
diff --git a/src/components/landing/LandingPage.tsx b/src/components/landing/LandingPage.tsx
index d8256e7eb..2539c2756 100644
--- a/src/components/landing/LandingPage.tsx
+++ b/src/components/landing/LandingPage.tsx
@@ -535,7 +535,7 @@ function StatItem({ value, label, delay }: { value: number; label: string; delay
function StatsSection() {
return (
-