diff --git a/components/calendar/calendar-sidebar-panel.tsx b/components/calendar/calendar-sidebar-panel.tsx index 07ad8479..5e067970 100644 --- a/components/calendar/calendar-sidebar-panel.tsx +++ b/components/calendar/calendar-sidebar-panel.tsx @@ -3,10 +3,11 @@ import { useState, useRef, useEffect } from "react"; import { useTranslations } from "next-intl"; import { Globe, Plus, RefreshCw, Trash2 } from "lucide-react"; -import { cn } from "@/lib/utils"; +import { cn, formatDateTime } from "@/lib/utils"; import type { Calendar } from "@/lib/jmap/types"; import { CalendarColorPicker } from "@/components/settings/calendar-management-settings"; import { useCalendarStore } from "@/stores/calendar-store"; +import { useSettingsStore } from "@/stores/settings-store"; import { toast } from "@/stores/toast-store"; import type { JMAPClient } from "@/lib/jmap/client"; @@ -33,6 +34,7 @@ export function CalendarSidebarPanel({ const icalSubscriptions = useCalendarStore((s) => s.icalSubscriptions); const refreshICalSubscription = useCalendarStore((s) => s.refreshICalSubscription); const removeICalSubscription = useCalendarStore((s) => s.removeICalSubscription); + const timeFormat = useSettingsStore((s) => s.timeFormat); const [colorPickerId, setColorPickerId] = useState(null); const [contextMenuCalId, setContextMenuCalId] = useState(null); @@ -169,7 +171,7 @@ export function CalendarSidebarPanel({ {sub.lastRefreshed && (
- {tSub('last_refreshed', { time: new Date(sub.lastRefreshed).toLocaleString() })} + {tSub('last_refreshed', { time: formatDateTime(sub.lastRefreshed, timeFormat, { month: 'short', day: 'numeric', year: 'numeric' }) })}
)} diff --git a/components/calendar/event-card.tsx b/components/calendar/event-card.tsx index 9dc5051c..e45bfa93 100644 --- a/components/calendar/event-card.tsx +++ b/components/calendar/event-card.tsx @@ -7,6 +7,7 @@ import type { CalendarEvent, Calendar } from "@/lib/jmap/types"; import { format, parseISO } from "date-fns"; import { Users } from "lucide-react"; import { getParticipantCount } from "@/lib/calendar-participants"; +import { useSettingsStore } from "@/stores/settings-store"; interface EventCardProps { event: CalendarEvent; @@ -68,11 +69,13 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM const [isBeingDragged, setIsBeingDragged] = useState(false); const color = getEventColor(event, calendar); const startDate = parseISO(event.start); + const timeFormat = useSettingsStore((state) => state.timeFormat); + const timeFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm"; const calendarName = calendar?.name || ""; const durationMinutes = parseDuration(event.duration); const endTime = new Date(startDate.getTime() + durationMinutes * 60000); - const timeString = `${format(startDate, "HH:mm")} – ${format(endTime, "HH:mm")}`; + const timeString = `${format(startDate, timeFmt)} – ${format(endTime, timeFmt)}`; const ariaLabel = `${event.title || t("events.no_title")}, ${timeString}${calendarName ? `, ${calendarName}` : ""}`; const handleDragStart = useCallback((e: DragEvent) => { diff --git a/components/calendar/event-modal.tsx b/components/calendar/event-modal.tsx index 748f0262..a99ec146 100644 --- a/components/calendar/event-modal.tsx +++ b/components/calendar/event-modal.tsx @@ -18,6 +18,7 @@ import { getStatusCounts, buildParticipantMap, } from "@/lib/calendar-participants"; +import { useSettingsStore } from "@/stores/settings-store"; interface EventModalProps { event?: CalendarEvent | null; @@ -108,6 +109,8 @@ export function EventModal({ isMobile = false, }: EventModalProps) { const t = useTranslations("calendar"); + const timeFormat = useSettingsStore((s) => s.timeFormat); + const timeDisplayFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm"; const isEdit = !!event; const [mode, setMode] = useState<"view" | "edit">(isEdit ? "view" : "edit"); @@ -452,7 +455,7 @@ export function EventModal({ {format(startD, "EEE, MMM d, yyyy")} {!event.showWithoutTime && ( - {format(startD, "HH:mm")} – {format(endD, "HH:mm")} + {format(startD, timeDisplayFmt)} – {format(endD, timeDisplayFmt)} )} @@ -576,7 +579,7 @@ export function EventModal({ {t("events.all_day")} ) : (
- {format(startD, "HH:mm")} – {format(endD, "HH:mm")} + {format(startD, timeDisplayFmt)} – {format(endD, timeDisplayFmt)} ({formatDurationDisplay(durMin)})
)} diff --git a/components/calendar/ical-import-modal.tsx b/components/calendar/ical-import-modal.tsx index 3ec240ed..3429b7c5 100644 --- a/components/calendar/ical-import-modal.tsx +++ b/components/calendar/ical-import-modal.tsx @@ -8,6 +8,7 @@ import { format, parseISO } from "date-fns"; import type { CalendarEvent, Calendar } from "@/lib/jmap/types"; import type { JMAPClient } from "@/lib/jmap/client"; import { useCalendarStore } from "@/stores/calendar-store"; +import { useSettingsStore } from "@/stores/settings-store"; import { toast } from "@/stores/toast-store"; interface ICalImportModalProps { @@ -28,6 +29,7 @@ export function ICalImportModal({ calendars, client, onClose }: ICalImportModalP const tCommon = useTranslations("common"); const tForm = useTranslations("calendar.form"); const importEvents = useCalendarStore((s) => s.importEvents); + const timeFormat = useSettingsStore((s) => s.timeFormat); const [step, setStep] = useState("select"); const [parsedEvents, setParsedEvents] = useState[]>([]); @@ -194,9 +196,10 @@ export function ICalImportModal({ calendars, client, onClose }: ICalImportModalP if (!event.start) return ""; try { const date = parseISO(event.start); + const timeFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm"; return event.showWithoutTime ? format(date, "MMM d, yyyy") - : format(date, "MMM d, yyyy HH:mm"); + : format(date, `MMM d, yyyy ${timeFmt}`); } catch { return event.start; } diff --git a/components/email/calendar-invitation-banner.tsx b/components/email/calendar-invitation-banner.tsx index 84f7f70b..43ac924d 100644 --- a/components/email/calendar-invitation-banner.tsx +++ b/components/email/calendar-invitation-banner.tsx @@ -360,6 +360,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp const client = useAuthStore((s) => s.client); const currentUserEmail = useAuthStore((s) => s.primaryIdentity?.email); const calendarInvitationParsingEnabled = useSettingsStore((s) => s.calendarInvitationParsingEnabled); + const timeFormat = useSettingsStore((s) => s.timeFormat); const { calendars, supportsCalendar, importEvents, rsvpEvent, updateEvent, events: storeEvents, setSelectedDate } = useCalendarStore(); const [state, setState] = useState('loading'); @@ -679,6 +680,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp day: 'numeric', hour: 'numeric', minute: '2-digit', + hour12: timeFormat === '12h', }); }; diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 8185cde3..cd7e090d 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -6,7 +6,7 @@ import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { X, Paperclip, Send, Save, Check, Loader2, AlertCircle, FileText, BookmarkPlus, ShieldCheck, Lock } from "lucide-react"; -import { cn, formatFileSize } from "@/lib/utils"; +import { cn, formatFileSize, formatDateTime } from "@/lib/utils"; import { debug } from "@/lib/debug"; import { toast } from "@/stores/toast-store"; import { sanitizeEmailHtml } from "@/lib/email-sanitization"; @@ -14,6 +14,7 @@ import { useAuthStore } from "@/stores/auth-store"; import { useIdentityStore } from "@/stores/identity-store"; import { useSmimeStore } from "@/stores/smime-store"; import { useEmailStore } from "@/stores/email-store"; +import { useSettingsStore } from "@/stores/settings-store"; import { buildMimeMessage, wrapCmsAsSmimeMessage } from "@/lib/smime/mime-builder"; import type { MimeAttachment } from "@/lib/smime/mime-builder"; import { smimeSign } from "@/lib/smime/smime-sign"; @@ -86,6 +87,7 @@ export function EmailComposer({ }: EmailComposerProps) { const t = useTranslations('email_composer'); const tCommon = useTranslations('common'); + const timeFormat = useSettingsStore((state) => state.timeFormat); // Initialize with reply/forward data if provided const getInitialTo = () => { @@ -124,7 +126,7 @@ export function EmailComposer({ const prefix = initialDraftText || ""; if (!replyTo?.body && !replyTo?.htmlBody) return prefix; - const date = replyTo.receivedAt ? new Date(replyTo.receivedAt).toLocaleString() : ""; + const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : ""; const from = replyTo.from?.[0]; const fromStr = from ? `${from.name || from.email}` : tCommon('unknown'); @@ -669,7 +671,7 @@ export function EmailComposer({ const signatureHtml = currentIdentity?.textSignature ? `

--
${currentIdentity.textSignature.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
')}` : ''; - const date = replyTo.receivedAt ? new Date(replyTo.receivedAt).toLocaleString() : ''; + const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : ''; const fromAddr = replyTo.from?.[0]; const fromStr = fromAddr ? `${fromAddr.name || fromAddr.email}` : tCommon('unknown'); const quoteHeader = mode === 'forward' @@ -1092,7 +1094,7 @@ export function EmailComposer({
{mode === 'forward' ? `---------- ${t('prefix.forward')} ----------` - : `${replyTo.receivedAt ? new Date(replyTo.receivedAt).toLocaleString() : ''}, ${replyTo.from?.[0]?.name || replyTo.from?.[0]?.email || tCommon('unknown')}:` + : `${replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : ''}, ${replyTo.from?.[0]?.name || replyTo.from?.[0]?.email || tCommon('unknown')}:` }
state.toolbarPosition); const showToolbarLabels = useSettingsStore((state) => state.showToolbarLabels); const calendarInvitationParsingEnabled = useSettingsStore((state) => state.calendarInvitationParsingEnabled); + const timeFormat = useSettingsStore((state) => state.timeFormat); // Detect if current mailbox is Junk folder const isInJunkFolder = currentMailboxRole === 'junk'; @@ -2224,7 +2225,7 @@ export function EmailViewer({ const handlePrint = () => { if (!email) return; const printSender = email.from?.[0]; - const date = email.sentAt ? new Date(email.sentAt).toLocaleString() : ''; + const date = email.sentAt ? formatDateTime(email.sentAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : ''; const toList = email.to?.map(r => r.name ? `${r.name} <${r.email}>` : r.email).join(', ') || ''; const ccList = email.cc?.map(r => r.name ? `${r.name} <${r.email}>` : r.email).join(', ') || ''; @@ -2976,14 +2977,7 @@ export function EmailViewer({
- {new Date(email.receivedAt).toLocaleString('en-US', { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - })} + {formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })} {isImportant && ( @@ -3458,14 +3452,7 @@ export function EmailViewer({ {/* Date and size on the right */}
- {new Date(email.receivedAt).toLocaleString('en-US', { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - })} + {formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}
{email.size > 0 && (
@@ -3596,16 +3583,7 @@ export function EmailViewer({
{t('date')}: - {new Date(email.receivedAt).toLocaleString('en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - timeZoneName: 'short' - })} + {formatDateTime(email.receivedAt, timeFormat, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', second: '2-digit', timeZoneName: 'short' })}
{/* Reply-To if different */} diff --git a/components/settings/calendar-management-settings.tsx b/components/settings/calendar-management-settings.tsx index dc02e548..991ea495 100644 --- a/components/settings/calendar-management-settings.tsx +++ b/components/settings/calendar-management-settings.tsx @@ -7,9 +7,10 @@ import { useAuthStore } from '@/stores/auth-store'; import { toast } from '@/stores/toast-store'; import { SettingsSection } from './settings-section'; import { Plus, Pencil, Trash2, Check, X, Calendar as CalendarIcon, Copy, Link, Upload, Globe, RefreshCw, Eraser } from 'lucide-react'; -import { cn } from '@/lib/utils'; +import { cn, formatDateTime } from '@/lib/utils'; import { ICalImportModal } from '@/components/calendar/ical-import-modal'; import { ICalSubscriptionModal } from '@/components/calendar/ical-subscription-modal'; +import { useSettingsStore } from '@/stores/settings-store'; const CALENDAR_COLORS = [ "#3b82f6", // blue @@ -164,6 +165,7 @@ export function CalendarManagementSettings() { const [refreshingSubId, setRefreshingSubId] = useState(null); const tImport = useTranslations('calendar.import'); const tSub = useTranslations('calendar.subscription'); + const timeFormat = useSettingsStore((s) => s.timeFormat); const colorPickerRef = useRef(null); // Load calendars if not yet loaded @@ -563,7 +565,7 @@ export function CalendarManagementSettings() { {sub.lastRefreshed && ( - {tSub('last_refreshed', { time: new Date(sub.lastRefreshed).toLocaleString() })} + {tSub('last_refreshed', { time: formatDateTime(sub.lastRefreshed, timeFormat, { month: 'short', day: 'numeric', year: 'numeric' }) })} )}
diff --git a/lib/utils.ts b/lib/utils.ts index 84775b56..3daf3252 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -27,6 +27,42 @@ export function formatDate(date: Date | string): string { }); } +/** + * Format a date/time string respecting the user's 12h/24h time format preference. + */ +export function formatDateTime( + date: Date | string, + timeFormat: '12h' | '24h', + options?: { + weekday?: 'short' | 'long'; + year?: 'numeric'; + month?: 'short' | 'long'; + day?: 'numeric'; + second?: '2-digit'; + timeZoneName?: 'short'; + dateOnly?: boolean; + } +): string { + const d = typeof date === 'string' ? new Date(date) : date; + if (isNaN(d.getTime())) return typeof date === 'string' ? date : ''; + + const localeOptions: Intl.DateTimeFormatOptions = {}; + if (options?.weekday) localeOptions.weekday = options.weekday; + if (options?.year) localeOptions.year = options.year; + if (options?.month) localeOptions.month = options.month; + if (options?.day) localeOptions.day = options.day; + + if (!options?.dateOnly) { + localeOptions.hour = '2-digit'; + localeOptions.minute = '2-digit'; + localeOptions.hour12 = timeFormat === '12h'; + if (options?.second) localeOptions.second = options.second; + if (options?.timeZoneName) localeOptions.timeZoneName = options.timeZoneName; + } + + return d.toLocaleString(undefined, localeOptions); +} + export function truncateText(text: string, maxLength: number): string { if (text.length <= maxLength) return text; return text.substring(0, maxLength).trim() + "...";