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
@@ -123,6 +123,31 @@ export function canAccessUserPhoto(requestingUserId: number, ownerUserId: number
if (requestingUserId === ownerUserId) {
return true;
}
// Journey photos use tripId=0 — check journey_photos + journey_contributors
if (tripId === '0') {
const journeyPhoto = db.prepare(`
SELECT jp.entry_id, je.journey_id
FROM journey_photos jp
JOIN journey_entries je ON je.id = jp.entry_id
WHERE jp.asset_id = ?
AND jp.provider = ?
AND jp.owner_id = ?
LIMIT 1
`).get(assetId, provider, ownerUserId) as { entry_id: number; journey_id: number } | undefined;
if (!journeyPhoto) return false;
// Check if requesting user is the journey owner or a contributor
const access = db.prepare(`
SELECT 1 FROM journeys WHERE id = ? AND user_id = ?
UNION ALL
SELECT 1 FROM journey_contributors WHERE journey_id = ? AND user_id = ?
LIMIT 1
`).get(journeyPhoto.journey_id, requestingUserId, journeyPhoto.journey_id, requestingUserId);
return !!access;
}
// Regular trip photos
const sharedAsset = db.prepare(`
SELECT 1
FROM trip_photos