feat: Journey addon — travel journal with entries, photos, public sharing & PDF export

- 5-table schema (journeys, entries, photos, trips, contributors) with migrations 87-91
- Trip-to-Journey sync engine with skeleton entries and photo sync
- Full CRUD API for journeys, entries, photos with Immich/Synology integration
- Timeline, Gallery and Map views with entry editor (markdown, mood, weather, pros/cons)
- Journey frontpage with hero card, stats and trip suggestions
- Public share links with token-based access and photo proxy
- PDF photo book export (Polarsteps-inspired)
- Dashboard redesign: mobile greeting, live trip hero, quick actions, unified card design
- BottomNav profile sheet with settings/admin/logout
- DayPlan mobile inline place picker
- TripFormModal members management
- Vacay calendar trip date indicator dots
- Fix contributor photo access (403) for journey Immich/Synology photos
- Trip deletion cleanup for journey skeleton entries
- i18n: 231 new keys across all 14 languages (native translations, no fallbacks)
This commit is contained in:
Maurice
2026-04-11 19:01:34 +02:00
parent 0df90086bf
commit 13956804c2
56 changed files with 10843 additions and 332 deletions
+4
View File
@@ -16,6 +16,7 @@ import {
importGoogleList,
searchPlaceImage,
} from '../services/placeService';
import { onPlaceCreated, onPlaceUpdated, onPlaceDeleted } from '../services/journeyService';
const gpxUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
@@ -49,6 +50,7 @@ router.post('/', authenticate, requireTripAccess, validateStringLengths({ name:
const place = createPlace(tripId, req.body);
res.status(201).json({ place });
broadcast(tripId, 'place:created', { place }, req.headers['x-socket-id'] as string);
try { onPlaceCreated(Number(tripId), place.id); } catch {}
});
// Import places from GPX file with full track geometry (must be before /:id)
@@ -142,6 +144,7 @@ router.put('/:id', authenticate, requireTripAccess, validateStringLengths({ name
res.json({ place });
broadcast(tripId, 'place:updated', { place }, req.headers['x-socket-id'] as string);
try { onPlaceUpdated(place.id); } catch {}
});
router.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => {
@@ -151,6 +154,7 @@ router.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Respo
const { tripId, id } = req.params;
try { onPlaceDeleted(Number(id)); } catch {} // sync before actual delete
const deleted = deletePlace(tripId, id);
if (!deleted) {
return res.status(404).json({ error: 'Place not found' });