P0-3/4/5: route error boundary, null guards, slug fallback, search

P0-3 (category page crash):
- Add src/app/error.tsx and src/app/global-error.tsx so any future render
  error shows a real ref id instead of the bare client-exception toast.
- NewsPageClient: null-guard every field accessed during search/category/
  sort filtering and article render (title, excerpt, author, tags,
  category, date, image, alt). The user-visible link
  /news?category=live-production now also hydrates filter state from
  ?category= and ?search= search-params, so the dropdown actually filters.

P0-4 (/articles/[slug] 404s):
- /articles/[slug] and /news/[slug]: on slug miss, redirect("/news")
  instead of notFound(). Dead links from the hardcoded NewsTicker /
  FeaturedBento / ArticleFeed arrays now land on the news index instead
  of dead-ending on 404.
- legacy-source: add searchLegacyArticles() (title/excerpt/author ilike).

P0-5 (header search):
- Header: wire Enter on the desktop and mobile search inputs and make
  both search icons clickable buttons. Submits to /search?q=<query>.
- New /search route: server component that runs searchLegacyArticles
  and renders results in the same card style as /news.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-05-14 16:35:18 +00:00
parent 243f4855a1
commit 70e2aebb83
8 changed files with 335 additions and 66 deletions

View File

@@ -1,6 +1,7 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import AppImage from "@/components/ui/AppImage";
import {
LinkedInIcon,
@@ -68,10 +69,18 @@ const navLinks: NavItem[] = [
];
export default function Header() {
const router = useRouter();
const [scrolled, setScrolled] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [searchFocused, setSearchFocused] = useState(false);
const submitSearch = () => {
const q = searchQuery.trim();
if (!q) return;
setMobileOpen(false);
router.push(`/search?q=${encodeURIComponent(q)}`);
};
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const [currentUserEmail, setCurrentUserEmail] = useState<string | null>(null);
const [isAdmin, setIsAdmin] = useState(false);
@@ -440,6 +449,12 @@ export default function Header() {
onChange={(e) => setSearchQuery(e?.target?.value)}
onFocus={() => setSearchFocused(true)}
onBlur={() => setSearchFocused(false)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
submitSearch();
}
}}
className="search-input search-input-enhanced pr-16"
aria-label="Search articles (press / to focus)"
/>
@@ -456,18 +471,24 @@ export default function Header() {
<span className="search-kbd-hint" aria-hidden="true">/</span>
)}
{(searchQuery || searchFocused) && (
<SearchIcon
size={13}
strokeWidth={1.75}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[#3b82f6] pointer-events-none transition-colors"
/>
<button
type="button"
onClick={submitSearch}
aria-label="Submit search"
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[#3b82f6] hover:text-[#60a5fa] transition-colors cursor-pointer"
>
<SearchIcon size={13} strokeWidth={1.75} />
</button>
)}
{!searchQuery && !searchFocused && (
<SearchIcon
size={13}
strokeWidth={1.75}
className="absolute right-8 top-1/2 -translate-y-1/2 text-[#666] pointer-events-none"
/>
<button
type="button"
onClick={() => searchRef.current?.focus()}
aria-label="Focus search"
className="absolute right-8 top-1/2 -translate-y-1/2 text-[#666] hover:text-[#3b82f6] transition-colors cursor-pointer"
>
<SearchIcon size={13} strokeWidth={1.75} />
</button>
)}
</div>
</div>
@@ -585,14 +606,25 @@ export default function Header() {
<input
type="search"
placeholder="Search articles..."
value={searchQuery}
onChange={(e) => setSearchQuery(e?.target?.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
submitSearch();
}
}}
className="search-input search-input-enhanced w-full pr-8"
aria-label="Search articles mobile"
/>
<SearchIcon
size={13}
strokeWidth={1.75}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[#666] pointer-events-none"
/>
<button
type="button"
onClick={submitSearch}
aria-label="Submit search"
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[#666] hover:text-[#3b82f6] transition-colors cursor-pointer"
>
<SearchIcon size={13} strokeWidth={1.75} />
</button>
</div>
</div>
</div>