refactor(live-log): Position-Terminologie und Modal-UX vereinheitlichen

Fix/Standort heißen überall Position (__live:position, Legacy __live:fix).
Nachfüll-Buttons + Diesel/+ Wasser, Abbruch statt Nein in Live-Modals.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 17:42:08 +02:00
co-authored by Cursor
parent 1bc449687d
commit e014e997de
15 changed files with 225 additions and 199 deletions
+2 -2
View File
@@ -216,7 +216,7 @@ export default function LiveCameraCapture({
className="btn secondary live-camera-close"
onClick={onClose}
disabled={busy}
aria-label={t('logs.confirm_no')}
aria-label={t('logs.live_cancel')}
>
<X size={18} />
</button>
@@ -287,7 +287,7 @@ export default function LiveCameraCapture({
<div className="live-log-modal-actions live-camera-actions">
<button type="button" className="btn secondary" onClick={onClose} disabled={busy}>
{t('logs.confirm_no')}
{t('logs.live_cancel')}
</button>
{showPreview ? (
+72 -72
View File
@@ -33,8 +33,8 @@ import {
import { formatEventSummary } from '../utils/formatEventSummary.js'
import {
getLastAutoPositionMs,
getLastPositionFixWithin,
getLatestPositionFix,
getLastLoggedPositionWithin,
getLatestLoggedPosition,
isMotorRunningFromEvents,
LIVE_EVENT_CODES,
LIVE_LOG_WEATHER_POSITION_MAX_AGE_MS,
@@ -96,7 +96,7 @@ type LiveModal =
| 'water'
| 'sog'
| 'stw'
| 'fix'
| 'position'
| 'photo'
| 'voice'
@@ -167,10 +167,10 @@ export default function LiveLogView({
const [valueInputSecondary, setValueInputSecondary] = useState('')
const [selectedSails, setSelectedSails] = useState<string[]>([])
const [undoVisible, setUndoVisible] = useState(false)
const [fixLat, setFixLat] = useState('')
const [fixLng, setFixLng] = useState('')
const [fixGpsLoading, setFixGpsLoading] = useState(false)
const [fixGpsUnavailable, setFixGpsUnavailable] = useState(false)
const [positionLat, setPositionLat] = useState('')
const [positionLng, setPositionLng] = useState('')
const [positionGpsLoading, setPositionGpsLoading] = useState(false)
const [positionGpsUnavailable, setPositionGpsUnavailable] = useState(false)
const [photoCaption, setPhotoCaption] = useState('')
const [photoSaving, setPhotoSaving] = useState(false)
const [voiceCaption, setVoiceCaption] = useState('')
@@ -202,8 +202,8 @@ export default function LiveLogView({
)
const motorRunning = isMotorRunningFromEvents(events)
const motorLabel = t('logs.motor_propulsion')
const hasPositionFix = useMemo(
() => (date ? getLatestPositionFix(events, date) != null : false),
const hasLoggedPosition = useMemo(
() => (date ? getLatestLoggedPosition(events, date) != null : false),
[events, date]
)
const voiceMemoLookup = useEntryVoiceMemos(logbookId, entryId)
@@ -358,7 +358,7 @@ export default function LiveLogView({
})
await refreshEntry(entryId)
} catch {
// Best-effort; hint banner shows when no position fix exists yet.
// Best-effort; hint banner shows when no position has been logged yet.
} finally {
autoPositionBusyRef.current = false
}
@@ -453,16 +453,16 @@ export default function LiveLogView({
}, 'moor')
}
const openFixModal = async () => {
setFixLat('')
setFixLng('')
setFixGpsUnavailable(false)
setFixGpsLoading(true)
setModal('fix')
const openPositionModal = async () => {
setPositionLat('')
setPositionLng('')
setPositionGpsUnavailable(false)
setPositionGpsLoading(true)
setModal('position')
try {
const permission = await queryGeolocationPermission()
if (permission !== 'granted') {
setFixGpsUnavailable(true)
setPositionGpsUnavailable(true)
return
}
const coords = await getCurrentPosition({
@@ -470,25 +470,25 @@ export default function LiveLogView({
enableHighAccuracy: false,
maximumAge: 60_000
})
setFixLat(coords.lat)
setFixLng(coords.lng)
setPositionLat(coords.lat)
setPositionLng(coords.lng)
} catch {
setFixGpsUnavailable(true)
setPositionGpsUnavailable(true)
} finally {
setFixGpsLoading(false)
setPositionGpsLoading(false)
}
}
const retryFixGps = async () => {
setFixGpsLoading(true)
setFixGpsUnavailable(false)
const retryPositionGps = async () => {
setPositionGpsLoading(true)
setPositionGpsUnavailable(false)
try {
const permission = await queryGeolocationPermission()
if (permission !== 'granted') {
setFixGpsUnavailable(true)
setPositionGpsUnavailable(true)
await showAlert(
`${t('logs.live_gps_error')}\n\n${t('logs.live_gps_start_hint')}`,
t('logs.live_fix')
t('logs.live_position')
)
return
}
@@ -497,23 +497,23 @@ export default function LiveLogView({
enableHighAccuracy: false,
maximumAge: 60_000
})
setFixLat(coords.lat)
setFixLng(coords.lng)
setPositionLat(coords.lat)
setPositionLng(coords.lng)
} catch {
setFixGpsUnavailable(true)
setPositionGpsUnavailable(true)
await showAlert(
`${t('logs.live_gps_error')}\n\n${t('logs.live_gps_start_hint')}`,
t('logs.live_fix')
t('logs.live_position')
)
} finally {
setFixGpsLoading(false)
setPositionGpsLoading(false)
}
}
const confirmFix = () => {
const coords = normalizeGpsCoordinates(fixLat, fixLng)
const confirmPosition = () => {
const coords = normalizeGpsCoordinates(positionLat, positionLng)
if (!coords) {
void showAlert(t('logs.live_fix_invalid'), t('logs.live_fix'))
void showAlert(t('logs.live_position_invalid'), t('logs.live_position'))
return
}
setModal('none')
@@ -522,9 +522,9 @@ export default function LiveLogView({
await appendQuickEvent(logbookId, entryId, {
gpsLat: coords.lat,
gpsLng: coords.lng,
remarks: LIVE_EVENT_CODES.FIX
remarks: LIVE_EVENT_CODES.POSITION
})
}, 'fix')
}, 'position')
}
const handleFetchOwmWeather = () => {
@@ -534,17 +534,17 @@ export default function LiveLogView({
return
}
const position = getLastPositionFixWithin(
const position = getLastLoggedPositionWithin(
events,
date,
LIVE_LOG_WEATHER_POSITION_MAX_AGE_MS
)
if (!position) {
const latest = getLatestPositionFix(events, date)
const latest = getLatestLoggedPosition(events, date)
void showAlert(
latest
? t('logs.live_weather_fix_stale')
: t('logs.live_weather_fix_required'),
? t('logs.live_weather_position_stale')
: t('logs.live_weather_position_required'),
t('logs.live_weather_owm_btn')
)
return
@@ -945,7 +945,7 @@ export default function LiveLogView({
{error && <div className="auth-error mb-4">{error}</div>}
{!hasPositionFix && (
{!hasLoggedPosition && (
<p className="live-log-gps-hint" role="status">
<MapPin size={16} aria-hidden />
{t('logs.live_gps_start_hint')}
@@ -1036,9 +1036,9 @@ export default function LiveLogView({
)}
</div>
<button type="button" className="live-log-action-btn" onClick={() => void openFixModal()} disabled={busy}>
<button type="button" className="live-log-action-btn" onClick={() => void openPositionModal()} disabled={busy}>
<MapPin size={18} />
{t('logs.live_fix')}
{t('logs.live_position')}
</button>
<button type="button" className="live-log-action-btn" onClick={() => { setCommentText(''); setModal('comment') }} disabled={busy}>
<MessageSquare size={18} />
@@ -1145,7 +1145,7 @@ export default function LiveLogView({
</p>
)}
<div className="live-log-modal-actions">
<button type="button" className="btn secondary" onClick={closeModal}>{t('logs.confirm_no')}</button>
<button type="button" className="btn secondary" onClick={closeModal}>{t('logs.live_cancel')}</button>
<button
type="button"
className="btn primary"
@@ -1161,68 +1161,68 @@ export default function LiveLogView({
</div>
)}
{modal === 'fix' && (
{modal === 'position' && (
<div
className="live-log-modal-backdrop"
onClick={(e) => { if (e.target === e.currentTarget) closeModal() }}
>
<div className="live-log-modal" onClick={(e) => e.stopPropagation()}>
<h3>{t('logs.live_fix')}</h3>
{fixGpsUnavailable && (
<h3>{t('logs.live_position')}</h3>
{positionGpsUnavailable && (
<>
<p className="live-log-modal-hint live-log-gps-hint-modal">{t('logs.live_gps_start_hint')}</p>
<p className="live-log-modal-hint">{t('logs.live_fix_manual_hint')}</p>
<p className="live-log-modal-hint">{t('logs.live_position_manual_hint')}</p>
</>
)}
<fieldset className="live-log-fix-coords" disabled={busy}>
<legend className="live-log-fix-label">{t('logs.event_gps')}</legend>
<div className="live-log-fix-coords-row">
<label className="live-log-fix-field">
<span className="live-log-fix-field-label">{t('logs.live_fix_lat_placeholder')}</span>
<fieldset className="live-log-position-coords" disabled={busy}>
<legend className="live-log-position-label">{t('logs.event_gps')}</legend>
<div className="live-log-position-coords-row">
<label className="live-log-position-field">
<span className="live-log-position-field-label">{t('logs.live_position_lat_placeholder')}</span>
<input
type="text"
inputMode="decimal"
className="input-text"
placeholder="54.123456"
value={fixLat}
onChange={(e) => setFixLat(e.target.value)}
value={positionLat}
onChange={(e) => setPositionLat(e.target.value)}
autoFocus
/>
</label>
<label className="live-log-fix-field">
<span className="live-log-fix-field-label">{t('logs.live_fix_lng_placeholder')}</span>
<label className="live-log-position-field">
<span className="live-log-position-field-label">{t('logs.live_position_lng_placeholder')}</span>
<input
type="text"
inputMode="decimal"
className="input-text"
placeholder="10.654321"
value={fixLng}
onChange={(e) => setFixLng(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') confirmFix() }}
value={positionLng}
onChange={(e) => setPositionLng(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') confirmPosition() }}
/>
</label>
</div>
<div className="live-log-fix-gps-row">
<div className="live-log-position-gps-row">
<button
type="button"
className="btn secondary live-log-fix-gps-btn"
onClick={() => void retryFixGps()}
className="btn secondary live-log-position-gps-btn"
onClick={() => void retryPositionGps()}
title={t('logs.gps_btn')}
disabled={fixGpsLoading}
disabled={positionGpsLoading}
aria-label={t('logs.gps_btn')}
>
<MapPin size={16} />
<span>{fixGpsLoading ? t('logs.live_fix_gps_loading') : t('logs.gps_btn')}</span>
<span>{positionGpsLoading ? t('logs.live_position_gps_loading') : t('logs.gps_btn')}</span>
</button>
</div>
</fieldset>
<div className="live-log-modal-actions">
<button type="button" className="btn secondary" onClick={closeModal}>{t('logs.confirm_no')}</button>
<button type="button" className="btn secondary" onClick={closeModal}>{t('logs.live_cancel')}</button>
<button
type="button"
className="btn primary"
onClick={confirmFix}
disabled={busy || !normalizeGpsCoordinates(fixLat, fixLng)}
onClick={confirmPosition}
disabled={busy || !normalizeGpsCoordinates(positionLat, positionLng)}
>
{t('logs.live_sails_confirm')}
</button>
@@ -1237,7 +1237,7 @@ export default function LiveLogView({
<h3>{t('logs.live_comment_btn')}</h3>
<input type="text" className="input-text" value={commentText} onChange={(e) => setCommentText(e.target.value)} placeholder={t('logs.live_comment_placeholder')} autoFocus onKeyDown={(e) => { if (e.key === 'Enter') confirmComment() }} />
<div className="live-log-modal-actions">
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.confirm_no')}</button>
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.live_cancel')}</button>
<button type="button" className="btn primary" onClick={confirmComment} disabled={!commentText.trim()}>{t('logs.live_comment_confirm')}</button>
</div>
</div>
@@ -1271,7 +1271,7 @@ export default function LiveLogView({
/>
</div>
<div className="live-log-modal-actions">
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.confirm_no')}</button>
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.live_cancel')}</button>
<button type="button" className="btn primary" onClick={confirmValueModal}>{t('logs.live_sails_confirm')}</button>
</div>
</div>
@@ -1293,7 +1293,7 @@ export default function LiveLogView({
/>
</div>
<div className="live-log-modal-actions">
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.confirm_no')}</button>
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.live_cancel')}</button>
<button type="button" className="btn primary" onClick={confirmValueModal}>{t('logs.live_sails_confirm')}</button>
</div>
</div>
@@ -1338,7 +1338,7 @@ export default function LiveLogView({
onKeyDown={(e) => { if (e.key === 'Enter') confirmValueModal() }}
/>
<div className="live-log-modal-actions">
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.confirm_no')}</button>
<button type="button" className="btn secondary" onClick={() => setModal('none')}>{t('logs.live_cancel')}</button>
<button type="button" className="btn primary" onClick={confirmValueModal}>{t('logs.live_sails_confirm')}</button>
</div>
</div>
+1 -1
View File
@@ -199,7 +199,7 @@ export default function LiveVoiceCapture({
className="btn-icon"
onClick={onClose}
disabled={busy || saving || phase === 'recording'}
aria-label={t('logs.confirm_no')}
aria-label={t('logs.live_cancel')}
>
<X size={18} />
</button>