import { Heart, RotateCcw } from "lucide-react"; import { useEffect, useState } from "react"; import { getJSON, itemsOrEmpty, readJSONCache, type Category } from "../../api"; import { isFavoritesAuthError, listFavorites } from "../../favorites/api"; import { useFavorites } from "../../favorites/FavoritesProvider"; import { langQuery, useI18n, type Lang } from "../../i18n"; import { Reveal } from "../../motion"; import { PopularRankRow } from "../../components/PopularRankList"; import { useSetPageTitle } from "../../components/PageTitleContext"; import { Skeleton } from "../../components/Skeleton"; import { useWallet } from "../../wallet/WalletProvider"; const pageSize = 50; type FavoritePosts = Awaited>["items"]; type FavoriteListCache = { address: string; lang: Lang; mutationVersion: number; posts: FavoritePosts; }; let favoriteListCache: FavoriteListCache | null = null; function useCategories(lang: Lang): Category[] { const [categories, setCategories] = useState(() => { const cached = readJSONCache( `/api/categories?lang=${encodeURIComponent(langQuery(lang))}`, ); return cached ? itemsOrEmpty(cached) : []; }); useEffect(() => { const url = `/api/categories?lang=${encodeURIComponent(langQuery(lang))}`; const cached = readJSONCache(url); if (cached) setCategories(itemsOrEmpty(cached)); let cancelled = false; getJSON(url) .then((items) => { if (!cancelled) setCategories(itemsOrEmpty(items)); }) .catch(() => { if (!cancelled && !cached) setCategories([]); }); return () => { cancelled = true; }; }, [lang]); return categories; } export default function Favorites() { const { lang, t } = useI18n(); const wallet = useWallet(); const { markFavorite, mutationVersion } = useFavorites(); const categories = useCategories(lang); const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(false); const [loaded, setLoaded] = useState(false); const [error, setError] = useState(""); const [reloadKey, setReloadKey] = useState(0); useSetPageTitle(t("favorites")); useEffect(() => { if (!wallet.token || wallet.status !== "loggedIn" || !wallet.address) { setPosts([]); setLoading(false); setLoaded(false); setError(""); return; } const walletAddress = wallet.address; const walletToken = wallet.token; if ( reloadKey === 0 && favoriteListCache?.address === walletAddress && favoriteListCache.lang === lang && favoriteListCache.mutationVersion === mutationVersion ) { setPosts(favoriteListCache.posts); setLoading(false); setLoaded(true); setError(""); return; } let cancelled = false; setLoading(true); setLoaded(false); setError(""); listFavorites(walletToken, { limit: pageSize, includeUnavailable: true, }) .then((data) => { if (cancelled) return; const items = itemsOrEmpty(data.items); favoriteListCache = { address: walletAddress, lang, mutationVersion, posts: items, }; setPosts(items); items.forEach((post) => markFavorite(post.id, true)); setLoaded(true); }) .catch((err) => { if (cancelled) return; if (isFavoritesAuthError(err)) { wallet.logout(); wallet.openLoginModal(); return; } setError(err instanceof Error ? err.message : t("loadFailed")); setLoaded(true); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [lang, markFavorite, mutationVersion, reloadKey, t, wallet]); if (wallet.status === "loading") { return ( {Array.from({ length: 4 }).map((_, index) => ( ))} ); } if (wallet.status !== "loggedIn") { return (

{t("favorites")}

{t("favoritesLoginDesc")}

); } return (
{loading || !loaded ? ( Array.from({ length: 4 }).map((_, index) => ( )) ) : error ? (

{error}

) : posts.length === 0 ? (

{t("favoritesEmptyTitle")}

{t("favoritesEmptyDesc")}

) : ( posts.map((post, index) => ( { if (!favorited) setReloadKey((value) => value + 1); }} /> )) )}
); }