mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 02:32:47 +00:00
* website: plugin page design mockups (four directions, throwaway routes) * website: final plugin page mockup (A directory, B browse frame, list cards, D detail) * website: browse pages use a four-column card grid in the final plugin mockup * feat(plugins): keep registry installs behind a daemon flag, off by default Off restores the pre-registry behavior: bare owner/repo is GitHub shorthand and the daemon never resolves or contacts a registry on install, update preview, or update. Enable with pluginRegistryEnabled: true or PASEO_PLUGIN_REGISTRY_ENABLED=1. * feat(website): ship the plugin directory, browse pages, and single-column detail Directory: count, build/submit links, What's new, the nine categories, and Most installed with a This week / This month / All time switch. Browse pages at /plugins/all and /plugins/category/<slug> with a category sidebar, tabs, and a four-column grid; old /plugins?category= links redirect. Install counts gain daily KV buckets kept 60 days. * fix(website): keep a README's leading GitHub alert on the plugin page * feat(website): search plugins from the directory and browse pages Typing replaces the URL with /plugins/all?q=<term> (or the current category with ?q=), which renders matching plugins on the server. Matching covers name, description, ID, and author. Old /plugins?q= links redirect. * fix(website): keep the directory's ranking window when searching
54 lines
2.1 KiB
TypeScript
54 lines
2.1 KiB
TypeScript
import { useNavigate } from "@tanstack/react-router";
|
|
import { Search } from "lucide-react";
|
|
import { type ChangeEvent, type FormEvent, useCallback, useState } from "react";
|
|
import { type BrowseQuery, browseHref, DEFAULT_WINDOW } from "./links";
|
|
|
|
/**
|
|
* Search box for the plugin pages. Typing replaces the URL with the browse page for the term,
|
|
* keeping `scope`'s category and sort. Without JavaScript the form submits the same URL.
|
|
*/
|
|
export function PluginSearch({ scope, className }: { scope: BrowseQuery; className?: string }) {
|
|
const navigate = useNavigate();
|
|
const [term, setTerm] = useState(scope.q ?? "");
|
|
const handleChange = useCallback(
|
|
(event: ChangeEvent<HTMLInputElement>) => {
|
|
const next = event.target.value;
|
|
setTerm(next);
|
|
void navigate({
|
|
href: browseHref({ ...scope, q: next.trim() ? next : undefined }),
|
|
replace: true,
|
|
});
|
|
},
|
|
[navigate, scope],
|
|
);
|
|
const handleSubmit = useCallback((event: FormEvent) => event.preventDefault(), []);
|
|
return (
|
|
<form
|
|
role="search"
|
|
method="get"
|
|
action={browseHref({ ...scope, q: undefined, sort: "installs", window: DEFAULT_WINDOW })}
|
|
onSubmit={handleSubmit}
|
|
className={className}
|
|
>
|
|
{scope.sort === "new" && <input type="hidden" name="sort" value="new" />}
|
|
{scope.sort === "installs" && scope.window !== DEFAULT_WINDOW && (
|
|
<input type="hidden" name="window" value={scope.window} />
|
|
)}
|
|
<label className="relative block">
|
|
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-extra-muted-foreground" />
|
|
<input
|
|
type="search"
|
|
name="q"
|
|
value={term}
|
|
onChange={handleChange}
|
|
placeholder="Search plugins"
|
|
aria-label="Search plugins"
|
|
// Typing on the directory lands here; keep the caret in the box.
|
|
autoFocus={Boolean(scope.q)}
|
|
className="w-full rounded-lg border border-white/10 bg-white/[0.03] py-1.5 pl-8 pr-3 text-sm text-foreground placeholder:text-extra-muted-foreground focus:border-white/20 focus:outline-none"
|
|
/>
|
|
</label>
|
|
</form>
|
|
);
|
|
}
|