feat(web): favicon + toast feedback (polish)
Add a vinyl-record favicon (icon.svg) — fixes the favicon.ico 404 console error. Lightweight toast system (ToastHost + toast()) fired on request, wanted add, search, follow, and want actions so submits give explicit feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Modal } from "./modal";
|
||||
import { CoverArt } from "./cover-art";
|
||||
import { toast } from "./toast";
|
||||
|
||||
type Rel = {
|
||||
rgMbid: string;
|
||||
@@ -61,7 +62,10 @@ export function ArtistModal({
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ mbid, name }),
|
||||
});
|
||||
if (res.ok || res.status === 409) setFollowed(true);
|
||||
if (res.ok || res.status === 409) {
|
||||
setFollowed(true);
|
||||
toast(`Following ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function want(r: Rel) {
|
||||
@@ -78,7 +82,10 @@ export function ArtistModal({
|
||||
firstReleaseDate: r.firstReleaseDate,
|
||||
}),
|
||||
});
|
||||
if (res.ok) setWanted((s) => new Set(s).add(r.rgMbid));
|
||||
if (res.ok) {
|
||||
setWanted((s) => new Set(s).add(r.rgMbid));
|
||||
toast(`Added ${r.album}`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
/** Fire a transient toast from anywhere: `toast("Queued")`. */
|
||||
export function toast(message: string) {
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("lyra-toast", { detail: message }));
|
||||
}
|
||||
}
|
||||
|
||||
/** Renders toasts fired via `toast()`. Mounted once in the root layout. */
|
||||
export function ToastHost() {
|
||||
const [items, setItems] = useState<{ id: number; text: string }[]>([]);
|
||||
const nextId = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
function onToast(e: Event) {
|
||||
const text = (e as CustomEvent<string>).detail;
|
||||
const id = ++nextId.current;
|
||||
setItems((xs) => [...xs, { id, text }]);
|
||||
setTimeout(() => setItems((xs) => xs.filter((x) => x.id !== id)), 2600);
|
||||
}
|
||||
window.addEventListener("lyra-toast", onToast);
|
||||
return () => window.removeEventListener("lyra-toast", onToast);
|
||||
}, []);
|
||||
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<div className="toast-host" role="status" aria-live="polite">
|
||||
{items.map((t) => (
|
||||
<div key={t.id} className="toast">
|
||||
{t.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user