Files
doormile_react/src/components/blog/BlogSearch.tsx

122 lines
3.5 KiB
TypeScript

"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-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>
);
}