126 lines
4.0 KiB
TypeScript
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>
|
|
);
|
|
}
|