feat: add auth
This commit is contained in:
+49
-69
@@ -1,29 +1,50 @@
|
||||
"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 { ReadingSettings } from "@/components/reading-settings";
|
||||
import { HeaderSearchButton } from "@/components/header-search";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const SEARCH_RETURN_KEY = "spsays_from_search";
|
||||
|
||||
export function PageHeader({
|
||||
children,
|
||||
sticky = false,
|
||||
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 [searchExpanded, setSearchExpanded] = useState(false);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
// Read (and consume) the search-return flag once on first render.
|
||||
// The lazy initialiser runs exactly once; on the server
|
||||
// (typeof window === "undefined") it returns false.
|
||||
const [fromSearch, setFromSearch] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
try {
|
||||
@@ -43,72 +64,31 @@ export function PageHeader({
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"border-b border-border bg-card px-6",
|
||||
sticky ? "py-4 sticky top-0 z-20" : "py-4",
|
||||
"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,
|
||||
)}
|
||||
>
|
||||
{/*
|
||||
Layout strategy:
|
||||
- Mobile: actions float to the right so the title text wraps around
|
||||
them on the first line and uses the FULL container width on every
|
||||
subsequent line (which is what `pr-*` cannot achieve since padding
|
||||
applies to all lines).
|
||||
- Desktop (md+): standard flex row so title and actions sit side-by-side.
|
||||
*/}
|
||||
<div className="max-w-5xl mx-auto md:flex md:items-start md:justify-between md:gap-4">
|
||||
{/* Actions — float on mobile (rendered first so float works correctly),
|
||||
inline flex on desktop. Order doesn't matter visually on desktop
|
||||
because the parent's `justify-between` places them at the end. */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1.5",
|
||||
// Desktop: inline at the right of the flex row
|
||||
"md:pt-1 md:order-1",
|
||||
// Mobile float (for title wrapping) only applies when collapsed.
|
||||
// When expanded the search bar fills the full header width and
|
||||
// should NOT have a left margin or be floated.
|
||||
searchExpanded
|
||||
? "max-md:w-full max-md:min-w-0"
|
||||
: "max-md:float-right max-md:ml-2 md:shrink-0",
|
||||
)}
|
||||
>
|
||||
<HeaderSearchButton
|
||||
expanded={searchExpanded}
|
||||
onExpandedChange={setSearchExpanded}
|
||||
/>
|
||||
{/* Hide reading settings on mobile when search is active */}
|
||||
<div className={cn(searchExpanded && "max-md:hidden")}>
|
||||
<ReadingSettings />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title / logo area — hidden on mobile when search is active */}
|
||||
<div
|
||||
className={cn(
|
||||
"md:flex-1 md:min-w-0",
|
||||
searchExpanded && "max-md:hidden",
|
||||
)}
|
||||
>
|
||||
{fromSearch ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFromSearch(false);
|
||||
router.back();
|
||||
}}
|
||||
className="block text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
← 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>
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user