round17+18: Emoji durch Font Awesome ersetzt, Lesezeichen-Kategorie-Vorschlaege, Favicon-Upload

This commit is contained in:
2026-07-23 14:56:40 +02:00
parent 050616a5cf
commit bae9e48fc1
12 changed files with 218 additions and 37 deletions

View File

@@ -1,4 +1,6 @@
import { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons";
/**
* Schwebender Button unten rechts, der zum Seitenanfang zurückscrollt.
@@ -29,7 +31,7 @@ export function ScrollToTopButton() {
transition-opacity hover:bg-white dark:border-white/10 dark:bg-neutral-900/90
dark:text-white/70 dark:hover:bg-neutral-900"
>
<FontAwesomeIcon icon={faArrowUp} />
</button>
);
}

View File

@@ -2,6 +2,8 @@ import { useEffect, useMemo, useRef, useState, type FormEvent } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { SearchInput, StatusBadge, ResultsList, FavoritesBar, Favicon, Button } from "@launchpad/ui";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGear, faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
import { rankServices, type SearchResult } from "@launchpad/shared";
import { useServices } from "../hooks/useServices.js";
import { useBookmarks } from "../hooks/useBookmarks.js";
@@ -248,7 +250,7 @@ export function HomePage() {
className="rounded-full border border-black/10 p-2 text-black/60 transition-colors
hover:bg-black/5 dark:border-white/10 dark:text-white/60 dark:hover:bg-white/5"
>
<FontAwesomeIcon icon={faGear} />
</Link>
<button
onClick={toggleTheme}
@@ -256,7 +258,7 @@ export function HomePage() {
className="rounded-full border border-black/10 p-2 text-black/60 transition-colors
hover:bg-black/5 dark:border-white/10 dark:text-white/60 dark:hover:bg-white/5"
>
{theme === "dark" ? "☀️" : "🌙"}
<FontAwesomeIcon icon={theme === "dark" ? faSun : faMoon} />
</button>
</div>

View File

@@ -1,22 +1,39 @@
import { useEffect, useState } from "react";
import { Link, Outlet, useRouterState } from "@tanstack/react-router";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import {
faBars,
faXmark,
faHouse,
faChartLine,
faDesktop,
faLink,
faBookmark,
faThumbtack,
faMagnifyingGlass,
faTag,
faPuzzlePiece,
faGear,
faScroll,
} from "@fortawesome/free-solid-svg-icons";
import { StatusBadge } from "@launchpad/ui";
import { useBackendHealth } from "../../hooks/useBackendHealth.js";
import { ScrollToTopButton } from "../../components/ScrollToTopButton.js";
const NAV_ITEMS = [
{ to: "/", label: "Startseite", icon: "🏠" },
{ to: "/admin/dashboard", label: "Dashboard", icon: "📊" },
{ to: "/admin/devices", label: "Geräte", icon: "🖥️" },
{ to: "/admin/services", label: "Dienste", icon: "🔗" },
{ to: "/admin/bookmarks", label: "Lesezeichen", icon: "🔖" },
{ to: "/admin/read-later", label: "Später lesen", icon: "📌" },
{ to: "/admin/scanner", label: "Scanner", icon: "🔍" },
{ to: "/admin/categories", label: "Kategorien", icon: "🏷️" },
{ to: "/admin/plugins", label: "Plugins", icon: "🧩" },
{ to: "/admin/settings", label: "Einstellungen", icon: "⚙️" },
{ to: "/admin/logs", label: "Logs", icon: "📜" },
] as const;
const NAV_ITEMS: { to: string; label: string; icon: IconDefinition }[] = [
{ to: "/", label: "Startseite", icon: faHouse },
{ to: "/admin/dashboard", label: "Dashboard", icon: faChartLine },
{ to: "/admin/devices", label: "Geräte", icon: faDesktop },
{ to: "/admin/services", label: "Dienste", icon: faLink },
{ to: "/admin/bookmarks", label: "Lesezeichen", icon: faBookmark },
{ to: "/admin/read-later", label: "Später lesen", icon: faThumbtack },
{ to: "/admin/scanner", label: "Scanner", icon: faMagnifyingGlass },
{ to: "/admin/categories", label: "Kategorien", icon: faTag },
{ to: "/admin/plugins", label: "Plugins", icon: faPuzzlePiece },
{ to: "/admin/settings", label: "Einstellungen", icon: faGear },
{ to: "/admin/logs", label: "Logs", icon: faScroll },
];
export function AdminLayout() {
const pathname = useRouterState({ select: (s) => s.location.pathname });
@@ -45,7 +62,7 @@ export function AdminLayout() {
className="rounded-lg p-1.5 text-black/70 hover:bg-black/5 dark:text-white/70 dark:hover:bg-white/10"
>
<span className="block text-xl leading-none" aria-hidden>
<FontAwesomeIcon icon={faBars} />
</span>
</button>
<Link to="/" className="text-base font-semibold text-black dark:text-white">
@@ -84,7 +101,7 @@ export function AdminLayout() {
aria-label="Menü schließen"
className="rounded-lg p-1 text-black/50 hover:bg-black/5 dark:text-white/50 dark:hover:bg-white/10 sm:hidden"
>
<FontAwesomeIcon icon={faXmark} />
</button>
</div>
@@ -101,7 +118,9 @@ export function AdminLayout() {
: "text-black/60 hover:bg-black/5 hover:text-black dark:text-white/60 dark:hover:bg-white/5 dark:hover:text-white"
}`}
>
<span aria-hidden>{item.icon}</span>
<span className="w-4 text-center" aria-hidden>
<FontAwesomeIcon icon={item.icon} />
</span>
{item.label}
</Link>
);

View File

@@ -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}

View File

@@ -1,4 +1,6 @@
import { Link } from "@tanstack/react-router";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHouse } from "@fortawesome/free-solid-svg-icons";
import { useServices } from "../../hooks/useServices.js";
import { useDevices } from "../../hooks/useDevices.js";
import { useCategories } from "../../hooks/useCategories.js";
@@ -43,7 +45,7 @@ export function DashboardPage() {
text-sm text-black/70 transition-colors hover:bg-black/5 dark:border-white/10
dark:text-white/70 dark:hover:bg-white/10"
>
<span aria-hidden>🏠</span> Startseite
<span aria-hidden><FontAwesomeIcon icon={faHouse} /></span> Startseite
</Link>
}
/>

View File

@@ -1,6 +1,6 @@
import { useMemo, useState, type FormEvent } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBan, faCheck, faFloppyDisk, faPen, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
import { faBan, faCheck, faFloppyDisk, faPen, faTrash, faXmark, faSort, faSortUp, faSortDown } from "@fortawesome/free-solid-svg-icons";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Button, Favicon } from "@launchpad/ui";
import type { Service } from "@launchpad/shared";
@@ -547,7 +547,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>
);

View File

@@ -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}

View File

@@ -1,6 +1,8 @@
import { useRef, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@launchpad/ui";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
import { useBackendHealth } from "../../hooks/useBackendHealth.js";
import { useTheme } from "../../hooks/useTheme.js";
import { useSettings } from "../../hooks/useSettings.js";
@@ -308,7 +310,7 @@ export function SettingsPage() {
transition-colors hover:bg-black/5 dark:border-white/10 dark:text-white
dark:hover:bg-white/10"
>
{theme === "dark" ? "🌙 Dunkel" : "☀️ Hell"} wechseln
<FontAwesomeIcon icon={theme === "dark" ? faMoon : faSun} /> {theme === "dark" ? "Dunkel" : "Hell"} wechseln
</button>
</div>
<LimitSetting

Binary file not shown.

View File

@@ -191,3 +191,38 @@ export function rankServices<T extends Rankable>(items: T[], query: string): T[]
})
.map((entry) => entry.item);
}
/**
* Bekannte Domain-Fragmente -> naheliegende Kategorie, für den Kategorie-
* Vorschlag beim Anlegen eines Lesezeichens (siehe BookmarksPage.tsx). Rein
* heuristisch anhand des Hostnamens - keine Netzwerkabfrage, kein externer
* Dienst. Bewusst knapp gehalten: lieber gar kein Vorschlag als ein falscher.
*/
const BOOKMARK_CATEGORY_HINTS: Array<{ pattern: RegExp; category: string }> = [
{ pattern: /github|gitlab|gitea|bitbucket|stackoverflow|npmjs|developer\.mozilla/i, category: "Entwicklung" },
{ pattern: /youtube|netflix|twitch|spotify|disneyplus|primevideo/i, category: "Unterhaltung" },
{ pattern: /amazon|ebay|etsy|otto\.de|zalando/i, category: "Shopping" },
{ pattern: /wikipedia|wiktionary/i, category: "Wissen" },
{ pattern: /reddit|twitter|x\.com|instagram|facebook|mastodon|threads\.net/i, category: "Social Media" },
{ pattern: /paypal|sparkasse|bank|n26\.com|dkb\.de|ing\.de/i, category: "Finanzen" },
{ pattern: /docs\.google|notion\.so|trello|asana|slack\.com|confluence/i, category: "Produktivität" },
{ pattern: /heise\.de|golem\.de|tagesschau|spiegel\.de|zeit\.de|faz\.net/i, category: "Nachrichten" },
];
/**
* Liefert eine Kategorie-Vermutung anhand des Hostnamens einer URL, oder
* null wenn keine der bekannten Regeln zutrifft. Nur ein Vorschlag - wird nie
* automatisch übernommen (siehe BookmarksPage.tsx: nur als Chip zum
* Bestätigen angezeigt).
*/
export function suggestBookmarkCategory(url: string): string | null {
// Kein URL()-Konstruktor - der ist ein DOM-Global und in diesem Pakets
// tsconfig-"lib" nicht garantiert verfügbar (das Paket wird auch vom
// Backend importiert). Einfache manuelle Extraktion reicht hier völlig.
const withoutProtocol = url.replace(/^[a-z]+:\/\//i, "");
const hostname = withoutProtocol.split(/[/?#]/)[0] ?? withoutProtocol;
for (const { pattern, category } of BOOKMARK_CATEGORY_HINTS) {
if (pattern.test(hostname)) return category;
}
return null;
}

View File

@@ -10,6 +10,10 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.3.1",
"@fortawesome/free-regular-svg-icons": "^7.3.1",
"@fortawesome/free-solid-svg-icons": "^7.3.1",
"@fortawesome/react-fontawesome": "^3.5.0",
"@launchpad/shared": "workspace:*",
"react": "^18.3.1"
},

View File

@@ -1,5 +1,8 @@
import { useState, type KeyboardEvent } from "react";
import type { SearchResult } from "@launchpad/shared";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faStar as faStarSolid, faCheck, faClone } from "@fortawesome/free-solid-svg-icons";
import { faStar } from "@fortawesome/free-regular-svg-icons";
import { Favicon } from "./Favicon.js";
function hexToRgba(hex: string, alpha: number): string | null {
@@ -136,7 +139,7 @@ export function ResultsList({
: "text-black/15 hover:text-amber-400 dark:text-white/15 dark:hover:text-amber-400"
} ${onToggleFavorite ? "" : "cursor-default"}`}
>
<FontAwesomeIcon icon={item.favorite ? faStarSolid : faStar} />
</button>
</div>
</li>
@@ -169,7 +172,7 @@ function CopyButton({ url }: { url: string }) {
className="shrink-0 text-sm leading-none text-black/20 transition-colors hover:text-black/60
dark:text-white/20 dark:hover:text-white/60"
>
{copied ? "✓" : "⧉"}
<FontAwesomeIcon icon={copied ? faCheck : faClone} />
</button>
);
}