From a86da72b043b9311306a4b0b75f2b429f2e057cb Mon Sep 17 00:00:00 2001 From: elpatron Date: Sat, 6 Jun 2026 21:02:35 +0200 Subject: [PATCH] feat: implement collapsible accordions for event protocol list and form --- client/src/App.css | 41 ++ client/src/components/LogEntryEditor.tsx | 483 +++++++++++++---------- 2 files changed, 310 insertions(+), 214 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index 23cd564..852971a 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -6421,3 +6421,44 @@ body.app-tour-active .feedback-modal-overlay--tour .disclaimer-modal-panel { color: #e2e8f0; word-break: break-word; } + +/* Accordion Styling */ +.accordion-header { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + user-select: none; + padding: 8px 12px; + margin: -8px -12px; + border-radius: 8px; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.accordion-header:hover { + background-color: var(--app-surface-hover, rgba(255, 255, 255, 0.03)); +} + +.accordion-header-title { + display: flex; + align-items: center; + gap: 8px; +} + +.accordion-chevron { + color: var(--app-text-muted, #94a3b8); + transition: transform 0.2s ease; +} + +/* Specific styling for nested member-editor-card header */ +.member-editor-card .accordion-header { + margin: 0 0 16px 0; + padding: 8px 12px; + border-radius: 6px; + background: rgba(255, 255, 255, 0.01); + border: 1px solid rgba(255, 255, 255, 0.03); +} + +.member-editor-card .accordion-header:hover { + background: var(--app-surface-hover, rgba(255, 255, 255, 0.03)); +} diff --git a/client/src/components/LogEntryEditor.tsx b/client/src/components/LogEntryEditor.tsx index c960258..e4939a4 100644 --- a/client/src/components/LogEntryEditor.tsx +++ b/client/src/components/LogEntryEditor.tsx @@ -296,6 +296,9 @@ export default function LogEntryEditor({ const [evLocationName, setEvLocationName] = useState('') const [activeCourseTab, setActiveCourseTab] = useState<'mgk' | 'rwk'>('mgk') + const [eventsCollapsed, setEventsCollapsed] = useState(true) + const [addEventFormCollapsed, setAddEventFormCollapsed] = useState(false) + const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [exporting, setExporting] = useState(false) @@ -1407,6 +1410,7 @@ export default function LogEntryEditor({ if (!ev) return fillEventForm(ev) setEditingEventIndex(index) + setAddEventFormCollapsed(false) } const handleCancelEventEdit = () => { @@ -1853,78 +1857,153 @@ export default function LogEntryEditor({ {/* Section 3: Event Journal Entries */}
-
- -

{t('logs.event_title')}

+
setEventsCollapsed(!eventsCollapsed)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + setEventsCollapsed(!eventsCollapsed) + } + }} + role="button" + aria-expanded={!eventsCollapsed} + tabIndex={0} + > +
+ +

{t('logs.event_title')}

