fix: info page — slug collision guard, error resilience, overflow, loyalty responsive

- Guard extraPages against slugs colliding with built-in tab IDs
- Handle tabReplacements API failure gracefully (proceed with built-in content)
- Fix overflow: use overflow-x-auto on inner divs instead of fighting bento-card
- Loyalty stat cards: responsive font size + truncate for 320px screens
- Clamp negative "remaining to next tier" amount to zero
- Fix toggleFaq stale closure with functional updater
This commit is contained in:
Fringg
2026-04-24 15:07:11 +03:00
parent 656946952a
commit d6918ee438

View File

@@ -374,7 +374,7 @@ export default function Info() {
const locale = i18n.language.split('-')[0]; const locale = i18n.language.split('-')[0];
// Fetch tab replacements // Fetch tab replacements
const { data: tabReplacements } = useQuery({ const { data: tabReplacements, isError: replacementsError } = useQuery({
queryKey: ['info-pages', 'tab-replacements'], queryKey: ['info-pages', 'tab-replacements'],
queryFn: infoPagesApi.getTabReplacements, queryFn: infoPagesApi.getTabReplacements,
staleTime: 60_000, staleTime: 60_000,
@@ -387,9 +387,9 @@ export default function Info() {
staleTime: 60_000, staleTime: 60_000,
}); });
// Filter to only pages that don't replace a built-in tab // Filter to only pages that don't replace a built-in tab and don't collide with built-in IDs
const extraPages = useMemo( const extraPages = useMemo(
() => (customPages ?? []).filter((p) => !p.replaces_tab), () => (customPages ?? []).filter((p) => !p.replaces_tab && !BUILTIN_TABS.has(p.slug)),
[customPages], [customPages],
); );
@@ -406,8 +406,8 @@ export default function Info() {
// Slug to fetch: either a custom page tab or a tab replacement // Slug to fetch: either a custom page tab or a tab replacement
const pageSlugToFetch = customTabSlug ?? currentTabSlug; const pageSlugToFetch = customTabSlug ?? currentTabSlug;
// Wait for tab replacements before firing built-in queries // Wait for tab replacements before firing built-in queries (also proceed on error — graceful degradation)
const replacementsLoaded = tabReplacements !== undefined; const replacementsLoaded = tabReplacements !== undefined || replacementsError;
// Fetch the InfoPage when needed (replacement or custom tab) // Fetch the InfoPage when needed (replacement or custom tab)
const { data: infoPage, isLoading: infoPageLoading } = useQuery({ const { data: infoPage, isLoading: infoPageLoading } = useQuery({
@@ -496,9 +496,9 @@ export default function Info() {
const tabs = [...builtinTabs, ...customTabs]; const tabs = [...builtinTabs, ...customTabs];
const toggleFaq = (id: number) => { const toggleFaq = useCallback((id: number) => {
setExpandedFaq(expandedFaq === id ? null : id); setExpandedFaq((prev) => (prev === id ? null : id));
}; }, []);
const renderInfoPageContent = () => { const renderInfoPageContent = () => {
if (infoPageLoading) { if (infoPageLoading) {
@@ -525,8 +525,8 @@ export default function Info() {
} }
return ( return (
<div className="bento-card prose prose-invert max-w-none !overflow-visible"> <div className="bento-card prose prose-invert max-w-none">
<div dangerouslySetInnerHTML={{ __html: infoPageHtml }} /> <div className="overflow-x-auto" dangerouslySetInnerHTML={{ __html: infoPageHtml }} />
</div> </div>
); );
}; };
@@ -601,7 +601,10 @@ export default function Info() {
return ( return (
<div className="bento-card prose prose-invert max-w-none"> <div className="bento-card prose prose-invert max-w-none">
<div dangerouslySetInnerHTML={{ __html: formatContent(rules.content) }} /> <div
className="overflow-x-auto"
dangerouslySetInnerHTML={{ __html: formatContent(rules.content) }}
/>
{rules.updated_at && ( {rules.updated_at && (
<p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400"> <p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400">
{t('info.updatedAt')}: {new Date(rules.updated_at).toLocaleDateString()} {t('info.updatedAt')}: {new Date(rules.updated_at).toLocaleDateString()}
@@ -626,7 +629,10 @@ export default function Info() {
return ( return (
<div className="bento-card prose prose-invert max-w-none"> <div className="bento-card prose prose-invert max-w-none">
<div dangerouslySetInnerHTML={{ __html: formatContent(privacy.content) }} /> <div
className="overflow-x-auto"
dangerouslySetInnerHTML={{ __html: formatContent(privacy.content) }}
/>
{privacy.updated_at && ( {privacy.updated_at && (
<p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400"> <p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400">
{t('info.updatedAt')}: {new Date(privacy.updated_at).toLocaleDateString()} {t('info.updatedAt')}: {new Date(privacy.updated_at).toLocaleDateString()}
@@ -651,7 +657,10 @@ export default function Info() {
return ( return (
<div className="bento-card prose prose-invert max-w-none"> <div className="bento-card prose prose-invert max-w-none">
<div dangerouslySetInnerHTML={{ __html: formatContent(offer.content) }} /> <div
className="overflow-x-auto"
dangerouslySetInnerHTML={{ __html: formatContent(offer.content) }}
/>
{offer.updated_at && ( {offer.updated_at && (
<p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400"> <p className="mt-6 border-t border-dark-700 pt-4 text-sm text-dark-400">
{t('info.updatedAt')}: {new Date(offer.updated_at).toLocaleDateString()} {t('info.updatedAt')}: {new Date(offer.updated_at).toLocaleDateString()}
@@ -723,13 +732,13 @@ export default function Info() {
<div className="mb-4 grid grid-cols-2 gap-4"> <div className="mb-4 grid grid-cols-2 gap-4">
<div className="rounded-xl bg-dark-800/50 p-3"> <div className="rounded-xl bg-dark-800/50 p-3">
<div className="mb-1 text-xs text-dark-400">{t('info.totalSpent')}</div> <div className="mb-1 text-xs text-dark-400">{t('info.totalSpent')}</div>
<div className="text-lg font-bold text-dark-50"> <div className="truncate text-base font-bold text-dark-50 sm:text-lg">
{formatCurrency(loyaltyData.current_spent_rubles)} {formatCurrency(loyaltyData.current_spent_rubles)}
</div> </div>
</div> </div>
<div className="rounded-xl bg-dark-800/50 p-3"> <div className="rounded-xl bg-dark-800/50 p-3">
<div className="mb-1 text-xs text-dark-400">{t('info.currentStatus')}</div> <div className="mb-1 text-xs text-dark-400">{t('info.currentStatus')}</div>
<div className="text-lg font-bold text-accent-400"> <div className="truncate text-base font-bold text-accent-400 sm:text-lg">
{loyaltyData.current_tier_name || '-'} {loyaltyData.current_tier_name || '-'}
</div> </div>
</div> </div>
@@ -745,7 +754,10 @@ export default function Info() {
<span> <span>
{t('info.toNextStatus')}:{' '} {t('info.toNextStatus')}:{' '}
{formatCurrency( {formatCurrency(
loyaltyData.next_tier_threshold_rubles - loyaltyData.current_spent_rubles, Math.max(
0,
loyaltyData.next_tier_threshold_rubles - loyaltyData.current_spent_rubles,
),
)} )}
</span> </span>
</div> </div>