mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
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:
@@ -4,7 +4,7 @@ declare global { interface Window { __dragData: DragDataPayload | null } }
|
||||
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { ChevronDown, ChevronRight, ChevronUp, Navigation, RotateCcw, ExternalLink, Clock, Pencil, GripVertical, Ticket, Plus, FileText, Check, Trash2, Info, MapPin, Star, Heart, Camera, Lightbulb, Flag, Bookmark, Train, Bus, Plane, Car, Ship, Coffee, ShoppingBag, AlertTriangle, FileDown, Lock, Hotel, Utensils, Users, Undo2 } from 'lucide-react'
|
||||
import { ChevronDown, ChevronRight, ChevronUp, Navigation, RotateCcw, ExternalLink, Clock, Pencil, GripVertical, Ticket, Plus, FileText, Check, Trash2, Info, MapPin, Star, Heart, Camera, Lightbulb, Flag, Bookmark, Train, Bus, Plane, Car, Ship, Coffee, ShoppingBag, AlertTriangle, FileDown, Lock, Hotel, Utensils, Users, Undo2, X } from 'lucide-react'
|
||||
|
||||
const RES_ICONS = { flight: Plane, hotel: Hotel, restaurant: Utensils, train: Train, car: Car, cruise: Ship, event: Ticket, tour: Users, other: FileText }
|
||||
import { assignmentsApi, reservationsApi } from '../../api/client'
|
||||
@@ -55,6 +55,99 @@ const TYPE_ICONS = {
|
||||
car: '🚗', cruise: '🚢', event: '🎫', other: '📋',
|
||||
}
|
||||
|
||||
function MobileAddPlaceButton({ dayId, places, assignments, onAssign, onAddNew }: {
|
||||
dayId: number
|
||||
places: Place[]
|
||||
assignments: AssignmentsMap
|
||||
onAssign?: (placeId: number, dayId: number) => void
|
||||
onAddNew?: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
// Find places not assigned to this day
|
||||
const assignedToDay = new Set((assignments[String(dayId)] || []).map(a => a.place_id))
|
||||
const available = places.filter(p => !assignedToDay.has(p.id))
|
||||
const filtered = search.trim()
|
||||
? available.filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
|
||||
: available
|
||||
|
||||
return (
|
||||
<div className="md:hidden" style={{ padding: '8px 12px 12px' }}>
|
||||
{!open ? (
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); setOpen(true) }}
|
||||
style={{
|
||||
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
padding: '10px 0', borderRadius: 12,
|
||||
border: '1.5px dashed var(--border-primary)',
|
||||
background: 'transparent', color: 'var(--text-muted)',
|
||||
fontSize: 12, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Plus size={14} />
|
||||
Add Place
|
||||
</button>
|
||||
) : (
|
||||
<div style={{ borderRadius: 14, border: '1px solid var(--border-primary)', background: 'var(--bg-card)', overflow: 'hidden' }}>
|
||||
<div style={{ padding: '8px 10px', borderBottom: '1px solid var(--border-faint)', display: 'flex', gap: 6 }}>
|
||||
<input
|
||||
autoFocus
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder={t('dayplan.mobile.searchPlaces')}
|
||||
style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 13, fontFamily: 'inherit', color: 'var(--text-primary)' }}
|
||||
/>
|
||||
<button onClick={() => { setOpen(false); setSearch('') }} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: 'var(--text-faint)' }}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ maxHeight: 200, overflowY: 'auto' }}>
|
||||
{filtered.length === 0 && (
|
||||
<div style={{ padding: '16px 12px', textAlign: 'center', fontSize: 12, color: 'var(--text-faint)' }}>
|
||||
{available.length === 0 ? t('dayplan.mobile.allAssigned') : t('dayplan.mobile.noMatch')}
|
||||
</div>
|
||||
)}
|
||||
{filtered.slice(0, 20).map(p => (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => {
|
||||
onAssign?.(p.id, dayId)
|
||||
setOpen(false)
|
||||
setSearch('')
|
||||
}}
|
||||
style={{
|
||||
width: '100%', display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '10px 12px', border: 'none', background: 'transparent',
|
||||
cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
<MapPin size={13} style={{ color: 'var(--text-faint)', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{onAddNew && (
|
||||
<button
|
||||
onClick={() => { onAddNew(); setOpen(false); setSearch('') }}
|
||||
style={{
|
||||
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
padding: '10px 0', borderTop: '1px solid var(--border-faint)',
|
||||
background: 'transparent', border: 'none', color: 'var(--text-muted)',
|
||||
fontSize: 12, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Plus size={13} />
|
||||
Create new place
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface DayPlanSidebarProps {
|
||||
tripId: number
|
||||
trip: Trip
|
||||
@@ -79,6 +172,8 @@ interface DayPlanSidebarProps {
|
||||
reservations?: Reservation[]
|
||||
onAddReservation: () => void
|
||||
onNavigateToFiles?: () => void
|
||||
onAddPlace?: () => void
|
||||
onAddPlaceToDay?: (placeId: number, dayId: number) => void
|
||||
onExpandedDaysChange?: (expandedDayIds: Set<number>) => void
|
||||
pushUndo?: (label: string, undoFn: () => Promise<void> | void) => void
|
||||
canUndo?: boolean
|
||||
@@ -95,6 +190,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
||||
onAssignToDay, onRemoveAssignment, onEditPlace, onDeletePlace,
|
||||
reservations = [],
|
||||
onAddReservation,
|
||||
onAddPlace,
|
||||
onAddPlaceToDay,
|
||||
onNavigateToFiles,
|
||||
onExpandedDaysChange,
|
||||
pushUndo,
|
||||
@@ -1623,6 +1720,15 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile: Add Place from list */}
|
||||
<MobileAddPlaceButton
|
||||
dayId={day.id}
|
||||
places={places}
|
||||
assignments={assignments}
|
||||
onAssign={onAssignToDay}
|
||||
onAddNew={onAddPlace}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user