mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
0f4766e131
* fix(map): fit MapLibre routes reliably * fix(map): default planner map to world view * fix(map): only await route geometry when a route is actually pending The fit armed a pending route-refit slot on every fitKey change, even with no route drawn, and only ever cleared it when a route arrived. So a route toggled on much later — after the user had panned elsewhere — was mistaken for the awaited geometry and yanked the camera back, and only on the first toggle. Arm the slot only when a route is already on screen: updateRouteForDay lays down straight lines in the same batch as the fit and upgrades them to real geometry a moment later, so an empty route at fit time means none is coming. * fix(collections): open the empty collection map on the world view A collection with no mappable places centred on Paris, the same hardcoded default this branch removes everywhere else. * feat(map): open the map framed on its places A trip in Japan opened on the world view at 0,0 and only then animated a fitBounds flight across the planet — the hardcoded default was the map's answer to a question its own places already answer. Each renderer now derives its opening camera from the places it receives, at construction: MapView for Leaflet, MapViewGL for MapLibre and Mapbox (whose zoom runs one level below Leaflet's, measuring against a 512px world tile rather than 256px). Doing it at construction is what confines it to load — the map is built once, and by then the trip's places are in hand. Nothing recomputes it afterwards, so the camera stays where the user leaves it, and the opening fit stands down rather than overruling the gentler zoom a lone place opens at. A trip with no coordinates still falls back to the world view. Collections and the public shared-trip page frame themselves the same way. * refactor(settings): drop the default map centre and zoom Nothing reads them now that every map frames itself on its own places, and a home-city default was the wrong answer for the next trip on the other side of the world. The style preview keeps a fixed location of its own: it needs a city to show label density and 3D buildings, which open ocean cannot. * fix(map): frame the map the way each renderer can actually draw Two defects the unit tests missed and running the app exposed. MapLibre and Mapbox opened on Null Island at zoom 2 regardless of the places: the effect that mirrors an external centre prop onto the camera also ran on mount, so it jumped straight to the default nobody passed and threw away the camera the map had just been built with. It now only responds to actual changes, which is what it was for. Leaflet was unaffected — its controller already guarded on the centre changing. A trip spanning Sydney, Reykjavik and Santiago lost Sydney's marker entirely. The narrowest arc containing all three crosses the antimeridian, and framing there is only sound on a renderer that repeats the world: MapLibre and Mapbox draw a marker on whichever copy is nearest the camera, Leaflet draws one world and puts the marker at its absolute position — off-screen. Leaflet now spans the long way round, as L.latLngBounds would. The test that should have caught this wrapped the x-offset in its own projection helper, quietly assuming behaviour Leaflet does not have; it now models each renderer's real wrapping. --------- Co-authored-by: Azalea <noreply@aza.moe>
302 lines
6.5 KiB
TypeScript
302 lines
6.5 KiB
TypeScript
/**
|
|
* Pure data builder functions for frontend tests.
|
|
* These return typed objects matching interfaces in src/types.ts.
|
|
* They do NOT touch a database.
|
|
*/
|
|
|
|
import type {
|
|
User,
|
|
Trip,
|
|
Day,
|
|
Place,
|
|
Assignment,
|
|
DayNote,
|
|
PackingItem,
|
|
TodoItem,
|
|
BudgetItem,
|
|
Reservation,
|
|
TripFile,
|
|
Tag,
|
|
Category,
|
|
Settings,
|
|
AppConfig,
|
|
} from '../../src/types';
|
|
|
|
// ── Counters ──────────────────────────────────────────────────────────────────
|
|
|
|
let _seq = 0;
|
|
function next(): number {
|
|
return ++_seq;
|
|
}
|
|
|
|
// ── InAppNotification (local interface, not in types.ts) ──────────────────────
|
|
|
|
export interface InAppNotification {
|
|
id: number;
|
|
type: string;
|
|
message: string;
|
|
read: boolean;
|
|
created_at: string;
|
|
trip_id?: number | null;
|
|
}
|
|
|
|
// ── Builders ──────────────────────────────────────────────────────────────────
|
|
|
|
export function buildUser(overrides: Partial<User> = {}): User {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
username: `user${id}`,
|
|
email: `user${id}@example.com`,
|
|
role: 'user',
|
|
avatar_url: null,
|
|
maps_api_key: null,
|
|
created_at: '2025-01-01T00:00:00.000Z',
|
|
mfa_enabled: false,
|
|
must_change_password: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildAdmin(overrides: Partial<User> = {}): User {
|
|
return buildUser({ role: 'admin', ...overrides });
|
|
}
|
|
|
|
export function buildTrip(overrides: Partial<Trip> = {}): Trip {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
user_id: 1,
|
|
title: `Trip ${id}`,
|
|
description: null,
|
|
start_date: '2025-06-01',
|
|
end_date: '2025-06-05',
|
|
currency: 'EUR',
|
|
cover_image: null,
|
|
is_archived: 0,
|
|
reminder_days: 7,
|
|
created_at: '2025-01-01T00:00:00.000Z',
|
|
updated_at: '2025-01-01T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildDay(overrides: Partial<Day> = {}): Day {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
trip_id: 1,
|
|
date: '2025-06-01',
|
|
title: null,
|
|
notes: null,
|
|
assignments: [],
|
|
notes_items: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildPlace(overrides: Partial<Place> = {}): Place {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
trip_id: 1,
|
|
name: `Place ${id}`,
|
|
description: null,
|
|
lat: 48.8566,
|
|
lng: 2.3522,
|
|
address: null,
|
|
category_id: null,
|
|
price: null,
|
|
currency: null,
|
|
image_url: null,
|
|
google_place_id: null,
|
|
osm_id: null,
|
|
route_geometry: null,
|
|
place_time: null,
|
|
end_time: null,
|
|
duration_minutes: 60,
|
|
notes: null,
|
|
transport_mode: 'walking',
|
|
website: null,
|
|
phone: null,
|
|
created_at: '2025-01-01T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildAssignment(overrides: Partial<Assignment> = {}): Assignment {
|
|
const id = next();
|
|
const place = overrides.place ?? buildPlace();
|
|
return {
|
|
id,
|
|
day_id: 1,
|
|
place_id: place.id,
|
|
order_index: 0,
|
|
notes: null,
|
|
place,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildDayNote(overrides: Partial<DayNote> = {}): DayNote {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
day_id: 1,
|
|
text: 'Test note',
|
|
time: null,
|
|
icon: null,
|
|
sort_order: 0,
|
|
created_at: '2025-01-01T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildPackingItem(overrides: Partial<PackingItem> = {}): PackingItem {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
trip_id: 1,
|
|
name: `Packing item ${id}`,
|
|
category: null,
|
|
checked: 0,
|
|
sort_order: 0,
|
|
quantity: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildTodoItem(overrides: Partial<TodoItem> = {}): TodoItem {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
trip_id: 1,
|
|
name: `Todo ${id}`,
|
|
category: null,
|
|
checked: 0,
|
|
sort_order: 0,
|
|
due_date: null,
|
|
description: null,
|
|
assigned_user_id: null,
|
|
priority: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildBudgetItem(overrides: Partial<BudgetItem> = {}): BudgetItem {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
trip_id: 1,
|
|
category: 'Other',
|
|
name: `Budget item ${id}`,
|
|
total_price: 100,
|
|
persons: 1,
|
|
days: null,
|
|
note: null,
|
|
sort_order: 0,
|
|
members: [],
|
|
expense_date: null,
|
|
created_at: '2025-01-01T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildReservation(overrides: Partial<Reservation> = {}): Reservation {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
trip_id: 1,
|
|
title: `Reservation ${id}`,
|
|
type: 'restaurant',
|
|
status: 'confirmed',
|
|
reservation_time: null,
|
|
reservation_end_time: null,
|
|
location: null,
|
|
confirmation_number: null,
|
|
notes: null,
|
|
created_at: '2025-01-01T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildTripFile(overrides: Partial<TripFile> = {}): TripFile {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
trip_id: 1,
|
|
filename: 'test.pdf',
|
|
original_name: 'test.pdf',
|
|
mime_type: 'application/pdf',
|
|
url: `/api/trips/1/files/${id}/download`,
|
|
created_at: '2025-01-01T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildTag(overrides: Partial<Tag> = {}): Tag {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
name: `Tag ${id}`,
|
|
color: '#ff0000',
|
|
user_id: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildCategory(overrides: Partial<Category> = {}): Category {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
name: `Category ${id}`,
|
|
color: '#6366f1',
|
|
icon: 'restaurant',
|
|
user_id: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildSettings(overrides: Partial<Settings> = {}): Settings {
|
|
return {
|
|
map_tile_url: '',
|
|
dark_mode: false,
|
|
default_currency: 'USD',
|
|
language: 'en',
|
|
temperature_unit: 'fahrenheit',
|
|
time_format: '12h',
|
|
show_place_description: false,
|
|
blur_booking_codes: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildInAppNotification(overrides: Partial<InAppNotification> = {}): InAppNotification {
|
|
const id = next();
|
|
return {
|
|
id,
|
|
type: 'trip_invite',
|
|
message: `Notification ${id}`,
|
|
read: false,
|
|
created_at: '2025-01-01T00:00:00.000Z',
|
|
trip_id: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function buildAppConfig(overrides: Partial<AppConfig> = {}): AppConfig {
|
|
return {
|
|
has_users: true,
|
|
allow_registration: true,
|
|
demo_mode: false,
|
|
oidc_configured: false,
|
|
oidc_only_mode: false,
|
|
password_login: true,
|
|
password_registration: true,
|
|
oidc_login: true,
|
|
oidc_registration: true,
|
|
env_override_oidc_only: false,
|
|
...overrides,
|
|
};
|
|
}
|