feat: separate chronological events list and add event form into separate cards
This commit is contained in:
@@ -2118,391 +2118,388 @@ export default function LogEntryEditor({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add New Event Form Sub-Card */}
|
</div>
|
||||||
{!readOnly && (
|
|
||||||
<div className="member-editor-card glass">
|
{/* Section 4: Add New Event Form Card */}
|
||||||
<div
|
{!readOnly && (
|
||||||
className="accordion-header"
|
<div className="form-card">
|
||||||
onClick={() => setAddEventFormCollapsed(!addEventFormCollapsed)}
|
<div
|
||||||
onKeyDown={(e) => {
|
className="form-header accordion-header"
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
onClick={() => setAddEventFormCollapsed(!addEventFormCollapsed)}
|
||||||
e.preventDefault()
|
onKeyDown={(e) => {
|
||||||
setAddEventFormCollapsed(!addEventFormCollapsed)
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
}
|
e.preventDefault()
|
||||||
}}
|
setAddEventFormCollapsed(!addEventFormCollapsed)
|
||||||
role="button"
|
}
|
||||||
aria-expanded={!addEventFormCollapsed}
|
}}
|
||||||
tabIndex={0}
|
role="button"
|
||||||
style={{
|
aria-expanded={!addEventFormCollapsed}
|
||||||
margin: '0 0 16px 0',
|
tabIndex={0}
|
||||||
padding: '8px 12px',
|
>
|
||||||
borderRadius: '6px',
|
<div className="accordion-header-title">
|
||||||
background: 'rgba(255, 255, 255, 0.01)',
|
<Plus size={20} className="form-icon" style={{ color: '#fbbf24' }} />
|
||||||
border: '1px solid rgba(255, 255, 255, 0.03)'
|
<h3 style={{ margin: 0, color: '#fbbf24' }}>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h4 style={{ margin: 0, color: '#fbbf24' }}>
|
|
||||||
{editingEventIndex !== null ? t('logs.edit_event') : t('logs.add_event')}
|
{editingEventIndex !== null ? t('logs.edit_event') : t('logs.add_event')}
|
||||||
</h4>
|
</h3>
|
||||||
{addEventFormCollapsed ? (
|
|
||||||
<ChevronDown size={18} style={{ color: '#fbbf24' }} className="accordion-chevron" />
|
|
||||||
) : (
|
|
||||||
<ChevronUp size={18} style={{ color: '#fbbf24' }} className="accordion-chevron" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{addEventFormCollapsed ? (
|
||||||
|
<ChevronDown size={20} style={{ color: '#fbbf24' }} className="accordion-chevron" />
|
||||||
|
) : (
|
||||||
|
<ChevronUp size={20} style={{ color: '#fbbf24' }} className="accordion-chevron" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{!addEventFormCollapsed && (
|
{!addEventFormCollapsed && (
|
||||||
<>
|
<div style={{ marginTop: '20px' }}>
|
||||||
<div className="form-grid mb-4">
|
<div className="form-grid mb-4">
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<label>
|
<label>
|
||||||
<Clock size={12} style={{ display: 'inline', marginRight: 4 }} />
|
<Clock size={12} style={{ display: 'inline', marginRight: 4 }} />
|
||||||
{t('logs.event_time')} *
|
{t('logs.event_time')} *
|
||||||
</label>
|
</label>
|
||||||
<EventTimeInput24h
|
<EventTimeInput24h
|
||||||
value={evTime}
|
value={evTime}
|
||||||
onChange={setEvTime}
|
onChange={setEvTime}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
aria-label={t('logs.event_time')}
|
aria-label={t('logs.event_time')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="input-group course-dial-section">
|
<div className="input-group course-dial-section">
|
||||||
<label>
|
<label>
|
||||||
<Compass size={12} style={{ display: 'inline', marginRight: 4 }} />
|
<Compass size={12} style={{ display: 'inline', marginRight: 4 }} />
|
||||||
{t('logs.event_course_section')}
|
{t('logs.event_course_section')}
|
||||||
</label>
|
</label>
|
||||||
<div className="course-dial-tabs" role="tablist" aria-label={t('logs.event_course_section')}>
|
<div className="course-dial-tabs" role="tablist" aria-label={t('logs.event_course_section')}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={activeCourseTab === 'mgk'}
|
aria-selected={activeCourseTab === 'mgk'}
|
||||||
className={`course-dial-tab${activeCourseTab === 'mgk' ? ' is-active' : ''}`}
|
className={`course-dial-tab${activeCourseTab === 'mgk' ? ' is-active' : ''}`}
|
||||||
onClick={() => setActiveCourseTab('mgk')}
|
onClick={() => setActiveCourseTab('mgk')}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
>
|
>
|
||||||
{t('logs.course_tab_mgk')}
|
{t('logs.course_tab_mgk')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={activeCourseTab === 'rwk'}
|
aria-selected={activeCourseTab === 'rwk'}
|
||||||
className={`course-dial-tab${activeCourseTab === 'rwk' ? ' is-active' : ''}`}
|
className={`course-dial-tab${activeCourseTab === 'rwk' ? ' is-active' : ''}`}
|
||||||
onClick={() => setActiveCourseTab('rwk')}
|
onClick={() => setActiveCourseTab('rwk')}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
>
|
>
|
||||||
{t('logs.course_tab_rwk')}
|
{t('logs.course_tab_rwk')}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<CourseDialInput
|
||||||
|
value={activeCourseTab === 'mgk' ? evMgk : evRwk}
|
||||||
|
onChange={activeCourseTab === 'mgk' ? setEvMgk : setEvRwk}
|
||||||
|
disabled={saving}
|
||||||
|
aria-label={activeCourseTab === 'mgk' ? t('logs.event_mgk') : t('logs.event_rwk')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-group">
|
||||||
|
<label>{t('logs.event_log')}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. 124.5"
|
||||||
|
className="input-text"
|
||||||
|
value={evLogReading}
|
||||||
|
onChange={(e) => setEvLogReading(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CourseDialInput
|
|
||||||
value={activeCourseTab === 'mgk' ? evMgk : evRwk}
|
|
||||||
onChange={activeCourseTab === 'mgk' ? setEvMgk : setEvRwk}
|
|
||||||
disabled={saving}
|
|
||||||
aria-label={activeCourseTab === 'mgk' ? t('logs.event_mgk') : t('logs.event_rwk')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group">
|
<div className="form-grid mb-4">
|
||||||
<label>{t('logs.event_log')}</label>
|
<div className="input-group">
|
||||||
<input
|
<label>{t('logs.event_location')}</label>
|
||||||
type="text"
|
<input
|
||||||
placeholder="e.g. 124.5"
|
type="text"
|
||||||
className="input-text"
|
placeholder={t('logs.event_location_placeholder')}
|
||||||
value={evLogReading}
|
className="input-text"
|
||||||
onChange={(e) => setEvLogReading(e.target.value)}
|
value={evLocationName}
|
||||||
disabled={saving}
|
onChange={(e) => setEvLocationName(e.target.value)}
|
||||||
/>
|
disabled={saving}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-grid mb-4">
|
<div className="input-group">
|
||||||
<div className="input-group">
|
<label>{t('logs.event_gps')} (Lat, Lng)</label>
|
||||||
<label>{t('logs.event_location')}</label>
|
<div className="gps-input-row">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('logs.event_location_placeholder')}
|
placeholder="Lat"
|
||||||
className="input-text"
|
className="input-text"
|
||||||
value={evLocationName}
|
value={evGpsLat}
|
||||||
onChange={(e) => setEvLocationName(e.target.value)}
|
onChange={(e) => { clearGpsSignal(); setEvGpsLat(e.target.value) }}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
</div>
|
<input
|
||||||
|
type="text"
|
||||||
<div className="input-group">
|
placeholder="Lng"
|
||||||
<label>{t('logs.event_gps')} (Lat, Lng)</label>
|
className="input-text"
|
||||||
<div className="gps-input-row">
|
value={evGpsLng}
|
||||||
<input
|
onChange={(e) => { clearGpsSignal(); setEvGpsLng(e.target.value) }}
|
||||||
type="text"
|
disabled={saving}
|
||||||
placeholder="Lat"
|
/>
|
||||||
className="input-text"
|
<button
|
||||||
value={evGpsLat}
|
type="button"
|
||||||
onChange={(e) => { clearGpsSignal(); setEvGpsLat(e.target.value) }}
|
className="btn secondary"
|
||||||
disabled={saving}
|
onClick={() => void handleGetGps()}
|
||||||
/>
|
title={t('logs.gps_btn')}
|
||||||
<input
|
style={{ width: 'auto', padding: '12px' }}
|
||||||
type="text"
|
disabled={saving}
|
||||||
placeholder="Lng"
|
>
|
||||||
className="input-text"
|
<MapPin size={16} />
|
||||||
value={evGpsLng}
|
</button>
|
||||||
onChange={(e) => { clearGpsSignal(); setEvGpsLng(e.target.value) }}
|
<button
|
||||||
disabled={saving}
|
type="button"
|
||||||
/>
|
className="btn secondary"
|
||||||
<button
|
onClick={handleFetchWeather}
|
||||||
type="button"
|
title={t('logs.weather_btn')}
|
||||||
className="btn secondary"
|
style={{ width: 'auto', padding: '12px' }}
|
||||||
onClick={() => void handleGetGps()}
|
disabled={
|
||||||
title={t('logs.gps_btn')}
|
saving ||
|
||||||
style={{ width: 'auto', padding: '12px' }}
|
weatherLoading ||
|
||||||
disabled={saving}
|
(!evGpsLat && !evLocationName.trim() && !departure.trim() && !destination.trim()) ||
|
||||||
>
|
(!!evGpsLat && !evGpsLng)
|
||||||
<MapPin size={16} />
|
}
|
||||||
</button>
|
>
|
||||||
<button
|
<CloudSun size={16} />
|
||||||
type="button"
|
</button>
|
||||||
className="btn secondary"
|
</div>
|
||||||
onClick={handleFetchWeather}
|
{gpsSignal && (
|
||||||
title={t('logs.weather_btn')}
|
<GpsSignalHint
|
||||||
style={{ width: 'auto', padding: '12px' }}
|
quality={gpsSignal.quality}
|
||||||
disabled={
|
accuracyM={gpsSignal.accuracyM}
|
||||||
saving ||
|
className="gps-signal-hint-editor"
|
||||||
weatherLoading ||
|
/>
|
||||||
(!evGpsLat && !evLocationName.trim() && !departure.trim() && !destination.trim()) ||
|
|
||||||
(!!evGpsLat && !evGpsLng)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CloudSun size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{gpsSignal && (
|
|
||||||
<GpsSignalHint
|
|
||||||
quality={gpsSignal.quality}
|
|
||||||
accuracyM={gpsSignal.accuracyM}
|
|
||||||
className="gps-signal-hint-editor"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-grid weather-metrics-grid mb-4">
|
|
||||||
<div className="input-group course-dial-section weather-metrics-span-2">
|
|
||||||
<label>{t('logs.event_wind_direction')}</label>
|
|
||||||
<CourseDialInput
|
|
||||||
value={evWindDirection}
|
|
||||||
onChange={setEvWindDirection}
|
|
||||||
disabled={saving || weatherLoading}
|
|
||||||
allowCardinal
|
|
||||||
displayMode="auto"
|
|
||||||
aria-label={t('logs.event_wind_direction')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group">
|
|
||||||
<label>{t('logs.event_wind_strength')}</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="e.g. 4 Bft"
|
|
||||||
className="input-text"
|
|
||||||
value={evWindStrength}
|
|
||||||
onChange={(e) => setEvWindStrength(e.target.value)}
|
|
||||||
disabled={saving || weatherLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MetricRangeInput
|
|
||||||
label={t('logs.event_wind_pressure')}
|
|
||||||
value={evWindPressure}
|
|
||||||
onChange={setEvWindPressure}
|
|
||||||
disabled={saving || weatherLoading}
|
|
||||||
min={PRESSURE_MIN_HPA}
|
|
||||||
max={PRESSURE_MAX_HPA}
|
|
||||||
step={1}
|
|
||||||
defaultNumeric={PRESSURE_DEFAULT_HPA}
|
|
||||||
parse={parsePressureHpa}
|
|
||||||
format={formatPressureHpa}
|
|
||||||
formatDisplay={(hpa) =>
|
|
||||||
t('logs.weather_slider_pressure', { value: hpa, defaultValue: `${hpa} hPa` })}
|
|
||||||
numberMin={PRESSURE_MIN_HPA}
|
|
||||||
numberMax={PRESSURE_MAX_HPA}
|
|
||||||
numberStep={1}
|
|
||||||
numberPlaceholder="1013"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MetricRangeInput
|
|
||||||
label={t('logs.event_sea_state')}
|
|
||||||
value={evSeaState}
|
|
||||||
onChange={setEvSeaState}
|
|
||||||
disabled={saving}
|
|
||||||
min={SEA_STATE_MIN}
|
|
||||||
max={SEA_STATE_MAX}
|
|
||||||
step={1}
|
|
||||||
defaultNumeric={0}
|
|
||||||
parse={parseSeaState}
|
|
||||||
format={formatSeaState}
|
|
||||||
formatDisplay={(level) =>
|
|
||||||
t('logs.weather_slider_sea_state', { value: level, defaultValue: `${level}` })}
|
|
||||||
numberMin={SEA_STATE_MIN}
|
|
||||||
numberMax={SEA_STATE_MAX}
|
|
||||||
numberStep={1}
|
|
||||||
numberPlaceholder="3"
|
|
||||||
allowLegacyText
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MetricRangeInput
|
|
||||||
label={t('logs.event_visibility')}
|
|
||||||
value={evVisibility}
|
|
||||||
onChange={setEvVisibility}
|
|
||||||
disabled={saving || weatherLoading}
|
|
||||||
discreteValues={VISIBILITY_STEPS_M}
|
|
||||||
defaultNumeric={10000}
|
|
||||||
parse={parseVisibilityMeters}
|
|
||||||
format={formatVisibilityMeters}
|
|
||||||
formatDisplay={(m) => formatVisibilityMeters(m)}
|
|
||||||
hideNumberInput
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MetricRangeInput
|
|
||||||
label={t('logs.event_heel')}
|
|
||||||
value={evHeel}
|
|
||||||
onChange={setEvHeel}
|
|
||||||
disabled={saving}
|
|
||||||
min={HEEL_MIN_DEG}
|
|
||||||
max={HEEL_MAX_DEG}
|
|
||||||
step={1}
|
|
||||||
defaultNumeric={0}
|
|
||||||
parse={parseHeelDeg}
|
|
||||||
format={formatHeelDeg}
|
|
||||||
formatDisplay={(deg) =>
|
|
||||||
t('logs.weather_slider_heel', { value: deg, defaultValue: `${deg}°` })}
|
|
||||||
numberMin={HEEL_MIN_DEG}
|
|
||||||
numberMax={HEEL_MAX_DEG}
|
|
||||||
numberStep={1}
|
|
||||||
numberPlaceholder="5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-grid mb-4">
|
|
||||||
<div className="input-group">
|
|
||||||
<label>{t('logs.event_sails')}</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="e.g. Mainsail + Jib"
|
|
||||||
className="input-text"
|
|
||||||
value={evSailsOrMotor}
|
|
||||||
onChange={(e) => setEvSailsOrMotor(e.target.value)}
|
|
||||||
disabled={saving}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group">
|
|
||||||
<label>{t('logs.event_distance')}</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="e.g. 12 nm"
|
|
||||||
className="input-text"
|
|
||||||
value={evDistance}
|
|
||||||
onChange={(e) => setEvDistance(e.target.value)}
|
|
||||||
disabled={saving}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
'sails-picker-container grid-span-2',
|
|
||||||
showSailsPickerToggle ? 'is-collapsible' : '',
|
|
||||||
showSailsPickerToggle && !sailsPickerExpanded ? 'is-collapsed' : '',
|
|
||||||
].filter(Boolean).join(' ')}
|
|
||||||
>
|
|
||||||
<div className="sails-picker-pills">
|
|
||||||
{isMotorActive && (
|
|
||||||
<span
|
|
||||||
className={`sail-pill motor-pill active`}
|
|
||||||
onClick={() => toggleSailOrMotor(motorPropulsionLabel)}
|
|
||||||
>
|
|
||||||
{motorPropulsionLabel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{sortedEventSailOptions.map((sail) => (
|
|
||||||
<span
|
|
||||||
key={sail}
|
|
||||||
className={`sail-pill ${isItemActive(sail) ? 'active' : ''}`}
|
|
||||||
onClick={() => toggleSailOrMotor(sail)}
|
|
||||||
>
|
|
||||||
{sail}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{!isMotorActive && (
|
|
||||||
<span
|
|
||||||
className="sail-pill motor-pill"
|
|
||||||
onClick={() => toggleSailOrMotor(motorPropulsionLabel)}
|
|
||||||
>
|
|
||||||
{motorPropulsionLabel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{showSailsPickerToggle && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="sails-picker-toggle"
|
|
||||||
onClick={() => setSailsPickerExpanded((prev) => !prev)}
|
|
||||||
aria-expanded={sailsPickerExpanded}
|
|
||||||
>
|
|
||||||
{sailsPickerExpanded ? (
|
|
||||||
<>
|
|
||||||
<ChevronUp size={14} aria-hidden="true" />
|
|
||||||
{t('logs.sails_picker_show_less')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ChevronDown size={14} aria-hidden="true" />
|
|
||||||
{t('logs.sails_picker_show_more')}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-grid weather-metrics-grid mb-4">
|
||||||
|
<div className="input-group course-dial-section weather-metrics-span-2">
|
||||||
|
<label>{t('logs.event_wind_direction')}</label>
|
||||||
|
<CourseDialInput
|
||||||
|
value={evWindDirection}
|
||||||
|
onChange={setEvWindDirection}
|
||||||
|
disabled={saving || weatherLoading}
|
||||||
|
allowCardinal
|
||||||
|
displayMode="auto"
|
||||||
|
aria-label={t('logs.event_wind_direction')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-group">
|
||||||
|
<label>{t('logs.event_wind_strength')}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. 4 Bft"
|
||||||
|
className="input-text"
|
||||||
|
value={evWindStrength}
|
||||||
|
onChange={(e) => setEvWindStrength(e.target.value)}
|
||||||
|
disabled={saving || weatherLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MetricRangeInput
|
||||||
|
label={t('logs.event_wind_pressure')}
|
||||||
|
value={evWindPressure}
|
||||||
|
onChange={setEvWindPressure}
|
||||||
|
disabled={saving || weatherLoading}
|
||||||
|
min={PRESSURE_MIN_HPA}
|
||||||
|
max={PRESSURE_MAX_HPA}
|
||||||
|
step={1}
|
||||||
|
defaultNumeric={PRESSURE_DEFAULT_HPA}
|
||||||
|
parse={parsePressureHpa}
|
||||||
|
format={formatPressureHpa}
|
||||||
|
formatDisplay={(hpa) =>
|
||||||
|
t('logs.weather_slider_pressure', { value: hpa, defaultValue: `${hpa} hPa` })}
|
||||||
|
numberMin={PRESSURE_MIN_HPA}
|
||||||
|
numberMax={PRESSURE_MAX_HPA}
|
||||||
|
numberStep={1}
|
||||||
|
numberPlaceholder="1013"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MetricRangeInput
|
||||||
|
label={t('logs.event_sea_state')}
|
||||||
|
value={evSeaState}
|
||||||
|
onChange={setEvSeaState}
|
||||||
|
disabled={saving}
|
||||||
|
min={SEA_STATE_MIN}
|
||||||
|
max={SEA_STATE_MAX}
|
||||||
|
step={1}
|
||||||
|
defaultNumeric={0}
|
||||||
|
parse={parseSeaState}
|
||||||
|
format={formatSeaState}
|
||||||
|
formatDisplay={(level) =>
|
||||||
|
t('logs.weather_slider_sea_state', { value: level, defaultValue: `${level}` })}
|
||||||
|
numberMin={SEA_STATE_MIN}
|
||||||
|
numberMax={SEA_STATE_MAX}
|
||||||
|
numberStep={1}
|
||||||
|
numberPlaceholder="3"
|
||||||
|
allowLegacyText
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MetricRangeInput
|
||||||
|
label={t('logs.event_visibility')}
|
||||||
|
value={evVisibility}
|
||||||
|
onChange={setEvVisibility}
|
||||||
|
disabled={saving || weatherLoading}
|
||||||
|
discreteValues={VISIBILITY_STEPS_M}
|
||||||
|
defaultNumeric={10000}
|
||||||
|
parse={parseVisibilityMeters}
|
||||||
|
format={formatVisibilityMeters}
|
||||||
|
formatDisplay={(m) => formatVisibilityMeters(m)}
|
||||||
|
hideNumberInput
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MetricRangeInput
|
||||||
|
label={t('logs.event_heel')}
|
||||||
|
value={evHeel}
|
||||||
|
onChange={setEvHeel}
|
||||||
|
disabled={saving}
|
||||||
|
min={HEEL_MIN_DEG}
|
||||||
|
max={HEEL_MAX_DEG}
|
||||||
|
step={1}
|
||||||
|
defaultNumeric={0}
|
||||||
|
parse={parseHeelDeg}
|
||||||
|
format={formatHeelDeg}
|
||||||
|
formatDisplay={(deg) =>
|
||||||
|
t('logs.weather_slider_heel', { value: deg, defaultValue: `${deg}°` })}
|
||||||
|
numberMin={HEEL_MIN_DEG}
|
||||||
|
numberMax={HEEL_MAX_DEG}
|
||||||
|
numberStep={1}
|
||||||
|
numberPlaceholder="5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-grid mb-4">
|
||||||
|
<div className="input-group">
|
||||||
|
<label>{t('logs.event_sails')}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. Mainsail + Jib"
|
||||||
|
className="input-text"
|
||||||
|
value={evSailsOrMotor}
|
||||||
|
onChange={(e) => setEvSailsOrMotor(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-group">
|
||||||
|
<label>{t('logs.event_distance')}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. 12 nm"
|
||||||
|
className="input-text"
|
||||||
|
value={evDistance}
|
||||||
|
onChange={(e) => setEvDistance(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
'sails-picker-container grid-span-2',
|
||||||
|
showSailsPickerToggle ? 'is-collapsible' : '',
|
||||||
|
showSailsPickerToggle && !sailsPickerExpanded ? 'is-collapsed' : '',
|
||||||
|
].filter(Boolean).join(' ')}
|
||||||
|
>
|
||||||
|
<div className="sails-picker-pills">
|
||||||
|
{isMotorActive && (
|
||||||
|
<span
|
||||||
|
className={`sail-pill motor-pill active`}
|
||||||
|
onClick={() => toggleSailOrMotor(motorPropulsionLabel)}
|
||||||
|
>
|
||||||
|
{motorPropulsionLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{sortedEventSailOptions.map((sail) => (
|
||||||
|
<span
|
||||||
|
key={sail}
|
||||||
|
className={`sail-pill ${isItemActive(sail) ? 'active' : ''}`}
|
||||||
|
onClick={() => toggleSailOrMotor(sail)}
|
||||||
|
>
|
||||||
|
{sail}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{!isMotorActive && (
|
||||||
|
<span
|
||||||
|
className="sail-pill motor-pill"
|
||||||
|
onClick={() => toggleSailOrMotor(motorPropulsionLabel)}
|
||||||
|
>
|
||||||
|
{motorPropulsionLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showSailsPickerToggle && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sails-picker-toggle"
|
||||||
|
onClick={() => setSailsPickerExpanded((prev) => !prev)}
|
||||||
|
aria-expanded={sailsPickerExpanded}
|
||||||
|
>
|
||||||
|
{sailsPickerExpanded ? (
|
||||||
|
<>
|
||||||
|
<ChevronUp size={14} aria-hidden="true" />
|
||||||
|
{t('logs.sails_picker_show_less')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ChevronDown size={14} aria-hidden="true" />
|
||||||
|
{t('logs.sails_picker_show_more')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-group grid-span-2">
|
||||||
|
<label>{t('logs.event_remarks')}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Remarks"
|
||||||
|
className="input-text"
|
||||||
|
value={evRemarks}
|
||||||
|
onChange={(e) => setEvRemarks(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto', flexWrap: 'wrap' }}>
|
||||||
|
{editingEventIndex !== null && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn secondary"
|
||||||
|
onClick={handleCancelEventEdit}
|
||||||
|
disabled={saving}
|
||||||
|
style={{ width: 'auto', padding: '10px 20px', display: 'flex' }}
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
{t('logs.cancel_event_edit')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn secondary"
|
||||||
|
onClick={handleSaveEvent}
|
||||||
|
disabled={saving || !isValidTimeHHMM(evTime)}
|
||||||
|
style={{ width: 'auto', padding: '10px 20px', display: 'flex' }}
|
||||||
|
>
|
||||||
|
{editingEventIndex !== null ? <Save size={16} /> : <Plus size={16} />}
|
||||||
|
{editingEventIndex !== null ? t('logs.save_event_btn') : t('logs.add_event_btn')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="input-group grid-span-2">
|
|
||||||
<label>{t('logs.event_remarks')}</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Remarks"
|
|
||||||
className="input-text"
|
|
||||||
value={evRemarks}
|
|
||||||
onChange={(e) => setEvRemarks(e.target.value)}
|
|
||||||
disabled={saving}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto', flexWrap: 'wrap' }}>
|
|
||||||
{editingEventIndex !== null && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn secondary"
|
|
||||||
onClick={handleCancelEventEdit}
|
|
||||||
disabled={saving}
|
|
||||||
style={{ width: 'auto', padding: '10px 20px', display: 'flex' }}
|
|
||||||
>
|
|
||||||
<X size={16} />
|
|
||||||
{t('logs.cancel_event_edit')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn secondary"
|
|
||||||
onClick={handleSaveEvent}
|
|
||||||
disabled={saving || !isValidTimeHHMM(evTime)}
|
|
||||||
style={{ width: 'auto', padding: '10px 20px', display: 'flex' }}
|
|
||||||
>
|
|
||||||
{editingEventIndex !== null ? <Save size={16} /> : <Plus size={16} />}
|
|
||||||
{editingEventIndex !== null ? t('logs.save_event_btn') : t('logs.add_event_btn')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Track file upload */}
|
{/* Track file upload */}
|
||||||
<div className="form-card" data-tour="entry-track">
|
<div className="form-card" data-tour="entry-track">
|
||||||
|
|||||||
Reference in New Issue
Block a user