From 084c002c45d31df50c7bf58c5282de99d323256d Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sun, 4 Oct 2026 18:01:49 +0700 Subject: [PATCH] Fix mobile grid overflow + add a Top Shops leaderboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /lab profile card and /stats Top Mappers were overflowing off the right edge: Tailwind `grid md:grid-cols-2` (no base column) renders one implicit `auto` track on mobile that can exceed the container when a name is long, so the whole card got pushed off-screen. Added the base `grid-cols-1` repo-wide (plus `min-w-0` on truncating flex/grid children). /stats also gained a "Top Shops — most shops added" leaderboard (kind 30017) beside Top Mappers (spots): `topShops` in useGrowthStats, a generalized MapperRow (count + unit), and EN/ES copy. PERSIST_BUSTER → v6 (new persisted field). --- AGENTS.md | 8 +++ src/components/auth/SignupMethodDialog.tsx | 2 +- src/hooks/useGrowthStats.ts | 16 +++++ src/i18n/en.ts | 4 ++ src/i18n/es.ts | 4 ++ src/lib/queryPersister.ts | 2 +- src/pages/AboutPage.tsx | 6 +- src/pages/CommunityStatsPage.tsx | 68 +++++++++++++++++++--- src/pages/LabPage.tsx | 8 +-- src/pages/MarketplacePage.tsx | 2 +- src/pages/OnboardingPage.tsx | 6 +- src/pages/PrivacyPage.tsx | 2 +- src/pages/RiderCardPage.tsx | 2 +- 13 files changed, 106 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4fefe00..ad42bb2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1257,6 +1257,14 @@ session with **no other memory of this project**. So: with the QR; the `bunker://` paste stays as the fallback. ⚠️ **NOT verified against a real signer — needs an on-device check with Amber** (scan → pair → signed in); only the URI shape was verified in Node. +- **Mobile grid overflow gotcha (2026-10-04):** a Tailwind grid **must** declare a + base column — `grid grid-cols-1 md:grid-cols-2`, **not** `grid md:grid-cols-2`. + Without the base, mobile renders one implicit `auto` track that can exceed the + container, so a long name (e.g. a `/lab` `ProfileCard`, or `/stats` Top Mappers) + pushes the whole card off the right edge and clips the values. Fixed repo-wide; + also add `min-w-0` to truncating flex/grid children. `/stats` also gained a + **Top Shops** leaderboard (`topShops` in `useGrowthStats`; `PERSIST_BUSTER` → v6) + alongside Top Mappers (spots). - **Community stats (`/stats`, 2026-10-01):** `useGrowthStats` + `CommunityStatsPage`. - ⚠️ **A relay outage used to render a board of ZEROS as truth** — every sub-query ended in `.catch(() => [])`. The queryFn now counts failures (a diff --git a/src/components/auth/SignupMethodDialog.tsx b/src/components/auth/SignupMethodDialog.tsx index 9e1c164..030aafa 100644 --- a/src/components/auth/SignupMethodDialog.tsx +++ b/src/components/auth/SignupMethodDialog.tsx @@ -32,7 +32,7 @@ const SignupMethodDialog: React.FC = ({

-
+
{/* Mnemonic Option (Recommended) */} diff --git a/src/hooks/useGrowthStats.ts b/src/hooks/useGrowthStats.ts index de3b43d..ec49cba 100644 --- a/src/hooks/useGrowthStats.ts +++ b/src/hooks/useGrowthStats.ts @@ -54,6 +54,11 @@ export interface TopMapper { totalContent: number; } +export interface TopShop { + pubkey: string; + shopCount: number; +} + export interface GrowthStats { // ── Totals ────────────────────────────────────────────────────────────── totalSpots: number; @@ -92,6 +97,8 @@ export interface GrowthStats { // ── Leaderboard ───────────────────────────────────────────────────────── topMappers: TopMapper[]; + /** Top shops by shop count (kind 30017). */ + topShops: TopShop[]; // ── Marketplace ────────────────────────────────────────────────────────── /** Live NIP-99 listings (stash "showcase-only" items excluded). */ @@ -485,6 +492,14 @@ export function useGrowthStats() { totalContent: mapperTotal.get(pubkey) ?? spotCount, })); + // ── Top shops by shop count ────────────────────────────────────────── + const mapperShops = new Map(); + for (const e of shops) { mapperShops.set(e.pubkey, (mapperShops.get(e.pubkey) ?? 0) + 1); } + const topShops: TopShop[] = Array.from(mapperShops.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) + .map(([pubkey, shopCount]) => ({ pubkey, shopCount })); + // ── Unique mappers ─────────────────────────────────────────────────── const allPubkeys = new Set(allContent.map(e => e.pubkey)); @@ -552,6 +567,7 @@ export function useGrowthStats() { monthly, cumulativeSpots, topMappers, + topShops, totalListings: listings.length, totalTrades: trades.length, tradedSats, diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 38d9ccc..7903f83 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -6337,6 +6337,10 @@ walletUi: { topMappers: 'Top Mappers', mostSpots: '— most spots added', noMapperData: 'No mapper data yet', + topShops: 'Top Shops', + mostShops: '— most shops added', + shopCount: '{{count}} shops', + noShopData: 'No shop data yet', supporters: 'Community Supporters', fundedTitle: 'Funded by the Community', fundedDesc1: 'Decentralized Bitcoin crowdfunding via Angor.', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 3acfbc5..2cb071b 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -6337,6 +6337,10 @@ walletUi: { topMappers: 'Mejores mapeadores', mostSpots: '— más spots añadidos', noMapperData: 'Aún no hay datos de mapeadores', + topShops: 'Mejores tiendas', + mostShops: '— más tiendas añadidas', + shopCount: '{{count}} tiendas', + noShopData: 'Aún no hay datos de tiendas', supporters: 'Patrocinadores de la comunidad', fundedTitle: 'Financiado por la comunidad', fundedDesc1: 'Crowdfunding descentralizado en Bitcoin vía Angor.', diff --git a/src/lib/queryPersister.ts b/src/lib/queryPersister.ts index 1bd0ec3..b3fc13b 100644 --- a/src/lib/queryPersister.ts +++ b/src/lib/queryPersister.ts @@ -40,7 +40,7 @@ import type { Query } from '@tanstack/react-query'; const IDB_KEY = 'skatespots:react-query-cache'; /** Bump when the persisted data shape changes to invalidate old caches. */ -export const PERSIST_BUSTER = 'v5'; +export const PERSIST_BUSTER = 'v6'; /** Discard any persisted cache older than this (ms). */ export const PERSIST_MAX_AGE = 24 * 60 * 60 * 1000; // 24h diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx index b65cb40..dd57ee9 100644 --- a/src/pages/AboutPage.tsx +++ b/src/pages/AboutPage.tsx @@ -113,7 +113,7 @@ Version: v0.1.0`;
-
+

{t('settingsUi.about.why1Title')}

@@ -147,7 +147,7 @@ Version: v0.1.0`; {/* Links */} -
+

{t('settingsUi.about.getInvolved')}

@@ -254,7 +254,7 @@ Version: v0.1.0`; {t('settingsUi.about.credits')} -
+
{[ { label: t('settingsUi.about.creditPoweredBy'), name: 'Nostr', url: 'https://github.com/nostr-protocol/nostr', desc: t('settingsUi.about.creditNostr') }, { label: t('settingsUi.about.creditCrewChat'), name: 'Cordn', url: 'https://cordn.net/', desc: t('settingsUi.about.creditCordn') }, diff --git a/src/pages/CommunityStatsPage.tsx b/src/pages/CommunityStatsPage.tsx index 8d3c989..716b51d 100644 --- a/src/pages/CommunityStatsPage.tsx +++ b/src/pages/CommunityStatsPage.tsx @@ -136,7 +136,7 @@ function StatCard({ // ─── Top mapper row ────────────────────────────────────────────────────────── -function MapperRow({ pubkey, rank, spotCount }: { pubkey: string; rank: number; spotCount: number }) { +function MapperRow({ pubkey, rank, count, unit }: { pubkey: string; rank: number; count: number; unit: 'spots' | 'shops' }) { const { t } = useTranslation(); const author = useAuthor(pubkey); const meta = author.data?.metadata; @@ -144,7 +144,7 @@ function MapperRow({ pubkey, rank, spotCount }: { pubkey: string; rank: number; const rankColors: Record = { 1: '#f7931a', 2: '#c0c0c0', 3: '#cd7f32' }; return ( -
+
{rank <= 3 ? ['🥇','🥈','🥉'][rank - 1] : rank} @@ -154,9 +154,9 @@ function MapperRow({ pubkey, rank, spotCount }: { pubkey: string; rank: number; {name.slice(0, 2).toUpperCase()} - {name} + {name} - {t('walletUi.stats.spotCount', { count: spotCount })} + {t(unit === 'shops' ? 'walletUi.stats.shopCount' : 'walletUi.stats.spotCount', { count })}
); @@ -389,7 +389,7 @@ export default function CommunityStatsPage() { {t('walletUi.stats.resolving', { count: derivingPending })}

)} -
+
{topCountries.length > 0 && ( @@ -479,7 +479,7 @@ export default function CommunityStatsPage() { {t('walletUi.stats.growth')} -
+
{/* Cumulative spots — area chart */} @@ -605,7 +605,7 @@ export default function CommunityStatsPage() { {t('walletUi.stats.mostSpots')} -
+
{isLoading ? ( <> @@ -638,14 +638,14 @@ export default function CommunityStatsPage() { {(stats.topMappers ?? []).slice(0, 5).map((m, i) => ( - + ))} {stats.topMappers.slice(5, 10).map((m, i) => ( - + ))} @@ -658,6 +658,56 @@ export default function CommunityStatsPage() {
+ {/* ── Section 3b: Top shops ── */} +
+

+ {t('walletUi.stats.topShops')} + {t('walletUi.stats.mostShops')} +

+ +
+ {isLoading ? ( + <> + {[0, 1].map((c) => ( + + + {Array.from({ length: 5 }).map((_, i) => ( +
+ + + + +
+ ))} +
+
+ ))} + + ) : stats && (stats.topShops ?? []).length > 0 ? ( + <> + + + {(stats.topShops ?? []).slice(0, 5).map((m, i) => ( + + ))} + + + + + {(stats.topShops ?? []).slice(5, 10).map((m, i) => ( + + ))} + + + + ) : ( +
+ {t('walletUi.stats.noShopData')} +
+ )} +
+
+ {/* ── Section 4: Supporters ── */}

diff --git a/src/pages/LabPage.tsx b/src/pages/LabPage.tsx index d4eb928..01c9067 100644 --- a/src/pages/LabPage.tsx +++ b/src/pages/LabPage.tsx @@ -1116,8 +1116,8 @@ function ProfileCard({ profile }: { profile: LabProfile }) { {/* Name row */}
-

- {displayName} +

+ {displayName} {profile.entityType === 'collective' && ( {isLoading ? ( -
+
{Array.from({ length: 6 }).map((_, i) => )}
) : filteredProfiles.length > 0 ? ( -
+
{filteredProfiles.map((profile) => ( diff --git a/src/pages/MarketplacePage.tsx b/src/pages/MarketplacePage.tsx index e04560b..25d30e1 100644 --- a/src/pages/MarketplacePage.tsx +++ b/src/pages/MarketplacePage.tsx @@ -81,7 +81,7 @@ export default function MarketplacePage() { {/* What You Can Buy/Sell */}

{t('marketUi.marketplace.buySellTitle')}

-
+
{[ { title: t('marketUi.marketplace.catCompletes'), diff --git a/src/pages/OnboardingPage.tsx b/src/pages/OnboardingPage.tsx index faee008..a1a4297 100644 --- a/src/pages/OnboardingPage.tsx +++ b/src/pages/OnboardingPage.tsx @@ -314,7 +314,7 @@ export default function OnboardingPage() { {/* ── Why skaters care (collapsed by default — keeps the first view light) ── */}
-