From 9cfa1f6a28b55a863edf0aef435739b1f8d39413 Mon Sep 17 00:00:00 2001 From: JSC Date: Fri, 15 Aug 2025 23:43:17 +0200 Subject: [PATCH] feat: refactor date formatting and timezone utilities for improved consistency and functionality --- src/components/LocaleProvider.tsx | 2 +- src/components/playlists/PlaylistRow.tsx | 33 +++-- src/components/playlists/PlaylistTable.tsx | 8 +- .../playlist-edit/PlaylistStatsCard.tsx | 5 +- src/pages/AccountPage.tsx | 9 +- src/pages/ExtractionsPage.tsx | 16 +-- src/pages/admin/UsersPage.tsx | 5 +- src/utils/format-date.ts | 115 ++++++++++++++++++ src/{lib => }/utils/locale.ts | 0 9 files changed, 148 insertions(+), 45 deletions(-) create mode 100644 src/utils/format-date.ts rename src/{lib => }/utils/locale.ts (100%) diff --git a/src/components/LocaleProvider.tsx b/src/components/LocaleProvider.tsx index 5476d3b..965fe13 100644 --- a/src/components/LocaleProvider.tsx +++ b/src/components/LocaleProvider.tsx @@ -1,5 +1,5 @@ import { type Locale, LocaleProviderContext } from '@/contexts/LocaleContext' -import { getSupportedTimezones } from '@/lib/utils/locale' +import { getSupportedTimezones } from '@/utils/locale' import { useEffect, useState } from 'react' type LocaleProviderProps = { diff --git a/src/components/playlists/PlaylistRow.tsx b/src/components/playlists/PlaylistRow.tsx index 5898d2a..619806e 100644 --- a/src/components/playlists/PlaylistRow.tsx +++ b/src/components/playlists/PlaylistRow.tsx @@ -2,6 +2,7 @@ import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { TableCell, TableRow } from '@/components/ui/table' import type { Playlist } from '@/lib/api/services/playlists' +import { formatDate, formatDateDistanceToNow } from '@/utils/format-date' import { formatDuration } from '@/utils/format-duration' import { Calendar, Clock, Edit, Music, Play, User } from 'lucide-react' @@ -12,10 +13,6 @@ interface PlaylistRowProps { } export function PlaylistRow({ playlist, onEdit, onSetCurrent }: PlaylistRowProps) { - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString() - } - return ( @@ -31,12 +28,14 @@ export function PlaylistRow({ playlist, onEdit, onSetCurrent }: PlaylistRowProps - - {playlist.genre ? ( - {playlist.genre} - ) : ( - - - )} + +
+ {playlist.genre ? ( + {playlist.genre} + ) : ( + - + )} +
{playlist.user_name ? ( @@ -48,22 +47,22 @@ export function PlaylistRow({ playlist, onEdit, onSetCurrent }: PlaylistRowProps System )} - -
+ +
{playlist.sound_count}
- -
+ +
{formatDuration(playlist.total_duration || 0)}
- -
+ +
- {formatDate(playlist.created_at)} + {formatDateDistanceToNow(playlist.created_at)}
diff --git a/src/components/playlists/PlaylistTable.tsx b/src/components/playlists/PlaylistTable.tsx index 3c3267a..78d4000 100644 --- a/src/components/playlists/PlaylistTable.tsx +++ b/src/components/playlists/PlaylistTable.tsx @@ -21,11 +21,11 @@ export function PlaylistTable({ playlists, onEdit, onSetCurrent }: PlaylistTable Name - Genre + Genre User - Tracks - Duration - Created + Tracks + Duration + Created Status Actions diff --git a/src/components/playlists/playlist-edit/PlaylistStatsCard.tsx b/src/components/playlists/playlist-edit/PlaylistStatsCard.tsx index 4d2e5ad..af5343d 100644 --- a/src/components/playlists/playlist-edit/PlaylistStatsCard.tsx +++ b/src/components/playlists/playlist-edit/PlaylistStatsCard.tsx @@ -2,6 +2,7 @@ import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { NumberFlowDuration } from '@/components/ui/number-flow-duration' import type { Playlist, PlaylistSound } from '@/lib/api/services/playlists' +import { formatDate } from '@/utils/format-date' import NumberFlow from '@number-flow/react' import { Clock } from 'lucide-react' @@ -42,14 +43,14 @@ export function PlaylistStatsCard({ playlist, sounds }: PlaylistStatsCardProps)
Created: - {new Date(playlist.created_at).toLocaleDateString()} + {formatDate(playlist.created_at, true, true)}
{playlist.updated_at && (
Updated: - {new Date(playlist.updated_at).toLocaleDateString()} + {formatDate(playlist.updated_at, true, true)}
)} diff --git a/src/pages/AccountPage.tsx b/src/pages/AccountPage.tsx index f399618..712ed59 100644 --- a/src/pages/AccountPage.tsx +++ b/src/pages/AccountPage.tsx @@ -21,7 +21,7 @@ import { Skeleton } from '@/components/ui/skeleton' import { useAuth } from '@/contexts/AuthContext' import { useTheme } from '@/hooks/use-theme' import { useLocale } from '@/hooks/use-locale' -import { getSupportedTimezones } from '@/lib/utils/locale' +import { getSupportedTimezones } from '@/utils/locale' import { type ApiTokenStatusResponse, type UserProvider, @@ -42,6 +42,7 @@ import { } from 'lucide-react' import { useEffect, useState } from 'react' import { toast } from 'sonner' +import { formatDate } from '@/utils/format-date' export function AccountPage() { const { user, setUser } = useAuth() @@ -320,7 +321,7 @@ export function AccountPage() {
Member since:{' '} - {new Date(user.created_at).toLocaleDateString()} + {formatDate(user.created_at)}
@@ -623,9 +624,7 @@ export function AccountPage() { {apiTokenStatus.expires_at && ( (Expires:{' '} - {new Date( - apiTokenStatus.expires_at, - ).toLocaleDateString()} + {formatDate(apiTokenStatus.expires_at, false)} ) )} diff --git a/src/pages/ExtractionsPage.tsx b/src/pages/ExtractionsPage.tsx index 88f255e..d76bd0d 100644 --- a/src/pages/ExtractionsPage.tsx +++ b/src/pages/ExtractionsPage.tsx @@ -23,7 +23,7 @@ import { type ExtractionInfo, extractionsService, } from '@/lib/api/services/extractions' -import { formatDistanceToNow } from 'date-fns' +import { formatDateDistanceToNow } from '@/utils/format-date' import { AlertCircle, Calendar, @@ -284,19 +284,7 @@ export function ExtractionsPage() {
- {(() => { - try { - const date = new Date(extraction.created_at) - if (isNaN(date.getTime())) { - return 'Invalid date' - } - return formatDistanceToNow(date, { - addSuffix: true, - }) - } catch { - return 'Invalid date' - } - })()} + {formatDateDistanceToNow(extraction.created_at)}
diff --git a/src/pages/admin/UsersPage.tsx b/src/pages/admin/UsersPage.tsx index 0ec070a..02e03a4 100644 --- a/src/pages/admin/UsersPage.tsx +++ b/src/pages/admin/UsersPage.tsx @@ -24,6 +24,7 @@ import { } from '@/components/ui/table' import { type Plan, adminService } from '@/lib/api/services/admin' import type { User } from '@/types/auth' +import { formatDate } from '@/utils/format-date' import { Edit, UserCheck, UserX } from 'lucide-react' import { useEffect, useState } from 'react' import { toast } from 'sonner' @@ -295,7 +296,7 @@ export function UsersPage() { Created: - {new Date(editingUser.created_at).toLocaleDateString()} + {formatDate(editingUser.created_at)}
@@ -303,7 +304,7 @@ export function UsersPage() { Last Updated: - {new Date(editingUser.updated_at).toLocaleDateString()} + {formatDate(editingUser.updated_at)}
diff --git a/src/utils/format-date.ts b/src/utils/format-date.ts new file mode 100644 index 0000000..279fdd3 --- /dev/null +++ b/src/utils/format-date.ts @@ -0,0 +1,115 @@ +import { formatDistanceToNow } from 'date-fns'; + +/** + * Parse and optionally convert a date string to a Date object with timezone handling + * @param dateString - The date string to parse + * @param isUTC - Whether to convert from UTC to local timezone (default: true) + * @returns Processed Date object or null if invalid + */ +function parseAndConvertDate(dateString: string, isUTC: boolean = true): Date | null { + try { + // If isUTC is true and the date string doesn't have timezone info, treat it as UTC + let dateToProcess = dateString; + if (isUTC && !dateString.endsWith('Z') && !dateString.includes('+') && !dateString.includes('-', 10)) { + dateToProcess = `${dateString}Z`; + } + + const date = new Date(dateToProcess); + + if (isNaN(date.getTime())) { + return null; + } + + if (!isUTC) { + return date; + } + + // Get timezone from localStorage, default to Europe/Paris + const timezone = localStorage.getItem('timezone') || 'Europe/Paris'; + + // Format the date in the target timezone + const formatter = new Intl.DateTimeFormat('fr-FR', { + timeZone: timezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); + + const parts = formatter.formatToParts(date); + const partsObj = parts.reduce((acc, part) => { + acc[part.type] = part.value; + return acc; + }, {} as Record); + + // Create new date object in the target timezone + return new Date( + parseInt(partsObj.year), + parseInt(partsObj.month) - 1, // Month is 0-indexed + parseInt(partsObj.day), + parseInt(partsObj.hour), + parseInt(partsObj.minute), + parseInt(partsObj.second) + ); + } catch (error) { + console.error('Error parsing date:', error); + return null; + } +} + +/** + * Format a date string to DD/MM/YYYY HH:MM:SS or DD/MM/YYYY + * @param dateString - The date string to format + * @param withTime - Whether to include time in the output (default: true) + * @param isUTC - Whether to convert from UTC to local timezone (default: true) + * @returns Formatted date string + */ +export function formatDate( + dateString: string, + withTime: boolean = true, + isUTC: boolean = true +): string { + const date = parseAndConvertDate(dateString, isUTC); + + if (!date) { + return 'Invalid Date'; + } + + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear().toString(); + + const dateFormatted = `${day}/${month}/${year}`; + + if (!withTime) { + return dateFormatted; + } + + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const seconds = date.getSeconds().toString().padStart(2, '0'); + + return `${dateFormatted} ${hours}:${minutes}:${seconds}`; +} + +/** + * Format a date string to show distance to now (e.g., "2 hours ago", "3 days ago") + * @param dateString - The date string to format + * @param isUTC - Whether to convert from UTC to local timezone (default: true) + * @returns Formatted distance string (e.g., "2 hours ago") + */ +export function formatDateDistanceToNow( + dateString: string, + isUTC: boolean = true +): string { + const date = parseAndConvertDate(dateString, isUTC); + + if (!date) { + return 'Invalid Date'; + } + + return formatDistanceToNow(date, { addSuffix: true }); +} \ No newline at end of file diff --git a/src/lib/utils/locale.ts b/src/utils/locale.ts similarity index 100% rename from src/lib/utils/locale.ts rename to src/utils/locale.ts