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
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 30 additions & 1 deletion src/app/api/metrics/streak/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { getServerSession } from "next-auth";
import { NextRequest } from "next/server";
import { authOptions } from "@/lib/auth";
import { getAccountToken, getAllAccounts } from "@/lib/github-accounts";
import { GITHUB_API } from "@/lib/github";
Expand All @@ -25,10 +24,36 @@
// stores each account's dates separately and merges them in the GET handler.
const key = metricsCacheKey(cacheContext.userId, "streak", { githubLogin });

<<<<<<< HEAD

Check failure on line 27 in src/app/api/metrics/streak/route.ts

View workflow job for this annotation

GitHub Actions / Type check

Merge conflict marker encountered.
// withMetricsCache returns cached dates if available within the TTL window,
// skipping all GitHub API calls below. This is the primary protection against
// exhausting the Search API rate limit on repeated page loads.
const dates = await withMetricsCache(
=======

Check failure on line 32 in src/app/api/metrics/streak/route.ts

View workflow job for this annotation

GitHub Actions / Type check

Merge conflict marker encountered.
function dateDiffDays(a: string, b: string): number {
return (
(new Date(b).getTime() - new Date(a).getTime()) / (1000 * 60 * 60 * 24)
);
}

function toDateStr(d: Date): string {
return d.toISOString().slice(0, 10);
}

export async function GET() {
const session = await getServerSession(authOptions);
if (!session?.accessToken || !session.githubLogin || !session.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

// Fetch 90 days to calculate a meaningful longest streak
const since = new Date();
since.setDate(since.getDate() - 90);
const sinceStr = since.toISOString().slice(0, 10);

const searchRes = await fetch(
`${GITHUB_API}/search/commits?q=author:${session.githubLogin}+author-date:>=${sinceStr}&per_page=100&sort=author-date&order=asc`,
>>>>>>> 8c942cb (fix: address PR review — unique constraint, created_at, trailing newlines, revert lockfile)

Check failure on line 56 in src/app/api/metrics/streak/route.ts

View workflow job for this annotation

GitHub Actions / Type check

Merge conflict marker encountered.
{
bypass: cacheContext.bypass,
key,
Expand Down Expand Up @@ -176,6 +201,7 @@
// totalActiveDays counts only days with real commits or freezes in the 90-day window,
// not the full streak length — useful for the "active days" stat on the dashboard.
totalActiveDays: commitDays.length,
<<<<<<< HEAD

Check failure on line 204 in src/app/api/metrics/streak/route.ts

View workflow job for this annotation

GitHub Actions / Type check

Merge conflict marker encountered.
freezeDates: Array.from(freezeDates),
};
}
Expand Down Expand Up @@ -352,4 +378,7 @@
} catch {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}
=======

Check failure on line 381 in src/app/api/metrics/streak/route.ts

View workflow job for this annotation

GitHub Actions / Type check

Merge conflict marker encountered.
});
>>>>>>> 8c942cb (fix: address PR review — unique constraint, created_at, trailing newlines, revert lockfile)

Check failure on line 383 in src/app/api/metrics/streak/route.ts

View workflow job for this annotation

GitHub Actions / Type check

Merge conflict marker encountered.
}
87 changes: 87 additions & 0 deletions src/app/api/streak/freeze/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<<<<<<< HEAD

Check failure on line 1 in src/app/api/streak/freeze/route.ts

View workflow job for this annotation

GitHub Actions / Type check

Merge conflict marker encountered.
import type { NextRequest } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
Expand All @@ -9,21 +10,36 @@
metricsCacheKey,
withMetricsCache,
} from "@/lib/metrics-cache";
=======

Check failure on line 13 in src/app/api/streak/freeze/route.ts

View workflow job for this annotation

GitHub Actions / Type check

