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
4 changes: 0 additions & 4 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ jobs:
NEXT_PUBLIC_SUPABASE_URL: https://placeholder.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY: placeholder-anon-key
SUPABASE_SERVICE_ROLE_KEY: placeholder-service-role-key
PLAYWRIGHT_SERVER_MODE: start
steps:
- uses: actions/checkout@v4

Expand All @@ -35,9 +34,6 @@ jobs:
- name: Install app dependencies
run: npm ci

- name: Build app
run: npm run build

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

Expand Down
21 changes: 12 additions & 9 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import LazyWidget from "@/components/LazyWidget";
import LazyWidget from "@/components/LazyWidget";
import DiscussionsWidget from "@/components/DiscussionsWidget";
import CommunityMetrics from "@/components/CommunityMetrics";
import GoalTracker from "@/components/GoalTracker";
Expand Down Expand Up @@ -107,7 +107,10 @@ export default async function DashboardPage() {
<DashboardHeader />

{/* Action bar */}
<div className="mb-6 flex flex-wrap items-stretch justify-center gap-2 sm:justify-end">
<div
id="overview"
className="mb-6 flex flex-wrap items-stretch justify-center gap-2 scroll-mt-24 sm:justify-end"
>
<Link
href="/wrapped"
className="flex min-w-0 flex-1 items-center justify-center rounded-lg border border-[var(--accent)] bg-[var(--accent-soft)] px-4 py-2 text-center text-sm font-semibold text-[var(--accent)] transition-opacity hover:opacity-90 sm:min-w-[140px] sm:flex-none"
Expand Down Expand Up @@ -138,8 +141,8 @@ export default async function DashboardPage() {
<AIMentorWidget />
</div>

{/* ── Row 1: Contribution graph (2/3) + Streak sidebar (1/3) ── */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* -- Row 1: Contribution graph (2/3) + Streak sidebar (1/3) -- */}
<div id="streaks" className="mt-6 grid grid-cols-1 gap-6 scroll-mt-24 lg:grid-cols-3">
{/* Left: contribution graph + heatmap */}
<div className="lg:col-span-2 flex flex-col gap-6">
<ContributionGraph />
Expand Down Expand Up @@ -170,8 +173,8 @@ export default async function DashboardPage() {
</LazyWidget>
</div>

{/* ── Row 2: PR metrics + Community metrics ── */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
{/* -- Row 2: PR metrics + Community metrics -- */}
<div id="pull-requests" className="mt-6 grid grid-cols-1 gap-6 scroll-mt-24 md:grid-cols-2">
<PRMetrics />
<CommunityMetrics />
</div>
Expand Down Expand Up @@ -207,8 +210,8 @@ export default async function DashboardPage() {
</LazyWidget>
</div>

{/* ── Row 3: Issues (2/3) + CI analytics (1/3) ── */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* -- Row 3: Issues (2/3) + CI analytics (1/3) -- */}
<div id="goals" className="mt-6 grid grid-cols-1 gap-6 scroll-mt-24 lg:grid-cols-3">
<div className="lg:col-span-2">
<LazyWidget fallback={<SkeletonCard />}>
<IssueMetrics />
Expand Down Expand Up @@ -240,7 +243,7 @@ export default async function DashboardPage() {
</LazyWidget>
</div>

{/* ── Row 4: Top repos + Language breakdown + Goal tracker ── */}
{/* -- Row 4: Top repos + Language breakdown + Goal tracker -- */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
<LazyWidget fallback={<SkeletonCard />}>
<TopRepos />
Expand Down
8 changes: 6 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -74,12 +75,15 @@ export default async function RootLayout({
<body
className={`${inter.className} min-h-screen bg-[var(--background)] text-[var(--foreground)]`}
>
<CustomCursor />
<CustomCursor />
<PWARegister />

<div className="flex min-h-screen flex-col">
<div className="flex-1">
<Providers>{children}</Providers>
<Providers>
<AppNavbar />
{children}
</Providers>
</div>

<Footer />
Expand Down
178 changes: 178 additions & 0 deletions src/components/AppNavbar.tsx
Original file line number Diff line number Diff line change
@@ -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<NavItem[]>(() => {
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 (
<header className="sticky top-0 z-50 border-b border-[var(--border)] bg-[color-mix(in_srgb,var(--background)_82%,transparent)] backdrop-blur-xl">
<div className="mx-auto flex w-full max-w-7xl items-center justify-between gap-4 px-4 py-3 sm:px-6 lg:px-8">
<Link
href={isAuthenticated ? "/dashboard" : "/"}
className="inline-flex items-center gap-2 text-sm font-semibold tracking-[0.16em] text-[var(--foreground)]"
style={{ fontFamily: "var(--font-jetbrains, ui-monospace, monospace)" }}
>
<span className="text-[var(--accent)]">{">"}</span>
<span>DEVTRACK</span>
</Link>

<nav className="hidden items-center gap-2 lg:flex">
{navItems.map((item) => {
const active = isActivePath(pathname, item.href);

return (
<Link
key={item.href}
href={item.href}
className={`rounded-full px-4 py-2 text-sm font-medium transition-colors ${
active
? "bg-[var(--accent-soft)] text-[var(--accent)]"
: "text-[var(--muted-foreground)] hover:bg-[var(--card)] hover:text-[var(--foreground)]"
}`}
>
{item.label}
</Link>
);
})}
</nav>

<div className="hidden items-center gap-3 lg:flex">
{isAuthenticated ? (
<>
<div className="hidden max-w-48 truncate rounded-full border border-[var(--border)] bg-[var(--card)] px-4 py-2 text-sm text-[var(--card-foreground)] xl:block">
{identityLabel}
</div>
<button
type="button"
onClick={() => signOut({ callbackUrl: "/" })}
className="rounded-full bg-[var(--destructive)] px-4 py-2 text-sm font-semibold text-[var(--destructive-foreground)] transition-opacity hover:opacity-90"
>
Sign out
</button>
</>
) : !isPublicProfileRoute ? (
<Link
href="/api/auth/signin/github?callbackUrl=/dashboard"
className="rounded-full bg-[var(--accent)] px-4 py-2 text-sm font-semibold text-[var(--accent-foreground)] transition-opacity hover:opacity-90"
>
Sign in with GitHub
</Link>
) : null}
</div>

<button
type="button"
onClick={() => setMobileOpen((open) => !open)}
className="inline-flex items-center justify-center rounded-full border border-[var(--border)] bg-[var(--card)] p-2 text-[var(--foreground)] lg:hidden"
aria-expanded={mobileOpen}
aria-controls="app-mobile-nav"
aria-label={mobileOpen ? "Close navigation menu" : "Open navigation menu"}
>
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>

{mobileOpen ? (
<div
id="app-mobile-nav"
className="border-t border-[var(--border)] bg-[var(--background)] lg:hidden"
>
<div className="mx-auto flex w-full max-w-7xl flex-col gap-2 px-4 py-4 sm:px-6 lg:px-8">
{navItems.map((item) => {
const active = isActivePath(pathname, item.href);

return (
<Link
key={item.href}
href={item.href}
className={`rounded-2xl px-4 py-3 text-sm font-medium transition-colors ${
active
? "bg-[var(--accent-soft)] text-[var(--accent)]"
: "bg-[var(--card)] text-[var(--card-foreground)] hover:bg-[var(--control)]"
}`}
>
{item.label}
</Link>
);
})}

{isAuthenticated ? (
<>
<div className="rounded-2xl border border-[var(--border)] bg-[var(--card)] px-4 py-3 text-sm text-[var(--card-foreground)]">
{identityLabel}
</div>
<button
type="button"
onClick={() => signOut({ callbackUrl: "/" })}
className="rounded-2xl bg-[var(--destructive)] px-4 py-3 text-left text-sm font-semibold text-[var(--destructive-foreground)]"
>
Sign out
</button>
</>
) : !isPublicProfileRoute ? (
<Link
href="/api/auth/signin/github?callbackUrl=/dashboard"
className="rounded-2xl bg-[var(--accent)] px-4 py-3 text-sm font-semibold text-[var(--accent-foreground)]"
>
Sign in with GitHub
</Link>
) : null}
</div>
</div>
) : null}
</header>
);
}
33 changes: 32 additions & 1 deletion src/components/WeeklySummaryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<WeeklySummaryData>;

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<WeeklySummaryData | null>(null);
Expand All @@ -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."
Expand Down
4 changes: 2 additions & 2 deletions src/components/landing/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ function StatItem({ value, label, delay }: { value: number; label: string; delay

function StatsSection() {
return (
<section style={{
<section id="features" style={{
padding: '64px clamp(20px,4vw,48px)',
display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px,1fr))',
gap: 24, borderTop: '1px solid #111',
Expand Down Expand Up @@ -633,6 +633,7 @@ function SetupSection() {
const [ref, vis] = useScrollReveal(0.2);
return (
<section
id="open-source"
ref={ref}
style={{
padding: '80px clamp(20px,4vw,48px)',
Expand Down Expand Up @@ -890,7 +891,6 @@ export default function LandingPage({ repoStats }: { repoStats: RepoStats }) {
style={{ background: BG, color: TEXT, minHeight: '100vh', position: 'relative', overflowX: 'hidden' }}
>
<MouseSpotlight />
<LandingNav />
<HeroSection />
<CommitTicker />
<HeatmapSection />
Expand Down
Loading