feat: refactor date formatting and timezone utilities for improved consistency and functionality
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { type Locale, LocaleProviderContext } from '@/contexts/LocaleContext'
|
import { type Locale, LocaleProviderContext } from '@/contexts/LocaleContext'
|
||||||
import { getSupportedTimezones } from '@/lib/utils/locale'
|
import { getSupportedTimezones } from '@/utils/locale'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
type LocaleProviderProps = {
|
type LocaleProviderProps = {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Badge } from '@/components/ui/badge'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { TableCell, TableRow } from '@/components/ui/table'
|
import { TableCell, TableRow } from '@/components/ui/table'
|
||||||
import type { Playlist } from '@/lib/api/services/playlists'
|
import type { Playlist } from '@/lib/api/services/playlists'
|
||||||
|
import { formatDate, formatDateDistanceToNow } from '@/utils/format-date'
|
||||||
import { formatDuration } from '@/utils/format-duration'
|
import { formatDuration } from '@/utils/format-duration'
|
||||||
import { Calendar, Clock, Edit, Music, Play, User } from 'lucide-react'
|
import { Calendar, Clock, Edit, Music, Play, User } from 'lucide-react'
|
||||||
|
|
||||||
@@ -12,10 +13,6 @@ interface PlaylistRowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PlaylistRow({ playlist, onEdit, onSetCurrent }: PlaylistRowProps) {
|
export function PlaylistRow({ playlist, onEdit, onSetCurrent }: PlaylistRowProps) {
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
return new Date(dateString).toLocaleDateString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow className="hover:bg-muted/50">
|
<TableRow className="hover:bg-muted/50">
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -31,12 +28,14 @@ export function PlaylistRow({ playlist, onEdit, onSetCurrent }: PlaylistRowProps
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="text-center">
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
{playlist.genre ? (
|
{playlist.genre ? (
|
||||||
<Badge variant="secondary">{playlist.genre}</Badge>
|
<Badge variant="secondary">{playlist.genre}</Badge>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground">-</span>
|
<span className="text-muted-foreground">-</span>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{playlist.user_name ? (
|
{playlist.user_name ? (
|
||||||
@@ -48,22 +47,22 @@ export function PlaylistRow({ playlist, onEdit, onSetCurrent }: PlaylistRowProps
|
|||||||
<span className="text-muted-foreground">System</span>
|
<span className="text-muted-foreground">System</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell>
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Music className="h-3 w-3 text-muted-foreground" />
|
<Music className="h-3 w-3 text-muted-foreground" />
|
||||||
{playlist.sound_count}
|
{playlist.sound_count}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell>
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Clock className="h-3 w-3 text-muted-foreground" />
|
<Clock className="h-3 w-3 text-muted-foreground" />
|
||||||
{formatDuration(playlist.total_duration || 0)}
|
{formatDuration(playlist.total_duration || 0)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell>
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Calendar className="h-3 w-3 text-muted-foreground" />
|
<Calendar className="h-3 w-3 text-muted-foreground" />
|
||||||
{formatDate(playlist.created_at)}
|
{formatDateDistanceToNow(playlist.created_at)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ export function PlaylistTable({ playlists, onEdit, onSetCurrent }: PlaylistTable
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Name</TableHead>
|
<TableHead>Name</TableHead>
|
||||||
<TableHead>Genre</TableHead>
|
<TableHead className="text-center">Genre</TableHead>
|
||||||
<TableHead>User</TableHead>
|
<TableHead>User</TableHead>
|
||||||
<TableHead className="text-center">Tracks</TableHead>
|
<TableHead>Tracks</TableHead>
|
||||||
<TableHead className="text-center">Duration</TableHead>
|
<TableHead>Duration</TableHead>
|
||||||
<TableHead className="text-center">Created</TableHead>
|
<TableHead>Created</TableHead>
|
||||||
<TableHead className="text-center">Status</TableHead>
|
<TableHead className="text-center">Status</TableHead>
|
||||||
<TableHead className="text-center">Actions</TableHead>
|
<TableHead className="text-center">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Badge } from '@/components/ui/badge'
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { NumberFlowDuration } from '@/components/ui/number-flow-duration'
|
import { NumberFlowDuration } from '@/components/ui/number-flow-duration'
|
||||||
import type { Playlist, PlaylistSound } from '@/lib/api/services/playlists'
|
import type { Playlist, PlaylistSound } from '@/lib/api/services/playlists'
|
||||||
|
import { formatDate } from '@/utils/format-date'
|
||||||
import NumberFlow from '@number-flow/react'
|
import NumberFlow from '@number-flow/react'
|
||||||
import { Clock } from 'lucide-react'
|
import { Clock } from 'lucide-react'
|
||||||
|
|
||||||
@@ -42,14 +43,14 @@ export function PlaylistStatsCard({ playlist, sounds }: PlaylistStatsCardProps)
|
|||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span>Created:</span>
|
<span>Created:</span>
|
||||||
<span>
|
<span>
|
||||||
{new Date(playlist.created_at).toLocaleDateString()}
|
{formatDate(playlist.created_at, true, true)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{playlist.updated_at && (
|
{playlist.updated_at && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span>Updated:</span>
|
<span>Updated:</span>
|
||||||
<span>
|
<span>
|
||||||
{new Date(playlist.updated_at).toLocaleDateString()}
|
{formatDate(playlist.updated_at, true, true)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Skeleton } from '@/components/ui/skeleton'
|
|||||||
import { useAuth } from '@/contexts/AuthContext'
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
import { useTheme } from '@/hooks/use-theme'
|
import { useTheme } from '@/hooks/use-theme'
|
||||||
import { useLocale } from '@/hooks/use-locale'
|
import { useLocale } from '@/hooks/use-locale'
|
||||||
import { getSupportedTimezones } from '@/lib/utils/locale'
|
import { getSupportedTimezones } from '@/utils/locale'
|
||||||
import {
|
import {
|
||||||
type ApiTokenStatusResponse,
|
type ApiTokenStatusResponse,
|
||||||
type UserProvider,
|
type UserProvider,
|
||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { formatDate } from '@/utils/format-date'
|
||||||
|
|
||||||
export function AccountPage() {
|
export function AccountPage() {
|
||||||
const { user, setUser } = useAuth()
|
const { user, setUser } = useAuth()
|
||||||
@@ -320,7 +321,7 @@ export function AccountPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Member since:{' '}
|
Member since:{' '}
|
||||||
{new Date(user.created_at).toLocaleDateString()}
|
{formatDate(user.created_at)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -623,9 +624,7 @@ export function AccountPage() {
|
|||||||
{apiTokenStatus.expires_at && (
|
{apiTokenStatus.expires_at && (
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
(Expires:{' '}
|
(Expires:{' '}
|
||||||
{new Date(
|
{formatDate(apiTokenStatus.expires_at, false)}
|
||||||
apiTokenStatus.expires_at,
|
|
||||||
).toLocaleDateString()}
|
|
||||||
)
|
)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
type ExtractionInfo,
|
type ExtractionInfo,
|
||||||
extractionsService,
|
extractionsService,
|
||||||
} from '@/lib/api/services/extractions'
|
} from '@/lib/api/services/extractions'
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
import { formatDateDistanceToNow } from '@/utils/format-date'
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Calendar,
|
Calendar,
|
||||||
@@ -284,19 +284,7 @@ export function ExtractionsPage() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
{(() => {
|
{formatDateDistanceToNow(extraction.created_at)}
|
||||||
try {
|
|
||||||
const date = new Date(extraction.created_at)
|
|
||||||
if (isNaN(date.getTime())) {
|
|
||||||
return 'Invalid date'
|
|
||||||
}
|
|
||||||
return formatDistanceToNow(date, {
|
|
||||||
addSuffix: true,
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
return 'Invalid date'
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { type Plan, adminService } from '@/lib/api/services/admin'
|
import { type Plan, adminService } from '@/lib/api/services/admin'
|
||||||
import type { User } from '@/types/auth'
|
import type { User } from '@/types/auth'
|
||||||
|
import { formatDate } from '@/utils/format-date'
|
||||||
import { Edit, UserCheck, UserX } from 'lucide-react'
|
import { Edit, UserCheck, UserX } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -295,7 +296,7 @@ export function UsersPage() {
|
|||||||
Created:
|
Created:
|
||||||
</span>
|
</span>
|
||||||
<span className="col-span-2">
|
<span className="col-span-2">
|
||||||
{new Date(editingUser.created_at).toLocaleDateString()}
|
{formatDate(editingUser.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 text-sm">
|
<div className="grid grid-cols-3 gap-2 text-sm">
|
||||||
@@ -303,7 +304,7 @@ export function UsersPage() {
|
|||||||
Last Updated:
|
Last Updated:
|
||||||
</span>
|
</span>
|
||||||
<span className="col-span-2">
|
<span className="col-span-2">
|
||||||
{new Date(editingUser.updated_at).toLocaleDateString()}
|
{formatDate(editingUser.updated_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
115
src/utils/format-date.ts
Normal file
115
src/utils/format-date.ts
Normal file
@@ -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<string, string>);
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user