Merge conflict marker encountered.
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { supabaseAdmin } from "@/lib/supabase";
>>>>>>> 1337d90 (feat: add streak freeze feature (#37))

Check failure on line 17 in src/app/api/streak/freeze/route.ts

View workflow job for this annotation

GitHub Actions / Type check

Merge conflict marker encountered.

export const dynamic = "force-dynamic";

function todayStr(): string {
return new Date().toISOString().slice(0, 10);
}

<<<<<<< HEAD

Check failure on line 25 in src/app/api/streak/freeze/route.ts

View workflow job for this annotation

GitHub Actions / Type check

Merge conflict marker encountered.
<<<<<<< HEAD
// GET /api/streak/freeze
// Returns whether the user currently has an unused freeze available.
export async function GET(req: NextRequest) {
=======
=======
// GET /api/streak/freeze
>>>>>>> 8c942cb (fix: address PR review — unique constraint, created_at, trailing newlines, revert lockfile)
// Returns whether the user currently has an unused freeze available.
export async function GET() {
>>>>>>> 1337d90 (feat: add streak freeze feature (#37))
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

<<<<<<< HEAD
const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

Expand All @@ -43,33 +59,68 @@
}

async function getFreezeStatus(userId: string) {
=======
const { data: user } = await supabaseAdmin
.from("users")
.select("id")
.eq("github_id", session.githubId)
.single();

if (!user) return Response.json({ error: "User not found" }, { status: 404 });

>>>>>>> 1337d90 (feat: add streak freeze feature (#37))
const today = todayStr();

const { data: pending } = await supabaseAdmin
.from("streak_freezes")
.select("id, freeze_date")
<<<<<<< HEAD
.eq("user_id", userId)
=======
.eq("user_id", user.id)
>>>>>>> 1337d90 (feat: add streak freeze feature (#37))
.gte("freeze_date", today)
.limit(1);

const hasFreeze = Array.isArray(pending) && pending.length > 0;

<<<<<<< HEAD
return { hasFreeze, freezeDate: hasFreeze ? pending![0].freeze_date : null };
}

// POST /api/streak/freeze
=======
return Response.json({ hasFreeze, freezeDate: hasFreeze ? pending![0].freeze_date : null });
}
<<<<<<< HEAD
>>>>>>> 1337d90 (feat: add streak freeze feature (#37))
=======

// POST /api/streak/freeze
>>>>>>> 8c942cb (fix: address PR review — unique constraint, created_at, trailing newlines, revert lockfile)
// Inserts a freeze for today. Fails if the user already holds an unused freeze.
export async function POST() {
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

<<<<<<< HEAD
const user = await resolveAppUser(session.githubId, session.githubLogin);
=======
const { data: user } = await supabaseAdmin
.from("users")
.select("id")
.eq("github_id", session.githubId)
.single();

>>>>>>> 1337d90 (feat: add streak freeze feature (#37))
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

const today = todayStr();

<<<<<<< HEAD
<<<<<<< HEAD
// Prevent users from stockpiling unused freezes
const { count } = await supabaseAdmin
.from("streak_freezes")
Expand All @@ -86,10 +137,14 @@
);
}

=======
// only 1 unused freeze at a time
>>>>>>> 1337d90 (feat: add streak freeze feature (#37))
const { data: existing } = await supabaseAdmin
.from("streak_freezes")
.select("id")
.eq("user_id", user.id)
<<<<<<< HEAD
.eq("freeze_date", today)
.maybeSingle();

Expand All @@ -99,13 +154,38 @@
{ user_id: user.id, freeze_date: today },
{ onConflict: "user_id,freeze_date" }
)
=======
.gte("freeze_date", today)
.limit(1);

if (Array.isArray(existing) && existing.length > 0) {
return Response.json(
{ error: "You already have an unused streak freeze." },
{ status: 409 }
);
}

=======
>>>>>>> 8c942cb (fix: address PR review — unique constraint, created_at, trailing newlines, revert lockfile)
const { data: freeze, error } = await supabaseAdmin
.from("streak_freezes")
.insert({ user_id: user.id, freeze_date: today })
>>>>>>> 1337d90 (feat: add streak freeze feature (#37))
.select()
.single();

if (error) {
// Unique constraint violation — already has a freeze for today
if (error.code === "23505") {
return Response.json(
{ error: "You already have an unused streak freeze." },
{ status: 409 }
);
}
return Response.json({ error: "Failed to apply freeze." }, { status: 500 });
}

<<<<<<< HEAD
const alreadyExisted = existing !== null;
const statusCode = alreadyExisted ? 200 : 201;

Expand Down Expand Up @@ -137,3 +217,10 @@

return Response.json({ success: true });
}
=======
return Response.json({ freeze }, { status: 201 });
}
<<<<<<< HEAD
>>>>>>> 1337d90 (feat: add streak freeze feature (#37))
=======
>>>>>>> 8c942cb (fix: address PR review — unique constraint, created_at, trailing newlines, revert lockfile)
44 changes: 43 additions & 1 deletion src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export default async function DashboardPage() {
const session = await getServerSession(authOptions);
if (!session) redirect("/");

<<<<<<< HEAD
return (
<DashboardSSEProvider>
<div className="min-h-screen bg-[var(--background)] px-4 py-8 text-[var(--foreground)] transition-colors sm:px-6 lg:px-8 max-w-[1600px] mx-auto">
Expand Down Expand Up @@ -265,5 +266,46 @@ export default async function DashboardPage() {
</section>
</div>
</DashboardSSEProvider>
=======
if (!session) {
redirect("/");
}

return (
<div
className="min-h-screen bg-[var(--background)] p-4 md:p-8 text-[var(--foreground)] transition-colors"
>
<DashboardHeader />

<main id="main-content" aria-label="Developer dashboard">
{/* Live region: announces dynamic alerts to screen readers */}
<div aria-live="polite" aria-atomic="true" className="sr-only" id="live-announcer" />

<StreakAtRiskBanner />

{/* Row 1: Contribution graph + Streak */}
<section aria-label="Activity overview" className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<ContributionGraph />
</div>
<div className="flex flex-col gap-6">
<StreakTracker />
</div>
</section>

{/* Row 2: PR metrics */}
<section aria-label="Pull request analytics" className="mt-6">
<PRMetrics />
</section>

{/* Row 3: Top repos + Language breakdown + Goal tracker */}
<section aria-label="Repositories, languages, and goals" className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
<TopRepos />
<LanguageBreakdown />
<GoalTracker />
</section>
</main>
</div>
>>>>>>> 393b334 (fix: add keyboard navigation and ARIA labels for accessibility (closes #1308))
);
}
}
Loading
Loading