All checks were successful
Deploy Staging (terry-wallet-login) / deploy (push) Successful in 1m20s
214 lines
7.1 KiB
TypeScript
214 lines
7.1 KiB
TypeScript
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<ReturnType<typeof listFavorites>>["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<Category[]>(() => {
|
|
const cached = readJSONCache<Category[]>(
|
|
`/api/categories?lang=${encodeURIComponent(langQuery(lang))}`,
|
|
);
|
|
return cached ? itemsOrEmpty(cached) : [];
|
|
});
|
|
|
|
useEffect(() => {
|
|
const url = `/api/categories?lang=${encodeURIComponent(langQuery(lang))}`;
|
|
const cached = readJSONCache<Category[]>(url);
|
|
if (cached) setCategories(itemsOrEmpty(cached));
|
|
let cancelled = false;
|
|
getJSON<Category[]>(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<FavoritePosts>([]);
|
|
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 (
|
|
<Reveal className="mx-auto grid w-full max-w-[980px] gap-3 overflow-x-clip px-0 py-2 md:py-4">
|
|
{Array.from({ length: 4 }).map((_, index) => (
|
|
<Skeleton key={index} className="h-[132px] rounded-2xl" />
|
|
))}
|
|
</Reveal>
|
|
);
|
|
}
|
|
|
|
if (wallet.status !== "loggedIn") {
|
|
return (
|
|
<Reveal className="flex min-h-[60vh] flex-col items-center justify-center gap-5 px-4 py-12 text-center">
|
|
<div className="flex h-20 w-20 items-center justify-center rounded-full border border-ark-gold/30 bg-ark-gold/5">
|
|
<Heart className="h-10 w-10 text-ark-gold/70" strokeWidth={1.8} />
|
|
</div>
|
|
<h1 className="text-2xl font-semibold text-neutral-100 md:text-3xl">
|
|
{t("favorites")}
|
|
</h1>
|
|
<p className="max-w-md text-sm leading-relaxed text-neutral-400 md:text-base">
|
|
{t("favoritesLoginDesc")}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={wallet.openLoginModal}
|
|
className="mt-2 inline-flex h-11 items-center justify-center rounded-full bg-ark-gold px-6 text-sm font-bold text-black transition hover:bg-ark-gold2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg"
|
|
>
|
|
{t("walletConnect")}
|
|
</button>
|
|
</Reveal>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto flex w-full max-w-full flex-col gap-2.5 overflow-x-clip py-2 md:max-w-[680px] md:gap-3 md:py-4 lg:max-w-[900px] xl:max-w-[1120px]">
|
|
{loading || !loaded ? (
|
|
Array.from({ length: 4 }).map((_, index) => (
|
|
<Skeleton key={index} className="h-[132px] rounded-2xl" />
|
|
))
|
|
) : error ? (
|
|
<div className="flex flex-col items-start gap-3 rounded-2xl border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-200">
|
|
<p>{error}</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => setReloadKey((value) => value + 1)}
|
|
className="inline-flex items-center gap-1.5 rounded-full border border-red-400/40 px-3 py-1.5 text-xs font-semibold text-red-100 transition hover:bg-red-500/20"
|
|
>
|
|
<RotateCcw className="h-3.5 w-3.5" />
|
|
{t("walletRetry")}
|
|
</button>
|
|
</div>
|
|
) : posts.length === 0 ? (
|
|
<div className="flex min-h-[280px] flex-col items-center justify-center gap-4 rounded-3xl border border-white/10 bg-[#17171d] p-8 text-center">
|
|
<Heart className="h-10 w-10 text-ark-gold/60" strokeWidth={1.8} />
|
|
<h2 className="text-xl font-semibold text-white">
|
|
{t("favoritesEmptyTitle")}
|
|
</h2>
|
|
<p className="max-w-md text-sm leading-6 text-neutral-400">
|
|
{t("favoritesEmptyDesc")}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
posts.map((post, index) => (
|
|
<Reveal key={post.id} delay={Math.min(index, 8) * 0.05}>
|
|
<PopularRankRow
|
|
post={post}
|
|
index={index}
|
|
categories={categories}
|
|
browseSort=""
|
|
showRank={false}
|
|
singlePostLink
|
|
onFavoriteChange={(_, favorited) => {
|
|
if (!favorited) setReloadKey((value) => value + 1);
|
|
}}
|
|
/>
|
|
</Reveal>
|
|
))
|
|
)}
|
|
</div>
|
|
);
|
|
}
|