Commit 4: Kategorien-API (CRUD + Reorder) und Favoriten-Toggle im Frontend

This commit is contained in:
2026-07-19 02:10:08 +02:00
parent f963f7eb4e
commit 129b4253b5
9 changed files with 261 additions and 19 deletions

View File

@@ -1,3 +1,4 @@
import type { KeyboardEvent } from "react";
import type { Service } from "@launchpad/shared";
export interface ResultsListProps {
@@ -6,12 +7,13 @@ export interface ResultsListProps {
emptyLabel?: string;
onHover: (index: number) => void;
onOpen: (service: Service) => void;
onToggleFavorite?: (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.
* gesteuert; diese Komponente ist rein darstellend + klick-/tastaturbar.
*/
export function ResultsList({
services,
@@ -19,6 +21,7 @@ export function ResultsList({
emptyLabel = "Keine Dienste gefunden.",
onHover,
onOpen,
onToggleFavorite,
}: ResultsListProps) {
if (services.length === 0) {
return (
@@ -42,15 +45,19 @@ export function ResultsList({
const active = index === selectedIndex;
return (
<li key={service.id} role="option" aria-selected={active}>
<button
type="button"
<div
tabIndex={-1}
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"
}`}
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter") onOpen(service);
}}
className={`flex w-full cursor-pointer 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" />
@@ -79,12 +86,26 @@ export function ResultsList({
</span>
) : null}
{service.favorite ? (
<span className="shrink-0 text-amber-500" aria-label="Favorit">
</span>
) : null}
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onToggleFavorite?.(service);
}}
disabled={!onToggleFavorite}
aria-pressed={service.favorite}
aria-label={
service.favorite ? "Als Favorit entfernen" : "Als Favorit markieren"
}
className={`shrink-0 text-lg leading-none transition-colors ${
service.favorite
? "text-amber-500"
: "text-black/15 hover:text-amber-400 dark:text-white/15 dark:hover:text-amber-400"
} ${onToggleFavorite ? "" : "cursor-default"}`}
>
</button>
</div>
</li>
);
})}