generated from Dicken/dickendock
round17+18: Emoji durch Font Awesome ersetzt, Lesezeichen-Kategorie-Vorschlaege, Favicon-Upload
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState, type DragEvent } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFloppyDisk, faPen, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faFloppyDisk, faPen, faTrash, faXmark, faSort, faSortUp, faSortDown, faStar as faStarSolid, faEye, faEyeSlash, faArrowLeft, faSignature } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faStar } from "@fortawesome/free-regular-svg-icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button, Favicon } from "@launchpad/ui";
|
||||
import type { Service } from "@launchpad/shared";
|
||||
@@ -116,9 +117,30 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
|
||||
const [hostname, setHostname] = useState(service.hostname);
|
||||
const [port, setPort] = useState(String(service.port));
|
||||
const [https, setHttps] = useState(service.https);
|
||||
// undefined = unverändert (Feld nicht anfassen), null = Nutzer hat's
|
||||
// explizit entfernt (fällt zurück auf automatische Erkennung/Buchstabe),
|
||||
// string = neu hochgeladenes Favicon als data:-URL.
|
||||
const [favicon, setFavicon] = useState<string | null | undefined>(undefined);
|
||||
const [faviconError, setFaviconError] = useState<string | null>(null);
|
||||
|
||||
const FAVICON_MAX_BYTES = 300 * 1024;
|
||||
|
||||
function handleFaviconFile(file: File | undefined) {
|
||||
setFaviconError(null);
|
||||
if (!file) return;
|
||||
if (file.size > FAVICON_MAX_BYTES) {
|
||||
setFaviconError("Datei zu groß (max. 300 KB) – bitte ein kleineres Bild wählen.");
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => setFavicon(reader.result as string);
|
||||
reader.onerror = () => setFaviconError("Datei konnte nicht gelesen werden.");
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
const portNumber = Number(port) || service.port;
|
||||
const previewUrl = `${https ? "https" : "http"}://${hostname || service.hostname}:${portNumber}`;
|
||||
const faviconPreview = favicon === undefined ? service.favicon : favicon;
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => {
|
||||
@@ -142,6 +164,7 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
|
||||
if (trimmedName !== service.displayName) patch.displayName = trimmedName;
|
||||
const trimmedCategory = category.trim() || null;
|
||||
if (trimmedCategory !== service.category) patch.category = trimmedCategory;
|
||||
if (favicon !== undefined) patch.favicon = favicon;
|
||||
return patchService(service.id, patch);
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -155,6 +178,32 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
|
||||
<tr className="border-b border-black/5 bg-black/[0.02] last:border-0 dark:border-white/5 dark:bg-white/5">
|
||||
<td colSpan={EDIT_FORM_COLSPAN} className="px-4 py-3">
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-black/50 dark:text-white/50">Favicon</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Favicon src={faviconPreview} fallbackLetter={displayName} size="sm" />
|
||||
<label className="cursor-pointer rounded-lg border border-black/10 px-2 py-1 text-xs
|
||||
text-black/70 hover:bg-black/5 dark:border-white/10 dark:text-white/70 dark:hover:bg-white/10">
|
||||
Hochladen
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => handleFaviconFile(e.target.files?.[0])}
|
||||
/>
|
||||
</label>
|
||||
{faviconPreview ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFavicon(null)}
|
||||
className="text-xs text-black/40 underline dark:text-white/40"
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{faviconError ? <p className="mt-1 text-xs text-red-500">{faviconError}</p> : null}
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-black/50 dark:text-white/50">Name</label>
|
||||
<input
|
||||
@@ -311,7 +360,7 @@ function ServiceRow({
|
||||
aria-label={service.favorite ? "Favorit entfernen" : "Als Favorit markieren"}
|
||||
className={`text-lg ${service.favorite ? "text-amber-500" : "text-black/15 hover:text-amber-400 dark:text-white/15"}`}
|
||||
>
|
||||
★
|
||||
<FontAwesomeIcon icon={service.favorite ? faStarSolid : faStar} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => visibleMutation.mutate()}
|
||||
@@ -319,7 +368,7 @@ function ServiceRow({
|
||||
title={service.visible ? "In der Suche sichtbar" : "In der Suche ausgeblendet"}
|
||||
className={`text-base ${service.visible ? "text-black/40 dark:text-white/40" : "text-black/60 dark:text-white/60"}`}
|
||||
>
|
||||
{service.visible ? "👁️" : "🙈"}
|
||||
{service.visible ? <FontAwesomeIcon icon={faEye} /> : <FontAwesomeIcon icon={faEyeSlash} />}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -330,7 +379,7 @@ function ServiceRow({
|
||||
<span className="font-medium text-black dark:text-white">{service.displayName}</span>
|
||||
{service.displayNameEditedManually ? (
|
||||
<span title="Name wurde manuell angepasst – Scan-Vorschläge dafür werden nicht mehr angezeigt">
|
||||
✍️
|
||||
<FontAwesomeIcon icon={faSignature} className="text-black/40 dark:text-white/40" />
|
||||
</span>
|
||||
) : null}
|
||||
{!service.visible ? (
|
||||
@@ -361,7 +410,7 @@ function ServiceRow({
|
||||
{service.category ?? "–"}
|
||||
{service.categoryEditedManually ? (
|
||||
<span title="Kategorie wurde manuell angepasst – Scan-Vorschläge dafür werden nicht mehr angezeigt">
|
||||
✍️
|
||||
<FontAwesomeIcon icon={faSignature} className="text-black/40 dark:text-white/40" />
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
@@ -429,7 +478,7 @@ function SortableHeader({
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
<span className="text-[10px]">{active ? (direction === "asc" ? "▲" : "▼") : "⇅"}</span>
|
||||
<span className="text-[10px]"><FontAwesomeIcon icon={active ? (direction === "asc" ? faSortUp : faSortDown) : faSort} /></span>
|
||||
</button>
|
||||
</th>
|
||||
);
|
||||
@@ -584,14 +633,14 @@ export function ServicesPage() {
|
||||
: "text-black/40 hover:text-black/70 dark:text-white/40 dark:hover:text-white/70"
|
||||
}`}
|
||||
>
|
||||
🙈 Ausgeblendet ({hiddenCount})
|
||||
<FontAwesomeIcon icon={faEyeSlash} /> Ausgeblendet ({hiddenCount})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{sortColumn ? (
|
||||
<div className="mb-3">
|
||||
<Button size="sm" variant="ghost" onClick={() => setSortColumn(null)}>
|
||||
← Zur manuellen Reihenfolge (Drag & Drop) zurück
|
||||
<FontAwesomeIcon icon={faArrowLeft} /> Zur manuellen Reihenfolge (Drag & Drop) zurück
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user