generated from Dicken/dickendock
round17+18: Emoji durch Font Awesome ersetzt, Lesezeichen-Kategorie-Vorschlaege, Favicon-Upload
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { useMemo, useState, type DragEvent, type FormEvent } 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 } 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 { Bookmark } from "@launchpad/shared";
|
||||
import { suggestBookmarkCategory } from "@launchpad/shared";
|
||||
import { useBookmarks } from "../../hooks/useBookmarks.js";
|
||||
import { useCategories } from "../../hooks/useCategories.js";
|
||||
import { AdminPageHeader } from "./AdminPageHeader.js";
|
||||
@@ -16,6 +18,7 @@ interface BookmarkPatch {
|
||||
favorite?: boolean;
|
||||
alias?: string[];
|
||||
order?: number;
|
||||
favicon?: string | null;
|
||||
}
|
||||
|
||||
async function createBookmarkRequest(input: {
|
||||
@@ -139,6 +142,11 @@ function AddBookmarkForm() {
|
||||
mutation.mutate();
|
||||
}
|
||||
|
||||
// Kategorie-Vorschlag anhand der eingegebenen URL (rein heuristisch anhand
|
||||
// des Hostnamens, siehe suggestBookmarkCategory in @launchpad/shared) - nur
|
||||
// solange der Nutzer noch keine eigene Kategorie gewählt hat.
|
||||
const suggestedCategory = category.trim() === "" ? suggestBookmarkCategory(url.trim()) : null;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="mb-6 flex flex-wrap items-end gap-2">
|
||||
<div>
|
||||
@@ -157,6 +165,15 @@ function AddBookmarkForm() {
|
||||
Kategorie
|
||||
</label>
|
||||
<CategorySelect value={category} onChange={setCategory} />
|
||||
{suggestedCategory ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCategory(suggestedCategory)}
|
||||
className="mt-1 block text-xs text-blue-600 hover:underline dark:text-blue-400"
|
||||
>
|
||||
Vorschlag: {suggestedCategory} übernehmen?
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-black/50 dark:text-white/50">
|
||||
@@ -194,6 +211,25 @@ function EditForm({ bookmark, onDone }: { bookmark: Bookmark; onDone: () => void
|
||||
const [description, setDescription] = useState(bookmark.description ?? "");
|
||||
const [alias, setAlias] = useState(bookmark.alias.join(", "));
|
||||
|
||||
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 faviconPreview = favicon === undefined ? bookmark.favicon : favicon;
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
patchBookmark(bookmark.id, {
|
||||
@@ -205,6 +241,7 @@ function EditForm({ bookmark, onDone }: { bookmark: Bookmark; onDone: () => void
|
||||
.split(",")
|
||||
.map((a) => a.trim())
|
||||
.filter(Boolean),
|
||||
...(favicon !== undefined ? { favicon } : {}),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["bookmarks"] });
|
||||
@@ -217,6 +254,32 @@ function EditForm({ bookmark, onDone }: { bookmark: Bookmark; 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
|
||||
@@ -328,7 +391,7 @@ function BookmarkRow({
|
||||
aria-label={bookmark.favorite ? "Favorit entfernen" : "Als Favorit markieren"}
|
||||
className={`text-lg ${bookmark.favorite ? "text-amber-500" : "text-black/15 hover:text-amber-400 dark:text-white/15"}`}
|
||||
>
|
||||
★
|
||||
<FontAwesomeIcon icon={bookmark.favorite ? faStarSolid : faStar} />
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
@@ -389,7 +452,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>
|
||||
);
|
||||
@@ -486,7 +549,7 @@ export function BookmarksPage() {
|
||||
{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