round19: Startseite aufgeraeumt, Favicon-Fallback ueber Ports, Favicon-Picker, Live-Status per Ping

This commit is contained in:
2026-07-23 18:10:03 +02:00
parent bae9e48fc1
commit ec49534b60
16 changed files with 393 additions and 51 deletions

View File

@@ -7,6 +7,7 @@ import { Button, Favicon } from "@launchpad/ui";
import type { Bookmark } from "@launchpad/shared";
import { suggestBookmarkCategory } from "@launchpad/shared";
import { useBookmarks } from "../../hooks/useBookmarks.js";
import { useServices } from "../../hooks/useServices.js";
import { useCategories } from "../../hooks/useCategories.js";
import { AdminPageHeader } from "./AdminPageHeader.js";
@@ -205,6 +206,8 @@ const EDIT_FORM_COLSPAN = 7;
function EditForm({ bookmark, onDone }: { bookmark: Bookmark; onDone: () => void }) {
const queryClient = useQueryClient();
const { data: allBookmarks } = useBookmarks();
const { data: allServices } = useServices();
const [displayName, setDisplayName] = useState(bookmark.displayName);
const [url, setUrl] = useState(bookmark.url);
const [category, setCategory] = useState(bookmark.category ?? "");
@@ -213,8 +216,20 @@ function EditForm({ bookmark, onDone }: { bookmark: Bookmark; onDone: () => void
const [favicon, setFavicon] = useState<string | null | undefined>(undefined);
const [faviconError, setFaviconError] = useState<string | null>(null);
const [pickerOpen, setPickerOpen] = useState(false);
const FAVICON_MAX_BYTES = 300 * 1024;
const existingFavicons = useMemo(() => {
const seen = new Map<string, string>();
for (const b of allBookmarks ?? []) {
if (b.favicon && b.id !== bookmark.id && !seen.has(b.favicon)) seen.set(b.favicon, b.displayName);
}
for (const s of allServices ?? []) {
if (s.favicon && !seen.has(s.favicon)) seen.set(s.favicon, s.displayName);
}
return Array.from(seen.entries());
}, [allBookmarks, allServices, bookmark.id]);
function handleFaviconFile(file: File | undefined) {
setFaviconError(null);
if (!file) return;
@@ -268,6 +283,16 @@ function EditForm({ bookmark, onDone }: { bookmark: Bookmark; onDone: () => void
onChange={(e) => handleFaviconFile(e.target.files?.[0])}
/>
</label>
{existingFavicons.length > 0 ? (
<button
type="button"
onClick={() => setPickerOpen((v) => !v)}
className="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"
>
Vorhandenes wählen
</button>
) : null}
{faviconPreview ? (
<button
type="button"
@@ -279,6 +304,25 @@ function EditForm({ bookmark, onDone }: { bookmark: Bookmark; onDone: () => void
) : null}
</div>
{faviconError ? <p className="mt-1 text-xs text-red-500">{faviconError}</p> : null}
{pickerOpen ? (
<div className="mt-2 flex max-w-xs flex-wrap gap-1.5 rounded-lg border border-black/10 p-2
dark:border-white/10">
{existingFavicons.map(([iconUrl, name]) => (
<button
key={iconUrl}
type="button"
title={name}
onClick={() => {
setFavicon(iconUrl);
setPickerOpen(false);
}}
className="rounded p-0.5 hover:bg-black/5 dark:hover:bg-white/10"
>
<Favicon src={iconUrl} fallbackLetter={name} size="sm" />
</button>
))}
</div>
) : null}
</div>
<div>
<label className="mb-1 block text-xs text-black/50 dark:text-white/50">Name</label>