Files

126 lines
4.0 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { browserFetch } from "../lib/api";
type FavoriteToggleProps = {
targetId: string;
targetType: "maker" | "work";
targetSlug?: string;
compact?: boolean;
icon?: "heart" | "bookmark";
};
const favoriteSyncEvent = "makers3d:favorite-updated";
function loginForCurrentPage() {
const currentPath = `${window.location.pathname}${window.location.search}`;
window.location.assign(`/login?returnTo=${encodeURIComponent(currentPath)}`);
}
function BookmarkIcon({ active }: { active: boolean }) {
return (
<svg viewBox="0 0 24 24" fill={active ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M7 4.8A1.8 1.8 0 0 1 8.8 3h6.4A1.8 1.8 0 0 1 17 4.8V21l-5-3-5 3V4.8Z" />
</svg>
);
}
export default function FavoriteToggle({ targetId, targetType, targetSlug, compact = false, icon = "heart" }: FavoriteToggleProps) {
const [favorited, setFavorited] = useState(false);
const [loading, setLoading] = useState(true);
const [status, setStatus] = useState("");
const syncKey = `${targetType}:${targetSlug || targetId}`;
useEffect(() => {
let active = true;
async function loadFavorite() {
try {
const statusQuery = targetType === "maker" && targetSlug
? `/favorites/status?targetType=maker&targetSlug=${encodeURIComponent(targetSlug)}`
: `/favorites/status?targetType=${targetType}&targetId=${targetId}`;
const response = await browserFetch<{ favorited: boolean }>(
statusQuery
);
if (active) {
setFavorited(response.favorited);
}
} catch {
if (active) {
setFavorited(false);
}
} finally {
if (active) {
setLoading(false);
}
}
}
void loadFavorite();
return () => {
active = false;
};
}, [targetId, targetSlug, targetType]);
useEffect(() => {
function syncFavorite(event: Event) {
const detail = (event as CustomEvent<{ key: string; favorited: boolean }>).detail;
if (detail?.key === syncKey) {
setFavorited(detail.favorited);
}
}
window.addEventListener(favoriteSyncEvent, syncFavorite);
return () => window.removeEventListener(favoriteSyncEvent, syncFavorite);
}, [syncKey]);
async function toggleFavorite() {
setStatus("");
try {
const identifier = targetSlug || targetId || "current";
const endpoint = targetType === "maker"
? `/favorites/makers/${encodeURIComponent(identifier)}`
: `/favorites/works/${encodeURIComponent(identifier)}`;
const response = await browserFetch<{ favorited: boolean }>(endpoint, {
method: favorited ? "DELETE" : "POST"
});
setFavorited(response.favorited);
window.dispatchEvent(new CustomEvent(favoriteSyncEvent, {
detail: {
key: syncKey,
favorited: response.favorited
}
}));
} catch (error) {
const apiError = error as Error & { status?: number };
const message = apiError.message || "";
if (apiError.status === 401 || apiError.status === 403 || message.includes("Authentication")) {
loginForCurrentPage();
return;
}
setStatus(apiError.status ? `No se pudo guardar (${apiError.status}).` : "No se pudo actualizar favoritos.");
}
}
return (
<div className={`favorite-toggle-wrap ${compact ? "is-compact" : ""} favorite-icon-${icon}`}>
<button
className={`favorite-toggle ${favorited ? "is-active" : ""}`}
type="button"
onClick={toggleFavorite}
disabled={loading}
aria-pressed={favorited}
>
<span className="favorite-heart" aria-hidden="true">
{icon === "bookmark" ? <BookmarkIcon active={favorited} /> : (favorited ? "\u2764\uFE0F" : "\u2661")}
</span>
<span>{favorited ? "Guardado" : "Guardar"}</span>
</button>
{status ? <span className="mini-note">{status}</span> : null}
</div>
);
}