Skip to content
Open
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
84 changes: 41 additions & 43 deletions src/components/PersonalRecords.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,31 @@

import { useCallback, useEffect, useState } from "react";
import { useAccount } from "@/components/AccountContext";

interface StreakData {
current: number;
longest: number;
lastCommitDate: string | null;
totalActiveDays: number;
}

interface ContributionData {
days: number;
total: number;
data: Record<string, number>;
}

interface Repo {
name: string;
commits: number;
url: string;
}

function getBestDay(data: Record<string, number>): { count: number; dateLabel: string | null } {
let maxCount = 0;
let bestDateStr: string | null = null;

for (const [dateStr, count] of Object.entries(data)) {
if (count > maxCount) {
maxCount = count;
bestDateStr = dateStr;
}
}

let dateLabel: string | null = null;
if (bestDateStr) {
const parts = bestDateStr.split("-").map(Number);
Expand All @@ -48,35 +42,29 @@ function getBestDay(data: Record<string, number>): { count: number; dateLabel: s
dateLabel = bestDateStr;
}
}

return { count: maxCount, dateLabel };
}

function getBestWeek(data: Record<string, number>): { count: number; weekLabel: string | null } {
const weeks: Record<string, number> = {};

for (const [dateStr, count] of Object.entries(data)) {
const parts = dateStr.split("-").map(Number);
if (parts.length === 3) {
const d = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2]));
const day = d.getUTCDay(); // 0 is Sunday, 1 is Monday
const diff = d.getUTCDate() - day + (day === 0 ? -6 : 1); // Monday week start
const day = d.getUTCDay();
const diff = d.getUTCDate() - day + (day === 0 ? -6 : 1);
const weekStart = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), diff));
const weekStr = weekStart.toISOString().slice(0, 10);
weeks[weekStr] = (weeks[weekStr] ?? 0) + count;
}
}

let maxCount = 0;
let bestWeekStr: string | null = null;

for (const [weekStr, count] of Object.entries(weeks)) {
if (count > maxCount) {
maxCount = count;
bestWeekStr = weekStr;
}
}

let weekLabel: string | null = null;
if (bestWeekStr) {
const parts = bestWeekStr.split("-").map(Number);
Expand All @@ -92,28 +80,22 @@ function getBestWeek(data: Record<string, number>): { count: number; weekLabel:
weekLabel = bestWeekStr;
}
}

return { count: maxCount, weekLabel };
}

function getBestMonth(data: Record<string, number>): { count: number; monthLabel: string | null } {
const months: Record<string, number> = {};

for (const [dateStr, count] of Object.entries(data)) {
const monthKey = dateStr.slice(0, 7); // YYYY-MM
const monthKey = dateStr.slice(0, 7);
months[monthKey] = (months[monthKey] ?? 0) + count;
}

let maxCount = 0;
let bestMonthKey: string | null = null;

for (const [mKey, count] of Object.entries(months)) {
if (count > maxCount) {
maxCount = count;
bestMonthKey = mKey;
}
}

let monthLabel: string | null = null;
if (bestMonthKey) {
const parts = bestMonthKey.split("-").map(Number);
Expand All @@ -128,61 +110,49 @@ function getBestMonth(data: Record<string, number>): { count: number; monthLabel
monthLabel = bestMonthKey;
}
}

return { count: maxCount, monthLabel };
}

