BREAKING: Reservations have been completely rebuilt. Existing place-level reservations are no longer used. All reservations must be re-created via the Bookings tab. Your trips, places, and other data are unaffected. Reservation System (rebuilt from scratch): - Reservations now link to specific day assignments instead of places - Same place on different days can have independent reservations - New assignment picker in booking modal (grouped by day, searchable) - Removed day/place dropdowns from booking form - Reservation badges in day plan sidebar with type-specific icons - Reservation details in place inspector (only for selected assignment) - Reservation summary in day detail panel Day Detail Panel (new): - Opens on day click in the sidebar - Detailed weather: hourly forecast, precipitation, wind, sunrise/sunset - Historical climate averages for dates beyond 16 days - Accommodation management with check-in/check-out, confirmation number - Hotel assignment across multiple days with day range picker - Reservation overview for the day Places: - Places can now be assigned to the same day multiple times - Start time + end time fields (replaces single time field) - Map badges show multiple position numbers (e.g. "1 · 4") - Route optimization fixed for duplicate places - File attachments during place editing (not just creation) - Cover image upload during trip creation (not just editing) - Paste support (Ctrl+V) for images in trip, place, and file forms Internationalization: - 200+ hardcoded German strings translated to i18n (EN + DE) - Server error messages in English - Category seeds in English for new installations - All planner, register, photo, packing components translated UI/UX: - Auto dark mode (follows system preference, configurable in settings) - Navbar toggle switches light/dark (overrides auto) - Sidebar minimize buttons z-index fixed - Transport mode selector removed from day plan - CustomSelect supports grouped headers (isHeader option) - Optimistic updates for day notes (instant feedback) - Booking cards redesigned with type-colored headers and structured details Weather: - Wind speed in mph when using Fahrenheit setting - Weather description language matches app language Admin: - Weather info panel replaces OpenWeatherMap key input - "Recommended" badge styling updated
110 lines
3.2 KiB
JavaScript
110 lines
3.2 KiB
JavaScript
import React, { useEffect, useState } from 'react'
|
|
import { useParams, useNavigate, Link } from 'react-router-dom'
|
|
import { useTripStore } from '../store/tripStore'
|
|
import { tripsApi, daysApi, placesApi } from '../api/client'
|
|
import Navbar from '../components/Layout/Navbar'
|
|
import PhotoGallery from '../components/Photos/PhotoGallery'
|
|
import { ArrowLeft } from 'lucide-react'
|
|
import { useTranslation } from '../i18n'
|
|
|
|
export default function PhotosPage() {
|
|
const { t } = useTranslation()
|
|
const { id: tripId } = useParams()
|
|
const navigate = useNavigate()
|
|
const tripStore = useTripStore()
|
|
|
|
const [trip, setTrip] = useState(null)
|
|
const [days, setDays] = useState([])
|
|
const [places, setPlaces] = useState([])
|
|
const [photos, setPhotos] = useState([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, [tripId])
|
|
|
|
const loadData = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const [tripData, daysData, placesData] = await Promise.all([
|
|
tripsApi.get(tripId),
|
|
daysApi.list(tripId),
|
|
placesApi.list(tripId),
|
|
])
|
|
setTrip(tripData.trip)
|
|
setDays(daysData.days)
|
|
setPlaces(placesData.places)
|
|
|
|
// Load photos
|
|
await tripStore.loadPhotos(tripId)
|
|
} catch (err) {
|
|
navigate('/dashboard')
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
// Sync photos from store
|
|
useEffect(() => {
|
|
setPhotos(tripStore.photos)
|
|
}, [tripStore.photos])
|
|
|
|
const handleUpload = async (formData) => {
|
|
await tripStore.addPhoto(tripId, formData)
|
|
}
|
|
|
|
const handleDelete = async (photoId) => {
|
|
await tripStore.deletePhoto(tripId, photoId)
|
|
}
|
|
|
|
const handleUpdate = async (photoId, data) => {
|
|
await tripStore.updatePhoto(tripId, photoId, data)
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-slate-50">
|
|
<div className="w-10 h-10 border-4 border-slate-200 border-t-slate-700 rounded-full animate-spin"></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-50">
|
|
<Navbar tripTitle={trip?.title} tripId={tripId} showBack onBack={() => navigate(`/trips/${tripId}`)} />
|
|
|
|
<div style={{ paddingTop: 'var(--nav-h)' }}>
|
|
<div className="max-w-7xl mx-auto px-4 py-6">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<Link
|
|
to={`/trips/${tripId}`}
|
|
className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
{t('common.backToPlanning')}
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Fotos</h1>
|
|
<p className="text-gray-500 text-sm">{photos.length} Fotos für {trip?.title}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<PhotoGallery
|
|
photos={photos}
|
|
onUpload={handleUpload}
|
|
onDelete={handleDelete}
|
|
onUpdate={handleUpdate}
|
|
places={places}
|
|
days={days}
|
|
tripId={tripId}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|