Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 53 additions & 36 deletions apps/f3-glossary/components/region-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,76 +13,93 @@ 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<Record<string, Record<string, string[]>>>({});
const [countries, setCountries] = useState<string[]>([]);
const [states, setStates] = useState<string[]>([]);
const [cities, setCities] = useState<string[]>([]);
const [countries, setCountries] = useState<string[]>([]);
const [selectedState, setSelectedState] = useState<string>(searchParams.get('state') || 'all');
const [selectedCity, setSelectedCity] = useState<string>(searchParams.get('city') || 'all');

const [selectedCountry, setSelectedCountry] = useState<string>(
searchParams.get('country') || 'all'
);
const [selectedState, setSelectedState] = useState<string>(searchParams.get('state') || 'all');
const [selectedCity, setSelectedCity] = useState<string>(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');

router.push(`/?${params.toString()}`);
setOpen(false);
};

const hasFilters = selectedState !== 'all' || selectedCity !== 'all' || selectedCountry !== 'all';
const hasFilters = selectedCountry !== 'all' || selectedState !== 'all' || selectedCity !== 'all';

return (
<Popover open={open} onOpenChange={setOpen}>
Expand All @@ -95,9 +112,9 @@ export function RegionFilter() {
<span>Filter</span>
{hasFilters && (
<span className="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-white">
{(selectedState !== 'all' ? 1 : 0) +
(selectedCity !== 'all' ? 1 : 0) +
(selectedCountry !== 'all' ? 1 : 0)}
{(selectedCountry !== 'all' ? 1 : 0) +
(selectedState !== 'all' ? 1 : 0) +
(selectedCity !== 'all' ? 1 : 0)}
</span>
)}
</Button>
Expand Down
2 changes: 1 addition & 1 deletion apps/f3-glossary/components/ui/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-sky-100 data-[state=active]:text-foreground data-[state=active]:shadow-sm',
className
)}
{...props}
Expand Down
3 changes: 2 additions & 1 deletion apps/f3-glossary/components/xicon-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface XiconCardProps {
}

export function XiconCard({ entry }: XiconCardProps) {
const { id, title, text, tags, type, city, state } = entry;
const { id, title, text, tags, type, city, state, country } = entry;

return (
<Link href={`/${id}`}>
Expand All @@ -35,6 +35,7 @@ export function XiconCard({ entry }: XiconCardProps) {
<p className="text-sm text-gray-600">
{city ? city + ', ' : ''}
{state}
{country ? ', ' + country : ''}
</p>
) : (
<p className="text-sm text-gray-600 line-clamp-3">{text.substring(0, 120)}</p>
Expand Down
3 changes: 3 additions & 0 deletions apps/f3-glossary/components/xicon-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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') || '',
Expand Down
42 changes: 42 additions & 0 deletions apps/f3-glossary/lib/xicon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ export async function getFilteredXicons(filter: XiconFilter): Promise<XiconItem[
});
}

// Filter by country (for regions)
if (filter.country) {
const countryLower = filter.country.toLowerCase();
items = items.filter(item => {
if (item.type !== 'region') return true;
return item.country?.toLowerCase().includes(countryLower);
});
}

return items;
}

Expand Down Expand Up @@ -273,3 +282,36 @@ export async function getNextPrevXicons(

return { next, prev };
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is added to organize country, state and city

export async function getCountryStateCityMap(): Promise<Record<string, Record<string, string[]>>> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function will map countries -> states -> cities

const map: Record<string, Record<string, Set<string>>> = {};

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<string, Record<string, string[]>> = {};
for (const country in map) {
result[country] = {};
for (const state in map[country]) {
result[country][state] = Array.from(map[country][state]).sort();
}
}

return result;
}
12 changes: 7 additions & 5 deletions apps/f3-glossary/scripts/db/regions/seed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async function fetchRegions(): Promise<Region[]> {

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}`;
Expand All @@ -40,7 +40,7 @@ async function fetchRegions(): Promise<Region[]> {
name,
city,
state,
country: 'United States',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default United states is placed as country in the db seeding script.

I changed it to parse the country from the location row from api.

country,
regionPageUrl: websiteUrl,
mapUrl,
tags: [],
Expand Down Expand Up @@ -161,12 +161,13 @@ const getLatLngByRegion = (rows: string[][], colNums: ColNums) => {
*/
const getLocation = (regionName: string, locationsByRegion: Record<string, string[]>) => {
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(),
};
};

Expand All @@ -180,7 +181,7 @@ const getLocation = (regionName: string, locationsByRegion: Record<string, strin
* @param location - a comma-separated address string
* @returns "City, State" or an empty string if it can't parse
*/
const extractCityState = (location: string) => {
const extractCityStateCountry = (location: string) => {
// Split on commas, trim whitespace, drop any empty segments
const parts = location
.split(',')
Expand All @@ -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];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This here extracts country from the location, logic is similar to the extracting logic for state & city


return `${city}, ${state}`;
return `${city}, ${state}, ${country}`;
};