terry-wallet-login #15
41
.unipi/docs/fix/2026-06-04-favorites-display-loading-fix.md
Normal file
41
.unipi/docs/fix/2026-06-04-favorites-display-loading-fix.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
title: "Favorites Display Loading Blank Page — Quick Fix"
|
||||||
|
type: quick-fix
|
||||||
|
date: 2026-06-04
|
||||||
|
---
|
||||||
|
|
||||||
|
# Favorites Display Loading Blank Page — Quick Fix
|
||||||
|
|
||||||
|
## Bug
|
||||||
|
|
||||||
|
When clicking the desktop header “我的收藏” button, the favorites page could briefly show the no-favorites empty state and then appear blank. The correct behavior is to show the user's favorited posts after loading.
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
|
||||||
|
Two issues combined:
|
||||||
|
|
||||||
|
1. The favorites page initialized with `loading=false` and `items=[]`. When the wallet was already logged in, React rendered the empty state once before the `useEffect` started the favorites request.
|
||||||
|
2. The desktop header favorites link had been changed to `reloadDocument` as a previous workaround. In the local Vite/dev-browser state this could force a full document reload and land in a broken empty document state instead of keeping the React app mounted.
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
|
||||||
|
- Added an explicit `loaded` state to `src/pages/Favorites/index.tsx`.
|
||||||
|
- The favorites page now shows loading skeletons while logged-in favorites have not completed their first load, so the empty state only appears after a completed request returns zero items.
|
||||||
|
- Added a loading UI for `wallet.status === "loading"` so a persisted wallet token does not briefly show the logged-out prompt.
|
||||||
|
- Removed `reloadDocument` from the desktop header favorites link and kept client-side navigation with a top scroll reset.
|
||||||
|
|
||||||
|
### Files Modified
|
||||||
|
|
||||||
|
- `src/pages/Favorites/index.tsx` — tracks loaded state and gates empty-state rendering until favorites data has loaded.
|
||||||
|
- `src/layouts/PublicLayout.tsx` — removes hard document reload from the desktop header favorites link.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `npx tsc --noEmit`
|
||||||
|
- `npm run format:check`
|
||||||
|
- `npm test`
|
||||||
|
- Browser native: opened `http://192.168.1.187:5173/cn/browse`, clicked the desktop header “我的收藏”, and verified the resulting page URL is `/cn/favorites`, `document.getElementById("root")` exists, and `window.scrollY === 0`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
No deploy was performed.
|
||||||
@@ -698,7 +698,6 @@ export function PublicLayout() {
|
|||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
to={lp("/favorites")}
|
to={lp("/favorites")}
|
||||||
reloadDocument
|
|
||||||
onClick={() => window.scrollTo({ top: 0, left: 0 })}
|
onClick={() => window.scrollTo({ top: 0, left: 0 })}
|
||||||
className={`hidden h-10 shrink-0 items-center justify-center gap-2 rounded-full border px-3 text-sm font-bold outline-none transition focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg md:inline-flex ${
|
className={`hidden h-10 shrink-0 items-center justify-center gap-2 rounded-full border px-3 text-sm font-bold outline-none transition focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg md:inline-flex ${
|
||||||
na("favorites")
|
na("favorites")
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ export default function Favorites() {
|
|||||||
const [items, setItems] = useState<Resource[]>([]);
|
const [items, setItems] = useState<Resource[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [reloadKey, setReloadKey] = useState(0);
|
const [reloadKey, setReloadKey] = useState(0);
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
@@ -151,10 +152,13 @@ export default function Favorites() {
|
|||||||
if (!wallet.token || wallet.status !== "loggedIn") {
|
if (!wallet.token || wallet.status !== "loggedIn") {
|
||||||
setItems([]);
|
setItems([]);
|
||||||
setTotal(0);
|
setTotal(0);
|
||||||
|
setLoading(false);
|
||||||
|
setLoaded(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setLoaded(false);
|
||||||
setError("");
|
setError("");
|
||||||
listFavorites(wallet.token, {
|
listFavorites(wallet.token, {
|
||||||
sort,
|
sort,
|
||||||
@@ -170,6 +174,7 @@ export default function Favorites() {
|
|||||||
setItems(resources);
|
setItems(resources);
|
||||||
setTotal(data.total ?? resources.length);
|
setTotal(data.total ?? resources.length);
|
||||||
resources.forEach((resource) => markFavorite(resource.id, true));
|
resources.forEach((resource) => markFavorite(resource.id, true));
|
||||||
|
setLoaded(true);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@@ -179,6 +184,7 @@ export default function Favorites() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(err instanceof Error ? err.message : t("loadFailed"));
|
setError(err instanceof Error ? err.message : t("loadFailed"));
|
||||||
|
setLoaded(true);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (!cancelled) setLoading(false);
|
if (!cancelled) setLoading(false);
|
||||||
@@ -200,6 +206,16 @@ export default function Favorites() {
|
|||||||
[t],
|
[t],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (wallet.status === "loading") {
|
||||||
|
return (
|
||||||
|
<Reveal className="mx-auto grid max-w-[980px] gap-3 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") {
|
if (wallet.status !== "loggedIn") {
|
||||||
return (
|
return (
|
||||||
<Reveal className="flex min-h-[60vh] flex-col items-center justify-center gap-5 px-4 py-12 text-center">
|
<Reveal className="flex min-h-[60vh] flex-col items-center justify-center gap-5 px-4 py-12 text-center">
|
||||||
@@ -330,7 +346,7 @@ export default function Favorites() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading || !loaded ? (
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
{Array.from({ length: 4 }).map((_, index) => (
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
<Skeleton key={index} className="h-[132px] rounded-2xl" />
|
<Skeleton key={index} className="h-[132px] rounded-2xl" />
|
||||||
|
|||||||
Reference in New Issue
Block a user