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
64 changes: 48 additions & 16 deletions src/components/DashboardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import SignOutButton from "@/components/SignOutButton";
import ThemeToggle from "@/components/ThemeToggle";
import UserAvatar from "@/components/UserAvatar";
import KeyboardShortcuts from "@/components/KeyboardShortcuts";
import SyncDataButton from "@/components/SyncDataButton";
import OnboardingTour from "@/components/OnboardingTour";
import { Moon, Sun } from "lucide-react";
import { toast } from "sonner";
Expand Down Expand Up @@ -51,7 +52,7 @@ const STALE_TIMES: Record<string, number> = {
"/api/metrics/repos": 5 * 60 * 1000,
"/api/metrics/languages": 5 * 60 * 1000,
"/api/notifications": 1 * 60 * 1000,
"default": 2 * 60 * 1000,
default: 2 * 60 * 1000,
};

function getStaleTime(url: string): number {
Expand Down Expand Up @@ -101,7 +102,9 @@ export function DashboardSyncProvider({ children }: { children: ReactNode }) {
useEffect(() => {
if (process.env.NODE_ENV === "test") return;
const handleSync = () => {
console.log("[Client Cache] Invalidating dashboard metrics cache due to sync event.");
console.log(
"[Client Cache] Invalidating dashboard metrics cache due to sync event."
);
clientCache.clear();
};

Expand All @@ -116,13 +119,22 @@ export function DashboardSyncProvider({ children }: { children: ReactNode }) {
const originalFetch = window.fetch;

window.fetch = async (input, init) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
const isGet = !init || !init.method || init.method.toUpperCase() === "GET";
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url;
const isGet =
!init || !init.method || init.method.toUpperCase() === "GET";

if (isDashboardDataRequest(input)) {
if (!isGet) {
// Clear cache on write requests (POST, PUT, DELETE, PATCH)
console.log("[Client Cache] Invalidate cache due to mutation request:", url);
console.log(
"[Client Cache] Invalidate cache due to mutation request:",
url
);
clientCache.clear();
return originalFetch(input, init);
}
Expand Down Expand Up @@ -150,7 +162,10 @@ export function DashboardSyncProvider({ children }: { children: ReactNode }) {

const nowTime = new Date();
setLastSynced(nowTime);
localStorage.setItem("devtrack-last-synced", nowTime.toISOString());
localStorage.setItem(
"devtrack-last-synced",
nowTime.toISOString()
);
}
return response;
} catch (error) {
Expand Down Expand Up @@ -258,7 +273,7 @@ export default function DashboardHeader() {
const { isLive: isHeaderLive } = useRealtimeSync(
"users",
["UPDATE"],
loadSettings,
loadSettings
);
useEffect(() => {
if (!session?.githubLogin) return;
Expand All @@ -285,7 +300,10 @@ export default function DashboardHeader() {
if (nightOwlCommitsCount >= 1) setIsNightOwl(true);
if (earlyBirdCommitsCount >= 1) setIsEarlyBird(true);
} catch (err) {
console.error("Failed to compile milestone hour distribution profiles:", err);
console.error(
"Failed to compile milestone hour distribution profiles:",
err
);
}
}

Expand All @@ -298,7 +316,8 @@ export default function DashboardHeader() {
const [now, setNow] = useState(() => Date.now());

// Extract a fallback username parameter from active session data strings
const displayName = session?.user?.name || session?.githubLogin || "Developer";
const displayName =
session?.user?.name || session?.githubLogin || "Developer";
useEffect(() => {
if (!lastSynced) return;

Expand All @@ -318,7 +337,6 @@ export default function DashboardHeader() {
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-[var(--accent)]/40 to-transparent" />
<div className="pointer-events-none absolute -right-10 -top-12 h-32 w-32 rounded-full bg-[var(--accent)]/10 blur-3xl" />
<div className="relative z-10 flex min-w-0 flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">

{/* Left Section */}
<div className="min-w-0 pr-0">
<div className="mb-1 flex min-w-0 flex-wrap items-center gap-2">
Expand All @@ -327,7 +345,9 @@ export default function DashboardHeader() {
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--accent)] opacity-75"></span>
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[var(--accent)]"></span>
</span>
<span className="truncate">{greeting}, {displayName}!</span>
<span className="truncate">
{greeting}, {displayName}!
</span>
</div>
{isNightOwl && (
<div
Expand All @@ -351,7 +371,9 @@ export default function DashboardHeader() {
<div className="min-w-0">
<p
className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[var(--muted-foreground)]"
style={{ fontFamily: "var(--font-jetbrains, ui-monospace, monospace)" }}
style={{
fontFamily: "var(--font-jetbrains, ui-monospace, monospace)",
}}
>
Dashboard overview
</p>
Expand All @@ -360,7 +382,10 @@ export default function DashboardHeader() {
</h1>
<p
className="mt-2 max-w-xl text-sm leading-6 text-[var(--muted-foreground)]"
style={{ fontFamily: "var(--font-jetbrains, ui-monospace, monospace)", letterSpacing: "0.06em" }}
style={{
fontFamily: "var(--font-jetbrains, ui-monospace, monospace)",
letterSpacing: "0.06em",
}}
>
coding activity at a glance
</p>
Expand All @@ -370,7 +395,9 @@ export default function DashboardHeader() {
aria-atomic="true"
className="mt-1 flex items-center gap-1.5 text-xs text-[var(--muted-foreground)]"
>
{minutesAgo <= 0 ? "Synced just now" : `Synced ${minutesAgo} min ago`}
{minutesAgo <= 0
? "Synced just now"
: `Synced ${minutesAgo} min ago`}
{isHeaderLive && (
<span
title="Live — connected to Supabase Realtime"
Expand All @@ -395,8 +422,10 @@ export default function DashboardHeader() {
{isPublic === true && session?.githubLogin && (
<ShareProfileButton githubLogin={session.githubLogin} />
)}

<div className="flex shrink-0 items-center gap-2 rounded-2xl border border-[var(--border)] bg-[var(--card-muted)]/50 p-2 shadow-sm backdrop-blur-sm">
<div className="transition-transform duration-200 hover:scale-[1.05]">
<SyncDataButton />
</div>
<div className="transition-transform duration-200 hover:scale-[1.05]">
<KeyboardShortcuts />
</div>
Expand Down Expand Up @@ -468,6 +497,9 @@ export default function DashboardHeader() {
{menuOpen && (
<div className="mt-4 space-y-3 rounded-2xl border border-[var(--border)] bg-[var(--card-muted)]/70 p-4 shadow-sm backdrop-blur-sm sm:hidden">
<div className="flex flex-wrap items-center gap-2">
<div className="transition-transform duration-200 hover:scale-[1.05]">
<SyncDataButton />
</div>
<div className="transition-transform duration-200 hover:scale-[1.05]">
<KeyboardShortcuts />
</div>
Expand Down Expand Up @@ -503,4 +535,4 @@ export default function DashboardHeader() {
{!seenOnboarding && <OnboardingTour />}
</header>
);
}
}
38 changes: 38 additions & 0 deletions src/components/SyncDataButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import { RefreshCw } from "lucide-react";
import { toast } from "sonner";

export default function SyncDataButton() {
const [isSyncing, setIsSyncing] = useState(false);
const router = useRouter();

const handleSync = useCallback(async () => {
if (isSyncing) return;
setIsSyncing(true);
try {
window.dispatchEvent(new Event("devtrack:sync"));
router.refresh();
toast.success("Dashboard data synced");
} catch (error) {
console.error("Failed to sync dashboard data:", error);
toast.error("Failed to sync data");
} finally {
setTimeout(() => setIsSyncing(false), 600);
}
}, [isSyncing, router]);

return (
<button
onClick={handleSync}
disabled={isSyncing}
title="Sync Data"
aria-label="Sync dashboard data"
className="inline-flex items-center justify-center rounded-lg border border-[var(--border)] bg-[var(--control)] p-2 text-[var(--muted-foreground)] transition-colors hover:bg-[var(--control-hover)] hover:text-[var(--foreground)] disabled:opacity-60"
>
<RefreshCw className={"h-4 w-4 " + (isSyncing ? "animate-spin" : "")} />
</button>
);
}
Loading