feat: Refactored SearchAutocomplete from heroSize boolean to variant st…
- "frontend/src/components/SearchAutocomplete.tsx" - "frontend/src/App.tsx" - "frontend/src/App.css" - "frontend/src/pages/Home.tsx" - "frontend/src/pages/SearchResults.tsx" GSD-Task: S03/T01
This commit is contained in:
parent
fa1fc82d5a
commit
fea0afdec0
5 changed files with 95 additions and 8 deletions
|
|
@ -1042,6 +1042,53 @@ a.app-footer__repo:hover {
|
||||||
border-radius: 0.625rem;
|
border-radius: 0.625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Nav search variant ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.search-container--nav {
|
||||||
|
position: relative;
|
||||||
|
max-width: 16rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form--nav {
|
||||||
|
gap: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input--nav {
|
||||||
|
padding: 0.375rem 2.75rem 0.375rem 0.75rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: var(--color-bg-input);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input--nav::placeholder {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-nav__shortcut {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
background: var(--color-bg-page);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.0625rem 0.3125rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container--nav .typeahead-dropdown {
|
||||||
|
z-index: 200;
|
||||||
|
min-width: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
.btn--search {
|
.btn--search {
|
||||||
background: var(--color-btn-search-bg);
|
background: var(--color-btn-search-bg);
|
||||||
color: var(--color-btn-search-text);
|
color: var(--color-btn-search-text);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Link, Navigate, Route, Routes } from "react-router-dom";
|
import { Link, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import SearchResults from "./pages/SearchResults";
|
import SearchResults from "./pages/SearchResults";
|
||||||
import TechniquePage from "./pages/TechniquePage";
|
import TechniquePage from "./pages/TechniquePage";
|
||||||
|
|
@ -11,14 +11,27 @@ import AdminPipeline from "./pages/AdminPipeline";
|
||||||
import About from "./pages/About";
|
import About from "./pages/About";
|
||||||
import AdminDropdown from "./components/AdminDropdown";
|
import AdminDropdown from "./components/AdminDropdown";
|
||||||
import AppFooter from "./components/AppFooter";
|
import AppFooter from "./components/AppFooter";
|
||||||
|
import SearchAutocomplete from "./components/SearchAutocomplete";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const showNavSearch = location.pathname !== "/";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
<Link to="/" className="app-header__brand">
|
<Link to="/" className="app-header__brand">
|
||||||
<h1>Chrysopedia</h1>
|
<h1>Chrysopedia</h1>
|
||||||
</Link>
|
</Link>
|
||||||
|
{showNavSearch && (
|
||||||
|
<SearchAutocomplete
|
||||||
|
variant="nav"
|
||||||
|
globalShortcut
|
||||||
|
placeholder="Search… ⌘K"
|
||||||
|
onSearch={(q) => navigate(`/search?q=${encodeURIComponent(q)}`)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="app-header__right">
|
<div className="app-header__right">
|
||||||
<nav className="app-nav">
|
<nav className="app-nav">
|
||||||
<Link to="/">Home</Link>
|
<Link to="/">Home</Link>
|
||||||
|
|
|
||||||
|
|
@ -19,18 +19,26 @@ import {
|
||||||
interface SearchAutocompleteProps {
|
interface SearchAutocompleteProps {
|
||||||
onSearch: (query: string) => void;
|
onSearch: (query: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
/** @deprecated Use variant="hero" instead */
|
||||||
heroSize?: boolean;
|
heroSize?: boolean;
|
||||||
|
variant?: 'hero' | 'inline' | 'nav';
|
||||||
initialQuery?: string;
|
initialQuery?: string;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
|
/** When true, Cmd+K / Ctrl+K focuses the input globally */
|
||||||
|
globalShortcut?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SearchAutocomplete({
|
export default function SearchAutocomplete({
|
||||||
onSearch,
|
onSearch,
|
||||||
placeholder = "Search techniques…",
|
placeholder = "Search techniques…",
|
||||||
heroSize = false,
|
heroSize = false,
|
||||||
|
variant: variantProp,
|
||||||
initialQuery = "",
|
initialQuery = "",
|
||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
|
globalShortcut = false,
|
||||||
}: SearchAutocompleteProps) {
|
}: SearchAutocompleteProps) {
|
||||||
|
// Resolve variant: explicit prop wins, then legacy heroSize, then default 'inline'
|
||||||
|
const variant = variantProp ?? (heroSize ? 'hero' : 'inline');
|
||||||
const [query, setQuery] = useState(initialQuery);
|
const [query, setQuery] = useState(initialQuery);
|
||||||
const [searchResults, setSearchResults] = useState<SearchResultItem[]>([]);
|
const [searchResults, setSearchResults] = useState<SearchResultItem[]>([]);
|
||||||
const [popularSuggestions, setPopularSuggestions] = useState<SuggestionItem[]>([]);
|
const [popularSuggestions, setPopularSuggestions] = useState<SuggestionItem[]>([]);
|
||||||
|
|
@ -64,6 +72,19 @@ export default function SearchAutocomplete({
|
||||||
if (autoFocus) inputRef.current?.focus();
|
if (autoFocus) inputRef.current?.focus();
|
||||||
}, [autoFocus]);
|
}, [autoFocus]);
|
||||||
|
|
||||||
|
// Global Cmd+K / Ctrl+K shortcut to focus input
|
||||||
|
useEffect(() => {
|
||||||
|
if (!globalShortcut) return;
|
||||||
|
function handleGlobalKey(e: KeyboardEvent) {
|
||||||
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("keydown", handleGlobalKey);
|
||||||
|
return () => document.removeEventListener("keydown", handleGlobalKey);
|
||||||
|
}, [globalShortcut]);
|
||||||
|
|
||||||
// Close dropdown on outside click
|
// Close dropdown on outside click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClick(e: MouseEvent) {
|
function handleClick(e: MouseEvent) {
|
||||||
|
|
@ -145,15 +166,15 @@ export default function SearchAutocomplete({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="search-container" ref={dropdownRef}>
|
<div className={`search-container${variant === 'nav' ? ' search-container--nav' : ''}`} ref={dropdownRef}>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className={`search-form ${heroSize ? "search-form--hero" : "search-form--inline"}`}
|
className={`search-form search-form--${variant}`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="search"
|
type="search"
|
||||||
className={`search-input ${heroSize ? "search-input--hero" : "search-input--inline"}`}
|
className={`search-input search-input--${variant}`}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => handleInputChange(e.target.value)}
|
onChange={(e) => handleInputChange(e.target.value)}
|
||||||
|
|
@ -161,9 +182,14 @@ export default function SearchAutocomplete({
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
aria-label="Search techniques"
|
aria-label="Search techniques"
|
||||||
/>
|
/>
|
||||||
|
{variant !== 'nav' && (
|
||||||
<button type="submit" className="btn btn--search">
|
<button type="submit" className="btn btn--search">
|
||||||
Search
|
Search
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
{variant === 'nav' && (
|
||||||
|
<kbd className="search-nav__shortcut">⌘K</kbd>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{showDropdown && (showPopular || showSearch) && (
|
{showDropdown && (showPopular || showSearch) && (
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ export default function Home() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SearchAutocomplete
|
<SearchAutocomplete
|
||||||
heroSize
|
variant="hero"
|
||||||
autoFocus
|
autoFocus
|
||||||
onSearch={(q) => navigate(`/search?q=${encodeURIComponent(q)}`)}
|
onSearch={(q) => navigate(`/search?q=${encodeURIComponent(q)}`)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ export default function SearchResults() {
|
||||||
<div className="search-results-page">
|
<div className="search-results-page">
|
||||||
{/* Inline search bar */}
|
{/* Inline search bar */}
|
||||||
<SearchAutocomplete
|
<SearchAutocomplete
|
||||||
|
variant="inline"
|
||||||
initialQuery={q}
|
initialQuery={q}
|
||||||
onSearch={(newQ) =>
|
onSearch={(newQ) =>
|
||||||
navigate(`/search?q=${encodeURIComponent(newQ)}`, { replace: true })
|
navigate(`/search?q=${encodeURIComponent(newQ)}`, { replace: true })
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue