96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* PageHeader — per-page sub-header carrying the page title / breadcrumb.
|
|
*
|
|
* The global SiteHeader (mounted in app/layout.tsx) owns the wordmark,
|
|
* primary nav, search, reading settings, and user menu. PageHeader is now
|
|
* purely typographic — it renders whatever `children` the page passes and
|
|
* applies a subtle bordered band so the page's title area has visual weight
|
|
* separate from the body content.
|
|
*
|
|
* Behaviour preserved from the previous (busier) implementation:
|
|
* • If sessionStorage has `spsays_from_search`, the first <Link> child is
|
|
* hidden and replaced with a "← Back to search results" button. This
|
|
* keeps the cross-page-navigation polish that the search page relies on.
|
|
*
|
|
* Pages that don't want a sub-header (e.g. the redesigned homepage which
|
|
* paints its own editorial hero) can simply not render PageHeader.
|
|
*/
|
|
|
|
import { useState } from "react";
|
|
import { useRouter, usePathname } from "next/navigation";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const SEARCH_RETURN_KEY = "spsays_from_search";
|
|
|
|
export function PageHeader({
|
|
children,
|
|
className,
|
|
bare = false,
|
|
sticky = false,
|
|
}: {
|
|
children: React.ReactNode;
|
|
/** Drop the bordered chrome — useful for pages that want flush title text. */
|
|
bare?: boolean;
|
|
/**
|
|
* Stick to the top below the global SiteHeader. Used by reading pages so
|
|
* the chapter title stays visible as the user scrolls long content.
|
|
* The offset is `top-14 md:top-16` to clear SiteHeader's height.
|
|
*/
|
|
sticky?: boolean;
|
|
className?: string;
|
|
}) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
|
|
// Read (and consume) the search-return flag once on first render.
|
|
const [fromSearch, setFromSearch] = useState(() => {
|
|
if (typeof window === "undefined") return false;
|
|
try {
|
|
if (
|
|
sessionStorage.getItem(SEARCH_RETURN_KEY) === "1" &&
|
|
!pathname.startsWith("/search")
|
|
) {
|
|
sessionStorage.removeItem(SEARCH_RETURN_KEY);
|
|
return true;
|
|
}
|
|
} catch {
|
|
// sessionStorage unavailable
|
|
}
|
|
return false;
|
|
});
|
|
|
|
return (
|
|
<header
|
|
className={cn(
|
|
"px-4 sm:px-6",
|
|
bare ? "py-6" : "border-b border-border bg-card/85 backdrop-blur py-5",
|
|
sticky && "sticky top-14 md:top-16 z-20",
|
|
className,
|
|
)}
|
|
>
|
|
<div className="max-w-5xl mx-auto">
|
|
{fromSearch ? (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setFromSearch(false);
|
|
router.back();
|
|
}}
|
|
className="block text-xs text-muted-foreground hover:text-foreground transition-colors mb-2"
|
|
>
|
|
← Back to search results
|
|
</button>
|
|
{/* Hide the page's own breadcrumb link (always the first <a>) */}
|
|
<div className="[&>a:first-child]:hidden">{children}</div>
|
|
</>
|
|
) : (
|
|
children
|
|
)}
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|