mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
dayplan toolbar polish + weather archive fallback
- weather: add archive API branch in getWeather for past dates (previously returned no_forecast, making the day-strip widget show "—") - dayplan: add expand/collapse-all toggle between ICS and Undo with animated icon swap (ChevronsUpDown <-> ChevronsDownUp) - dayplan: drop the trip title + date range block from the sidebar header (already shown in the page header), toolbar now right-aligned
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 React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
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, X, Route as RouteIcon } from 'lucide-react'
|
import { ChevronDown, ChevronRight, ChevronUp, ChevronsDownUp, ChevronsUpDown, 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, Route as RouteIcon } from 'lucide-react'
|
||||||
|
|
||||||
const RES_ICONS = { flight: Plane, hotel: Hotel, restaurant: Utensils, train: Train, car: Car, cruise: Ship, event: Ticket, tour: Users, other: FileText }
|
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'
|
import { assignmentsApi, reservationsApi } from '../../api/client'
|
||||||
@@ -940,18 +940,9 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}>
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}>
|
||||||
{/* Reise-Titel */}
|
{/* Toolbar */}
|
||||||
<div style={{ padding: '16px 16px 12px', borderBottom: '1px solid var(--border-faint)', flexShrink: 0 }}>
|
<div style={{ padding: '12px 16px', borderBottom: '1px solid var(--border-faint)', flexShrink: 0 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8 }}>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)', lineHeight: '1.3' }}>{trip?.title}</div>
|
|
||||||
{(trip?.start_date || trip?.end_date) && (
|
|
||||||
<div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: 3 }}>
|
|
||||||
{[trip.start_date, trip.end_date].filter(Boolean).map(d => new Date(d + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' })).join(' – ')}
|
|
||||||
{days.length > 0 && ` · ${days.length} ${t('dayplan.days')}`}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={{ position: 'relative', flexShrink: 0 }}>
|
<div style={{ position: 'relative', flexShrink: 0 }}>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -1033,6 +1024,51 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{(() => {
|
||||||
|
const allExpanded = days.length > 0 && days.every(d => expandedDays.has(d.id))
|
||||||
|
const label = allExpanded ? t('dayplan.collapseAll') : t('dayplan.expandAll')
|
||||||
|
return (
|
||||||
|
<Tooltip label={label} placement="bottom">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const next = allExpanded ? new Set() : new Set(days.map(d => d.id))
|
||||||
|
setExpandedDays(next)
|
||||||
|
try { sessionStorage.setItem(`day-expanded-${tripId}`, JSON.stringify([...next])) } catch {}
|
||||||
|
}}
|
||||||
|
aria-label={label}
|
||||||
|
aria-pressed={allExpanded}
|
||||||
|
style={{
|
||||||
|
position: 'relative', flexShrink: 0,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
width: 30, height: 30, borderRadius: 8,
|
||||||
|
border: '1px solid var(--border-primary)', background: 'none',
|
||||||
|
color: 'var(--text-primary)', cursor: 'pointer', fontFamily: 'inherit', padding: 0,
|
||||||
|
transition: 'color 0.15s, border-color 0.15s, background 0.15s',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-hover)' }}
|
||||||
|
onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
|
||||||
|
>
|
||||||
|
<span style={{
|
||||||
|
position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
transition: 'opacity 0.2s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||||
|
opacity: allExpanded ? 0 : 1,
|
||||||
|
transform: allExpanded ? 'translateY(-8px) scale(0.6)' : 'translateY(0) scale(1)',
|
||||||
|
}}>
|
||||||
|
<ChevronsUpDown size={14} strokeWidth={2} />
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
transition: 'opacity 0.2s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||||
|
opacity: allExpanded ? 1 : 0,
|
||||||
|
transform: allExpanded ? 'translateY(0) scale(1)' : 'translateY(8px) scale(0.6)',
|
||||||
|
}}>
|
||||||
|
<ChevronsDownUp size={14} strokeWidth={2} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
{onUndo && (
|
{onUndo && (
|
||||||
<div style={{ position: 'relative', flexShrink: 0 }}>
|
<div style={{ position: 'relative', flexShrink: 0 }}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -908,6 +908,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'dayplan.cannotDropOnTimed': 'Orte können nicht zwischen zeitgebundene Einträge geschoben werden',
|
'dayplan.cannotDropOnTimed': 'Orte können nicht zwischen zeitgebundene Einträge geschoben werden',
|
||||||
'dayplan.cannotBreakChronology': 'Die zeitliche Reihenfolge von Uhrzeiten und Buchungen darf nicht verletzt werden',
|
'dayplan.cannotBreakChronology': 'Die zeitliche Reihenfolge von Uhrzeiten und Buchungen darf nicht verletzt werden',
|
||||||
'dayplan.addNote': 'Notiz hinzufügen',
|
'dayplan.addNote': 'Notiz hinzufügen',
|
||||||
|
'dayplan.expandAll': 'Alle Tage ausklappen',
|
||||||
|
'dayplan.collapseAll': 'Alle Tage einklappen',
|
||||||
'dayplan.editNote': 'Notiz bearbeiten',
|
'dayplan.editNote': 'Notiz bearbeiten',
|
||||||
'dayplan.noteAdd': 'Notiz hinzufügen',
|
'dayplan.noteAdd': 'Notiz hinzufügen',
|
||||||
'dayplan.noteEdit': 'Notiz bearbeiten',
|
'dayplan.noteEdit': 'Notiz bearbeiten',
|
||||||
|
|||||||
@@ -965,6 +965,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'dayplan.cannotDropOnTimed': 'Items cannot be placed between time-bound entries',
|
'dayplan.cannotDropOnTimed': 'Items cannot be placed between time-bound entries',
|
||||||
'dayplan.cannotBreakChronology': 'This would break the chronological order of timed items and bookings',
|
'dayplan.cannotBreakChronology': 'This would break the chronological order of timed items and bookings',
|
||||||
'dayplan.addNote': 'Add Note',
|
'dayplan.addNote': 'Add Note',
|
||||||
|
'dayplan.expandAll': 'Expand all days',
|
||||||
|
'dayplan.collapseAll': 'Collapse all days',
|
||||||
'dayplan.editNote': 'Edit Note',
|
'dayplan.editNote': 'Edit Note',
|
||||||
'dayplan.noteAdd': 'Add Note',
|
'dayplan.noteAdd': 'Add Note',
|
||||||
'dayplan.noteEdit': 'Edit Note',
|
'dayplan.noteEdit': 'Edit Note',
|
||||||
|
|||||||
@@ -194,6 +194,37 @@ async function _getWeatherImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Past date: use archive API for the actual date
|
||||||
|
if (diffDays < -1) {
|
||||||
|
const dateStr = targetDate.toISOString().slice(0, 10);
|
||||||
|
const url = `https://archive-api.open-meteo.com/v1/archive?latitude=${lat}&longitude=${lng}&start_date=${dateStr}&end_date=${dateStr}&daily=temperature_2m_max,temperature_2m_min,weathercode,precipitation_sum&timezone=auto`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json() as OpenMeteoForecast;
|
||||||
|
|
||||||
|
if (!response.ok || data.error) {
|
||||||
|
throw new ApiError(response.status || 500, data.reason || 'Open-Meteo Archive API error');
|
||||||
|
}
|
||||||
|
|
||||||
|
const daily = data.daily;
|
||||||
|
if (daily && daily.time && daily.time.length > 0 && daily.temperature_2m_max[0] != null) {
|
||||||
|
const code = daily.weathercode?.[0];
|
||||||
|
const descriptions = lang === 'de' ? WMO_DESCRIPTION_DE : WMO_DESCRIPTION_EN;
|
||||||
|
const tMax = daily.temperature_2m_max[0];
|
||||||
|
const tMin = daily.temperature_2m_min[0];
|
||||||
|
const result: WeatherResult = {
|
||||||
|
temp: Math.round((tMax + tMin) / 2),
|
||||||
|
temp_max: Math.round(tMax),
|
||||||
|
temp_min: Math.round(tMin),
|
||||||
|
main: WMO_MAP[code!] || estimateCondition((tMax + tMin) / 2, daily.precipitation_sum?.[0] || 0),
|
||||||
|
description: descriptions[code!] || '',
|
||||||
|
type: 'forecast',
|
||||||
|
};
|
||||||
|
setCache(ck, result, TTL_CLIMATE_MS);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return { temp: 0, main: '', description: '', type: '', error: 'no_forecast' };
|
||||||
|
}
|
||||||
|
|
||||||
// Climate / archive fallback (far-future dates)
|
// Climate / archive fallback (far-future dates)
|
||||||
if (diffDays > -1) {
|
if (diffDays > -1) {
|
||||||
const month = targetDate.getMonth() + 1;
|
const month = targetDate.getMonth() + 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user