{country.name}
diff --git a/src/components/world/CountryStatsDialog.tsx b/src/components/world/CountryStatsDialog.tsx
index c61612ce..64098e13 100644
--- a/src/components/world/CountryStatsDialog.tsx
+++ b/src/components/world/CountryStatsDialog.tsx
@@ -7,7 +7,8 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { CommunityStatsPanel } from '@/components/CommunityStatsPanel';
-import { COUNTRIES } from '@/lib/countries';
+import { CountryFlag } from '@/components/CountryFlag';
+import { getCountryInfo } from '@/lib/countries';
interface CountryStatsDialogProps {
/** ISO 3166-1 alpha-2 country code (e.g. `VE`). */
@@ -31,8 +32,8 @@ interface CountryStatsDialogProps {
* trigger inside a dropdown menu without rendering two visible affordances.
*/
export function CountryStatsDialog({ countryCode, open, onOpenChange }: CountryStatsDialogProps) {
- const country = COUNTRIES[countryCode.toUpperCase()];
- const countryName = country?.name ?? countryCode;
+ const country = getCountryInfo(countryCode);
+ const countryName = country?.subdivisionName ?? country?.name ?? countryCode;
const flag = country?.flag ?? '';
return (
@@ -43,13 +44,12 @@ export function CountryStatsDialog({ countryCode, open, onOpenChange }: CountryS
{flag ? (
-
- {flag}
-
+
) : (
)}
diff --git a/src/lib/countries.ts b/src/lib/countries.ts
index 8fbfc4fe..c743d75e 100644
--- a/src/lib/countries.ts
+++ b/src/lib/countries.ts
@@ -95,6 +95,7 @@ export const COUNTRIES: Record = {
KZ: { name: 'Kazakhstan', flag: '🇰🇿' },
KE: { name: 'Kenya', flag: '🇰🇪' },
KI: { name: 'Kiribati', flag: '🇰🇮' },
+ XK: { name: 'Kosovo', flag: '🌍' },
KP: { name: 'North Korea', flag: '🇰🇵' },
KR: { name: 'South Korea', flag: '🇰🇷' },
KW: { name: 'Kuwait', flag: '🇰🇼' },
@@ -199,15 +200,30 @@ export const COUNTRIES: Record = {
VA: { name: 'Vatican City', flag: '🇻🇦' },
VE: { name: 'Venezuela', flag: '🇻🇪' },
VN: { name: 'Vietnam', flag: '🇻🇳' },
+ EH: { name: 'Western Sahara', flag: '🇪🇭' },
YE: { name: 'Yemen', flag: '🇾🇪' },
ZM: { name: 'Zambia', flag: '🇿🇲' },
ZW: { name: 'Zimbabwe', flag: '🇿🇼' },
};
/** Pre-sorted array of country entries for searching. */
-export const COUNTRY_LIST = Object.entries(COUNTRIES)
- .map(([code, { name, flag }]) => ({ code, name, flag }))
- .sort((a, b) => a.name.localeCompare(b.name));
+export const COUNTRY_LIST = (() => {
+ const base = Object.entries(COUNTRIES).map(([code, { name, flag }]) => ({ code, name, flag }));
+
+ // Promote a handful of ISO 3166-2 subdivisions to country-level entries
+ // in the search list. These are editorial choices to surface places that
+ // are commonly thought of as countries but lack their own ISO 3166-1
+ // code. The on-wire identifier stays `iso3166:CC-XX` so we don't fork
+ // a parallel addressing scheme — only the picker pretends.
+ const promoted: { code: string; name: string; flag: string }[] = [
+ // Tibet (CN-XZ) — bundled Snow Lion SVG renders via CountryFlag; the
+ // `flag` field here is the text fallback for raw-text consumers, so
+ // we use the parent-country emoji rather than nothing.
+ { code: 'CN-XZ', name: 'Tibet', flag: '🇨🇳' },
+ ];
+
+ return [...base, ...promoted].sort((a, b) => a.name.localeCompare(b.name));
+})();
export type CountryEntry = typeof COUNTRY_LIST[number];
@@ -390,15 +406,15 @@ export function isValidGeoCode(code: string): boolean {
// ---------------------------------------------------------------------------
/**
- * Return the list of ISO 3166-1 countries Agora knows about, sorted
- * alphabetically by English name. Pathos exposes a localized variant — Agora
- * is currently English-only so the `lang` argument is ignored. Kept for
- * call-site compatibility with ports.
+ * Return the list of countries Agora surfaces for picker UIs, sorted
+ * alphabetically by English name. Mirrors {@link COUNTRY_LIST} (which
+ * also includes editorially promoted ISO 3166-2 entries like Tibet).
+ * Pathos exposes a localized variant — Agora is currently English-only
+ * so the `lang` argument is ignored. Kept for call-site compatibility
+ * with ports.
*/
export function getAllCountries(_lang?: string): { code: string; name: string; flag: string }[] {
- return Object.entries(COUNTRIES)
- .map(([code, info]) => ({ code, name: info.name, flag: info.flag }))
- .sort((a, b) => a.name.localeCompare(b.name));
+ return COUNTRY_LIST.map(({ code, name, flag }) => ({ code, name, flag }));
}
/**
@@ -428,6 +444,11 @@ export function countryCodeToFlag(code: string): string {
const upper = code.toUpperCase();
const parentCode = upper.includes('-') ? upper.split('-')[0] : upper;
if (!/^[A-Z]{2}$/.test(parentCode)) return '';
+ // Honour explicit overrides first — covers user-assigned codes like
+ // Kosovo (`XK`) whose regional-indicator sequence has no associated
+ // Unicode flag glyph and would otherwise render as raw letters.
+ const explicit = COUNTRIES[parentCode]?.flag;
+ if (explicit) return explicit;
// Regional indicator symbols start at U+1F1E6 (🇦); A=0x41.
return parentCode
.split('')
diff --git a/src/lib/customFlags.ts b/src/lib/customFlags.ts
index 9f4c8258..3bd0eea8 100644
--- a/src/lib/customFlags.ts
+++ b/src/lib/customFlags.ts
@@ -4,13 +4,18 @@
* these as `
` elements; callers that need the raw URL (e.g. a
* card backdrop, a CSS background) use {@link customFlagAsset}.
*
- * Editorial choice: Tibet (ISO 3166-2 `CN-XZ`) is surfaced as a
- * country-level entity with the Snow Lion flag, matching the older
- * Agora codebase. Add additional entries here as new bundled assets
- * land in `/public`.
+ * Editorial choices:
+ * - Tibet (ISO 3166-2 `CN-XZ`) is surfaced as a country-level entity
+ * with the Snow Lion flag, matching the older Agora codebase.
+ * - Kosovo (`XK`) is a user-assigned ISO 3166-1 code with no Unicode
+ * emoji flag, so we bundle its SVG to render alongside the rest of
+ * the country list.
+ *
+ * Add additional entries here as new bundled assets land in `/public`.
*/
const CUSTOM_FLAG_ASSETS: Record = {
'CN-XZ': '/flag-tibet.svg',
+ 'XK': '/flag-kosovo.svg',
};
/**
diff --git a/src/pages/CreateActionPage.tsx b/src/pages/CreateActionPage.tsx
index 524648fd..0d8ede58 100644
--- a/src/pages/CreateActionPage.tsx
+++ b/src/pages/CreateActionPage.tsx
@@ -15,6 +15,7 @@ import {
} from 'lucide-react';
import { CoverImageField } from '@/components/CoverImageField';
+import { CountryFlag } from '@/components/CountryFlag';
import { FormSection } from '@/components/FormSection';
import { OrganizationContextChip } from '@/components/OrganizationContextChip';
import { TimezoneSwitcher } from '@/components/TimezoneSwitcher';
@@ -31,7 +32,7 @@ import { useManageableOrganizations } from '@/hooks/useManageableOrganizations';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { useToast } from '@/hooks/useToast';
import { usdToSats } from '@/lib/bitcoin';
-import { COUNTRIES, searchCountries, type CountryEntry } from '@/lib/countries';
+import { getCountryInfo, searchCountries, type CountryEntry } from '@/lib/countries';
import { parseContentTagInput } from '@/lib/contentTags';
import { createCountryIdentifier } from '@/lib/countryIdentifiers';
import { getTodayDateInput } from '@/lib/dateInput';
@@ -87,7 +88,7 @@ export function CreateActionPage() {
const [coverImage, setCoverImage] = useState('');
const [coverUploading, setCoverUploading] = useState(false);
const [countryCode, setCountryCode] = useState(pageCountryCode);
- const [countryQuery, setCountryQuery] = useState(pageCountryCode ? (COUNTRIES[pageCountryCode]?.name ?? pageCountryCode) : '');
+ const [countryQuery, setCountryQuery] = useState(pageCountryCode ? (getCountryInfo(pageCountryCode)?.subdivisionName ?? getCountryInfo(pageCountryCode)?.name ?? pageCountryCode) : '');
// Effective org coordinate to attach on publish. Sourced only from the
// URL — never editable inside the form. Drops to '' when the user
// isn't authorized for the param's org.
@@ -293,8 +294,9 @@ export function CreateActionPage() {
selectedCode={countryCode}
onQueryChange={(value) => {
setCountryQuery(value);
- const selectedCountry = countryCode ? COUNTRIES[countryCode] : undefined;
- if (selectedCountry && value !== selectedCountry.name && value.toUpperCase() !== countryCode) {
+ const selectedCountry = countryCode ? getCountryInfo(countryCode) : undefined;
+ const selectedName = selectedCountry?.subdivisionName ?? selectedCountry?.name;
+ if (selectedCountry && value !== selectedName && value.toUpperCase() !== countryCode) {
setCountryCode('');
}
}}
@@ -447,7 +449,7 @@ function CountrySelect({
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
- const selectedCountry = selectedCode ? COUNTRIES[selectedCode] : undefined;
+ const selectedCountry = selectedCode ? getCountryInfo(selectedCode) : undefined;
const results = useMemo(() => searchCountries(query), [query]);
const showResults = open && results.length > 0;
@@ -523,8 +525,13 @@ function CountrySelect({
index === selectedIndex && 'bg-secondary/60',
)}
>
-
- {country.flag}
+
+
{country.name}
diff --git a/src/pages/CreateCampaignPage.tsx b/src/pages/CreateCampaignPage.tsx
index 2690a08d..0aa989ac 100644
--- a/src/pages/CreateCampaignPage.tsx
+++ b/src/pages/CreateCampaignPage.tsx
@@ -17,6 +17,7 @@ import {
} from 'lucide-react';
import { CoverImageField } from '@/components/CoverImageField';
+import { CountryFlag } from '@/components/CountryFlag';
import { FormSection } from '@/components/FormSection';
import { OrganizationContextChip } from '@/components/OrganizationContextChip';
import { LoginArea } from '@/components/auth/LoginArea';
@@ -54,7 +55,7 @@ import { genUserName } from '@/lib/genUserName';
import { createOrganizationAssociationTags, decodeOrganizationParam } from '@/lib/organizationContext';
import { sanitizeUrl } from '@/lib/sanitizeUrl';
import { withAgoraTag } from '@/lib/agoraNoteTags';
-import { COUNTRIES, searchCountries, type CountryEntry } from '@/lib/countries';
+import { getCountryInfo, searchCountries, type CountryEntry } from '@/lib/countries';
import { getEditableContentTags, parseContentTagInput } from '@/lib/contentTags';
import { createCountryIdentifier } from '@/lib/countryIdentifiers';
import { cn } from '@/lib/utils';
@@ -281,7 +282,7 @@ export function CreateCampaignPage() {
setDeadline(formatDateInput(editCampaign.deadline));
const editCountryCode = editCampaign.countryCode ?? '';
setCountryCode(editCountryCode);
- setCountryQuery(editCountryCode ? COUNTRIES[editCountryCode]?.name ?? editCountryCode : '');
+ setCountryQuery(editCountryCode ? (getCountryInfo(editCountryCode)?.subdivisionName ?? getCountryInfo(editCountryCode)?.name ?? editCountryCode) : '');
setTagInput(getEditableContentTags(editCampaign.event.tags).join(', '));
const existingOrgATag = editCampaign.event.tags.find(
([n, v]) => n === 'A' && typeof v === 'string' && v.startsWith('34550:'),
@@ -692,8 +693,9 @@ export function CreateCampaignPage() {
selectedCode={countryCode}
onQueryChange={(value) => {
setCountryQuery(value);
- const selectedCountry = countryCode ? COUNTRIES[countryCode] : undefined;
- if (selectedCountry && value !== selectedCountry.name && value.toUpperCase() !== countryCode) {
+ const selectedCountry = countryCode ? getCountryInfo(countryCode) : undefined;
+ const selectedName = selectedCountry?.subdivisionName ?? selectedCountry?.name;
+ if (selectedCountry && value !== selectedName && value.toUpperCase() !== countryCode) {
setCountryCode('');
}
}}
@@ -1058,7 +1060,7 @@ function CountrySelect({
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
- const selectedCountry = selectedCode ? COUNTRIES[selectedCode] : undefined;
+ const selectedCountry = selectedCode ? getCountryInfo(selectedCode) : undefined;
const results = useMemo(() => searchCountries(query), [query]);
const showResults = open && results.length > 0;
@@ -1134,8 +1136,13 @@ function CountrySelect({
index === selectedIndex && 'bg-secondary/60',
)}
>
-
- {country.flag}
+
+
{country.name}
diff --git a/src/pages/CreateCommunityPage.tsx b/src/pages/CreateCommunityPage.tsx
index 8768e45e..ffeaaa15 100644
--- a/src/pages/CreateCommunityPage.tsx
+++ b/src/pages/CreateCommunityPage.tsx
@@ -17,6 +17,7 @@ import {
import { PersonSearch } from '@/components/PersonSearch';
import { CoverImageField } from '@/components/CoverImageField';
+import { CountryFlag } from '@/components/CountryFlag';
import { FormSection } from '@/components/FormSection';
import { LoginArea } from '@/components/auth/LoginArea';
import { Alert, AlertDescription } from '@/components/ui/alert';
@@ -38,7 +39,7 @@ import {
type ParsedCommunity,
} from '@/lib/communityUtils';
import { fetchFreshEvent } from '@/lib/fetchFreshEvent';
-import { COUNTRIES, searchCountries, type CountryEntry } from '@/lib/countries';
+import { getCountryInfo, searchCountries, type CountryEntry } from '@/lib/countries';
import { parseContentTagInput } from '@/lib/contentTags';
import { createCountryIdentifier } from '@/lib/countryIdentifiers';
import { genUserName } from '@/lib/genUserName';
@@ -257,7 +258,7 @@ export function CreateCommunityPage() {
setImageUrl(editCommunity.community.image ?? '');
const editCountryCode = editCommunity.community.countryCode ?? '';
setCountryCode(editCountryCode);
- setCountryQuery(editCountryCode ? COUNTRIES[editCountryCode]?.name ?? editCountryCode : '');
+ setCountryQuery(editCountryCode ? (getCountryInfo(editCountryCode)?.subdivisionName ?? getCountryInfo(editCountryCode)?.name ?? editCountryCode) : '');
setTagInput(editCommunity.community.topicTags.join(', '));
setModerators(editCommunity.community.moderatorPubkeys.map(makeProfileFromPubkey));
setPrepopulatedEventId(editCommunity.event.id);
@@ -608,8 +609,9 @@ export function CreateCommunityPage() {
selectedCode={countryCode}
onQueryChange={(value) => {
setCountryQuery(value);
- const selectedCountry = countryCode ? COUNTRIES[countryCode] : undefined;
- if (selectedCountry && value !== selectedCountry.name && value.toUpperCase() !== countryCode) {
+ const selectedCountry = countryCode ? getCountryInfo(countryCode) : undefined;
+ const selectedName = selectedCountry?.subdivisionName ?? selectedCountry?.name;
+ if (selectedCountry && value !== selectedName && value.toUpperCase() !== countryCode) {
setCountryCode('');
}
}}
@@ -729,7 +731,7 @@ function CountrySelect({
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
- const selectedCountry = selectedCode ? COUNTRIES[selectedCode] : undefined;
+ const selectedCountry = selectedCode ? getCountryInfo(selectedCode) : undefined;
const results = useMemo(() => searchCountries(query), [query]);
const showResults = open && results.length > 0;
@@ -805,8 +807,13 @@ function CountrySelect({
index === selectedIndex && 'bg-secondary/60',
)}
>
-
- {country.flag}
+
+
{country.name}
diff --git a/src/pages/MyDashboardPage.tsx b/src/pages/MyDashboardPage.tsx
index a914ae62..29dc1be0 100644
--- a/src/pages/MyDashboardPage.tsx
+++ b/src/pages/MyDashboardPage.tsx
@@ -41,7 +41,7 @@ import { useNotificationPreview } from '@/hooks/useNotificationPreview';
import { useUserOrganizations, type UserOrganization } from '@/hooks/useUserOrganizations';
import { satsToUSD, formatBTC } from '@/lib/bitcoin';
-import { COUNTRIES } from '@/lib/countries';
+import { getCountryInfo } from '@/lib/countries';
import { getDisplayName } from '@/lib/genUserName';
import { sanitizeUrl } from '@/lib/sanitizeUrl';
import { cn } from '@/lib/utils';
@@ -533,8 +533,8 @@ function CountriesSection({
) : followedCountries.length > 0 ? (
{followedCountries.map((code) => {
- const info = COUNTRIES[code];
- const name = info?.name ?? code;
+ const info = getCountryInfo(code);
+ const name = info?.subdivisionName ?? info?.name ?? code;
const flag = info?.flag ?? '';
return (