Skip to content
Open
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
108 changes: 61 additions & 47 deletions package-lock.json

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

9 changes: 8 additions & 1 deletion src/components/CIAnalytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,17 @@ export default function CIAnalytics() {
</div>

{loading ? (
<div className="grid grid-cols-2 gap-4">
<div
role="status"
aria-live="polite"
aria-busy="true"
className="grid grid-cols-2 gap-4"
>
<span className="sr-only">Loading CI analytics</span>
{[1, 2, 3, 4].map((item) => (
<div
key={item}
aria-hidden="true"
className="h-20 rounded-lg bg-[var(--card-muted)] animate-pulse"
/>
))}
Expand Down
9 changes: 8 additions & 1 deletion src/components/CommitTimeChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,17 @@ export default function CommitTimeChart() {

<div className="flex-1 min-h-[250px]">
{loading ? (
<div className="flex h-full flex-col justify-end space-y-3 pt-6 pb-2">
<div
role="status"
aria-live="polite"
aria-busy="true"
className="flex h-full flex-col justify-end space-y-3 pt-6 pb-2"
>
<span className="sr-only">Loading commit time chart</span>
{[1, 2, 3, 4].map((i) => (
<div
key={i}
aria-hidden="true"
className="h-10 rounded bg-[var(--card-muted)] animate-pulse"
/>
))}
Expand Down
12 changes: 11 additions & 1 deletion src/components/ContributionGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,17 @@ export default function ContributionGraph() {
</div>

{loading ? (
<div className="h-[220px] rounded border border-[var(--border)] bg-[var(--background)] animate-pulse" />
<div
role="status"
aria-live="polite"
aria-busy="true"
>
<span className="sr-only">Loading contribution graph</span>
<div
aria-hidden="true"
className="h-[220px] rounded border border-[var(--border)] bg-[var(--background)] animate-pulse"
/>
</div>
) : error ? (
<div className="flex h-[220px] items-center rounded-lg border border-[var(--border)] bg-[var(--background)] px-4">
<p className="text-sm text-[var(--muted-foreground)]">
Expand Down
56 changes: 55 additions & 1 deletion src/components/DashboardHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client"

import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useSession } from "next-auth/react";
import AccountToggle from "@/components/AccountToggle";
import SignOutButton from "@/components/SignOutButton";
Expand All @@ -10,7 +10,12 @@ import KeyboardShortcuts from "@/components/KeyboardShortcuts";

export default function DashboardHeader() {
const { data: session } = useSession();

const [isPublic, setIsPublic] = useState<boolean | null>(null);
const [syncing, setSyncing] = useState(false);
const [announcement, setAnnouncement] = useState("");
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);


useEffect(() => {
if (!session) {
Expand All @@ -36,8 +41,43 @@ export default function DashboardHeader() {
loadSettings();
}, [session]);

// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);

async function handleSync() {
if (syncing) return;
setSyncing(true);
setAnnouncement("");

try {
const res = await fetch("/api/metrics/contributions?days=30");
if (!res.ok) throw new Error("Sync failed");
setAnnouncement("Metrics refreshed successfully");
} catch {
setAnnouncement("Sync failed. Please try again.");
} finally {
setSyncing(false);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setAnnouncement(""), 3000);
}
}

return (
<header className="mb-8 border-b border-[var(--border)] p-4 pb-6">
{/* Visually hidden aria-live region for screen readers */}
<span
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcement}
</span>

<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-[var(--foreground)]">
Expand All @@ -49,6 +89,7 @@ export default function DashboardHeader() {
</div>

<div className="flex flex-wrap items-center gap-3">

{isPublic === true && session?.githubLogin && (
<a
href={`/u/${session.githubLogin}`}
Expand All @@ -60,7 +101,20 @@ export default function DashboardHeader() {
Share Profile
</a>
)}

<KeyboardShortcuts />

{/* Sync button */}
<button
type="button"
onClick={handleSync}
disabled={syncing}
aria-label="Sync metrics"
className="px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--control)] text-[var(--card-foreground)] text-sm font-medium hover:bg-[var(--accent)] hover:text-[var(--accent-foreground)] transition-colors disabled:opacity-50"
>
{syncing ? "Syncing..." : "Sync"}
</button>

<UserAvatar />
<ThemeToggle />
<SignOutButton />
Expand Down
Loading