Spaces:
Sleeping
Sleeping
| import { | |
| Body, | |
| Controller, | |
| Delete, | |
| Get, | |
| Header, | |
| HttpCode, | |
| HttpException, | |
| Param, | |
| Post, | |
| Put, | |
| Query, | |
| Res, | |
| UseGuards, | |
| } from '@nestjs/common'; | |
| import type { Response } from 'express'; | |
| import type { RegionGeo } from '@trek/shared'; | |
| import type { User } from '../../types'; | |
| import { AtlasService } from './atlas.service'; | |
| import { JwtAuthGuard } from '../auth/jwt-auth.guard'; | |
| import { CurrentUser } from '../auth/current-user.decorator'; | |
| /** | |
| * /api/addons/atlas — visited countries/regions, region GeoJSON, bucket list. | |
| * | |
| * Byte-identical to the legacy Express route (server/src/routes/atlas.ts): all | |
| * endpoints require auth; country/region codes are upper-cased; /regions is | |
| * always no-store while /regions/geo is cached for a day only on a non-empty | |
| * result; the mark POSTs answer 200 (not Nest's default 201); and the bespoke | |
| * 400/404 bodies are reproduced exactly. No addon gate — the legacy route has | |
| * none, so adding one would break clients when the addon is off. | |
| */ | |
| ('api/addons/atlas') | |
| (JwtAuthGuard) | |
| export class AtlasController { | |
| constructor(private readonly atlas: AtlasService) {} | |
| ('stats') | |
| stats(() user: User) { | |
| return this.atlas.stats(user.id); | |
| } | |
| ('regions') | |
| ('Cache-Control', 'no-cache, no-store') | |
| regions(() user: User) { | |
| return this.atlas.visitedRegions(user.id); | |
| } | |
| ('regions/geo') | |
| async regionGeo( | |
| ('countries') countries: string | undefined, | |
| ({ passthrough: true }) res: Response, | |
| ): Promise<RegionGeo> { | |
| const list = (countries || '').split(',').filter(Boolean); | |
| if (list.length === 0) { | |
| return { type: 'FeatureCollection', features: [] }; | |
| } | |
| const geo = await this.atlas.regionGeo(list); | |
| // Cache only a non-empty result, matching the legacy route (the empty | |
| // short-circuit above sends no Cache-Control header). | |
| res.setHeader('Cache-Control', 'public, max-age=86400'); | |
| return geo; | |
| } | |
| ('countries/geo') | |
| ('Cache-Control', 'public, max-age=86400') | |
| countryGeo(): RegionGeo { | |
| return this.atlas.countryGeo(); | |
| } | |
| ('country/:code') | |
| countryPlaces(() user: User, ('code') code: string) { | |
| return this.atlas.countryPlaces(user.id, code.toUpperCase()); | |
| } | |
| ('country/:code/mark') | |
| (200) | |
| markCountry(() user: User, ('code') code: string): { success: boolean } { | |
| this.atlas.markCountry(user.id, code.toUpperCase()); | |
| return { success: true }; | |
| } | |
| ('country/:code/mark') | |
| unmarkCountry(() user: User, ('code') code: string): { success: boolean } { | |
| this.atlas.unmarkCountry(user.id, code.toUpperCase()); | |
| return { success: true }; | |
| } | |
| ('region/:code/mark') | |
| (200) | |
| markRegion( | |
| () user: User, | |
| ('code') code: string, | |
| ('name') name?: string, | |
| ('country_code') countryCode?: string, | |
| ): { success: boolean } { | |
| if (!name || !countryCode) { | |
| throw new HttpException({ error: 'name and country_code are required' }, 400); | |
| } | |
| this.atlas.markRegion(user.id, code.toUpperCase(), name, countryCode.toUpperCase()); | |
| return { success: true }; | |
| } | |
| ('region/:code/mark') | |
| unmarkRegion(() user: User, ('code') code: string): { success: boolean } { | |
| this.atlas.unmarkRegion(user.id, code.toUpperCase()); | |
| return { success: true }; | |
| } | |
| ('bucket-list') | |
| bucketList(() user: User) { | |
| return { items: this.atlas.bucketList(user.id) }; | |
| } | |
| ('bucket-list') | |
| createBucketItem( | |
| () user: User, | |
| () body: { name?: string; lat?: number | null; lng?: number | null; country_code?: string | null; notes?: string | null; target_date?: string | null }, | |
| ): { item: unknown } { | |
| if (!body.name?.trim()) { | |
| throw new HttpException({ error: 'Name is required' }, 400); | |
| } | |
| const { name, lat, lng, country_code, notes, target_date } = body; | |
| return { item: this.atlas.createBucketItem(user.id, { name, lat, lng, country_code, notes, target_date }) }; | |
| } | |
| ('bucket-list/:id') | |
| updateBucketItem( | |
| () user: User, | |
| ('id') id: string, | |
| () body: { name?: string; notes?: string; lat?: number | null; lng?: number | null; country_code?: string | null; target_date?: string | null }, | |
| ): { item: unknown } { | |
| const { name, notes, lat, lng, country_code, target_date } = body; | |
| const item = this.atlas.updateBucketItem(user.id, id, { name, notes, lat, lng, country_code, target_date }); | |
| if (!item) { | |
| throw new HttpException({ error: 'Item not found' }, 404); | |
| } | |
| return { item }; | |
| } | |
| ('bucket-list/:id') | |
| deleteBucketItem(() user: User, ('id') id: string): { success: boolean } { | |
| if (!this.atlas.deleteBucketItem(user.id, id)) { | |
| throw new HttpException({ error: 'Item not found' }, 404); | |
| } | |
| return { success: true }; | |
| } | |
| } | |