2026-06-04 17:25:55 +08:00
|
|
|
import { Heart, RotateCcw } from "lucide-react";
|
2026-06-05 18:14:48 +08:00
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
import { itemsOrEmpty } from "../../api";
|
2026-06-04 17:25:55 +08:00
|
|
|
import { isFavoritesAuthError, listFavorites } from "../../favorites/api";
|
2026-06-02 00:39:36 +08:00
|
|
|
import { useFavorites } from "../../favorites/FavoritesProvider";
|
2026-06-05 18:14:48 +08:00
|
|
|
import { useI18n } from "../../i18n";
|
2026-05-29 11:50:27 +08:00
|
|
|
import { Reveal } from "../../motion";
|
2026-06-05 18:14:48 +08:00
|
|
|
import { MessageBubble } from "../../components/messageStream/MessageBubble";
|
2026-05-30 02:37:30 +08:00
|
|
|
import { useSetPageTitle } from "../../components/PageTitleContext";
|
2026-06-02 00:39:36 +08:00
|
|
|
import { Skeleton } from "../../components/Skeleton";
|
|
|
|
|
import { useWallet } from "../../wallet/WalletProvider";
|
|
|
|
|
|
2026-06-04 17:25:55 +08:00
|
|
|
const pageSize = 50;
|
2026-06-02 00:39:36 +08:00
|
|
|
|
2026-05-28 10:36:38 +08:00
|
|
|
export default function Favorites() {
|
2026-06-05 18:14:48 +08:00
|
|
|
const { t } = useI18n();
|
2026-06-02 00:39:36 +08:00
|
|
|
const wallet = useWallet();
|
|
|
|
|
const { markFavorite } = useFavorites();
|
2026-06-04 17:46:09 +08:00
|
|
|
const [posts, setPosts] = useState<
|
|
|
|
|
Awaited<ReturnType<typeof listFavorites>>["items"]
|
|
|
|
|
>([]);
|
2026-06-02 00:39:36 +08:00
|
|
|
const [loading, setLoading] = useState(false);
|
2026-06-04 17:15:14 +08:00
|
|
|
const [loaded, setLoaded] = useState(false);
|
2026-06-02 00:39:36 +08:00
|
|
|
const [error, setError] = useState("");
|
2026-06-02 03:43:13 +08:00
|
|
|
const [reloadKey, setReloadKey] = useState(0);
|
2026-06-02 00:39:36 +08:00
|
|
|
|
2026-05-30 02:37:30 +08:00
|
|
|
useSetPageTitle(t("favorites"));
|
2026-05-28 10:36:38 +08:00
|
|
|
|
2026-06-02 00:39:36 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!wallet.token || wallet.status !== "loggedIn") {
|
2026-06-04 17:46:09 +08:00
|
|
|
setPosts([]);
|
2026-06-04 17:15:14 +08:00
|
|
|
setLoading(false);
|
|
|
|
|
setLoaded(false);
|
2026-06-04 17:25:55 +08:00
|
|
|
setError("");
|
2026-06-02 00:39:36 +08:00
|
|
|
return;
|
|
|
|
|
}
|
2026-06-04 17:25:55 +08:00
|
|
|
|
2026-06-02 00:39:36 +08:00
|
|
|
let cancelled = false;
|
|
|
|
|
setLoading(true);
|
2026-06-04 17:15:14 +08:00
|
|
|
setLoaded(false);
|
2026-06-02 00:39:36 +08:00
|
|
|
setError("");
|
2026-06-04 17:25:55 +08:00
|
|
|
|
2026-06-02 00:39:36 +08:00
|
|
|
listFavorites(wallet.token, {
|
|
|
|
|
limit: pageSize,
|
|
|
|
|
includeUnavailable: true,
|
|
|
|
|
})
|
|
|
|
|
.then((data) => {
|
|
|
|
|
if (cancelled) return;
|
2026-06-04 17:46:09 +08:00
|
|
|
const items = itemsOrEmpty(data.items);
|
|
|
|
|
setPosts(items);
|
|
|
|
|
items.forEach((post) => markFavorite(post.id, true));
|
2026-06-04 17:15:14 +08:00
|
|
|
setLoaded(true);
|
2026-06-02 00:39:36 +08:00
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
2026-06-02 03:43:13 +08:00
|
|
|
if (cancelled) return;
|
|
|
|
|
if (isFavoritesAuthError(err)) {
|
|
|
|
|
wallet.logout();
|
|
|
|
|
wallet.openLoginModal();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setError(err instanceof Error ? err.message : t("loadFailed"));
|
2026-06-04 17:15:14 +08:00
|
|
|
setLoaded(true);
|
2026-06-02 00:39:36 +08:00
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
if (!cancelled) setLoading(false);
|
|
|
|
|
});
|
2026-06-04 17:25:55 +08:00
|
|
|
|
2026-06-02 00:39:36 +08:00
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
};
|
2026-06-04 17:25:55 +08:00
|
|
|
}, [markFavorite, reloadKey, t, wallet]);
|
2026-06-02 00:39:36 +08:00
|
|
|
|
2026-06-04 17:15:14 +08:00
|
|
|
if (wallet.status === "loading") {
|
|
|
|
|
return (
|
2026-06-05 18:14:48 +08:00
|
|
|
<Reveal className="mx-auto grid w-full max-w-[980px] gap-3 overflow-x-clip px-0 py-2 md:py-4">
|
2026-06-04 17:15:14 +08:00
|
|
|
{Array.from({ length: 4 }).map((_, index) => (
|
|
|
|
|
<Skeleton key={index} className="h-[132px] rounded-2xl" />
|
|
|
|
|
))}
|
|
|
|
|
</Reveal>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 00:39:36 +08:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-28 10:36:38 +08:00
|
|
|
return (
|
2026-06-05 18:14:48 +08:00
|
|
|
<div className="mx-auto flex w-full max-w-full flex-col gap-3 overflow-x-clip py-2 md:max-w-[820px] md:py-4 lg:max-w-[1080px] xl:max-w-[1180px]">
|
2026-06-04 17:15:14 +08:00
|
|
|
{loading || !loaded ? (
|
2026-06-04 17:25:55 +08:00
|
|
|
Array.from({ length: 4 }).map((_, index) => (
|
|
|
|
|
<Skeleton key={index} className="h-[132px] rounded-2xl" />
|
|
|
|
|
))
|
2026-06-02 00:39:36 +08:00
|
|
|
) : error ? (
|
2026-06-02 03:43:13 +08:00
|
|
|
<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>
|
2026-06-02 00:39:36 +08:00
|
|
|
</div>
|
2026-06-05 18:14:48 +08:00
|
|
|
) : posts.length === 0 ? (
|
2026-06-02 00:39:36 +08:00
|
|
|
<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">
|
2026-06-04 17:25:55 +08:00
|
|
|
{t("favoritesEmptyTitle")}
|
2026-06-02 00:39:36 +08:00
|
|
|
</h2>
|
|
|
|
|
<p className="max-w-md text-sm leading-6 text-neutral-400">
|
2026-06-04 17:25:55 +08:00
|
|
|
{t("favoritesEmptyDesc")}
|
2026-06-02 00:39:36 +08:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-06-05 18:14:48 +08:00
|
|
|
posts.map((post, index) => (
|
|
|
|
|
<Reveal key={post.id} delay={Math.min(index, 8) * 0.05}>
|
|
|
|
|
<MessageBubble post={post} />
|
|
|
|
|
</Reveal>
|
2026-06-04 17:25:55 +08:00
|
|
|
))
|
2026-06-02 00:39:36 +08:00
|
|
|
)}
|
2026-06-04 17:25:55 +08:00
|
|
|
</div>
|
2026-05-28 10:36:38 +08:00
|
|
|
);
|
|
|
|
|
}
|