Spaces:
Sleeping
Sleeping
| import { useEffect } from 'react' | |
| import { MapContainer, TileLayer, Marker, Tooltip, useMap } from 'react-leaflet' | |
| import L from 'leaflet' | |
| import { useTranslation, SUPPORTED_LANGUAGES } from '../i18n' | |
| import { useSettingsStore } from '../store/settingsStore' | |
| import { getLocaleForLanguage } from '../i18n' | |
| import { useSharedTrip } from './sharedTrip/useSharedTrip' | |
| import { getCategoryIcon } from '../components/shared/categoryIcons' | |
| import { createElement } from 'react' | |
| import { renderToStaticMarkup } from 'react-dom/server' | |
| import { Clock, MapPin, FileText, Train, Plane, Bus, Car, Ship, Ticket, Hotel, Map, Luggage, Wallet, MessageCircle } from 'lucide-react' | |
| import { isDayInAccommodationRange } from '../utils/dayOrder' | |
| import { getTransportForDay, getMergedItems } from '../utils/dayMerge' | |
| import { splitReservationDateTime } from '../utils/formatters' | |
| const TRANSPORT_ICONS = { flight: Plane, train: Train, bus: Bus, car: Car, cruise: Ship } | |
| function createMarkerIcon(place: any) { | |
| const cat = place.category | |
| const color = cat?.color || '#6366f1' | |
| const CatIcon = getCategoryIcon(cat?.icon) | |
| const iconSvg = renderToStaticMarkup(createElement(CatIcon, { size: 14, strokeWidth: 2, color: 'white' })) | |
| return L.divIcon({ | |
| className: '', | |
| iconSize: [28, 28], | |
| iconAnchor: [14, 14], | |
| html: `<div style="width:28px;height:28px;border-radius:50%;background:${color};display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,0.3);border:2px solid white;">${iconSvg}</div>`, | |
| }) | |
| } | |
| function FitBoundsToPlaces({ places }: { places: any[] }) { | |
| const map = useMap() | |
| useEffect(() => { | |
| if (places.length === 0) return | |
| const bounds = L.latLngBounds(places.map(p => [p.lat, p.lng])) | |
| map.fitBounds(bounds, { padding: [40, 40], maxZoom: 14 }) | |
| }, [places, map]) | |
| return null | |
| } | |
| export default function SharedTripPage() { | |
| const { t, locale } = useTranslation() | |
| // Page = wiring container: share fetch + view state live in the hook. | |
| const { data, error, selectedDay, setSelectedDay, activeTab, setActiveTab, showLangPicker, setShowLangPicker } = useSharedTrip() | |
| if (error) return ( | |
| <div className="bg-[#f3f4f6]" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh' }}> | |
| <div style={{ textAlign: 'center', padding: 40 }}> | |
| <div style={{ fontSize: 48, marginBottom: 16 }}>🔒</div> | |
| <h1 className="text-[#111827]" style={{ fontSize: 20, fontWeight: 700 }}>{t('shared.expired')}</h1> | |
| <p className="text-[#6b7280]" style={{ marginTop: 8 }}>{t('shared.expiredHint')}</p> | |
| </div> | |
| </div> | |
| ) | |
| if (!data) return ( | |
| <div className="bg-[#f3f4f6]" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh' }}> | |
| <div style={{ width: 32, height: 32, border: '3px solid #e5e7eb', borderTopColor: '#111827', borderRadius: '50%', animation: 'spin 0.6s linear infinite' }} /> | |
| <style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style> | |
| </div> | |
| ) | |
| const { trip, days, assignments, dayNotes, places, reservations, accommodations, packing, budget, categories, permissions, collab } = data | |
| const sortedDays = [...(days || [])].sort((a: any, b: any) => a.day_number - b.day_number) | |
| // Map places | |
| const mapPlaces = selectedDay | |
| ? (assignments[String(selectedDay)] || []).map((a: any) => a.place).filter((p: any) => p?.lat && p?.lng) | |
| : (places || []).filter((p: any) => p?.lat && p?.lng) | |
| const center = mapPlaces.length > 0 ? [mapPlaces[0].lat, mapPlaces[0].lng] : [48.85, 2.35] | |
| return ( | |
| <div className="bg-surface-secondary" style={{ minHeight: '100vh', fontFamily: "var(--font-system)" }}> | |
| {/* Header */} | |
| <div className="text-white" style={{ background: 'linear-gradient(135deg, #000 0%, #0f172a 50%, #1e293b 100%)', padding: '32px 20px 28px', textAlign: 'center', position: 'relative' }}> | |
| {/* Cover image background */} | |
| {trip.cover_image && ( | |
| <div style={{ position: 'absolute', inset: 0, backgroundImage: `url(${trip.cover_image.startsWith('http') ? trip.cover_image : trip.cover_image.startsWith('/') ? trip.cover_image : '/uploads/' + trip.cover_image})`, backgroundSize: 'cover', backgroundPosition: 'center', opacity: 0.15 }} /> | |
| )} | |
| {/* Background decoration */} | |
| <div className="bg-[rgba(255,255,255,0.03)]" style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%' }} /> | |
| <div className="bg-[rgba(255,255,255,0.02)]" style={{ position: 'absolute', bottom: -40, left: -40, width: 150, height: 150, borderRadius: '50%' }} /> | |
| {/* Logo */} | |
| <div className="bg-[rgba(255,255,255,0.08)]" style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 44, height: 44, borderRadius: 12, backdropFilter: 'blur(8px)', marginBottom: 12, border: '1px solid rgba(255,255,255,0.1)' }}> | |
| <img src="/icons/icon-white.svg" alt="TREK" width="26" height="26" /> | |
| </div> | |
| <div style={{ fontSize: 10, fontWeight: 600, letterSpacing: 3, textTransform: 'uppercase', opacity: 0.35, marginBottom: 12 }}>Travel Resource & Exploration Kit</div> | |
| <h1 style={{ margin: '0 0 4px', fontSize: 26, fontWeight: 700, letterSpacing: -0.5 }}>{trip.title}</h1> | |
| {trip.description && ( | |
| <div style={{ fontSize: 13, opacity: 0.5, maxWidth: 400, margin: '0 auto', lineHeight: 1.5 }}>{trip.description}</div> | |
| )} | |
| {(trip.start_date || trip.end_date) && ( | |
| <div className="bg-[rgba(255,255,255,0.08)]" style={{ marginTop: 10, display: 'inline-flex', alignItems: 'center', gap: 8, padding: '6px 14px', borderRadius: 20, backdropFilter: 'blur(4px)', border: '1px solid rgba(255,255,255,0.08)' }}> | |
| <span style={{ fontSize: 12, fontWeight: 500, opacity: 0.8 }}> | |
| {[trip.start_date, trip.end_date].filter(Boolean).map((d: string) => new Date(d + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' })).join(' — ')} | |
| </span> | |
| {days?.length > 0 && <span style={{ fontSize: 11, opacity: 0.4 }}>·</span>} | |
| {days?.length > 0 && <span style={{ fontSize: 11, opacity: 0.5 }}>{days.length} {t('shared.days')}</span>} | |
| </div> | |
| )} | |
| <div style={{ marginTop: 12, fontSize: 9, fontWeight: 500, letterSpacing: 1.5, textTransform: 'uppercase', opacity: 0.25 }}>{t('shared.readOnly')}</div> | |
| {/* Language picker - top right */} | |
| <div style={{ position: 'absolute', top: 12, right: 12, zIndex: 10 }}> | |
| <button onClick={() => setShowLangPicker(v => !v)} | |
| className="bg-[rgba(255,255,255,0.1)] text-[rgba(255,255,255,0.7)]" | |
| style={{ | |
| padding: '5px 12px', borderRadius: 20, border: '1px solid rgba(255,255,255,0.15)', | |
| backdropFilter: 'blur(8px)', | |
| fontSize: 11, fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit', | |
| }}> | |
| {SUPPORTED_LANGUAGES.find(l => l.value === (locale?.split('-')[0] || 'en'))?.label || 'Language'} | |
| </button> | |
| {showLangPicker && ( | |
| <div className="bg-white" style={{ position: 'absolute', top: '100%', right: 0, marginTop: 6, borderRadius: 10, boxShadow: '0 4px 16px rgba(0,0,0,0.2)', padding: 4, zIndex: 50, minWidth: 150 }}> | |
| {SUPPORTED_LANGUAGES.map(lang => ( | |
| <button key={lang.value} onClick={() => { | |
| // Set language locally without API call (shared page has no auth) | |
| useSettingsStore.setState(s => ({ settings: { ...s.settings, language: lang.value } })) | |
| setShowLangPicker(false) | |
| }} | |
| className="text-[#374151]" | |
| style={{ display: 'block', width: '100%', padding: '6px 12px', border: 'none', background: 'none', textAlign: 'left', cursor: 'pointer', fontSize: 12, borderRadius: 6, fontFamily: 'inherit' }} | |
| onMouseEnter={e => e.currentTarget.style.background = '#f3f4f6'} | |
| onMouseLeave={e => e.currentTarget.style.background = 'none'} | |
| >{lang.label}</button> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <div style={{ maxWidth: 900, margin: '0 auto', padding: '20px 16px' }}> | |
| {/* Tabs */} | |
| <div style={{ display: 'flex', gap: 6, marginBottom: 20, overflowX: 'auto', padding: '2px 0' }}> | |
| {[ | |
| { id: 'plan', label: t('shared.tabPlan'), Icon: Map }, | |
| ...(permissions?.share_bookings ? [{ id: 'bookings', label: t('shared.tabBookings'), Icon: Ticket }] : []), | |
| ...(permissions?.share_packing ? [{ id: 'packing', label: t('shared.tabPacking'), Icon: Luggage }] : []), | |
| ...(permissions?.share_budget ? [{ id: 'budget', label: t('shared.tabBudget'), Icon: Wallet }] : []), | |
| ...(permissions?.share_collab ? [{ id: 'collab', label: t('shared.tabChat'), Icon: MessageCircle }] : []), | |
| ].map(tab => ( | |
| <button key={tab.id} onClick={() => setActiveTab(tab.id)} | |
| className={activeTab === tab.id ? 'bg-[#111827] text-white' : 'bg-surface-card text-[#6b7280]'} | |
| style={{ | |
| padding: '8px 18px', borderRadius: 12, border: '1.5px solid', cursor: 'pointer', | |
| fontSize: 12, fontWeight: 600, fontFamily: 'inherit', transition: 'all 0.15s', whiteSpace: 'nowrap', | |
| display: 'flex', alignItems: 'center', gap: 6, | |
| borderColor: activeTab === tab.id ? '#111827' : 'var(--border-faint, #e5e7eb)', | |
| boxShadow: activeTab === tab.id ? '0 2px 8px rgba(0,0,0,0.15)' : '0 1px 3px rgba(0,0,0,0.04)', | |
| }}><tab.Icon size={13} /><span className="hidden sm:inline">{tab.label}</span></button> | |
| ))} | |
| </div> | |
| {/* Map */} | |
| {activeTab === 'plan' && (<> | |
| <div style={{ borderRadius: 16, overflow: 'hidden', height: 300, marginBottom: 20, boxShadow: '0 2px 12px rgba(0,0,0,0.08)' }}> | |
| <MapContainer center={center as [number, number]} zoom={11} zoomControl={false} style={{ width: '100%', height: '100%' }}> | |
| <TileLayer url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png" referrerPolicy="strict-origin-when-cross-origin" /> | |
| <FitBoundsToPlaces places={mapPlaces} /> | |
| {mapPlaces.map((p: any) => ( | |
| <Marker key={p.id} position={[p.lat, p.lng]} icon={createMarkerIcon(p)}> | |
| <Tooltip>{p.name}</Tooltip> | |
| </Marker> | |
| ))} | |
| </MapContainer> | |
| </div> | |
| {/* Day Plan */} | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> | |
| {sortedDays.map((day: any, di: number) => { | |
| const da = assignments[String(day.id)] || [] | |
| const notes = (dayNotes[String(day.id)] || []) | |
| const dayAssignmentIds: number[] = da.map((a: any) => a.id) | |
| const dayTransport = getTransportForDay({ reservations: reservations || [], dayId: day.id, dayAssignmentIds, days: sortedDays }) | |
| const dayAccs = (accommodations || []).filter((a: any) => isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, sortedDays)) | |
| const merged = getMergedItems({ | |
| dayAssignments: da, | |
| dayNotes: notes, | |
| dayTransports: dayTransport, | |
| dayId: day.id, | |
| }) | |
| return ( | |
| <div key={day.id} className="bg-surface-card border border-edge-faint" style={{ borderRadius: 14, overflow: 'hidden' }}> | |
| <div onClick={() => setSelectedDay(selectedDay === day.id ? null : day.id)} | |
| style={{ padding: '12px 16px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10 }}> | |
| <div className={selectedDay === day.id ? 'bg-[#111827] text-white' : 'bg-[#f3f4f6] text-[#6b7280]'} style={{ width: 28, height: 28, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, flexShrink: 0 }}>{di + 1}</div> | |
| <div style={{ flex: 1 }}> | |
| <div className="text-[#111827]" style={{ fontSize: 14, fontWeight: 600 }}>{day.title || `Day ${day.day_number}`}</div> | |
| {day.date && <div className="text-[#9ca3af]" style={{ fontSize: 11, marginTop: 1 }}>{new Date(day.date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })}</div>} | |
| </div> | |
| {dayAccs.map((acc: any) => ( | |
| <span key={acc.id} className="bg-[#f3f4f6] text-[#6b7280]" style={{ fontSize: 9, padding: '2px 6px', borderRadius: 4, display: 'flex', alignItems: 'center', gap: 3 }}> | |
| <Hotel size={8} /> {acc.place_name} | |
| </span> | |
| ))} | |
| <span className="text-[#9ca3af]" style={{ fontSize: 11 }}>{da.length} {t('shared.places')}</span> | |
| </div> | |
| {selectedDay === day.id && merged.length > 0 && ( | |
| <div style={{ padding: '0 16px 12px', display: 'flex', flexDirection: 'column', gap: 6 }}> | |
| {merged.map((item: any) => { | |
| if (item.type === 'transport') { | |
| const r = item.data | |
| const TIcon = TRANSPORT_ICONS[r.type] || Ticket | |
| const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {}) | |
| const time = splitReservationDateTime(r.reservation_time).time ?? '' | |
| let sub = '' | |
| if (r.type === 'flight') sub = [meta.airline, meta.flight_number, meta.departure_airport && meta.arrival_airport ? `${meta.departure_airport} → ${meta.arrival_airport}` : ''].filter(Boolean).join(' · ') | |
| else if (r.type === 'train') sub = [meta.train_number, meta.platform ? `Gl. ${meta.platform}` : ''].filter(Boolean).join(' · ') | |
| return ( | |
| <div key={`t-${r.id}`} className="bg-[rgba(59,130,246,0.06)]" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px', borderRadius: 6, border: '1px solid rgba(59,130,246,0.15)' }}> | |
| <div className="bg-[rgba(59,130,246,0.12)]" style={{ width: 24, height: 24, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> | |
| <TIcon size={12} color="#3b82f6" /> | |
| </div> | |
| <div style={{ flex: 1, minWidth: 0 }}> | |
| <div className="text-[#111827]" style={{ fontSize: 12, fontWeight: 500 }}>{r.title}{time ? ` · ${time}` : ''}</div> | |
| {sub && <div className="text-[#6b7280]" style={{ fontSize: 10 }}>{sub}</div>} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| if (item.type === 'note') { | |
| return ( | |
| <div key={`n-${item.data.id}`} className="bg-[#f9fafb]" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 8px', borderRadius: 6, border: '1px solid #f3f4f6' }}> | |
| <FileText size={12} color="#9ca3af" /> | |
| <div> | |
| <div className="text-[#374151]" style={{ fontSize: 12 }}>{item.data.text}</div> | |
| {item.data.time && <div className="text-[#9ca3af]" style={{ fontSize: 10 }}>{item.data.time}</div>} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| const place = item.data.place | |
| if (!place) return null | |
| const cat = categories?.find((c: any) => c.id === place.category_id) | |
| return ( | |
| <div key={`p-${item.data.id}`} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 8px', borderRadius: 6 }}> | |
| <div style={{ width: 28, height: 28, borderRadius: '50%', background: cat?.color || '#6366f1', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> | |
| {place.image_url ? <img src={place.image_url} style={{ width: 28, height: 28, borderRadius: '50%', objectFit: 'cover' }} /> : <MapPin size={13} color="white" />} | |
| </div> | |
| <div style={{ flex: 1, minWidth: 0 }}> | |
| <div className="text-[#111827]" style={{ fontSize: 12.5, fontWeight: 500 }}>{place.name}</div> | |
| {(place.address || place.description) && <div className="text-[#9ca3af]" style={{ fontSize: 10, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{place.address || place.description}</div>} | |
| </div> | |
| {place.place_time && <span className="text-[#6b7280]" style={{ fontSize: 10, display: 'flex', alignItems: 'center', gap: 3, flexShrink: 0 }}><Clock size={9} />{place.place_time}{place.end_time ? ` – ${place.end_time}` : ''}</span>} | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| </>)} | |
| {/* Bookings */} | |
| {activeTab === 'bookings' && (reservations || []).length > 0 && ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> | |
| {(reservations || []).map((r: any) => { | |
| const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {}) | |
| const TIcon = TRANSPORT_ICONS[r.type] || Ticket | |
| const { date: rDate, time: rTime } = splitReservationDateTime(r.reservation_time) | |
| const time = rTime ?? '' | |
| const date = rDate ? new Date(rDate + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' }) : '' | |
| return ( | |
| <div key={r.id} className="bg-surface-card border border-edge-faint" style={{ borderRadius: 10, padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12 }}> | |
| <div className="bg-[#f3f4f6]" style={{ width: 32, height: 32, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> | |
| <TIcon size={15} color="#6b7280" /> | |
| </div> | |
| <div style={{ flex: 1, minWidth: 0 }}> | |
| <div className="text-[#111827]" style={{ fontSize: 13, fontWeight: 600 }}>{r.title}</div> | |
| <div className="text-[#9ca3af]" style={{ fontSize: 11, display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 2 }}> | |
| {date && <span>{date}</span>} | |
| {time && <span>{time}</span>} | |
| {r.location && <span>{r.location}</span>} | |
| {meta.airline && <span>{meta.airline} {meta.flight_number || ''}</span>} | |
| {meta.train_number && <span>{meta.train_number}</span>} | |
| </div> | |
| </div> | |
| <span className={r.status === 'confirmed' ? 'bg-[rgba(22,163,74,0.1)] text-[#16a34a]' : 'bg-[rgba(217,119,6,0.1)] text-[#d97706]'} style={{ fontSize: 10, padding: '2px 8px', borderRadius: 20, fontWeight: 600 }}> | |
| {r.status === 'confirmed' ? t('shared.confirmed') : t('shared.pending')} | |
| </span> | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| )} | |
| {/* Packing */} | |
| {activeTab === 'packing' && (packing || []).length > 0 && ( | |
| <div className="bg-surface-card border border-edge-faint" style={{ borderRadius: 14, overflow: 'hidden' }}> | |
| {Object.entries((packing || []).reduce((g: any, i: any) => { const c = i.category || t('shared.other'); (g[c] = g[c] || []).push(i); return g }, {})).map(([cat, items]: [string, any]) => ( | |
| <div key={cat}> | |
| <div className="bg-[#f9fafb] text-[#6b7280]" style={{ padding: '8px 16px', fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', borderBottom: '1px solid #f3f4f6' }}>{cat}</div> | |
| {items.map((item: any) => ( | |
| <div key={item.id} style={{ padding: '6px 16px', display: 'flex', alignItems: 'center', gap: 8, borderBottom: '1px solid #f9fafb' }}> | |
| <span className={item.checked ? 'text-[#9ca3af]' : 'text-[#111827]'} style={{ fontSize: 13, textDecoration: item.checked ? 'line-through' : 'none' }}>{item.name}</span> | |
| </div> | |
| ))} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {/* Budget */} | |
| {activeTab === 'budget' && (budget || []).length > 0 && (() => { | |
| const grouped = (budget || []).reduce((g: any, i: any) => { const c = i.category || t('shared.other'); (g[c] = g[c] || []).push(i); return g }, {}) | |
| const total = (budget || []).reduce((s: number, i: any) => s + (parseFloat(i.total_price) || 0), 0) | |
| return ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> | |
| {/* Total card */} | |
| <div className="text-white" style={{ background: 'linear-gradient(135deg, #000 0%, #1a1a2e 100%)', borderRadius: 14, padding: '20px 24px' }}> | |
| <div style={{ fontSize: 10, fontWeight: 500, letterSpacing: 1, textTransform: 'uppercase', opacity: 0.5 }}>{t('shared.totalBudget')}</div> | |
| <div style={{ fontSize: 28, fontWeight: 700, marginTop: 4 }}>{total.toLocaleString(locale, { minimumFractionDigits: 2 })} {trip.currency || 'EUR'}</div> | |
| </div> | |
| {/* By category */} | |
| {Object.entries(grouped).map(([cat, items]: [string, any]) => ( | |
| <div key={cat} className="bg-surface-card border border-edge-faint" style={{ borderRadius: 12, overflow: 'hidden' }}> | |
| <div className="bg-[#f9fafb]" style={{ padding: '10px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #f3f4f6' }}> | |
| <span className="text-[#374151]" style={{ fontSize: 12, fontWeight: 700 }}>{cat}</span> | |
| <span className="text-[#6b7280]" style={{ fontSize: 12, fontWeight: 600 }}>{items.reduce((s: number, i: any) => s + (parseFloat(i.total_price) || 0), 0).toLocaleString(locale, { minimumFractionDigits: 2 })} {trip.currency || ''}</span> | |
| </div> | |
| {items.map((item: any) => ( | |
| <div key={item.id} style={{ padding: '8px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #fafafa' }}> | |
| <span className="text-[#111827]" style={{ fontSize: 13 }}>{item.name}</span> | |
| <span className="text-[#111827]" style={{ fontSize: 13, fontWeight: 600 }}>{item.total_price ? Number(item.total_price).toLocaleString(locale, { minimumFractionDigits: 2 }) : '—'}</span> | |
| </div> | |
| ))} | |
| </div> | |
| ))} | |
| </div> | |
| ) | |
| })()} | |
| {/* Collab Chat */} | |
| {activeTab === 'collab' && (collab || []).length > 0 && ( | |
| <div className="bg-surface-card border border-edge-faint" style={{ borderRadius: 14, overflow: 'hidden' }}> | |
| <div className="bg-[#f9fafb]" style={{ padding: '12px 16px', borderBottom: '1px solid #f3f4f6', display: 'flex', alignItems: 'center', gap: 8 }}> | |
| <MessageCircle size={14} color="#6b7280" /> | |
| <span className="text-[#374151]" style={{ fontSize: 12, fontWeight: 700 }}>{t('shared.tabChat')} · {(collab || []).length} {t('shared.messages')}</span> | |
| </div> | |
| <div style={{ maxHeight: 500, overflowY: 'auto', padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 10 }}> | |
| {(collab || []).map((msg: any, i: number) => { | |
| const prevMsg = i > 0 ? collab[i - 1] : null | |
| const showDate = !prevMsg || new Date(msg.created_at).toDateString() !== new Date(prevMsg.created_at).toDateString() | |
| return ( | |
| <div key={msg.id}> | |
| {showDate && ( | |
| <div className="text-[#9ca3af]" style={{ textAlign: 'center', margin: '8px 0', fontSize: 10, fontWeight: 600 }}> | |
| {new Date(msg.created_at).toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })} | |
| </div> | |
| )} | |
| <div style={{ display: 'flex', gap: 10 }}> | |
| <div className="bg-[#e5e7eb] text-[#6b7280]" style={{ width: 32, height: 32, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0, overflow: 'hidden' }}> | |
| {msg.avatar ? <img src={`/uploads/avatars/${msg.avatar}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : (msg.username || '?')[0].toUpperCase()} | |
| </div> | |
| <div style={{ flex: 1 }}> | |
| <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}> | |
| <span className="text-[#111827]" style={{ fontSize: 12, fontWeight: 600 }}>{msg.username}</span> | |
| <span className="text-[#9ca3af]" style={{ fontSize: 10 }}>{new Date(msg.created_at).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })}</span> | |
| </div> | |
| <div className="text-[#374151]" style={{ fontSize: 13, marginTop: 3, lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>{msg.text}</div> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| </div> | |
| )} | |
| {/* Footer */} | |
| <div style={{ textAlign: 'center', padding: '40px 0 20px' }}> | |
| <div className="bg-surface-card border border-edge-faint" style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '8px 16px', borderRadius: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.04)' }}> | |
| <img src="/icons/icon.svg" alt="TREK" width="18" height="18" style={{ borderRadius: 4 }} /> | |
| <span className="text-[#9ca3af]" style={{ fontSize: 11 }}>{t('shared.sharedVia')} <strong className="text-[#6b7280]">TREK</strong></span> | |
| </div> | |
| <div className="text-[#d1d5db]" style={{ marginTop: 8, fontSize: 10 }}>Made with <span className="text-[#ef4444]">♥</span> by Maurice · <a href="https://github.com/mauriceboe/TREK" className="text-[#9ca3af]" style={{ textDecoration: 'none' }}>GitHub</a></div> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |