Skip to content
Merged
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
9 changes: 9 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ export const metadata: Metadata = {

export const viewport: Viewport = {
colorScheme: "dark light",
// Matches the light/dark backgrounds in globals.css so mobile browser
// chrome (the address/status bar) doesn't clash with the page the
// instant it loads — without this, browsers fall back to their own
// default (usually white), which visibly contradicts a near-black dark
// mode page (#224).
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#fbfbfd" },
{ media: "(prefers-color-scheme: dark)", color: "#0a0a0f" },
],
};

export default function RootLayout({
Expand Down
11 changes: 10 additions & 1 deletion src/components/dashboard/DashboardShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,16 @@ export function DashboardShell({
</p>
<nav className="mt-3 space-y-1">
{items.map((item) => {
const active = pathname === item.href;
// Overview links (/dashboard/*) use exact matching so they
// don't stay highlighted while browsing unrelated sections.
// Every other item (e.g. /issues, /milestones) also matches
// its nested routes (/issues/abc123), so drilling into a
// specific bounty/milestone keeps its parent section
// highlighted instead of the sidebar appearing to have
// nothing selected (#220).
const active =
pathname === item.href ||
(!item.href.startsWith("/dashboard/") && pathname.startsWith(`${item.href}/`));
return (
<Link
key={item.label}
Expand Down
32 changes: 32 additions & 0 deletions src/components/layout/CopyrightYear.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import { useEffect, useState } from "react";

/**
* Footer is a Server Component with no dynamic API usage of its own, so
* Next.js can statically optimize routes that render it — new Date() called
* server-side is then evaluated once, at build time, not per-request. On a
* statically-optimized route this silently froze the displayed year at
* whatever `next build` was run, with nothing correcting it for the rest of
* that build's lifetime (#221).
*
* Isolating just the year into this small Client Component lets it
* self-correct on every page load: the initial render (SSR/build-time) uses
* whatever Date.now() was available then, but the effect below always runs
* client-side, on every hydration, and re-reads the browser's actual
* current date — so a stale build-time year gets fixed the moment any
* visitor's browser hydrates the page, rather than staying wrong until the
* next deploy.
*/
export function CopyrightYear() {
const [year, setYear] = useState(() => new Date().getFullYear());

useEffect(() => {
// Corrects a stale build-time year on mount — the same pattern used
// elsewhere in this app for reconciling client-only state after hydration.
// eslint-disable-next-line react-hooks/set-state-in-effect
setYear(new Date().getFullYear());
}, []);

return <>{year}</>;
}
5 changes: 4 additions & 1 deletion src/components/layout/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from "next/link";
import { GitMerge } from "lucide-react";
import { CopyrightYear } from "./CopyrightYear";

const columns = [
{
Expand Down Expand Up @@ -56,7 +57,9 @@ export function Footer() {
))}
</div>
<div className="mt-12 flex flex-col items-center justify-between gap-4 border-t border-slate-100 pt-6 text-sm text-slate-400 dark:border-slate-800 dark:text-slate-500 sm:flex-row">
<p>© {new Date().getFullYear()} MergeFi. All rights reserved.</p>
<p>
© <CopyrightYear /> MergeFi. All rights reserved.
</p>
<p>Where open source meets finance.</p>
</div>
</div>
Expand Down
15 changes: 13 additions & 2 deletions src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,22 @@ export function Navbar() {
</Link>
))}
<div className="group relative">
<button className="flex items-center gap-1 hover:text-slate-900 dark:hover:text-white">
<button
aria-haspopup="menu"
className="flex items-center gap-1 hover:text-slate-900 dark:hover:text-white"
>
Dashboards
<ChevronDown className="h-3.5 w-3.5" />
</button>
<div className="invisible absolute left-0 top-full pt-3 opacity-0 transition-all group-hover:visible group-hover:opacity-100">
{/* group-focus-within alongside group-hover: the submenu was
only reachable via mouse hover — a keyboard user tabbing
to the trigger never made it visible, and the links
inside stayed in the tab order (invisible/opacity-0, not
display: none) so focus could land on an invisible link
(#222). focus-within keeps it open while focus is
anywhere inside this wrapper, including on the links
themselves. */}
<div className="invisible absolute left-0 top-full pt-3 opacity-0 transition-all group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100">
<div className="w-44 rounded-xl border border-slate-200 bg-white p-1.5 shadow-lg shadow-slate-900/5 dark:border-slate-800 dark:bg-slate-900">
{dashboardLinks.map((link) => (
<Link
Expand Down
Loading