import { useTranslation, SUPPORTED_LANGUAGES } from '../i18n' import { useSettingsStore } from '../store/settingsStore' import { List, Grid, MapPin, Camera, BookOpen, Image, Clock, Laugh, Smile, Meh, Frown, Sun, CloudSun, Cloud, CloudRain, CloudLightning, Snowflake, ThumbsUp, ThumbsDown, } from 'lucide-react' import JourneyMap from '../components/Journey/JourneyMap' import JournalBody from '../components/Journey/JournalBody' import PhotoLightbox from '../components/Journey/PhotoLightbox' import MobileMapTimeline from '../components/Journey/MobileMapTimeline' import MobileEntryView from '../components/Journey/MobileEntryView' import { formatLocationName } from '../utils/formatters' import { DAY_COLORS } from '../components/Journey/dayColors' import { useJourneyPublic } from './journeyPublic/useJourneyPublic' const MOOD_CONFIG: Record = { amazing: { icon: Laugh, label: 'Amazing', bg: 'bg-pink-50 dark:bg-pink-900/20', text: 'text-pink-600 dark:text-pink-400' }, good: { icon: Smile, label: 'Good', bg: 'bg-amber-50 dark:bg-amber-900/20', text: 'text-amber-600 dark:text-amber-400' }, neutral: { icon: Meh, label: 'Neutral', bg: 'bg-zinc-100 dark:bg-zinc-800', text: 'text-zinc-500 dark:text-zinc-400' }, rough: { icon: Frown, label: 'Rough', bg: 'bg-violet-50 dark:bg-violet-900/20', text: 'text-violet-600 dark:text-violet-400' }, } const WEATHER_CONFIG: Record = { sunny: { icon: Sun, label: 'Sunny' }, partly: { icon: CloudSun, label: 'Partly cloudy' }, cloudy: { icon: Cloud, label: 'Cloudy' }, rainy: { icon: CloudRain, label: 'Rainy' }, stormy: { icon: CloudLightning, label: 'Stormy' }, cold: { icon: Snowflake, label: 'Cold' }, } function photoUrl(p: { photo_id: number }, shareToken: string, kind: 'thumbnail' | 'original' = 'original'): string { return `/api/public/journey/${shareToken}/photos/${p.photo_id}/${kind}` } function formatDate(d: string, locale?: string): { weekday: string; month: string; day: number } { const date = new Date(d + 'T00:00:00') return { weekday: date.toLocaleDateString(locale || 'en', { weekday: 'long' }), month: date.toLocaleDateString(locale || 'en', { month: 'long' }), day: date.getDate(), } } export default function JourneyPublicPage() { const { t } = useTranslation() // Page = wiring container: the share fetch, view state and all timeline/map // derivations live in the hook; the render helpers below stay next to the JSX. const { token, data, loading, error, isMobile, locale, view, setView, lightbox, setLightbox, showLangPicker, setShowLangPicker, mapRef, activeEntryId, setActiveEntryId, viewingEntry, setViewingEntry, handleMarkerClick, perms, journey, stats, timelineEntries, groupedEntries, sortedDates, sidebarMapItems, allPhotos, desktopTwoColumn, } = useJourneyPublic() if (loading) { return (
) } if (error || !data) { return (

{t('journey.public.notFound')}

{t('journey.public.notFoundMessage')}

) } // In desktop two-column mode the map is always visible — exclude the standalone 'map' tab const availableViews = [ perms.share_timeline && { id: 'timeline' as const, icon: List, label: t('journey.share.timeline') }, perms.share_gallery && { id: 'gallery' as const, icon: Grid, label: t('journey.share.gallery') }, !desktopTwoColumn && !isMobile && perms.share_map && { id: 'map' as const, icon: MapPin, label: t('journey.share.map') }, ].filter(Boolean) as { id: 'timeline' | 'gallery' | 'map'; icon: any; label: string }[] // Shared timeline renderer used in both layout modes const renderTimeline = () => (
{sortedDates.length === 0 && (

No entries yet

)} {sortedDates.map((date, dayIdx) => { const dayEntries = groupedEntries.get(date)! const fd = formatDate(date, locale) const dayColor = DAY_COLORS[dayIdx % DAY_COLORS.length] return (
{/* Day header */}
{dayIdx + 1}
{fd.weekday}
{fd.month} {fd.day}
{/* Entries */}
{dayEntries.map(entry => { const photos = entry.photos || [] const mood = entry.mood ? MOOD_CONFIG[entry.mood] : null const weather = entry.weather ? WEATHER_CONFIG[entry.weather] : null const prosArr = entry.pros_cons?.pros ?? [] const consArr = entry.pros_cons?.cons ?? [] const hasProscons = prosArr.length > 0 || consArr.length > 0 const lightboxPhotos = photos.map(p => ({ id: String(p.id), src: photoUrl(p, token!, 'original'), caption: p.caption })) const isActive = activeEntryId === String(entry.id) return (
{ if (!desktopTwoColumn) return setActiveEntryId(String(entry.id)) mapRef.current?.highlightMarker(String(entry.id)) }} style={isActive && desktopTwoColumn ? { outline: `2px solid ${dayColor}`, outlineOffset: '3px', borderRadius: '16px' } : undefined} className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-2xl overflow-hidden"> {/* Photo area */} {photos.length === 1 && (
setLightbox({ photos: lightboxPhotos, index: 0 })}>
{entry.location_name && (
{formatLocationName(entry.location_name)}
)} {entry.title && (

{entry.title}

)}
)} {photos.length === 2 && (
{photos.slice(0, 2).map((p, i) => ( setLightbox({ photos: lightboxPhotos, index: i })} /> ))}
)} {photos.length >= 3 && (
setLightbox({ photos: lightboxPhotos, index: 0 })}>
setLightbox({ photos: lightboxPhotos, index: 1 })}>
setLightbox({ photos: lightboxPhotos, index: 2 })}> {photos.length > 3 && (
+{photos.length - 3}
)}
)} {/* Content */}
setViewingEntry(entry)}> {/* Title (only when no single photo — photo has it in overlay) */} {photos.length !== 1 && entry.title && (

{entry.title}

)} {/* Location + time badges */} {(entry.location_name || entry.entry_time) && photos.length !== 1 && (
{entry.location_name && ( {formatLocationName(entry.location_name)} )} {entry.entry_time && ( {entry.entry_time.slice(0, 5)} )}
)} {entry.entry_time && photos.length === 1 && (
{entry.entry_time.slice(0, 5)}
)} {/* Story */} {entry.story && (
)} {/* Pros & Cons */} {hasProscons && (
0 && consArr.length > 0 ? 'grid-cols-2' : 'grid-cols-1'}`}> {prosArr.length > 0 && (
Pros
{prosArr.map((p, i) => (
{p}
))}
)} {consArr.length > 0 && (
Cons
{consArr.map((c, i) => (
{c}
))}
)}
)} {/* Mood + weather */} {(mood || weather) && (
{mood && ( {mood.label} )} {weather && ( {weather.label} )}
)}
) })}
) })}
) // Shared gallery renderer const renderGallery = () => (
{allPhotos.map((photo, idx) => (
setLightbox({ photos: allPhotos.map(p => ({ id: String(p.id), src: photoUrl(p, token!, 'original'), caption: p.caption })), index: idx })} >
))}
) // Shared view tab bar const renderTabs = (views: typeof availableViews) => views.length > 1 && (
{views.map(v => ( ))}
) return (
{/* Hero */}
{journey.cover_image && (
)}
{/* Language picker */}
{showLangPicker && (
{SUPPORTED_LANGUAGES.map(lang => ( ))}
)}
{/* Logo */}
TREK
{t('journey.public.tagline')}

{journey.title}

{journey.subtitle && (
{journey.subtitle}
)} {/* Stats pill */}
{stats.entries} {t('journey.stats.entries')} · {stats.photos} {t('journey.stats.photos')} · {stats.places} {t('journey.stats.places')}
{t('journey.public.readOnly')}
{/* Content */} {desktopTwoColumn ? ( // ── Desktop two-column: scrollable timeline feed + sticky map ──────────
{/* Left: feed */}
{renderTabs(availableViews)} {view === 'timeline' && perms.share_timeline && renderTimeline()} {view === 'gallery' && perms.share_gallery && renderGallery()}
{/* Right: sticky map — matches auth page aside proportions */}
) : ( // ── Single-column layout (mobile + desktop-without-map) ───────────────
{/* Floating view toggle — visible above the fullscreen map on mobile */} {isMobile && view === 'timeline' && perms.share_timeline && perms.share_map && availableViews.length > 1 && (
{availableViews.map(v => ( ))}
)} {renderTabs(availableViews)} {/* Mobile combined map+timeline (public, read-only) */} {isMobile && view === 'timeline' && perms.share_timeline && perms.share_map && ( setViewingEntry(entry as any)} publicPhotoUrl={(photoId) => `/api/public/journey/${token}/photos/${photoId}/original`} carouselBottom="calc(env(safe-area-inset-bottom, 16px) + 8px)" /> )} {/* Timeline (desktop, or mobile without map permission) */} {(!isMobile || !perms.share_map) && view === 'timeline' && perms.share_timeline && renderTimeline()} {/* Gallery */} {view === 'gallery' && perms.share_gallery && renderGallery()} {/* Map (standalone tab — only in single-column mode) */} {view === 'map' && perms.share_map && (
)}
)} {/* Powered by */}
TREK {t('journey.public.sharedVia')} TREK
Made with by Maurice · GitHub
{/* Lightbox */} {lightbox && ( setLightbox(null)} /> )} {/* Mobile entry detail view (public share) */} {viewingEntry && ( `/api/public/journey/${token}/photos/${photoId}/original`} onClose={() => setViewingEntry(null)} onEdit={() => {}} onDelete={() => {}} onPhotoClick={(photos, idx) => setLightbox({ photos: photos.map(p => ({ id: String(p.id), src: photoUrl(p as any, token!, 'original'), caption: (p as any).caption ?? null, })), index: idx, })} /> )}
) }