feat: scaffold PulseNode dashboard (M1)
Next.js 16 + TypeScript + Tailwind v4 app with a YAML-driven config system: schema validation (zod), .env interpolation, and a widget registry rendering bookmark/search/group widgets. CSS custom properties (--pn-*) drive theming and are overridable from config.yml's theme.variables. Config load errors surface as a readable error page instead of crashing the app. Hot reload, docker/system/http collectors, and the WebSocket push layer land in later milestones per the approved plan.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
import type { SearchEngine, Widget } from "@/lib/config/schema";
|
||||
|
||||
type SearchWidget = Extract<Widget, { type: "search" }>;
|
||||
|
||||
const ENGINE_URLS: Record<string, string> = {
|
||||
duckduckgo: "https://duckduckgo.com/?q=",
|
||||
google: "https://www.google.com/search?q=",
|
||||
bing: "https://www.bing.com/search?q=",
|
||||
};
|
||||
|
||||
export function SearchWidget({ widget }: { widget: SearchWidget }) {
|
||||
const [engine, setEngine] = useState(widget.defaultEngine ?? widget.engines[0]);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
function handleSubmit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) return;
|
||||
window.open(`${ENGINE_URLS[engine]}${encodeURIComponent(trimmed)}`, "_blank", "noreferrer");
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex items-center gap-2 rounded-[var(--radius-widget)] border border-border bg-surface p-2 sm:col-span-2"
|
||||
>
|
||||
{widget.engines.length > 1 && (
|
||||
<select
|
||||
value={engine}
|
||||
onChange={(event) => setEngine(event.target.value as SearchEngine)}
|
||||
className="rounded bg-transparent text-xs text-fg-muted outline-none"
|
||||
>
|
||||
{widget.engines.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search..."
|
||||
className="flex-1 bg-transparent text-sm text-fg outline-none placeholder:text-fg-muted"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user