feat(logs): Ereignis-Uhrzeit vorbelegen und 24h-Format vereinheitlichen

Neue Ereignisse starten mit der aktuellen Uhrzeit; Datums-/Zeitanzeigen und Zeit-Picker nutzen durchgängig das 24-Stunden-Format.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 10:57:47 +02:00
parent dee2f7b95b
commit 658bc6c0c9
9 changed files with 69 additions and 16 deletions
+5 -3
View File
@@ -22,7 +22,8 @@ import {
hasAnySignature
} from '../utils/signatures.js'
import type { SignatureValue } from '../types/signatures.js'
import { buildLogEntryPayload, sortLogEventsByTime, normalizeLogEvent, logEventsEqual, type LogEventPayload } from '../utils/logEntryPayload.js'
import { buildLogEntryPayload, sortLogEventsByTime, normalizeLogEvent, logEventsEqual, currentLocalTimeHHMM, type LogEventPayload } from '../utils/logEntryPayload.js'
import { resolveIntlLocale } from '../utils/dateTimeFormat.js'
import { hashEntryForSigning } from '../utils/entryCanonicalHash.js'
import { signLogEntry } from '../services/entrySigning.js'
import { getLogbookAccess } from '../services/logbookAccess.js'
@@ -162,7 +163,7 @@ export default function LogEntryEditor({
const [events, setEvents] = useState<LogEvent[]>([])
// Add Event Form State
const [evTime, setEvTime] = useState('')
const [evTime, setEvTime] = useState(() => currentLocalTimeHHMM())
const [evMgk, setEvMgk] = useState('')
const [evRwk, setEvRwk] = useState('')
const [evWindPressure, setEvWindPressure] = useState('')
@@ -865,7 +866,7 @@ export default function LogEntryEditor({
}
const clearEventForm = () => {
setEvTime('')
setEvTime(currentLocalTimeHHMM())
setEvMgk('')
setEvRwk('')
setEvWindPressure('')
@@ -1371,6 +1372,7 @@ export default function LogEntryEditor({
<input
type="time"
className="input-text"
lang={resolveIntlLocale(i18n.language)}
value={evTime}
onChange={(e) => setEvTime(e.target.value)}
disabled={saving}
+3 -2
View File
@@ -12,6 +12,7 @@ import {
type LogbookBackupPreview
} from '../services/logbookBackup.js'
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
import { formatAppDateTime } from '../utils/dateTimeFormat.js'
interface LogbookBackupPanelProps {
logbookId: string
@@ -41,7 +42,7 @@ function mapBackupError(code: string, t: (key: string) => string): string {
}
export default function LogbookBackupPanel({ logbookId, onRestored }: LogbookBackupPanelProps) {
const { t } = useTranslation()
const { t, i18n } = useTranslation()
const { showConfirm } = useDialog()
const fileInputRef = useRef<HTMLInputElement>(null)
@@ -334,7 +335,7 @@ export default function LogbookBackupPanel({ logbookId, onRestored }: LogbookBac
</ul>
<p className="text-muted backup-preview-date">
{t('settings.backup_exported_at', {
date: new Date(importPreview.exportedAt).toLocaleString()
date: formatAppDateTime(importPreview.exportedAt, i18n.language)
})}
</p>
</div>
+2 -3
View File
@@ -2,6 +2,7 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Fingerprint, Loader2, AlertTriangle } from 'lucide-react'
import type { PasskeySignature } from '../types/signatures.js'
import { formatAppDateTime } from '../utils/dateTimeFormat.js'
interface PasskeySignButtonProps {
label: string
@@ -42,9 +43,7 @@ export default function PasskeySignButton({
}
}
const formattedDate = signature
? new Date(signature.signedAt).toLocaleString(i18n.language === 'de' ? 'de-DE' : 'en-GB')
: ''
const formattedDate = signature ? formatAppDateTime(signature.signedAt, i18n.language) : ''
return (
<div className="passkey-sign-block">
+2 -3
View File
@@ -5,6 +5,7 @@ import SignaturePad from './SignaturePad.tsx'
import PasskeySignButton from './PasskeySignButton.tsx'
import type { PasskeySignature, SignatureValue } from '../types/signatures.js'
import { isPasskeySignature, getSignaturePayload, getSignatureAttribution } from '../utils/signatures.js'
import { formatAppDateTime } from '../utils/dateTimeFormat.js'
type SignatureMode = 'passkey' | 'classic'
@@ -30,9 +31,7 @@ function SignerAttributionBadge({ value }: { value: SignatureValue | '' }) {
const attribution = getSignatureAttribution(value)
if (!attribution) return null
const formattedDate = new Date(attribution.signedAt).toLocaleString(
i18n.language === 'de' ? 'de-DE' : 'en-GB'
)
const formattedDate = formatAppDateTime(attribution.signedAt, i18n.language)
return (
<div className="passkey-sign-badge valid signature-attribution-badge">
+3 -2
View File
@@ -5,6 +5,7 @@ import { decryptJson } from './crypto.js'
import { formatSignatureForExport, normalizeSignature } from '../utils/signatures.js'
import { sortLogEventsByTime } from '../utils/logEntryPayload.js'
import i18n from '../i18n/index.js'
import { formatAppDateTime } from '../utils/dateTimeFormat.js'
function escapeCsvValue(val: string | number | undefined | null): string {
if (val === null || val === undefined) return '';
@@ -94,11 +95,11 @@ export async function exportLogbookToCsv(logbookId: string, preloadedData?: { ya
const exportLabels = {
imagePlaceholder: i18n.t('logs.sign_export_image'),
passkeyLabel: (username: string, signedAt: string) => {
const date = new Date(signedAt).toLocaleString(i18n.language === 'de' ? 'de-DE' : 'en-GB')
const date = formatAppDateTime(signedAt, i18n.language)
return i18n.t('logs.sign_passkey_export', { username, date })
},
attributionLabel: (username: string, signedAt: string) => {
const date = new Date(signedAt).toLocaleString(i18n.language === 'de' ? 'de-DE' : 'en-GB')
const date = formatAppDateTime(signedAt, i18n.language)
return i18n.t('logs.sign_attribution_export', { username, date })
}
};
+2 -2
View File
@@ -6,10 +6,10 @@ import { decryptJson } from './crypto.js'
import { isSignatureImage, isPasskeySignature, isClassicSignature, getSignaturePayload } from '../utils/signatures.js'
import { sortLogEventsByTime } from '../utils/logEntryPayload.js'
import i18n from '../i18n/index.js'
import { formatAppDateTime } from '../utils/dateTimeFormat.js'
function formatPasskeySignDate(signedAt: string): string {
const locale = i18n.language === 'de' ? 'de-DE' : 'en-GB'
return new Date(signedAt).toLocaleString(locale)
return formatAppDateTime(signedAt, i18n.language)
}
export async function generateLogbookPagePdf(logbookId: string, entryId: string, preloadedData?: { yacht: any; entry: any }): Promise<jsPDF> {
+43
View File
@@ -0,0 +1,43 @@
/** BCP 47 locales that use 24-hour clock for Intl and native pickers. */
export function resolveIntlLocale(language?: string): string {
const lng = (language ?? 'en').toLowerCase()
return lng.startsWith('de') ? 'de-DE' : 'en-GB'
}
/** `lang` for `<html>` and `<input type="time">` (24h-friendly). */
export function resolveDocumentLang(language?: string): string {
const lng = (language ?? 'en').toLowerCase()
return lng.startsWith('de') ? 'de' : 'en-GB'
}
const APP_DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}
const APP_TIME_OPTIONS: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
hour12: false
}
function toDate(value: Date | string | number): Date | null {
const date = value instanceof Date ? value : new Date(value)
return Number.isNaN(date.getTime()) ? null : date
}
export function formatAppDateTime(value: Date | string | number, language?: string): string {
const date = toDate(value)
if (!date) return String(value)
return date.toLocaleString(resolveIntlLocale(language), APP_DATE_TIME_OPTIONS)
}
export function formatAppTime(value: Date | string | number, language?: string): string {
const date = toDate(value)
if (!date) return String(value)
return date.toLocaleTimeString(resolveIntlLocale(language), APP_TIME_OPTIONS)
}
+7
View File
@@ -17,6 +17,13 @@ export interface LogEventPayload {
remarks: string
}
/** Local time as HH:MM for HTML `<input type="time">`. */
export function currentLocalTimeHHMM(date: Date = new Date()): string {
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
}
const LOG_EVENT_FIELDS: (keyof LogEventPayload)[] = [
'time', 'mgk', 'rwk', 'windPressure', 'windDirection', 'windStrength', 'seaState',
'weatherIcon', 'current', 'heel', 'sailsOrMotor', 'logReading', 'distance',
+2 -1
View File
@@ -1,4 +1,5 @@
import type { i18n as I18nInstance } from 'i18next'
import { resolveDocumentLang } from './dateTimeFormat.js'
const SITE_ORIGIN = 'https://kapteins-daagbok.eu'
@@ -34,7 +35,7 @@ export function updatePageSeo(lng?: string) {
if (!i18nRef?.isInitialized) return
const lang = normalizeSeoLang(lng ?? i18nRef.language)
document.documentElement.lang = lang
document.documentElement.lang = resolveDocumentLang(lang)
const title = i18nRef.t('seo.title')
document.title = title