Commit 3: Suche im Frontend mit echten Backend-Daten, Ranking, Tastatur-Navigation

This commit is contained in:
2026-07-19 02:00:13 +02:00
parent a5b998b421
commit f963f7eb4e
10 changed files with 224 additions and 16 deletions

View File

@@ -10,6 +10,7 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@launchpad/shared": "workspace:*",
"react": "^18.3.1"
},
"devDependencies": {

View File

@@ -0,0 +1,93 @@
import type { Service } from "@launchpad/shared";
export interface ResultsListProps {
services: Service[];
selectedIndex: number;
emptyLabel?: string;
onHover: (index: number) => void;
onOpen: (service: Service) => void;
}
/**
* Zeigt die (bereits per rankServices sortierten) Suchtreffer an.
* Die Tastatur-Navigation (Pfeiltasten/Enter) wird vom Elternelement
* gesteuert; diese Komponente ist rein darstellend + klickbar.
*/
export function ResultsList({
services,
selectedIndex,
emptyLabel = "Keine Dienste gefunden.",
onHover,
onOpen,
}: ResultsListProps) {
if (services.length === 0) {
return (
<div
className="mt-4 rounded-2xl border border-black/5 bg-white/50 px-5 py-8 text-center
text-sm text-black/40 dark:border-white/5 dark:bg-white/5 dark:text-white/40"
>
{emptyLabel}
</div>
);
}
return (
<ul
role="listbox"
className="mt-4 flex max-h-[60vh] flex-col overflow-y-auto rounded-2xl border
border-black/10 bg-white/80 shadow-lg backdrop-blur-md dark:border-white/10
dark:bg-white/5"
>
{services.map((service, index) => {
const active = index === selectedIndex;
return (
<li key={service.id} role="option" aria-selected={active}>
<button
type="button"
onMouseEnter={() => onHover(index)}
onClick={() => onOpen(service)}
className={`flex w-full items-center gap-3 px-5 py-3 text-left transition-colors ${
active
? "bg-black/5 dark:bg-white/10"
: "hover:bg-black/[0.03] dark:hover:bg-white/5"
}`}
>
{service.favicon ? (
<img src={service.favicon} alt="" className="h-5 w-5 shrink-0 rounded" />
) : (
<span
className="flex h-5 w-5 shrink-0 items-center justify-center rounded
bg-black/10 text-[10px] font-medium text-black/50 dark:bg-white/10
dark:text-white/50"
>
{service.displayName.charAt(0).toUpperCase()}
</span>
)}
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-black dark:text-white">
{service.displayName}
</span>
<span className="block truncate text-xs text-black/40 dark:text-white/40">
{service.hostname}
</span>
</span>
{service.category ? (
<span className="shrink-0 text-xs text-black/30 dark:text-white/30">
{service.category}
</span>
) : null}
{service.favorite ? (
<span className="shrink-0 text-amber-500" aria-label="Favorit">
</span>
) : null}
</button>
</li>
);
})}
</ul>
);
}

View File

@@ -3,3 +3,6 @@ export type { SearchInputProps } from "./SearchInput.js";
export { StatusBadge } from "./StatusBadge.js";
export type { StatusBadgeProps } from "./StatusBadge.js";
export { ResultsList } from "./ResultsList.js";
export type { ResultsListProps } from "./ResultsList.js";