+
+ {eventsCollapsed ? ( + + ) : ( + + )}
{/* List existing events */} - {events.length === 0 ? ( -
{t('logs.no_events')}
- ) : ( - <> - {/* Desktop view */} -
- - - - - - - - - - - - - - - {!readOnly && } - - - - {events.map((ev, idx) => ( - - - - - - - - - - )} - - ))} - -
{t('logs.event_time')}{t('logs.event_creator')}{t('logs.event_mgk')}{t('logs.event_rwk')}{t('logs.event_wind_direction')}{t('logs.event_wind_strength')}{t('logs.event_sea_state')}{t('logs.event_weather')}{t('logs.event_log')}{t('logs.event_gps')}{t('logs.event_remarks')}
{ev.time} - - {ev.mgk ? `${ev.mgk}°` : '—'}{ev.rwk ? `${ev.rwk}°` : '—'}{ev.windDirection || '—'}{ev.windStrength || '—'}{ev.seaState || '—'} - {ev.weatherIcon ? ( - Weather{t('logs.no_events')} + ) : ( + <> + {/* Desktop view */} +
+ + + + + + + + + + + + + + + {!readOnly && } + + + + {events.map((ev, idx) => ( + + + + + + + + + + + + + {!readOnly && ( + )} - - - - - {!readOnly && ( - + ))} + +
{t('logs.event_time')}{t('logs.event_creator')}{t('logs.event_mgk')}{t('logs.event_rwk')}{t('logs.event_wind_direction')}{t('logs.event_wind_strength')}{t('logs.event_sea_state')}{t('logs.event_weather')}{t('logs.event_log')}{t('logs.event_gps')}{t('logs.event_remarks')}
{ev.time} + - ) : ( - '—' + {ev.mgk ? `${ev.mgk}°` : '—'}{ev.rwk ? `${ev.rwk}°` : '—'}{ev.windDirection || '—'}{ev.windStrength || '—'}{ev.seaState || '—'} + {ev.weatherIcon ? ( + Weather + ) : ( + '—' + )} + {ev.logReading ? `${ev.logReading} nm` : '—'} + {ev.gpsLat && ev.gpsLng ? `${ev.gpsLat}, ${ev.gpsLng}` : '—'} + + + +
+ + +
+
{ev.logReading ? `${ev.logReading} nm` : '—'} - {ev.gpsLat && ev.gpsLng ? `${ev.gpsLat}, ${ev.gpsLng}` : '—'} - - - -
+
+
+ + {/* Mobile view */} +
+ {events.map((ev, idx) => { + const hasCourse = ev.mgk || ev.rwk; + const hasWind = ev.windDirection || ev.windStrength || ev.windPressure; + const hasSeaState = ev.seaState; + const hasWeather = ev.weatherIcon; + const hasLog = ev.logReading; + const hasGps = ev.gpsLat && ev.gpsLng; + const hasVisibility = ev.visibility; + const hasHeel = ev.heel; + const hasSailsOrMotor = ev.sailsOrMotor; + + return ( +
+
+
+
+ + {ev.time} +
+ +
+ {!readOnly && ( +
-
-
+ )} +
- {/* Mobile view */} -
- {events.map((ev, idx) => { - const hasCourse = ev.mgk || ev.rwk; - const hasWind = ev.windDirection || ev.windStrength || ev.windPressure; - const hasSeaState = ev.seaState; - const hasWeather = ev.weatherIcon; - const hasLog = ev.logReading; - const hasGps = ev.gpsLat && ev.gpsLng; - const hasVisibility = ev.visibility; - const hasHeel = ev.heel; - const hasSailsOrMotor = ev.sailsOrMotor; +
- return ( -
-
-
-
- - {ev.time} -
- + {hasCourse && ( + + + + {ev.mgk ? `MgK: ${ev.mgk}°` : ''} + {ev.mgk && ev.rwk ? ' / ' : ''} + {ev.rwk ? `rwK: ${ev.rwk}°` : ''} + + + )} + + {hasWind && ( + + + Wind:{' '} + {[ + ev.windDirection, + ev.windStrength ? `${ev.windStrength} Bft` : '', + ev.windPressure ? `${ev.windPressure} hPa` : '' + ] + .filter(Boolean) + .join(' / ')} + + + )} + + {hasSeaState && ( + + {t('logs.event_sea_state')}: {ev.seaState} + + )} + + {hasWeather && ( + + Weather + + )} + + {hasLog && ( + + Log: {ev.logReading} nm + + )} + + {hasGps && ( + + + {ev.gpsLat}, {ev.gpsLng} + + )} + + {hasVisibility && ( + + {t('logs.event_visibility')}: {ev.visibility} + + )} + + {hasHeel && ( + + {t('logs.event_heel')}: {ev.heel}° + + )} + + {hasSailsOrMotor && ( + + {ev.sailsOrMotor} + + )} +
+ +
+
- {!readOnly && ( -
- - -
- )}
- -
- -
- {hasCourse && ( - - - - {ev.mgk ? `MgK: ${ev.mgk}°` : ''} - {ev.mgk && ev.rwk ? ' / ' : ''} - {ev.rwk ? `rwK: ${ev.rwk}°` : ''} - - - )} - - {hasWind && ( - - - Wind:{' '} - {[ - ev.windDirection, - ev.windStrength ? `${ev.windStrength} Bft` : '', - ev.windPressure ? `${ev.windPressure} hPa` : '' - ] - .filter(Boolean) - .join(' / ')} - - - )} - - {hasSeaState && ( - - {t('logs.event_sea_state')}: {ev.seaState} - - )} - - {hasWeather && ( - - Weather - - )} - - {hasLog && ( - - Log: {ev.logReading} nm - - )} - - {hasGps && ( - - - {ev.gpsLat}, {ev.gpsLng} - - )} - - {hasVisibility && ( - - {t('logs.event_visibility')}: {ev.visibility} - - )} - - {hasHeel && ( - - {t('logs.event_heel')}: {ev.heel}° - - )} - - {hasSailsOrMotor && ( - - {ev.sailsOrMotor} - - )} -
- -
- -
-
- ); - })} -
- + ); + })} +
+ + ) )} {/* Add New Event Form Sub-Card */} {!readOnly && (
-

- {editingEventIndex !== null ? t('logs.edit_event') : t('logs.add_event')} -

+
setAddEventFormCollapsed(!addEventFormCollapsed)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + setAddEventFormCollapsed(!addEventFormCollapsed) + } + }} + role="button" + aria-expanded={!addEventFormCollapsed} + tabIndex={0} + style={{ + margin: '0 0 16px 0', + padding: '8px 12px', + borderRadius: '6px', + background: 'rgba(255, 255, 255, 0.01)', + border: '1px solid rgba(255, 255, 255, 0.03)' + }} + > +

+ {editingEventIndex !== null ? t('logs.edit_event') : t('logs.add_event')} +

+ {addEventFormCollapsed ? ( + + ) : ( + + )} +
-
+ {!addEventFormCollapsed && ( + <> +
+ + )}
)}