fix blog page
This commit is contained in:
122
src/components/blog/BlogSearch.tsx
Normal file
122
src/components/blog/BlogSearch.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState, useRef, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { blogPosts } from "@/data/blog";
|
||||
|
||||
/**
|
||||
* Client-side blog search. The site is a static export, so there is no search
|
||||
* server — we filter the known posts in the browser and link straight to the
|
||||
* matching /blog/[slug] routes.
|
||||
*/
|
||||
export default function BlogSearch() {
|
||||
const [query, setQuery] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const results = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return [];
|
||||
return blogPosts
|
||||
.filter(
|
||||
(p) =>
|
||||
p.title.toLowerCase().includes(q) ||
|
||||
p.category.toLowerCase().includes(q) ||
|
||||
p.excerpt.toLowerCase().includes(q)
|
||||
)
|
||||
.slice(0, 6);
|
||||
}, [query]);
|
||||
|
||||
// Close the results panel on outside click.
|
||||
useEffect(() => {
|
||||
function onDocClick(e: MouseEvent) {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", onDocClick);
|
||||
return () => document.removeEventListener("mousedown", onDocClick);
|
||||
}, []);
|
||||
|
||||
const showPanel = open && query.trim().length > 0;
|
||||
|
||||
return (
|
||||
<div className="dm-blog-search" ref={containerRef}>
|
||||
<form
|
||||
role="search"
|
||||
className="dm-blog-search-form"
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="dm-blog-search-input" className="dm-sr-only">
|
||||
Search articles
|
||||
</label>
|
||||
<input
|
||||
id="dm-blog-search-input"
|
||||
type="search"
|
||||
className="dm-blog-search-input"
|
||||
placeholder="Search articles…"
|
||||
value={query}
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
setOpen(true);
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
aria-expanded={showPanel}
|
||||
aria-controls="dm-blog-search-results"
|
||||
/>
|
||||
<span className="dm-blog-search-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
</span>
|
||||
</form>
|
||||
|
||||
{showPanel && (
|
||||
<div
|
||||
id="dm-blog-search-results"
|
||||
className="dm-blog-search-results"
|
||||
role="listbox"
|
||||
>
|
||||
{results.length === 0 ? (
|
||||
<p className="dm-blog-search-empty">
|
||||
No articles match “{query.trim()}”.
|
||||
</p>
|
||||
) : (
|
||||
<ul>
|
||||
{results.map((p) => (
|
||||
<li key={p.slug} role="option" aria-selected="false">
|
||||
<Link
|
||||
href={`/blog/${p.slug}`}
|
||||
className="dm-blog-search-result"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<span className="dm-blog-search-result-cat">
|
||||
{p.category}
|
||||
</span>
|
||||
<span className="dm-blog-search-result-title">
|
||||
{p.title}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user