feat: redesign wallet login and favorites, fix desktop/mobile bugs
- Remove forced BNB chain switch on injected login (signature is chain-agnostic) - Refine isMobileDevice so touch Macs stay on desktop flow - Wire RainbowKit/WalletConnect as a real MetaMask/imToken QR fallback, gated on a valid VITE_WALLETCONNECT_PROJECT_ID - Rebuild login modal: single desktop primary action, collapsible other methods, mobile open-app fallback feedback, brand icons - Add My Favorites entry points (header, mobile menu, wallet dropdown) - Favorites page: error retry, mobile filter drawer - Auto sign-out and re-login prompt on favorites 401 - Full native translations for all wallet strings across 7 locales Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Heart, Search, SlidersHorizontal, X } from "lucide-react";
|
||||
import { Heart, RotateCcw, Search, SlidersHorizontal, X } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
type Resource,
|
||||
} from "../../api";
|
||||
import { FavoriteButton } from "../../favorites/FavoriteButton";
|
||||
import { listFavorites, type FavoriteSort } from "../../favorites/api";
|
||||
import {
|
||||
isFavoritesAuthError,
|
||||
listFavorites,
|
||||
type FavoriteSort,
|
||||
} from "../../favorites/api";
|
||||
import { useFavorites } from "../../favorites/FavoritesProvider";
|
||||
import { langQuery, useI18n, type Lang } from "../../i18n";
|
||||
import { homePathForLang } from "../../languageRoutes";
|
||||
@@ -134,6 +138,8 @@ export default function Favorites() {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
useSetPageTitle(t("favorites"));
|
||||
|
||||
@@ -167,8 +173,13 @@ export default function Favorites() {
|
||||
resources.forEach((resource) => markFavorite(resource.id, true));
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled)
|
||||
setError(err instanceof Error ? err.message : t("loadFailed"));
|
||||
if (cancelled) return;
|
||||
if (isFavoritesAuthError(err)) {
|
||||
wallet.logout();
|
||||
wallet.openLoginModal();
|
||||
return;
|
||||
}
|
||||
setError(err instanceof Error ? err.message : t("loadFailed"));
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
@@ -176,17 +187,7 @@ export default function Favorites() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
category,
|
||||
lang,
|
||||
markFavorite,
|
||||
page,
|
||||
query,
|
||||
sort,
|
||||
t,
|
||||
wallet.status,
|
||||
wallet.token,
|
||||
]);
|
||||
}, [category, lang, markFavorite, page, query, reloadKey, sort, t, wallet]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const hasFilters = Boolean(category || query || sort !== "favorited_at");
|
||||
@@ -260,33 +261,50 @@ export default function Favorites() {
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="relative block">
|
||||
<SlidersHorizontal className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-500" />
|
||||
{/* Mobile-only toggle: collapse sort/category into a "Filters" drawer. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFilters((value) => !value)}
|
||||
aria-expanded={showFilters}
|
||||
className="inline-flex h-11 items-center justify-center gap-2 rounded-full border border-white/10 bg-[#101016] px-4 text-sm font-medium text-neutral-200 transition hover:border-ark-gold/40 hover:text-ark-gold md:hidden"
|
||||
>
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
{t("favoritesFilters")}
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={`${showFilters ? "grid" : "hidden"} gap-3 md:contents`}
|
||||
>
|
||||
<label className="relative block">
|
||||
<SlidersHorizontal className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-500" />
|
||||
<select
|
||||
value={sort}
|
||||
onChange={(event) =>
|
||||
setSort(event.target.value as FavoriteSort)
|
||||
}
|
||||
className="h-11 w-full appearance-none rounded-full border border-white/10 bg-[#101016] pl-10 pr-4 text-sm text-white outline-none focus:border-ark-gold/60"
|
||||
>
|
||||
{sortOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<select
|
||||
value={sort}
|
||||
onChange={(event) => setSort(event.target.value as FavoriteSort)}
|
||||
className="h-11 w-full appearance-none rounded-full border border-white/10 bg-[#101016] pl-10 pr-4 text-sm text-white outline-none focus:border-ark-gold/60"
|
||||
value={category}
|
||||
onChange={(event) => setCategory(event.target.value)}
|
||||
className="h-11 w-full rounded-full border border-white/10 bg-[#101016] px-4 text-sm text-white outline-none focus:border-ark-gold/60"
|
||||
>
|
||||
{sortOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
<option value="">{t("favoritesFilterAllCategories")}</option>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat.slug} value={cat.slug}>
|
||||
{cleanCategoryDisplayName(cat.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<select
|
||||
value={category}
|
||||
onChange={(event) => setCategory(event.target.value)}
|
||||
className="h-11 w-full rounded-full border border-white/10 bg-[#101016] px-4 text-sm text-white outline-none focus:border-ark-gold/60"
|
||||
>
|
||||
<option value="">{t("favoritesFilterAllCategories")}</option>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat.slug} value={cat.slug}>
|
||||
{cleanCategoryDisplayName(cat.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
@@ -320,8 +338,16 @@ export default function Favorites() {
|
||||
))}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="rounded-2xl border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-200">
|
||||
{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>
|
||||
) : items.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">
|
||||
|
||||
Reference in New Issue
Block a user