Files
TREK/shared/src/atlas/atlas.schema.ts
T
Maurice 17245c5a8c 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.
2026-06-17 23:12:30 +02:00

97 lines
4.7 KiB
TypeScript

import { z } from 'zod';
/**
* Atlas API contract — single source of truth for the /api/addons/atlas endpoints
* (visited countries/regions, region GeoJSON, and the travel bucket list).
*
* Parity note: unlike the journey addon, the legacy atlas route is NOT gated by
* an addon-enabled check (app.ts mounts it without one), so the migration does
* not add a gate either — adding one would be a breaking 404.
*
* Stats, visited-regions and GeoJSON are wide, externally-derived shapes kept as
* open records; the request schemas and the bespoke 400/404 controller messages
* pin the parts the client depends on.
*/
const open = z.record(z.string(), z.unknown());
export const markRegionRequestSchema = z.object({
name: z.string().min(1),
country_code: z.string().min(1),
});
export type MarkRegionRequest = z.infer<typeof markRegionRequestSchema>;
export const createBucketItemRequestSchema = z.object({
name: z.string().min(1),
lat: z.number().nullable().optional(),
lng: z.number().nullable().optional(),
country_code: z.string().nullable().optional(),
notes: z.string().nullable().optional(),
target_date: z.string().nullable().optional(),
});
export type CreateBucketItemRequest = z.infer<
typeof createBucketItemRequestSchema
>;
export const updateBucketItemRequestSchema = z.object({
name: z.string().optional(),
notes: z.string().optional(),
lat: z.number().nullable().optional(),
lng: z.number().nullable().optional(),
country_code: z.string().nullable().optional(),
target_date: z.string().nullable().optional(),
});
export type UpdateBucketItemRequest = z.infer<
typeof updateBucketItemRequestSchema
>;
/** A bucket-list item row (DB-shaped; kept open). */
export const bucketItemSchema = open;
export const bucketListResponseSchema = z.object({
items: z.array(bucketItemSchema),
});
export type BucketListResponse = z.infer<typeof bucketListResponseSchema>;
/** GeoJSON FeatureCollection (kept open — provider-derived geometry). */
export const regionGeoSchema = z.object({
type: z.literal('FeatureCollection'),
features: z.array(z.unknown()),
});
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';
}