feat: centralise date/time formatting with European conventions

All dates now use day-first ordering (14 Mar 2025) and 24-hour time
(14:30) via a single formatDate.ts module, replacing scattered inline
toLocaleDateString/toLocaleTimeString calls across 12 files.
This commit is contained in:
Usman Baig
2026-03-14 13:31:30 +01:00
parent 7ba5e063ca
commit 25210013d3
13 changed files with 126 additions and 77 deletions

View File

@@ -11,6 +11,7 @@ import { Checkbox } from '@ciphera-net/ui'
import { ArrowUpRight, ArrowDownRight } from '@phosphor-icons/react'
import { motion } from 'framer-motion'
import { cn } from '@/lib/utils'
import { formatTime, formatDateShort, formatDate } from '@/lib/utils/formatDate'
const ANNOTATION_COLORS: Record<string, string> = {
deploy: '#3b82f6',
@@ -84,8 +85,7 @@ type MetricType = 'pageviews' | 'visitors' | 'bounce_rate' | 'avg_duration'
// ─── Helpers ─────────────────────────────────────────────────────────
function formatEU(dateStr: string): string {
const [y, m, d] = dateStr.split('-')
return `${d}/${m}/${y}`
return formatDate(new Date(dateStr + 'T00:00:00'))
}
// ─── Metric configurations ──────────────────────────────────────────
@@ -216,15 +216,12 @@ export default function Chart({
const chartData = useMemo(() => data.map((item) => {
let formattedDate: string
if (interval === 'minute') {
formattedDate = new Date(item.date).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
formattedDate = formatTime(new Date(item.date))
} else if (interval === 'hour') {
const d = new Date(item.date)
const isMidnight = d.getHours() === 0 && d.getMinutes() === 0
formattedDate = isMidnight
? d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' 12:00 AM'
: d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ', ' + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: 'numeric' })
formattedDate = formatDateShort(d) + ', ' + formatTime(d)
} else {
formattedDate = new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
formattedDate = formatDateShort(new Date(item.date))
}
return {

View File

@@ -7,6 +7,7 @@ import jsPDF from 'jspdf'
import autoTable from 'jspdf-autotable'
import type { DailyStat } from './Chart'
import { formatNumber, formatDuration } from '@ciphera-net/ui'
import { formatDateISO, formatDate, formatDateTime } from '@/lib/utils/formatDate'
import { getReferrerDisplayName, mergeReferrersByDisplayName } from '@/lib/utils/icons'
import type { TopPage, TopReferrer, CampaignStat } from '@/lib/api/stats'
@@ -47,7 +48,7 @@ const loadImage = (src: string): Promise<string> => {
export default function ExportModal({ isOpen, onClose, data, stats, topPages, topReferrers, campaigns }: ExportModalProps) {
const [format, setFormat] = useState<ExportFormat>('csv')
const [filename, setFilename] = useState(`pulse_export_${new Date().toISOString().split('T')[0]}`)
const [filename, setFilename] = useState(`pulse_export_${formatDateISO(new Date())}`)
const [includeHeader, setIncludeHeader] = useState(true)
const [isExporting, setIsExporting] = useState(false)
const [selectedFields, setSelectedFields] = useState<Record<keyof DailyStat, boolean>>({
@@ -153,9 +154,9 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
// Metadata (Top Right)
doc.setFontSize(9)
doc.setTextColor(150, 150, 150)
const generatedDate = new Date().toLocaleDateString()
const generatedDate = formatDate(new Date())
const dateRange = data.length > 0
? `${new Date(data[0].date).toLocaleDateString()} - ${new Date(data[data.length - 1].date).toLocaleDateString()}`
? `${formatDate(new Date(data[0].date))} - ${formatDate(new Date(data[data.length - 1].date))}`
: generatedDate
const pageWidth = doc.internal.pageSize.width
@@ -202,9 +203,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
const val = row[field]
if (field === 'date' && typeof val === 'string') {
const date = new Date(val)
return isHourly
? date.toLocaleString('en-US', { month: 'numeric', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' })
: date.toLocaleDateString()
return isHourly ? formatDateTime(date) : formatDate(date)
}
if (typeof val === 'number') {
if (field === 'bounce_rate') return `${Math.round(val)}%`