function getBusiestRepo(repos: Repo[]): { count: number; repoLabel: string | null } {
function getBusiestRepo(repos: Repo[]): { count: number; repoLabel: string | null; repoUrl: string | null } {
if (!repos || repos.length === 0) {
return { count: 0, repoLabel: null };
return { count: 0, repoLabel: null, repoUrl: null };
}

const best = repos[0];
if (!best) {
return { count: 0, repoLabel: null };
return { count: 0, repoLabel: null, repoUrl: null };
}

const shortName = best.name.split("/")[1] ?? best.name;
return { count: best.commits, repoLabel: shortName };
return { count: best.commits, repoLabel: shortName, repoUrl: best.url ?? null };
}

export default function PersonalRecords() {
const { selectedAccount } = useAccount();
const [streak, setStreak] = useState<StreakData | null>(null);
const [contributions, setContributions] = useState<ContributionData | null>(null);
const [repos, setRepos] = useState<Repo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

const fetchRecords = useCallback(async () => {
setLoading(true);
setError(null);

try {
const paramStreak =
selectedAccount !== null
? `?accountId=${encodeURIComponent(selectedAccount)}`
: "";

const paramContrib =
selectedAccount !== null
? `&accountId=${encodeURIComponent(selectedAccount)}`
: "";

const [streakRes, contribRes, reposRes] = await Promise.all([
fetch(`/api/metrics/streak${paramStreak}`),
fetch(`/api/metrics/contributions?days=365${paramContrib}`),
fetch(`/api/metrics/repos?days=365${paramContrib}`),
]);

if (!streakRes.ok || !contribRes.ok || !reposRes.ok) {
throw new Error("Failed to fetch personal records data");
}

const streakData = (await streakRes.json()) as StreakData;
const contribData = (await contribRes.json()) as ContributionData;
const reposData = (await reposRes.json()) as { repos: Repo[] };

setStreak(streakData);
setContributions(contribData);
setRepos(reposData.repos ?? []);
Expand All @@ -192,16 +162,13 @@ export default function PersonalRecords() {
setLoading(false);
}
}, [selectedAccount]);

useEffect(() => {
fetchRecords();
}, [fetchRecords, selectedAccount]);

const bestDay = getBestDay(contributions?.data ?? {});
const bestWeek = getBestWeek(contributions?.data ?? {});
const bestMonth = getBestMonth(contributions?.data ?? {});
const busiestRepo = getBusiestRepo(repos);

const records = [
{
label: "Longest Streak",
Expand All @@ -210,6 +177,7 @@ export default function PersonalRecords() {
subtext: "All time",
icon: "🏆",
isRepo: false,
repoUrl: null,
},
{
label: "Best Day",
Expand All @@ -218,6 +186,7 @@ export default function PersonalRecords() {
subtext: bestDay.dateLabel ?? "—",
icon: "⚡",
isRepo: false,
repoUrl: null,
},
{
label: "Best Week",
Expand All @@ -226,6 +195,7 @@ export default function PersonalRecords() {
subtext: bestWeek.weekLabel ?? "—",
icon: "🔥",
isRepo: false,
repoUrl: null,
},
{
label: "Most Active Month",
Expand All @@ -234,6 +204,7 @@ export default function PersonalRecords() {
subtext: bestMonth.monthLabel ?? "—",
icon: "📅",
isRepo: false,
repoUrl: null,
},
{
label: "Busiest Repo",
Expand All @@ -242,9 +213,9 @@ export default function PersonalRecords() {
subtext: busiestRepo.repoLabel ?? "—",
icon: "⭐",
isRepo: true,
repoUrl: busiestRepo.repoUrl ?? null,
},
];

return (
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm">
<h2 className="mb-4 text-lg font-semibold text-[var(--card-foreground)]">
Expand Down Expand Up @@ -299,12 +270,39 @@ export default function PersonalRecords() {
}`}
title={rec.subtext}
>
{rec.subtext}
{rec.isRepo && rec.repoUrl ? (
<a
href={rec.repoUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 hover:text-[var(--accent)] transition-colors"
title="Open in GitHub"
>
{rec.subtext}
<svg
xmlns="http://www.w3.org/2000/svg"
width="10"
height="10"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<polyline points="15 3 21 3 21 9" />
<line x1="10" y1="14" x2="21" y2="3" />
</svg>
</a>
) : (
rec.subtext
)}
</div>
</div>
))}
</div>
)}
</div>
);
}
}