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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user