Signing you in...
Please wait while we load your personal trips.
/* @component-map * App — Main container, tab navigation [app.jsx] * TripList — List of trips with create/edit [components/TripList.jsx] * TripDetail — Single trip view with day-by-day activities [components/TripDetail.jsx] * DayPlanner — Day activities + map view [components/DayPlanner.jsx] * PackingChecklist — Packing list management [components/PackingChecklist.jsx] * @end-component-map */ import { useEffect, useMemo, useState } from 'react'; import { useAuth, useCollection, useProxy } from '@deplixo/sdk'; import { TripList } from './components/TripList.jsx'; import { TripDetail } from './components/TripDetail.jsx'; import { PackingChecklist } from './components/PackingChecklist.jsx'; function App() { const { user, loading, login, logout } = useAuth(); const { fetch: proxyFetch, loading: proxyLoading } = useProxy(); const sharedTrips = useCollection('shared-itineraries', { personal: true }); const sharedLinks = useCollection('trip-share-links', { personal: true }); const tripsCollection = useCollection('trips', { personal: true }); const [activeTab, setActiveTab] = useState('trips'); const [selectedTripId, setSelectedTripId] = useState(null); const [shareDraft, setShareDraft] = useState(''); const [shareError, setShareError] = useState(''); const [sharedView, setSharedView] = useState({ tripId: null, dayId: null }); const [weatherCache, setWeatherCache] = useState({}); const [weatherError, setWeatherError] = useState(''); const parseShareUrl = (value) => { try { const url = new URL(value); const tripId = url.searchParams.get('trip') || url.searchParams.get('tripId'); const dayId = url.searchParams.get('day') || url.searchParams.get('dayId'); const token = url.searchParams.get('token') || url.searchParams.get('share'); const view = url.searchParams.get('view'); return { tripId, dayId, token, view, }; } catch { const tripMatch = value.match(/[?&](?:trip|tripId)=([^&]+)/i); const dayMatch = value.match(/[?&](?:day|dayId)=([^&]+)/i); const tokenMatch = value.match(/[?&](?:token|share)=([^&]+)/i); const viewMatch = value.match(/[?&]view=([^&]+)/i); return { tripId: tripMatch ? decodeURIComponent(tripMatch[1]) : null, dayId: dayMatch ? decodeURIComponent(dayMatch[1]) : null, token: tokenMatch ? decodeURIComponent(tokenMatch[1]) : null, view: viewMatch ? decodeURIComponent(viewMatch[1]) : null, }; } }; const findSharedPayload = (tripId, dayId) => { const items = sharedTrips?.items || []; return items.find((item) => { if (dayId) return item.tripId === tripId && item.dayId === dayId; return item.tripId === tripId && !item.dayId; }) || null; }; const isSharedLinkValid = (parsed) => { if (!parsed.tripId) return false; if (!parsed.token) return false; const links = sharedLinks?.items || []; return links.some((item) => item.token === parsed.token && item.tripId === parsed.tripId && (!item.expiresAt || new Date(item.expiresAt).getTime() > Date.now())); }; const getTripForWeather = (tripId) => { const trips = tripsCollection?.items || []; return trips.find((trip) => trip.id === tripId || trip.tripId === tripId) || null; }; const getDayForWeather = (trip, dayId) => { if (!trip || !dayId || !Array.isArray(trip.days)) return null; return trip.days.find((day) => day.id === dayId) || null; }; const buildWeatherQuery = (trip, day) => { const destination = trip?.destination || trip?.name || 'Travel destination'; const date = day?.date || trip?.startDate || ''; return { destination, date }; }; const loadWeather = async (tripId, dayId = null) => { if (!tripId) return; const cacheKey = `${tripId}:${dayId || 'trip'}`; if (weatherCache[cacheKey]) return; const trip = getTripForWeather(tripId); const day = getDayForWeather(trip, dayId); const { destination, date } = buildWeatherQuery(trip, day); try { setWeatherError(''); const query = encodeURIComponent(`${destination}${date ? ` ${date}` : ''}`.trim()); const data = await proxyFetch(`https://api.weatherapi.com/v1/forecast.json?key=${WEATHER_KEY}&q=${query}&days=3&aqi=no&alerts=no`, { method: 'GET', }); const forecastDay = data?.forecast?.forecastday?.[0]?.day || null; const current = data?.current || null; const summary = forecastDay?.condition?.text || current?.condition?.text || 'Weather unavailable'; const temp = forecastDay?.avgtemp_c ?? current?.temp_c; const icon = forecastDay?.condition?.icon || current?.condition?.icon || ''; const location = data?.location?.name || destination; setWeatherCache((prev) => ({ ...prev, [cacheKey]: { summary, temp, icon, location, sourceDate: date, }, })); } catch (error) { setWeatherError('Unable to load weather right now.'); setWeatherCache((prev) => ({ ...prev, [cacheKey]: { summary: 'Weather unavailable', temp: null, icon: '', location: destination, sourceDate: date, }, })); } }; const applySharedLink = (raw) => { const parsed = parseShareUrl(raw.trim()); if (!parsed.tripId || !parsed.token) { setShareError('Please paste a valid shared itinerary link.'); return; } if (!isSharedLinkValid(parsed)) { setShareError('This share link is invalid or has expired.'); return; } setShareError(''); setSharedView({ tripId: parsed.tripId, dayId: parsed.dayId, }); setSelectedTripId(parsed.tripId); setActiveTab('detail'); }; useEffect(() => { const params = new URLSearchParams(window.location.search); const tripId = params.get('trip') || params.get('tripId'); const dayId = params.get('day') || params.get('dayId'); const token = params.get('token') || params.get('share'); if (tripId && token) { const parsed = { tripId, dayId, token }; if (isSharedLinkValid(parsed)) { setSharedView({ tripId, dayId }); setSelectedTripId(tripId); setActiveTab('detail'); } } }, [sharedLinks?.items]); useEffect(() => { if (sharedView.tripId) { loadWeather(sharedView.tripId, sharedView.dayId); return; } if (selectedTripId) { loadWeather(selectedTripId, null); } }, [sharedView.tripId, sharedView.dayId, selectedTripId, tripsCollection?.items]); const handleSelectTrip = (tripId) => { setSharedView({ tripId: null, dayId: null }); setSelectedTripId(tripId); setActiveTab('detail'); }; const handleBack = () => { setSelectedTripId(null); setSharedView({ tripId: null, dayId: null }); setActiveTab('trips'); }; const activeSharedPayload = useMemo(() => { if (!sharedView.tripId) return null; return findSharedPayload(sharedView.tripId, sharedView.dayId); }, [sharedView, sharedTrips?.items]); const activeWeatherKey = sharedView.tripId ? `${sharedView.tripId}:${sharedView.dayId || 'trip'}` : selectedTripId ? `${selectedTripId}:trip` : null; const activeWeather = activeWeatherKey ? weatherCache[activeWeatherKey] : null; const renderWeatherCard = () => { if (!activeWeatherKey) return null; return (
This itinerary was shared with you. You can view trip details without signing in.
{activeSharedPayload.description || 'No description provided.'}
{activeSharedPayload.days && Array.isArray(activeSharedPayload.days) ? (Please wait while we load your personal trips.
Sign in with Google to create, view, and manage your private trips.