feat: implement time format preference across calendar and email components
This commit is contained in:
@@ -3,10 +3,11 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Globe, Plus, RefreshCw, Trash2 } from "lucide-react";
|
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 type { Calendar } from "@/lib/jmap/types";
|
||||||
import { CalendarColorPicker } from "@/components/settings/calendar-management-settings";
|
import { CalendarColorPicker } from "@/components/settings/calendar-management-settings";
|
||||||
import { useCalendarStore } from "@/stores/calendar-store";
|
import { useCalendarStore } from "@/stores/calendar-store";
|
||||||
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
import { toast } from "@/stores/toast-store";
|
import { toast } from "@/stores/toast-store";
|
||||||
import type { JMAPClient } from "@/lib/jmap/client";
|
import type { JMAPClient } from "@/lib/jmap/client";
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ export function CalendarSidebarPanel({
|
|||||||
const icalSubscriptions = useCalendarStore((s) => s.icalSubscriptions);
|
const icalSubscriptions = useCalendarStore((s) => s.icalSubscriptions);
|
||||||
const refreshICalSubscription = useCalendarStore((s) => s.refreshICalSubscription);
|
const refreshICalSubscription = useCalendarStore((s) => s.refreshICalSubscription);
|
||||||
const removeICalSubscription = useCalendarStore((s) => s.removeICalSubscription);
|
const removeICalSubscription = useCalendarStore((s) => s.removeICalSubscription);
|
||||||
|
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||||
|
|
||||||
const [colorPickerId, setColorPickerId] = useState<string | null>(null);
|
const [colorPickerId, setColorPickerId] = useState<string | null>(null);
|
||||||
const [contextMenuCalId, setContextMenuCalId] = useState<string | null>(null);
|
const [contextMenuCalId, setContextMenuCalId] = useState<string | null>(null);
|
||||||
@@ -169,7 +171,7 @@ export function CalendarSidebarPanel({
|
|||||||
</button>
|
</button>
|
||||||
{sub.lastRefreshed && (
|
{sub.lastRefreshed && (
|
||||||
<div className="px-3 py-1.5 text-xs text-muted-foreground border-t border-border mt-1 pt-1">
|
<div className="px-3 py-1.5 text-xs text-muted-foreground border-t border-border mt-1 pt-1">
|
||||||
{tSub('last_refreshed', { time: new Date(sub.lastRefreshed).toLocaleString() })}
|
{tSub('last_refreshed', { time: formatDateTime(sub.lastRefreshed, timeFormat, { month: 'short', day: 'numeric', year: 'numeric' }) })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
|
|||||||
import { format, parseISO } from "date-fns";
|
import { format, parseISO } from "date-fns";
|
||||||
import { Users } from "lucide-react";
|
import { Users } from "lucide-react";
|
||||||
import { getParticipantCount } from "@/lib/calendar-participants";
|
import { getParticipantCount } from "@/lib/calendar-participants";
|
||||||
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
|
|
||||||
interface EventCardProps {
|
interface EventCardProps {
|
||||||
event: CalendarEvent;
|
event: CalendarEvent;
|
||||||
@@ -68,11 +69,13 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM
|
|||||||
const [isBeingDragged, setIsBeingDragged] = useState(false);
|
const [isBeingDragged, setIsBeingDragged] = useState(false);
|
||||||
const color = getEventColor(event, calendar);
|
const color = getEventColor(event, calendar);
|
||||||
const startDate = parseISO(event.start);
|
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 calendarName = calendar?.name || "";
|
||||||
const durationMinutes = parseDuration(event.duration);
|
const durationMinutes = parseDuration(event.duration);
|
||||||
const endTime = new Date(startDate.getTime() + durationMinutes * 60000);
|
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 ariaLabel = `${event.title || t("events.no_title")}, ${timeString}${calendarName ? `, ${calendarName}` : ""}`;
|
||||||
|
|
||||||
const handleDragStart = useCallback((e: DragEvent) => {
|
const handleDragStart = useCallback((e: DragEvent) => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
getStatusCounts,
|
getStatusCounts,
|
||||||
buildParticipantMap,
|
buildParticipantMap,
|
||||||
} from "@/lib/calendar-participants";
|
} from "@/lib/calendar-participants";
|
||||||
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
|
|
||||||
interface EventModalProps {
|
interface EventModalProps {
|
||||||
event?: CalendarEvent | null;
|
event?: CalendarEvent | null;
|
||||||
@@ -108,6 +109,8 @@ export function EventModal({
|
|||||||
isMobile = false,
|
isMobile = false,
|
||||||
}: EventModalProps) {
|
}: EventModalProps) {
|
||||||
const t = useTranslations("calendar");
|
const t = useTranslations("calendar");
|
||||||
|
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||||
|
const timeDisplayFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm";
|
||||||
const isEdit = !!event;
|
const isEdit = !!event;
|
||||||
const [mode, setMode] = useState<"view" | "edit">(isEdit ? "view" : "edit");
|
const [mode, setMode] = useState<"view" | "edit">(isEdit ? "view" : "edit");
|
||||||
|
|
||||||
@@ -452,7 +455,7 @@ export function EventModal({
|
|||||||
<span className="font-medium">{format(startD, "EEE, MMM d, yyyy")}</span>
|
<span className="font-medium">{format(startD, "EEE, MMM d, yyyy")}</span>
|
||||||
{!event.showWithoutTime && (
|
{!event.showWithoutTime && (
|
||||||
<span className="text-muted-foreground ml-2">
|
<span className="text-muted-foreground ml-2">
|
||||||
{format(startD, "HH:mm")} – {format(endD, "HH:mm")}
|
{format(startD, timeDisplayFmt)} – {format(endD, timeDisplayFmt)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -576,7 +579,7 @@ export function EventModal({
|
|||||||
<span className="text-muted-foreground ml-1.5">{t("events.all_day")}</span>
|
<span className="text-muted-foreground ml-1.5">{t("events.all_day")}</span>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
{format(startD, "HH:mm")} – {format(endD, "HH:mm")}
|
{format(startD, timeDisplayFmt)} – {format(endD, timeDisplayFmt)}
|
||||||
<span className="ml-1.5 text-xs">({formatDurationDisplay(durMin)})</span>
|
<span className="ml-1.5 text-xs">({formatDurationDisplay(durMin)})</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { format, parseISO } from "date-fns";
|
|||||||
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
|
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
|
||||||
import type { JMAPClient } from "@/lib/jmap/client";
|
import type { JMAPClient } from "@/lib/jmap/client";
|
||||||
import { useCalendarStore } from "@/stores/calendar-store";
|
import { useCalendarStore } from "@/stores/calendar-store";
|
||||||
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
import { toast } from "@/stores/toast-store";
|
import { toast } from "@/stores/toast-store";
|
||||||
|
|
||||||
interface ICalImportModalProps {
|
interface ICalImportModalProps {
|
||||||
@@ -28,6 +29,7 @@ export function ICalImportModal({ calendars, client, onClose }: ICalImportModalP
|
|||||||
const tCommon = useTranslations("common");
|
const tCommon = useTranslations("common");
|
||||||
const tForm = useTranslations("calendar.form");
|
const tForm = useTranslations("calendar.form");
|
||||||
const importEvents = useCalendarStore((s) => s.importEvents);
|
const importEvents = useCalendarStore((s) => s.importEvents);
|
||||||
|
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||||
|
|
||||||
const [step, setStep] = useState<ImportStep>("select");
|
const [step, setStep] = useState<ImportStep>("select");
|
||||||
const [parsedEvents, setParsedEvents] = useState<Partial<CalendarEvent>[]>([]);
|
const [parsedEvents, setParsedEvents] = useState<Partial<CalendarEvent>[]>([]);
|
||||||
@@ -194,9 +196,10 @@ export function ICalImportModal({ calendars, client, onClose }: ICalImportModalP
|
|||||||
if (!event.start) return "";
|
if (!event.start) return "";
|
||||||
try {
|
try {
|
||||||
const date = parseISO(event.start);
|
const date = parseISO(event.start);
|
||||||
|
const timeFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm";
|
||||||
return event.showWithoutTime
|
return event.showWithoutTime
|
||||||
? format(date, "MMM d, yyyy")
|
? format(date, "MMM d, yyyy")
|
||||||
: format(date, "MMM d, yyyy HH:mm");
|
: format(date, `MMM d, yyyy ${timeFmt}`);
|
||||||
} catch {
|
} catch {
|
||||||
return event.start;
|
return event.start;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,6 +360,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
|||||||
const client = useAuthStore((s) => s.client);
|
const client = useAuthStore((s) => s.client);
|
||||||
const currentUserEmail = useAuthStore((s) => s.primaryIdentity?.email);
|
const currentUserEmail = useAuthStore((s) => s.primaryIdentity?.email);
|
||||||
const calendarInvitationParsingEnabled = useSettingsStore((s) => s.calendarInvitationParsingEnabled);
|
const calendarInvitationParsingEnabled = useSettingsStore((s) => s.calendarInvitationParsingEnabled);
|
||||||
|
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||||
const { calendars, supportsCalendar, importEvents, rsvpEvent, updateEvent, events: storeEvents, setSelectedDate } = useCalendarStore();
|
const { calendars, supportsCalendar, importEvents, rsvpEvent, updateEvent, events: storeEvents, setSelectedDate } = useCalendarStore();
|
||||||
|
|
||||||
const [state, setState] = useState<BannerState>('loading');
|
const [state, setState] = useState<BannerState>('loading');
|
||||||
@@ -679,6 +680,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
|||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
|
hour12: timeFormat === '12h',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { X, Paperclip, Send, Save, Check, Loader2, AlertCircle, FileText, BookmarkPlus, ShieldCheck, Lock } from "lucide-react";
|
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 { debug } from "@/lib/debug";
|
||||||
import { toast } from "@/stores/toast-store";
|
import { toast } from "@/stores/toast-store";
|
||||||
import { sanitizeEmailHtml } from "@/lib/email-sanitization";
|
import { sanitizeEmailHtml } from "@/lib/email-sanitization";
|
||||||
@@ -14,6 +14,7 @@ import { useAuthStore } from "@/stores/auth-store";
|
|||||||
import { useIdentityStore } from "@/stores/identity-store";
|
import { useIdentityStore } from "@/stores/identity-store";
|
||||||
import { useSmimeStore } from "@/stores/smime-store";
|
import { useSmimeStore } from "@/stores/smime-store";
|
||||||
import { useEmailStore } from "@/stores/email-store";
|
import { useEmailStore } from "@/stores/email-store";
|
||||||
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
import { buildMimeMessage, wrapCmsAsSmimeMessage } from "@/lib/smime/mime-builder";
|
import { buildMimeMessage, wrapCmsAsSmimeMessage } from "@/lib/smime/mime-builder";
|
||||||
import type { MimeAttachment } from "@/lib/smime/mime-builder";
|
import type { MimeAttachment } from "@/lib/smime/mime-builder";
|
||||||
import { smimeSign } from "@/lib/smime/smime-sign";
|
import { smimeSign } from "@/lib/smime/smime-sign";
|
||||||
@@ -86,6 +87,7 @@ export function EmailComposer({
|
|||||||
}: EmailComposerProps) {
|
}: EmailComposerProps) {
|
||||||
const t = useTranslations('email_composer');
|
const t = useTranslations('email_composer');
|
||||||
const tCommon = useTranslations('common');
|
const tCommon = useTranslations('common');
|
||||||
|
const timeFormat = useSettingsStore((state) => state.timeFormat);
|
||||||
|
|
||||||
// Initialize with reply/forward data if provided
|
// Initialize with reply/forward data if provided
|
||||||
const getInitialTo = () => {
|
const getInitialTo = () => {
|
||||||
@@ -124,7 +126,7 @@ export function EmailComposer({
|
|||||||
const prefix = initialDraftText || "";
|
const prefix = initialDraftText || "";
|
||||||
if (!replyTo?.body && !replyTo?.htmlBody) return prefix;
|
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 from = replyTo.from?.[0];
|
||||||
const fromStr = from ? `${from.name || from.email}` : tCommon('unknown');
|
const fromStr = from ? `${from.name || from.email}` : tCommon('unknown');
|
||||||
|
|
||||||
@@ -669,7 +671,7 @@ export function EmailComposer({
|
|||||||
const signatureHtml = currentIdentity?.textSignature
|
const signatureHtml = currentIdentity?.textSignature
|
||||||
? `<br><br>-- <br>${currentIdentity.textSignature.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}`
|
? `<br><br>-- <br>${currentIdentity.textSignature.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}`
|
||||||
: '';
|
: '';
|
||||||
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 fromAddr = replyTo.from?.[0];
|
||||||
const fromStr = fromAddr ? `${fromAddr.name || fromAddr.email}` : tCommon('unknown');
|
const fromStr = fromAddr ? `${fromAddr.name || fromAddr.email}` : tCommon('unknown');
|
||||||
const quoteHeader = mode === 'forward'
|
const quoteHeader = mode === 'forward'
|
||||||
@@ -1092,7 +1094,7 @@ export function EmailComposer({
|
|||||||
<div className="px-4 py-2 text-xs text-muted-foreground">
|
<div className="px-4 py-2 text-xs text-muted-foreground">
|
||||||
{mode === 'forward'
|
{mode === 'forward'
|
||||||
? `---------- ${t('prefix.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')}:`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Email, ContactCard, Mailbox } from "@/lib/jmap/types";
|
|||||||
import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers } from "@/lib/email-sanitization";
|
import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers } from "@/lib/email-sanitization";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Avatar } from "@/components/ui/avatar";
|
import { Avatar } from "@/components/ui/avatar";
|
||||||
import { formatFileSize, cn, buildMailboxTree, MailboxNode } from "@/lib/utils";
|
import { formatFileSize, cn, buildMailboxTree, MailboxNode, formatDateTime } from "@/lib/utils";
|
||||||
import { getSecurityStatus, extractListHeaders } from "@/lib/email-headers";
|
import { getSecurityStatus, extractListHeaders } from "@/lib/email-headers";
|
||||||
import {
|
import {
|
||||||
Reply,
|
Reply,
|
||||||
@@ -818,6 +818,7 @@ export function EmailViewer({
|
|||||||
const toolbarPosition = useSettingsStore((state) => state.toolbarPosition);
|
const toolbarPosition = useSettingsStore((state) => state.toolbarPosition);
|
||||||
const showToolbarLabels = useSettingsStore((state) => state.showToolbarLabels);
|
const showToolbarLabels = useSettingsStore((state) => state.showToolbarLabels);
|
||||||
const calendarInvitationParsingEnabled = useSettingsStore((state) => state.calendarInvitationParsingEnabled);
|
const calendarInvitationParsingEnabled = useSettingsStore((state) => state.calendarInvitationParsingEnabled);
|
||||||
|
const timeFormat = useSettingsStore((state) => state.timeFormat);
|
||||||
|
|
||||||
// Detect if current mailbox is Junk folder
|
// Detect if current mailbox is Junk folder
|
||||||
const isInJunkFolder = currentMailboxRole === 'junk';
|
const isInJunkFolder = currentMailboxRole === 'junk';
|
||||||
@@ -2224,7 +2225,7 @@ export function EmailViewer({
|
|||||||
const handlePrint = () => {
|
const handlePrint = () => {
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
const printSender = email.from?.[0];
|
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 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(', ') || '';
|
const ccList = email.cc?.map(r => r.name ? `${r.name} <${r.email}>` : r.email).join(', ') || '';
|
||||||
|
|
||||||
@@ -2976,14 +2977,7 @@ export function EmailViewer({
|
|||||||
<div className="flex items-center gap-2 lg:gap-3 mt-1 lg:mt-1.5 text-xs lg:text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 lg:gap-3 mt-1 lg:mt-1.5 text-xs lg:text-sm text-muted-foreground">
|
||||||
<span className="flex items-center gap-1 lg:gap-1.5 whitespace-nowrap">
|
<span className="flex items-center gap-1 lg:gap-1.5 whitespace-nowrap">
|
||||||
<Clock className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
|
<Clock className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
|
||||||
{new Date(email.receivedAt).toLocaleString('en-US', {
|
{formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}
|
||||||
weekday: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
{isImportant && (
|
{isImportant && (
|
||||||
<span className="px-1.5 lg:px-2 py-0.5 bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 rounded-full text-xs font-medium whitespace-nowrap">
|
<span className="px-1.5 lg:px-2 py-0.5 bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 rounded-full text-xs font-medium whitespace-nowrap">
|
||||||
@@ -3458,14 +3452,7 @@ export function EmailViewer({
|
|||||||
{/* Date and size on the right */}
|
{/* Date and size on the right */}
|
||||||
<div className="text-right flex-shrink-0">
|
<div className="text-right flex-shrink-0">
|
||||||
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
||||||
{new Date(email.receivedAt).toLocaleString('en-US', {
|
{formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}
|
||||||
weekday: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
{email.size > 0 && (
|
{email.size > 0 && (
|
||||||
<div className="text-xs text-muted-foreground/70 mt-0.5">
|
<div className="text-xs text-muted-foreground/70 mt-0.5">
|
||||||
@@ -3596,16 +3583,7 @@ export function EmailViewer({
|
|||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<span className="text-muted-foreground font-medium w-12 shrink-0">{t('date')}:</span>
|
<span className="text-muted-foreground font-medium w-12 shrink-0">{t('date')}:</span>
|
||||||
<span className="text-foreground">
|
<span className="text-foreground">
|
||||||
{new Date(email.receivedAt).toLocaleString('en-US', {
|
{formatDateTime(email.receivedAt, timeFormat, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', second: '2-digit', timeZoneName: 'short' })}
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
timeZoneName: 'short'
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Reply-To if different */}
|
{/* Reply-To if different */}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { useAuthStore } from '@/stores/auth-store';
|
|||||||
import { toast } from '@/stores/toast-store';
|
import { toast } from '@/stores/toast-store';
|
||||||
import { SettingsSection } from './settings-section';
|
import { SettingsSection } from './settings-section';
|
||||||
import { Plus, Pencil, Trash2, Check, X, Calendar as CalendarIcon, Copy, Link, Upload, Globe, RefreshCw, Eraser } from 'lucide-react';
|
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 { ICalImportModal } from '@/components/calendar/ical-import-modal';
|
||||||
import { ICalSubscriptionModal } from '@/components/calendar/ical-subscription-modal';
|
import { ICalSubscriptionModal } from '@/components/calendar/ical-subscription-modal';
|
||||||
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
|
|
||||||
const CALENDAR_COLORS = [
|
const CALENDAR_COLORS = [
|
||||||
"#3b82f6", // blue
|
"#3b82f6", // blue
|
||||||
@@ -164,6 +165,7 @@ export function CalendarManagementSettings() {
|
|||||||
const [refreshingSubId, setRefreshingSubId] = useState<string | null>(null);
|
const [refreshingSubId, setRefreshingSubId] = useState<string | null>(null);
|
||||||
const tImport = useTranslations('calendar.import');
|
const tImport = useTranslations('calendar.import');
|
||||||
const tSub = useTranslations('calendar.subscription');
|
const tSub = useTranslations('calendar.subscription');
|
||||||
|
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||||
const colorPickerRef = useRef<HTMLDivElement>(null);
|
const colorPickerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Load calendars if not yet loaded
|
// Load calendars if not yet loaded
|
||||||
@@ -563,7 +565,7 @@ export function CalendarManagementSettings() {
|
|||||||
</span>
|
</span>
|
||||||
{sub.lastRefreshed && (
|
{sub.lastRefreshed && (
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{tSub('last_refreshed', { time: new Date(sub.lastRefreshed).toLocaleString() })}
|
{tSub('last_refreshed', { time: formatDateTime(sub.lastRefreshed, timeFormat, { month: 'short', day: 'numeric', year: 'numeric' }) })}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 {
|
export function truncateText(text: string, maxLength: number): string {
|
||||||
if (text.length <= maxLength) return text;
|
if (text.length <= maxLength) return text;
|
||||||
return text.substring(0, maxLength).trim() + "...";
|
return text.substring(0, maxLength).trim() + "...";
|
||||||
|
|||||||
Reference in New Issue
Block a user