diff --git a/src/components/CountrySelect.tsx b/src/components/CountrySelect.tsx
new file mode 100644
index 00000000..b6252ad6
--- /dev/null
+++ b/src/components/CountrySelect.tsx
@@ -0,0 +1,126 @@
+import { useMemo, useState } from 'react';
+import { MapPin, X } from 'lucide-react';
+
+import { Input } from '@/components/ui/input';
+import { COUNTRIES, searchCountries, type CountryEntry } from '@/lib/countries';
+import { cn } from '@/lib/utils';
+
+interface CountrySelectProps {
+ id: string;
+ query: string;
+ selectedCode: string;
+ onQueryChange: (value: string) => void;
+ onSelect: (country: CountryEntry) => void;
+ onClear: () => void;
+ placeholder?: string;
+}
+
+export function CountrySelect({
+ id,
+ query,
+ selectedCode,
+ onQueryChange,
+ onSelect,
+ onClear,
+ placeholder = 'Search countries, e.g. Venezuela',
+}: CountrySelectProps) {
+ const [open, setOpen] = useState(false);
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const selectedCountry = selectedCode ? COUNTRIES[selectedCode] : undefined;
+ const results = useMemo(() => searchCountries(query), [query]);
+ const showResults = open && results.length > 0;
+ const resultsId = `${id}-results`;
+
+ const selectCountry = (country: CountryEntry) => {
+ onSelect(country);
+ setOpen(false);
+ setSelectedIndex(0);
+ };
+
+ return (
+
+
+
+
{
+ onQueryChange(e.target.value);
+ setOpen(true);
+ setSelectedIndex(0);
+ }}
+ onFocus={() => setOpen(true)}
+ onBlur={() => window.setTimeout(() => setOpen(false), 120)}
+ onKeyDown={(e) => {
+ if (!showResults) return;
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setSelectedIndex((prev) => (prev + 1) % results.length);
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setSelectedIndex((prev) => (prev - 1 + results.length) % results.length);
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ selectCountry(results[selectedIndex]);
+ } else if (e.key === 'Escape') {
+ setOpen(false);
+ }
+ }}
+ className="h-9 rounded-full border-0 bg-secondary pl-10 pr-10 text-sm focus-visible:ring-0 focus-visible:ring-offset-0"
+ placeholder={placeholder}
+ autoComplete="off"
+ role="combobox"
+ aria-expanded={showResults}
+ aria-controls={resultsId}
+ />
+ {(query || selectedCode) && (
+
+ )}
+
+ {showResults && (
+
+ {results.map((country, index) => (
+
+ ))}
+
+ )}
+
+
+ {selectedCountry && (
+
+ Publishes i: iso3166:{selectedCode} for country sorting.
+
+ )}
+
+ );
+}
diff --git a/src/components/CreateCommunityEventDialog.tsx b/src/components/CreateCommunityEventDialog.tsx
index b50e6996..5fece354 100644
--- a/src/components/CreateCommunityEventDialog.tsx
+++ b/src/components/CreateCommunityEventDialog.tsx
@@ -17,12 +17,15 @@ import { Label } from '@/components/ui/label';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
+import { CountrySelect } from '@/components/CountrySelect';
import { ImageUploadField } from '@/components/ImageUploadField';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { usePublishRSVP } from '@/hooks/usePublishRSVP';
import { useToast } from '@/hooks/useToast';
import { fetchFreshEvent } from '@/lib/fetchFreshEvent';
+import { COUNTRIES } from '@/lib/countries';
+import { createCountryIdentifier, parseCountryIdentifier } from '@/lib/countryIdentifiers';
import { createOrganizationAssociationTags } from '@/lib/organizationContext';
import { sanitizeUrl } from '@/lib/sanitizeUrl';
import { withAgoraTag } from '@/lib/agoraNoteTags';
@@ -53,6 +56,11 @@ const MANAGED_EDIT_TAGS = new Set([
't',
]);
+function isManagedEditTag([name, value]: string[]): boolean {
+ if (MANAGED_EDIT_TAGS.has(name)) return true;
+ return name === 'i' && !!value && !!parseCountryIdentifier(value);
+}
+
function slugify(text: string): string {
return text
.toLowerCase()
@@ -101,6 +109,8 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange,
const [startTime, setStartTime] = useState('');
const [endDate, setEndDate] = useState('');
const [endTime, setEndTime] = useState('');
+ const [countryCode, setCountryCode] = useState('');
+ const [countryQuery, setCountryQuery] = useState('');
const [location, setLocation] = useState('');
const [tagInput, setTagInput] = useState('');
const [isImageUploading, setIsImageUploading] = useState(false);
@@ -124,6 +134,8 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange,
setStartTime('');
setEndDate('');
setEndTime('');
+ setCountryCode('');
+ setCountryQuery('');
setLocation('');
setTagInput('');
setIsImageUploading(false);
@@ -141,6 +153,9 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange,
const summaryTag = event.tags.find(([name]) => name === 'summary')?.[1] ?? '';
const imageTag = event.tags.find(([name]) => name === 'image')?.[1] ?? '';
const locationTag = event.tags.find(([name]) => name === 'location')?.[1] ?? '';
+ const editCountryCode = event.tags
+ .map(([name, value]) => name === 'i' && value ? parseCountryIdentifier(value) : undefined)
+ .find((code): code is string => !!code && /^[A-Z]{2}$/.test(code)) ?? '';
const startTag = event.tags.find(([name]) => name === 'start')?.[1] ?? '';
const endTag = event.tags.find(([name]) => name === 'end')?.[1] ?? '';
const isAllDay = event.kind === 31922;
@@ -150,6 +165,8 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange,
setDescription(summaryTag || event.content);
setImageUrl(imageTag);
setLocation(locationTag);
+ setCountryCode(editCountryCode);
+ setCountryQuery(editCountryCode ? COUNTRIES[editCountryCode]?.name ?? editCountryCode : '');
setTagInput(getEditableContentTags(event.tags).join(', '));
setAllDay(isAllDay);
setIsImageUploading(false);
@@ -241,7 +258,7 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange,
})
: undefined;
const preservedTags = isEditing
- ? (prev?.tags ?? event?.tags ?? []).filter(([name]) => !MANAGED_EDIT_TAGS.has(name))
+ ? (prev?.tags ?? event?.tags ?? []).filter((tag) => !isManagedEditTag(tag))
: [];
const tags: string[][] = [
['d', dTag],
@@ -262,6 +279,10 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange,
tags.push(['location', location.trim()]);
}
+ if (countryCode) {
+ tags.push(['i', createCountryIdentifier(countryCode)]);
+ }
+
for (const tag of contentTags) tags.push(['t', tag]);
if (imageUrl.trim()) {
@@ -363,6 +384,7 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange,
}
}, [
allDay,
+ countryCode,
description,
endDate,
endTime,
@@ -519,7 +541,31 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange,
)}
-
+
+
+
+
+
getTodayDateInput(), []);
+ const pageCountryCode = (searchParams.get('country') || '').toUpperCase();
+ const initialCountryCode = COUNTRIES[pageCountryCode] ? pageCountryCode : '';
const orgParam = searchParams.get('org');
const orgFromParam = useMemo(() => decodeOrganizationParam(orgParam), [orgParam]);
@@ -79,6 +84,8 @@ export function CreateEventPage() {
const [startTime, setStartTime] = useState('');
const [endDate, setEndDate] = useState('');
const [endTime, setEndTime] = useState('');
+ const [countryCode, setCountryCode] = useState(initialCountryCode);
+ const [countryQuery, setCountryQuery] = useState(initialCountryCode ? COUNTRIES[initialCountryCode].name : '');
const [location, setLocation] = useState('');
const [tagInput, setTagInput] = useState('');
const [timezone, setTimezone] = useState(browserTimezone);
@@ -125,6 +132,10 @@ export function CreateEventPage() {
tags.push(['location', trimmedLocation]);
}
+ if (countryCode) {
+ tags.push(['i', createCountryIdentifier(countryCode)]);
+ }
+
for (const tag of contentTags) tags.push(['t', tag]);
const trimmedCoverImage = coverImage.trim();
@@ -403,7 +414,30 @@ export function CreateEventPage() {
)}
-
+
+ {
+ setCountryQuery(value);
+ const selectedCountry = countryCode ? COUNTRIES[countryCode] : undefined;
+ if (selectedCountry && value !== selectedCountry.name && value.toUpperCase() !== countryCode) {
+ setCountryCode('');
+ }
+ }}
+ onSelect={(country) => {
+ setCountryCode(country.code);
+ setCountryQuery(country.name);
+ }}
+ onClear={() => {
+ setCountryCode('');
+ setCountryQuery('');
+ }}
+ />
+
+
+