mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(atlas): keep the continent breakdown in sync on mark/unmark (#1225)
The optimistic mark/unmark updates bumped the country total but never the per-continent counts, so the continent column froze until a full reload. Move the country to continent map into @trek/shared (single source for server and client) and adjust the matching continent count at every optimistic site: the country confirm flow plus the choose / region mark and region unmark handlers.
This commit is contained in:
@@ -6,6 +6,7 @@ import CustomSelect from '../components/shared/CustomSelect'
|
|||||||
import { Globe, MapPin, Briefcase, Calendar, Flag, PanelLeftOpen, PanelLeftClose, X, Star, Plus, Trash2, Search } from 'lucide-react'
|
import { Globe, MapPin, Briefcase, Calendar, Flag, PanelLeftOpen, PanelLeftClose, X, Star, Plus, Trash2, Search } from 'lucide-react'
|
||||||
import type { TranslationFn } from '../types'
|
import type { TranslationFn } from '../types'
|
||||||
import { A2_TO_A3, countryCodeToFlag, type AtlasCountry, type AtlasStats, type AtlasData, type CountryDetail } from './atlas/atlasModel'
|
import { A2_TO_A3, countryCodeToFlag, type AtlasCountry, type AtlasStats, type AtlasData, type CountryDetail } from './atlas/atlasModel'
|
||||||
|
import { continentForCountry } from '@trek/shared'
|
||||||
import { useAtlas } from './atlas/useAtlas'
|
import { useAtlas } from './atlas/useAtlas'
|
||||||
import AtlasCountrySearch from './atlas/AtlasCountrySearch'
|
import AtlasCountrySearch from './atlas/AtlasCountrySearch'
|
||||||
import { useToast } from '../components/shared/Toast'
|
import { useToast } from '../components/shared/Toast'
|
||||||
@@ -212,7 +213,8 @@ export default function AtlasPage(): React.ReactElement {
|
|||||||
await apiClient.post(`/addons/atlas/country/${confirmAction.code}/mark`)
|
await apiClient.post(`/addons/atlas/country/${confirmAction.code}/mark`)
|
||||||
setData(prev => {
|
setData(prev => {
|
||||||
if (!prev || prev.countries.find(c => c.code === confirmAction.code)) return prev
|
if (!prev || prev.countries.find(c => c.code === confirmAction.code)) return prev
|
||||||
return { ...prev, countries: [...prev.countries, { code: confirmAction.code, placeCount: 0, tripCount: 0, firstVisit: null, lastVisit: null }], stats: { ...prev.stats, totalCountries: prev.stats.totalCountries + 1 } }
|
const cont = continentForCountry(confirmAction.code)
|
||||||
|
return { ...prev, countries: [...prev.countries, { code: confirmAction.code, placeCount: 0, tripCount: 0, firstVisit: null, lastVisit: null }], stats: { ...prev.stats, totalCountries: prev.stats.totalCountries + 1 }, continents: { ...prev.continents, [cont]: (prev.continents?.[cont] || 0) + 1 } }
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(getApiErrorMessage(err, t('common.error')))
|
toast.error(getApiErrorMessage(err, t('common.error')))
|
||||||
@@ -260,7 +262,8 @@ export default function AtlasPage(): React.ReactElement {
|
|||||||
})
|
})
|
||||||
setData(prev => {
|
setData(prev => {
|
||||||
if (!prev || prev.countries.find(c => c.code === countryCode)) return prev
|
if (!prev || prev.countries.find(c => c.code === countryCode)) return prev
|
||||||
return { ...prev, countries: [...prev.countries, { code: countryCode, placeCount: 0, tripCount: 0, firstVisit: null, lastVisit: null }], stats: { ...prev.stats, totalCountries: prev.stats.totalCountries + 1 } }
|
const cont = continentForCountry(countryCode)
|
||||||
|
return { ...prev, countries: [...prev.countries, { code: countryCode, placeCount: 0, tripCount: 0, firstVisit: null, lastVisit: null }], stats: { ...prev.stats, totalCountries: prev.stats.totalCountries + 1 }, continents: { ...prev.continents, [cont]: (prev.continents?.[cont] || 0) + 1 } }
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(getApiErrorMessage(err, t('common.error')))
|
toast.error(getApiErrorMessage(err, t('common.error')))
|
||||||
@@ -339,10 +342,12 @@ export default function AtlasPage(): React.ReactElement {
|
|||||||
if (!c || c.placeCount > 0 || c.tripCount > 0) return prev
|
if (!c || c.placeCount > 0 || c.tripCount > 0) return prev
|
||||||
const remainingRegions = (visitedRegions[countryCode] || []).filter(r => r.code !== rCode && r.manuallyMarked)
|
const remainingRegions = (visitedRegions[countryCode] || []).filter(r => r.code !== rCode && r.manuallyMarked)
|
||||||
if (remainingRegions.length > 0) return prev
|
if (remainingRegions.length > 0) return prev
|
||||||
|
const cont = continentForCountry(countryCode)
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
countries: prev.countries.filter(c => c.code !== countryCode),
|
countries: prev.countries.filter(c => c.code !== countryCode),
|
||||||
stats: { ...prev.stats, totalCountries: Math.max(0, prev.stats.totalCountries - 1) },
|
stats: { ...prev.stats, totalCountries: Math.max(0, prev.stats.totalCountries - 1) },
|
||||||
|
continents: { ...prev.continents, [cont]: Math.max(0, (prev.continents?.[cont] || 0) - 1) },
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import apiClient, { mapsApi } from '../../api/client'
|
|||||||
import L from 'leaflet'
|
import L from 'leaflet'
|
||||||
import type { GeoJsonFeatureCollection } from '../../types'
|
import type { GeoJsonFeatureCollection } from '../../types'
|
||||||
import { A2_TO_A3, type AtlasData, type CountryDetail, type BucketItem } from './atlasModel'
|
import { A2_TO_A3, type AtlasData, type CountryDetail, type BucketItem } from './atlasModel'
|
||||||
|
import { continentForCountry } from '@trek/shared'
|
||||||
|
|
||||||
function useCountryNames(language: string): (code: string) => string {
|
function useCountryNames(language: string): (code: string) => string {
|
||||||
const [resolver, setResolver] = useState<(code: string) => string>(() => (code: string) => code)
|
const [resolver, setResolver] = useState<(code: string) => string>(() => (code: string) => code)
|
||||||
@@ -582,10 +583,12 @@ export function useAtlas() {
|
|||||||
apiClient.post(`/addons/atlas/country/${code}/mark`).catch(() => {})
|
apiClient.post(`/addons/atlas/country/${code}/mark`).catch(() => {})
|
||||||
setData(prev => {
|
setData(prev => {
|
||||||
if (!prev || prev.countries.find(c => c.code === code)) return prev
|
if (!prev || prev.countries.find(c => c.code === code)) return prev
|
||||||
|
const cont = continentForCountry(code)
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
countries: [...prev.countries, { code, placeCount: 0, tripCount: 0, firstVisit: null, lastVisit: null }],
|
countries: [...prev.countries, { code, placeCount: 0, tripCount: 0, firstVisit: null, lastVisit: null }],
|
||||||
stats: { ...prev.stats, totalCountries: prev.stats.totalCountries + 1 },
|
stats: { ...prev.stats, totalCountries: prev.stats.totalCountries + 1 },
|
||||||
|
continents: { ...prev.continents, [cont]: (prev.continents?.[cont] || 0) + 1 },
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -596,10 +599,12 @@ export function useAtlas() {
|
|||||||
if (!prev) return prev
|
if (!prev) return prev
|
||||||
const c = prev.countries.find(c => c.code === code)
|
const c = prev.countries.find(c => c.code === code)
|
||||||
if (!c || c.placeCount > 0 || c.tripCount > 0) return prev
|
if (!c || c.placeCount > 0 || c.tripCount > 0) return prev
|
||||||
|
const cont = continentForCountry(code)
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
countries: prev.countries.filter(c => c.code !== code),
|
countries: prev.countries.filter(c => c.code !== code),
|
||||||
stats: { ...prev.stats, totalCountries: Math.max(0, prev.stats.totalCountries - 1) },
|
stats: { ...prev.stats, totalCountries: Math.max(0, prev.stats.totalCountries - 1) },
|
||||||
|
continents: { ...prev.continents, [cont]: Math.max(0, (prev.continents?.[cont] || 0) - 1) },
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
setVisitedRegions(prev => {
|
setVisitedRegions(prev => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import path from 'path';
|
|||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
import { db } from '../db/database';
|
import { db } from '../db/database';
|
||||||
import { Trip, Place } from '../types';
|
import { Trip, Place } from '../types';
|
||||||
|
import { CONTINENT_MAP } from '@trek/shared';
|
||||||
|
|
||||||
// ── Bundled boundary GeoJSON (admin-0 countries + admin-1 regions) ─────────
|
// ── Bundled boundary GeoJSON (admin-0 countries + admin-1 regions) ─────────
|
||||||
//
|
//
|
||||||
@@ -168,30 +169,6 @@ export const NAME_TO_CODE: Record<string, string> = {
|
|||||||
'liechtenstein':'LI','gibraltar':'GI','puerto rico':'PR',
|
'liechtenstein':'LI','gibraltar':'GI','puerto rico':'PR',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CONTINENT_MAP: Record<string, string> = {
|
|
||||||
AF:'Asia',AL:'Europe',DZ:'Africa',AD:'Europe',AO:'Africa',AR:'South America',AM:'Asia',AU:'Oceania',AT:'Europe',AZ:'Asia',
|
|
||||||
BA:'Europe',BD:'Asia',BF:'Africa',BH:'Asia',BI:'Africa',BJ:'Africa',BN:'Asia',BO:'South America',
|
|
||||||
BR:'South America',BE:'Europe',BG:'Europe',BW:'Africa',
|
|
||||||
CA:'North America',CD:'Africa',CG:'Africa',CI:'Africa',CL:'South America',CM:'Africa',CN:'Asia',CO:'South America',
|
|
||||||
CR:'North America',CU:'North America',CV:'Africa',CY:'Europe',HR:'Europe',CZ:'Europe',
|
|
||||||
DJ:'Africa',DK:'Europe',DO:'North America',EC:'South America',EG:'Africa',EE:'Europe',ER:'Africa',ET:'Africa',
|
|
||||||
FI:'Europe',FR:'Europe',DE:'Europe',GE:'Asia',GH:'Africa',GN:'Africa',GR:'Europe',GT:'North America',
|
|
||||||
HN:'North America',HT:'North America',HU:'Europe',IS:'Europe',IN:'Asia',ID:'Asia',IR:'Asia',IQ:'Asia',
|
|
||||||
IE:'Europe',IL:'Asia',IT:'Europe',JM:'North America',JO:'Asia',JP:'Asia',KE:'Africa',KG:'Asia',KH:'Asia',
|
|
||||||
KR:'Asia',KW:'Asia',KZ:'Asia',LA:'Asia',LB:'Asia',LK:'Asia',LV:'Europe',LT:'Europe',LU:'Europe',LY:'Africa',
|
|
||||||
MA:'Africa',MD:'Europe',ME:'Europe',MG:'Africa',MK:'Europe',ML:'Africa',MM:'Asia',MN:'Asia',MR:'Africa',
|
|
||||||
MT:'Europe',MU:'Africa',MV:'Asia',MW:'Africa',MY:'Asia',MX:'North America',MZ:'Africa',
|
|
||||||
NA:'Africa',NE:'Africa',NI:'North America',NL:'Europe',NP:'Asia',NZ:'Oceania',NO:'Europe',OM:'Asia',
|
|
||||||
PA:'North America',PG:'Oceania',PK:'Asia',PE:'South America',PH:'Asia',PL:'Europe',PS:'Asia',
|
|
||||||
PT:'Europe',PY:'South America',QA:'Asia',RO:'Europe',RU:'Europe',RW:'Africa',SA:'Asia',SC:'Africa',
|
|
||||||
SD:'Africa',SG:'Asia',SI:'Europe',SK:'Europe',SN:'Africa',SO:'Africa',RS:'Europe',SV:'North America',
|
|
||||||
SY:'Asia',TG:'Africa',TJ:'Asia',TM:'Asia',TN:'Africa',TT:'North America',TW:'Asia',TZ:'Africa',
|
|
||||||
ZA:'Africa',SE:'Europe',CH:'Europe',TH:'Asia',TR:'Europe',UA:'Europe',UG:'Africa',UY:'South America',
|
|
||||||
UZ:'Asia',VE:'South America',AE:'Asia',GB:'Europe',US:'North America',VN:'Asia',XK:'Europe',
|
|
||||||
YE:'Asia',ZM:'Africa',ZW:'Africa',NG:'Africa',
|
|
||||||
HK:'Asia',MO:'Asia',SM:'Europe',VA:'Europe',MC:'Europe',LI:'Europe',GI:'Europe',PR:'North America',
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Geocoding helpers ───────────────────────────────────────────────────────
|
// ── Geocoding helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
let lastNominatimCall = 0;
|
let lastNominatimCall = 0;
|
||||||
|
|||||||
@@ -59,3 +59,38 @@ export const regionGeoSchema = z.object({
|
|||||||
features: z.array(z.unknown()),
|
features: z.array(z.unknown()),
|
||||||
});
|
});
|
||||||
export type RegionGeo = z.infer<typeof regionGeoSchema>;
|
export type RegionGeo = z.infer<typeof regionGeoSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISO 3166-1 alpha-2 country code → continent. Single source of truth for the
|
||||||
|
* Atlas continent breakdown, used by the server (stats aggregation) and the
|
||||||
|
* client (keeping the per-continent counts in sync on optimistic mark/unmark).
|
||||||
|
*/
|
||||||
|
export const CONTINENT_MAP: Record<string, string> = {
|
||||||
|
AF:'Asia',AL:'Europe',DZ:'Africa',AD:'Europe',AO:'Africa',AR:'South America',AM:'Asia',AU:'Oceania',AT:'Europe',AZ:'Asia',
|
||||||
|
BA:'Europe',BD:'Asia',BF:'Africa',BH:'Asia',BI:'Africa',BJ:'Africa',BN:'Asia',BO:'South America',
|
||||||
|
BR:'South America',BE:'Europe',BG:'Europe',BW:'Africa',
|
||||||
|
CA:'North America',CD:'Africa',CG:'Africa',CI:'Africa',CL:'South America',CM:'Africa',CN:'Asia',CO:'South America',
|
||||||
|
CR:'North America',CU:'North America',CV:'Africa',CY:'Europe',HR:'Europe',CZ:'Europe',
|
||||||
|
DJ:'Africa',DK:'Europe',DO:'North America',EC:'South America',EG:'Africa',EE:'Europe',ER:'Africa',ET:'Africa',
|
||||||
|
FI:'Europe',FR:'Europe',DE:'Europe',GE:'Asia',GH:'Africa',GN:'Africa',GR:'Europe',GT:'North America',
|
||||||
|
HN:'North America',HT:'North America',HU:'Europe',IS:'Europe',IN:'Asia',ID:'Asia',IR:'Asia',IQ:'Asia',
|
||||||
|
IE:'Europe',IL:'Asia',IT:'Europe',JM:'North America',JO:'Asia',JP:'Asia',KE:'Africa',KG:'Asia',KH:'Asia',
|
||||||
|
KR:'Asia',KW:'Asia',KZ:'Asia',LA:'Asia',LB:'Asia',LK:'Asia',LV:'Europe',LT:'Europe',LU:'Europe',LY:'Africa',
|
||||||
|
MA:'Africa',MD:'Europe',ME:'Europe',MG:'Africa',MK:'Europe',ML:'Africa',MM:'Asia',MN:'Asia',MR:'Africa',
|
||||||
|
MT:'Europe',MU:'Africa',MV:'Asia',MW:'Africa',MY:'Asia',MX:'North America',MZ:'Africa',
|
||||||
|
NA:'Africa',NE:'Africa',NI:'North America',NL:'Europe',NP:'Asia',NZ:'Oceania',NO:'Europe',OM:'Asia',
|
||||||
|
PA:'North America',PG:'Oceania',PK:'Asia',PE:'South America',PH:'Asia',PL:'Europe',PS:'Asia',
|
||||||
|
PT:'Europe',PY:'South America',QA:'Asia',RO:'Europe',RU:'Europe',RW:'Africa',SA:'Asia',SC:'Africa',
|
||||||
|
SD:'Africa',SG:'Asia',SI:'Europe',SK:'Europe',SN:'Africa',SO:'Africa',RS:'Europe',SV:'North America',
|
||||||
|
SY:'Asia',TG:'Africa',TJ:'Asia',TM:'Asia',TN:'Africa',TT:'North America',TW:'Asia',TZ:'Africa',
|
||||||
|
ZA:'Africa',SE:'Europe',CH:'Europe',TH:'Asia',TR:'Europe',UA:'Europe',UG:'Africa',UY:'South America',
|
||||||
|
UZ:'Asia',VE:'South America',AE:'Asia',GB:'Europe',US:'North America',VN:'Asia',XK:'Europe',
|
||||||
|
YE:'Asia',ZM:'Africa',ZW:'Africa',NG:'Africa',
|
||||||
|
HK:'Asia',MO:'Asia',SM:'Europe',VA:'Europe',MC:'Europe',LI:'Europe',GI:'Europe',PR:'North America',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Continent for an ISO alpha-2 country code; 'Other' when unknown. */
|
||||||
|
export function continentForCountry(code: string | null | undefined): string {
|
||||||
|
if (!code) return 'Other';
|
||||||
|
return CONTINENT_MAP[code.toUpperCase()] || 'Other';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user