- {data.total.toLocaleString()}{" "}
- {data.total === 1 ? "result" : "results"} for “
- {data.query}”
+ {effectiveData.total.toLocaleString()}{" "}
+ {effectiveData.total === 1 ? "result" : "results"} for “
+ {effectiveData.query}”
- {data.total_pages > 1 && (
+ {effectiveData.total_pages > 1 && (
@@ -426,7 +437,7 @@ function SearchInner() {
{/* Result list */}
- {data.results.map((result, i) => (
+ {effectiveData.results.map((result, i) => (
{/* Bottom pagination */}
- {data.total_pages > 1 && (
+ {effectiveData.total_pages > 1 && (
@@ -541,7 +552,17 @@ function ResultCard({ result }: { result: SearchResult }) {
const Icon = config.icon;
return (
-
+ {
+ try {
+ sessionStorage.setItem("spsays_from_search", "1");
+ } catch {
+ /* sessionStorage unavailable */
+ }
+ }}
+ >
{/* Collection badge */}
diff --git a/components/header-search.tsx b/components/header-search.tsx
index af70af9b..0a941af5 100644
--- a/components/header-search.tsx
+++ b/components/header-search.tsx
@@ -1,8 +1,8 @@
"use client";
-import { useState, useRef, useEffect, useCallback } from "react";
-import { useRouter } from "next/navigation";
-import { motion, AnimatePresence } from "framer-motion";
+import { useRef, useEffect, useCallback } from "react";
+import { useRouter, usePathname } from "next/navigation";
+import { motion } from "framer-motion";
import { Search, X } from "lucide-react";
import { cn } from "@/lib/utils";
@@ -11,22 +11,42 @@ import { cn } from "@/lib/utils";
* into a full search input on click. Typing a query and pressing Enter
* (or clicking the search icon again) navigates to /search?q=...
*
- * The expansion/collapse is animated with framer-motion.
+ * State is controlled by the parent (PageHeader) so the header can
+ * hide/show adjacent elements (logo, reading settings) on mobile.
+ *
* Pressing Escape or clicking outside collapses the input.
* Keyboard shortcut: "/" focuses the search from anywhere on the page.
*/
-export function HeaderSearchButton() {
- const [expanded, setExpanded] = useState(false);
- const [query, setQuery] = useState("");
+/** Map the current pathname to the search `type` filter value. */
+function searchTypeFromPath(pathname: string): string | null {
+ if (pathname.startsWith("/books")) return "books";
+ if (pathname.startsWith("/letters")) return "letters";
+ if (pathname.startsWith("/essays")) return "essays";
+ if (pathname.startsWith("/transcriptions")) return "transcriptions";
+ if (pathname.startsWith("/glossary")) return "glossary";
+ if (pathname.startsWith("/journals")) return "journals";
+ if (pathname.startsWith("/magazines")) return "magazines";
+ if (pathname.startsWith("/other-documents")) return "other_documents";
+ return null; // homepage, search page, about, etc. → "all"
+}
+
+export function HeaderSearchButton({
+ expanded,
+ onExpandedChange,
+}: {
+ expanded: boolean;
+ onExpandedChange: (expanded: boolean) => void;
+}) {
+ const queryRef = useRef("");
const inputRef = useRef(null);
const containerRef = useRef(null);
const router = useRouter();
+ const pathname = usePathname();
// Focus input when expanded
useEffect(() => {
if (expanded && inputRef.current) {
- // Small delay to let the animation start before focusing
- const timer = setTimeout(() => inputRef.current?.focus(), 50);
+ const timer = setTimeout(() => inputRef.current?.focus(), 80);
return () => clearTimeout(timer);
}
}, [expanded]);
@@ -40,18 +60,17 @@ export function HeaderSearchButton() {
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
- setExpanded(false);
+ onExpandedChange(false);
}
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
- }, [expanded]);
+ }, [expanded, onExpandedChange]);
// Global "/" shortcut to open search
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
- // Don't trigger if user is typing in an input/textarea
const target = e.target as HTMLElement;
if (
target.tagName === "INPUT" ||
@@ -64,25 +83,29 @@ export function HeaderSearchButton() {
if (e.key === "/") {
e.preventDefault();
- setExpanded(true);
+ onExpandedChange(true);
}
}
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
- }, []);
+ }, [onExpandedChange]);
const handleSubmit = useCallback(() => {
- const trimmed = query.trim();
- if (trimmed) {
- router.push(`/search?q=${encodeURIComponent(trimmed)}`);
- setExpanded(false);
- setQuery("");
- } else {
- router.push("/search");
- setExpanded(false);
- }
- }, [query, router]);
+ const trimmed = queryRef.current.trim();
+ const params = new URLSearchParams();
+ if (trimmed) params.set("q", trimmed);
+
+ // Pre-select the search filter matching the current section
+ const type = searchTypeFromPath(pathname);
+ if (type) params.set("type", type);
+
+ const qs = params.toString();
+ router.push(`/search${qs ? `?${qs}` : ""}`);
+ onExpandedChange(false);
+ queryRef.current = "";
+ if (inputRef.current) inputRef.current.value = "";
+ }, [router, onExpandedChange, pathname]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -91,83 +114,91 @@ export function HeaderSearchButton() {
handleSubmit();
} else if (e.key === "Escape") {
e.preventDefault();
- setExpanded(false);
- setQuery("");
+ onExpandedChange(false);
+ queryRef.current = "";
+ if (inputRef.current) inputRef.current.value = "";
}
},
- [handleSubmit],
+ [handleSubmit, onExpandedChange],
);
+ const collapse = useCallback(() => {
+ onExpandedChange(false);
+ queryRef.current = "";
+ if (inputRef.current) inputRef.current.value = "";
+ }, [onExpandedChange]);
+
return (
-
-
- {expanded ? (
-
-
- setQuery(e.target.value)}
- onKeyDown={handleKeyDown}
- placeholder="Search..."
- className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none min-w-0"
- />
-
-
- ) : (
-
+ {expanded ? (
+
+
- )}
-
+
+
+
{
+ queryRef.current = e.target.value;
+ }}
+ onKeyDown={handleKeyDown}
+ placeholder="Search..."
+ className="flex-1 bg-transparent text-md md:text-sm text-foreground placeholder:text-muted-foreground outline-none min-w-0"
+ />
+
+
+ ) : (
+
onExpandedChange(true)}
+ aria-label="Search (press /)"
+ title="Search (press /)"
+ className={cn(
+ "size-9 inline-flex items-center justify-center rounded-lg",
+ "text-muted-foreground hover:text-foreground hover:bg-muted",
+ "transition-colors outline-none",
+ "focus-visible:ring-2 focus-visible:ring-ring",
+ )}
+ >
+
+
+ )}
);
}
diff --git a/components/page-header.tsx b/components/page-header.tsx
index e23c63ce..facf0e24 100644
--- a/components/page-header.tsx
+++ b/components/page-header.tsx
@@ -1,7 +1,13 @@
+"use client";
+
+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,
@@ -11,6 +17,29 @@ export function PageHeader({
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 {
+ if (
+ sessionStorage.getItem(SEARCH_RETURN_KEY) === "1" &&
+ !pathname.startsWith("/search")
+ ) {
+ sessionStorage.removeItem(SEARCH_RETURN_KEY);
+ return true;
+ }
+ } catch {
+ // sessionStorage unavailable
+ }
+ return false;
+ });
+
return (