diff --git a/apps/f3-glossary/components/region-filter.tsx b/apps/f3-glossary/components/region-filter.tsx index ea2a5b5..7fd5c77 100644 --- a/apps/f3-glossary/components/region-filter.tsx +++ b/apps/f3-glossary/components/region-filter.tsx @@ -13,68 +13,85 @@ import { } from '@/components/ui/select'; import { Label } from '@/components/ui/label'; import { Filter, X } from 'lucide-react'; -import { getAllStates, getAllCities, getAllCountries } from '@/lib/xicon'; +import { getCountryStateCityMap } from '@/lib/xicon'; export function RegionFilter() { const router = useRouter(); const searchParams = useSearchParams(); + + const [locationMap, setLocationMap] = useState>>({}); + const [countries, setCountries] = useState([]); const [states, setStates] = useState([]); const [cities, setCities] = useState([]); - const [countries, setCountries] = useState([]); - const [selectedState, setSelectedState] = useState(searchParams.get('state') || 'all'); - const [selectedCity, setSelectedCity] = useState(searchParams.get('city') || 'all'); + const [selectedCountry, setSelectedCountry] = useState( searchParams.get('country') || 'all' ); + const [selectedState, setSelectedState] = useState(searchParams.get('state') || 'all'); + const [selectedCity, setSelectedCity] = useState(searchParams.get('city') || 'all'); const [open, setOpen] = useState(false); - // Load all states and cities + // Load location map and countries useEffect(() => { (async () => { - const [states, cities, countries] = await Promise.all([ - getAllStates(), - getAllCities(), - getAllCountries(), - ]); - setStates(states); - setCities(cities); - setCountries(countries); + const map = await getCountryStateCityMap(); + setLocationMap(map); + setCountries(Object.keys(map).sort()); })(); }, []); - // Update URL when filters change - const updateFilters = () => { - const params = new URLSearchParams(searchParams.toString()); - - if (selectedState !== 'all') { - params.set('state', selectedState); - } else { - params.delete('state'); + // Update states when country changes + useEffect(() => { + if (selectedCountry === 'all' || !locationMap[selectedCountry]) { + setStates([]); + setSelectedState('all'); + setCities([]); + setSelectedCity('all'); + return; } - if (selectedCity !== 'all') { - params.set('city', selectedCity); - } else { - params.delete('city'); - } + const statesForCountry = Object.keys(locationMap[selectedCountry]); + setStates(statesForCountry.sort()); + setSelectedState('all'); + setCities([]); + setSelectedCity('all'); + }, [selectedCountry, locationMap]); - if (selectedCountry !== 'all') { - params.set('country', selectedCountry); - } else { - params.delete('country'); + // Update cities when state changes + useEffect(() => { + if ( + selectedCountry === 'all' || + selectedState === 'all' || + !locationMap[selectedCountry]?.[selectedState] + ) { + setCities([]); + setSelectedCity('all'); + return; } + const citiesForState = locationMap[selectedCountry][selectedState]; + setCities(citiesForState.sort()); + setSelectedCity('all'); + }, [selectedState, selectedCountry, locationMap]); + + const updateFilters = () => { + const params = new URLSearchParams(searchParams.toString()); + + selectedCountry !== 'all' ? params.set('country', selectedCountry) : params.delete('country'); + selectedState !== 'all' ? params.set('state', selectedState) : params.delete('state'); + selectedCity !== 'all' ? params.set('city', selectedCity) : params.delete('city'); + router.push(`/?${params.toString()}`); setOpen(false); }; - // Clear all filters const clearFilters = () => { + setSelectedCountry('all'); setSelectedState('all'); setSelectedCity('all'); - setSelectedCountry('all'); const params = new URLSearchParams(searchParams.toString()); + params.delete('country'); params.delete('state'); params.delete('city'); @@ -82,7 +99,7 @@ export function RegionFilter() { setOpen(false); }; - const hasFilters = selectedState !== 'all' || selectedCity !== 'all' || selectedCountry !== 'all'; + const hasFilters = selectedCountry !== 'all' || selectedState !== 'all' || selectedCity !== 'all'; return ( @@ -95,9 +112,9 @@ export function RegionFilter() { Filter {hasFilters && ( - {(selectedState !== 'all' ? 1 : 0) + - (selectedCity !== 'all' ? 1 : 0) + - (selectedCountry !== 'all' ? 1 : 0)} + {(selectedCountry !== 'all' ? 1 : 0) + + (selectedState !== 'all' ? 1 : 0) + + (selectedCity !== 'all' ? 1 : 0)} )} diff --git a/apps/f3-glossary/components/ui/tabs.tsx b/apps/f3-glossary/components/ui/tabs.tsx index 7315537..471dc6a 100644 --- a/apps/f3-glossary/components/ui/tabs.tsx +++ b/apps/f3-glossary/components/ui/tabs.tsx @@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef< @@ -35,6 +35,7 @@ export function XiconCard({ entry }: XiconCardProps) {

{city ? city + ', ' : ''} {state} + {country ? ', ' + country : ''}

) : (

{text.substring(0, 120)}

diff --git a/apps/f3-glossary/components/xicon-list.tsx b/apps/f3-glossary/components/xicon-list.tsx index ec7a65d..f64f4c1 100644 --- a/apps/f3-glossary/components/xicon-list.tsx +++ b/apps/f3-glossary/components/xicon-list.tsx @@ -28,6 +28,9 @@ export function XiconList() { kind: (searchParams.get('kind') as any) || undefined, tags: searchParams.get('tags')?.split(',').filter(Boolean) || [], query: searchParams.get('q') || '', + country: searchParams.get('country') || undefined, + state: searchParams.get('state') || undefined, + city: searchParams.get('city') || undefined, tagsOperator: (searchParams.get('tagsOperator') as 'AND' | 'OR') || 'OR', country: searchParams.get('country') || '', state: searchParams.get('state') || '', diff --git a/apps/f3-glossary/lib/xicon.ts b/apps/f3-glossary/lib/xicon.ts index 0f0cea8..0a0dcf3 100644 --- a/apps/f3-glossary/lib/xicon.ts +++ b/apps/f3-glossary/lib/xicon.ts @@ -124,6 +124,15 @@ export async function getFilteredXicons(filter: XiconFilter): Promise { + if (item.type !== 'region') return true; + return item.country?.toLowerCase().includes(countryLower); + }); + } + return items; } @@ -273,3 +282,36 @@ export async function getNextPrevXicons( return { next, prev }; } + +export async function getCountryStateCityMap(): Promise>> { + const map: Record>> = {}; + + const regions = (await getAllXicons()).filter(item => item.type === 'region'); + + regions.forEach(region => { + const country = region.country?.trim() || 'Unknown'; + const state = region.state?.trim() || 'Unknown'; + const city = region.city?.trim() || 'Unknown'; + + if (!map[country]) { + map[country] = {}; + } + + if (!map[country][state]) { + map[country][state] = new Set(); + } + + map[country][state].add(city); + }); + + // Convert Set → Array + const result: Record> = {}; + for (const country in map) { + result[country] = {}; + for (const state in map[country]) { + result[country][state] = Array.from(map[country][state]).sort(); + } + } + + return result; +} diff --git a/apps/f3-glossary/scripts/db/regions/seed/index.ts b/apps/f3-glossary/scripts/db/regions/seed/index.ts index b8de7cc..104fbe4 100644 --- a/apps/f3-glossary/scripts/db/regions/seed/index.ts +++ b/apps/f3-glossary/scripts/db/regions/seed/index.ts @@ -30,7 +30,7 @@ async function fetchRegions(): Promise { for (let i = 0; i < regionNames.length; i++) { const name = regionNames[i]; - const { city, state } = getLocation(name, locationsByRegion); + const { city, state, country } = getLocation(name, locationsByRegion); const mapUrl = getMapUrl(name, latLngByRegion); const slug = kebabCase(name); const websiteUrl = `https://freemensworkout.org/regions/${slug}`; @@ -40,7 +40,7 @@ async function fetchRegions(): Promise { name, city, state, - country: 'United States', + country, regionPageUrl: websiteUrl, mapUrl, tags: [], @@ -161,12 +161,13 @@ const getLatLngByRegion = (rows: string[][], colNums: ColNums) => { */ const getLocation = (regionName: string, locationsByRegion: Record) => { const locations = [ - ...new Set(locationsByRegion[regionName].map(location => extractCityState(location))), + ...new Set(locationsByRegion[regionName].map(location => extractCityStateCountry(location))), ]; const location = locations[0].split(','); return { city: location[0].trim(), state: location[1].trim(), + country: location[2]?.trim(), }; }; @@ -180,7 +181,7 @@ const getLocation = (regionName: string, locationsByRegion: Record { +const extractCityStateCountry = (location: string) => { // Split on commas, trim whitespace, drop any empty segments const parts = location .split(',') @@ -195,6 +196,7 @@ const extractCityState = (location: string) => { // City is the 4th-to-last segment, state is the 3rd-to-last const city = parts[parts.length - 4]; const state = parts[parts.length - 3]; + const country = parts[parts.length - 1]; - return `${city}, ${state}`; + return `${city}, ${state}, ${country}`; };