diff --git a/public/flag-tibet.svg b/public/flag-tibet.svg new file mode 100644 index 00000000..0bc13ea6 --- /dev/null +++ b/public/flag-tibet.svg @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/CountryFlag.tsx b/src/components/CountryFlag.tsx new file mode 100644 index 00000000..36a1f6d3 --- /dev/null +++ b/src/components/CountryFlag.tsx @@ -0,0 +1,75 @@ +import { cn } from '@/lib/utils'; + +interface CountryFlagProps { + /** + * ISO 3166-1 alpha-2 country code (e.g. `US`, `BR`) or ISO 3166-2 + * subdivision code (e.g. `CN-XZ`, `GB-SCT`). Case-insensitive. + */ + code: string; + /** The flag emoji to render when no custom asset is available. */ + emoji: string; + /** Accessible label / `alt` for the flag. */ + label: string; + /** Optional extra classes applied to the rendering element. */ + className?: string; +} + +/** + * Map of ISO codes that render as a bundled `` asset instead of an + * emoji. Unicode does not define flag emoji for everything that has a + * recognised flag — Tibet (the Snow Lion flag) being the headline case — + * so we ship the canonical SVG and swap it in when we can. + * + * Note we treat `CN-XZ` as country-level Tibet here, mirroring the + * editorial choice the older Agora codebase made: the ISO 3166-2 code is + * used as the identifier, but the display reads as a country. + */ +const CUSTOM_FLAG_ASSETS: Record = { + 'CN-XZ': '/flag-tibet.svg', +}; + +/** + * Render a flag for a country or subdivision. For codes with a bundled + * SVG (currently Tibet) we emit an `` that visually matches the + * surrounding emoji line-height; for everything else we drop the emoji + * straight into a `` so it inherits font color and selection + * behaviour like the rest of the text run. + * + * Callers control sizing via Tailwind classes — pass `text-3xl` to size + * the emoji and the SVG will scale to match (`h-[1em] w-auto`). + */ +export function CountryFlag({ code, emoji, label, className }: CountryFlagProps) { + const upper = code.toUpperCase(); + const customAsset = CUSTOM_FLAG_ASSETS[upper]; + + if (customAsset) { + return ( + // The wrapper span carries the font-size class so the inner image + // can size itself in `em` units and stay in lockstep with the + // emoji glyphs on neighbouring chips. + + + + ); + } + + return ( + + {emoji} + + ); +} diff --git a/src/components/discovery/CountryPulseStrip.tsx b/src/components/discovery/CountryPulseStrip.tsx index be7dca39..7a4f6396 100644 --- a/src/components/discovery/CountryPulseStrip.tsx +++ b/src/components/discovery/CountryPulseStrip.tsx @@ -2,8 +2,9 @@ import { useMemo } from 'react'; import { Link } from 'react-router-dom'; import { useGlobalActivity } from '@/hooks/useGlobalActivity'; -import { getCountryInfo } from '@/lib/countries'; +import { getCountryInfo, subdivisionFlag } from '@/lib/countries'; import { Skeleton } from '@/components/ui/skeleton'; +import { CountryFlag } from '@/components/CountryFlag'; import { cn } from '@/lib/utils'; interface CountryPulseStripProps { @@ -13,30 +14,61 @@ interface CountryPulseStripProps { } interface CountryEntry { - /** ISO 3166-1 alpha-2 code. */ + /** Full ISO 3166 identifier — either `XX` (country) or `XX-YY` (subdivision). */ code: string; - /** Display name (from `getCountryInfo`). */ + /** + * Display name. For subdivisions this is the subdivision name + * (e.g. "California"); for countries it's the country name + * (e.g. "United States"). + */ name: string; - /** Flag emoji. */ + /** + * Best emoji flag for this entry. Subdivisions with an RGI tag + * sequence (currently England, Scotland, Wales) get their actual + * subnational flag; everything else falls back to the parent country + * flag and a subdivision-code badge. + */ flag: string; + /** + * Subdivision token (e.g. `CA`, `TX`, `BY`) shown as a small badge + * overlay when the entry is a state/province *and* no native + * subdivision flag exists. `undefined` for country-level entries and + * for subdivisions that already have their own flag emoji. + */ + subdivisionToken?: string; /** Activity count from kind 30385 stats. */ count: number; } /** - * Horizontal strip of country flag chips, ordered by trailing-window - * activity from the trusted kind 30385 stats snapshots. Hovering a chip - * lifts its flag and brightens its warm gradient sleeve; clicking opens - * the country's NIP-73 external-identifier feed at `/i/iso3166:XX`. + * Horizontal strip of country (and subdivision) flag chips, ordered by + * trailing-window activity from the trusted kind 30385 stats snapshots. + * Hovering a chip lifts its flag and brightens its warm gradient + * sleeve; clicking opens the entity's NIP-73 external-identifier feed + * at `/i/iso3166:XX` (or `/i/iso3166:XX-YY` for subdivisions). * * Sits below the hero on the Discover page as a low-friction entry into - * country-scoped browsing — the "this is where the world is showing up + * geo-scoped browsing — the "this is where the world is showing up * today" rail. * * Renders a soft-pulse skeleton while activity data is loading so the * page never collapses to zero height between the hero and the campaign * shelf. */ +/** + * ISO 3166-2 codes we display as country-level entries rather than + * subdivisions. The strip shows the subdivision's *own* name (no parent + * country fallback) and suppresses the small ISO-suffix badge — these + * are entities with their own widely-recognised flag and identity. + * + * Currently just Tibet: ISO 3166-2 lists `CN-XZ` as "Tibet Autonomous + * Region" under China, but the editorial position here is to surface it + * as a country in its own right with the Snow Lion flag. + */ +const COUNTRY_LEVEL_SUBDIVISIONS: Record = { + 'CN-XZ': 'Tibet', +}; + export function CountryPulseStrip({ limit = 24, className }: CountryPulseStripProps) { const { data: activityByCountry, isLoading } = useGlobalActivity(); @@ -46,15 +78,59 @@ export function CountryPulseStrip({ limit = 24, className }: CountryPulseStripPr for (const [code, count] of activityByCountry) { const info = getCountryInfo(code); if (!info) continue; - out.push({ code, name: info.name, flag: info.flag, count }); + const upperCode = code.toUpperCase(); + const countryLevelOverride = COUNTRY_LEVEL_SUBDIVISIONS[upperCode]; + const isSubdivision = !!info.subdivision; + + // Country-level override (Tibet etc.) wins: use the editorial + // name, drop the subdivision-token badge, and let CountryFlag pick + // up the bundled SVG asset on render. + if (countryLevelOverride) { + out.push({ + code, + name: countryLevelOverride, + flag: info.flag, + subdivisionToken: undefined, + count, + }); + continue; + } + + // Prefer subdivision-level naming when available — "California" reads + // far more usefully than yet another "United States" tile. + const name = (isSubdivision ? info.subdivisionName : info.name) ?? info.name; + // Use the real subdivision flag (England/Scotland/Wales tag + // sequences) when one exists; otherwise fall back to the parent + // country flag and surface the ISO 3166-2 suffix as a badge. + const nativeSubFlag = isSubdivision && info.subdivision + ? subdivisionFlag(info.subdivision) + : null; + const flag = nativeSubFlag ?? info.flag; + const subdivisionToken = isSubdivision && !nativeSubFlag + ? info.subdivision?.split('-')[1] + : undefined; + out.push({ + code, + name, + flag, + subdivisionToken, + count, + }); } out.sort((a, b) => b.count - a.count); return out.slice(0, limit); }, [activityByCountry, limit]); + // Vertical padding (`py-2`) on the scroll track gives the chips room + // to lift on hover (`-translate-y-0.5` plus the slightly larger glyph) + // without `overflow-x-auto` cropping the top edge — `overflow-x-auto` + // implicitly clips on the Y axis too, and `overflow-y-visible` is + // unreliable across browsers, so we pad instead of fight the clip. + const scrollClass = 'flex gap-3 overflow-x-auto scrollbar-none px-4 py-2'; + if (isLoading && entries.length === 0) { return ( -
+
{Array.from({ length: 8 }).map((_, i) => ( ))} @@ -65,7 +141,7 @@ export function CountryPulseStrip({ limit = 24, className }: CountryPulseStripPr if (entries.length === 0) return null; return ( -
+
{entries.map((entry) => ( ))} @@ -74,6 +150,10 @@ export function CountryPulseStrip({ limit = 24, className }: CountryPulseStripPr } function CountryChip({ entry }: { entry: CountryEntry }) { + const ariaLabel = entry.subdivisionToken + ? `${entry.name} (${entry.code}): ${entry.count.toLocaleString()} recent comments` + : `${entry.name}: ${entry.count.toLocaleString()} recent comments`; + return ( -
+ {/* Readability scrim. Sits above the globe + atmosphere but below + the foreground content so the headline / paragraph stay legible + regardless of which hue the palette is currently cycling + through. Top-down so the eye-line lands on the darkest pixels; + we taper to transparent before the ticker pill so the CTAs and + stat row underneath keep their warm wash. */} +