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:
@@ -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}
|
||||
|
||||
@@ -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,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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user