|
| 1 | +import { |
| 2 | + formatAbsoluteDate, |
| 3 | + formatCombinedDate, |
| 4 | + formatRelativeDate, |
| 5 | + formatTimezoneDate, |
| 6 | + formatUTCDate, |
| 7 | + getTimestamp, |
| 8 | + getTimezoneAbbreviation, |
| 9 | + parseDate, |
| 10 | +} from './formatters'; |
| 11 | +import type { DateTimeProps, FormatterOptions } from './types'; |
| 12 | +import { cn } from '@/modules/shadcn/lib/utils'; |
| 13 | +import { useApp } from '@/providers/app.provider'; |
| 14 | +import { getBrowserTimezone } from '@/utils/helpers/timezone.helper'; |
| 15 | +import { Tooltip } from '@datum-ui/tooltip'; |
| 16 | +import { useEffect, useState } from 'react'; |
| 17 | + |
| 18 | +/** |
| 19 | + * Unified component for displaying dates in absolute, relative, or combined formats |
| 20 | + * with intelligent tooltip support and timezone awareness. |
| 21 | + * |
| 22 | + * @example |
| 23 | + * // Absolute date |
| 24 | + * <DateTime date={createdAt} /> |
| 25 | + * |
| 26 | + * @example |
| 27 | + * // Relative time |
| 28 | + * <DateTime date={createdAt} variant="relative" /> |
| 29 | + * |
| 30 | + * @example |
| 31 | + * // Combined format |
| 32 | + * <DateTime date={createdAt} variant="both" /> |
| 33 | + */ |
| 34 | +export const DateTime = ({ |
| 35 | + date, |
| 36 | + variant = 'detailed', |
| 37 | + format, |
| 38 | + addSuffix, |
| 39 | + tooltip = 'auto', |
| 40 | + timezone, |
| 41 | + disableTimezone = false, |
| 42 | + className, |
| 43 | + separator = ' ', |
| 44 | + disableHydrationProtection = false, |
| 45 | + showTooltip = true, // Legacy prop from DateFormat |
| 46 | +}: DateTimeProps) => { |
| 47 | + const { settings } = useApp(); |
| 48 | + const [mounted, setMounted] = useState(false); |
| 49 | + |
| 50 | + // Hydration protection for relative dates (client-side only) |
| 51 | + const needsHydrationProtection = variant === 'relative' || variant === 'both'; |
| 52 | + |
| 53 | + useEffect(() => { |
| 54 | + if (needsHydrationProtection && !disableHydrationProtection) { |
| 55 | + setMounted(true); |
| 56 | + } |
| 57 | + }, [needsHydrationProtection, disableHydrationProtection]); |
| 58 | + |
| 59 | + if (!date) { |
| 60 | + return null; |
| 61 | + } |
| 62 | + |
| 63 | + // Parse and validate date |
| 64 | + const parsedDate = parseDate(date); |
| 65 | + |
| 66 | + if (!parsedDate) { |
| 67 | + return null; |
| 68 | + } |
| 69 | + |
| 70 | + // Show loading state during hydration |
| 71 | + if (needsHydrationProtection && !disableHydrationProtection && !mounted) { |
| 72 | + return <span className={className}>...</span>; |
| 73 | + } |
| 74 | + |
| 75 | + // Prepare formatter options |
| 76 | + const timeZone = timezone ?? settings?.timezone ?? getBrowserTimezone(); |
| 77 | + const formatterOptions: FormatterOptions = { |
| 78 | + timezone: timeZone, |
| 79 | + disableTimezone, |
| 80 | + format, |
| 81 | + addSuffix, |
| 82 | + }; |
| 83 | + |
| 84 | + // Format content based on variant |
| 85 | + let content: string; |
| 86 | + switch (variant) { |
| 87 | + case 'detailed': |
| 88 | + content = formatTimezoneDate(parsedDate, timeZone); |
| 89 | + break; |
| 90 | + case 'relative': |
| 91 | + content = formatRelativeDate(parsedDate, formatterOptions); |
| 92 | + break; |
| 93 | + case 'both': |
| 94 | + content = formatCombinedDate(parsedDate, formatterOptions, separator); |
| 95 | + break; |
| 96 | + case 'absolute': |
| 97 | + default: |
| 98 | + content = formatAbsoluteDate(parsedDate, formatterOptions); |
| 99 | + break; |
| 100 | + } |
| 101 | + |
| 102 | + // Determine tooltip behavior |
| 103 | + const shouldShowTooltip = determineTooltipVisibility(tooltip, showTooltip); |
| 104 | + |
| 105 | + if (!shouldShowTooltip || disableTimezone) { |
| 106 | + return <span className={cn(className)}>{content}</span>; |
| 107 | + } |
| 108 | + |
| 109 | + // Determine tooltip content |
| 110 | + const tooltipContent = getTooltipContent( |
| 111 | + parsedDate, |
| 112 | + variant, |
| 113 | + tooltip, |
| 114 | + formatterOptions, |
| 115 | + timeZone |
| 116 | + ); |
| 117 | + |
| 118 | + return ( |
| 119 | + <Tooltip message={tooltipContent}> |
| 120 | + <span className={cn('cursor-pointer', className)}>{content}</span> |
| 121 | + </Tooltip> |
| 122 | + ); |
| 123 | +}; |
| 124 | + |
| 125 | +/** |
| 126 | + * Determines if tooltip should be shown |
| 127 | + */ |
| 128 | +function determineTooltipVisibility( |
| 129 | + tooltip: DateTimeProps['tooltip'], |
| 130 | + showTooltip: boolean |
| 131 | +): boolean { |
| 132 | + if (typeof tooltip === 'boolean') { |
| 133 | + return tooltip; |
| 134 | + } |
| 135 | + |
| 136 | + // Legacy support for showTooltip prop |
| 137 | + if (tooltip === 'auto' && !showTooltip) { |
| 138 | + return false; |
| 139 | + } |
| 140 | + |
| 141 | + // Auto mode shows tooltip by default |
| 142 | + return true; |
| 143 | +} |
| 144 | + |
| 145 | +/** |
| 146 | + * Gets the appropriate tooltip content based on variant and mode |
| 147 | + */ |
| 148 | +function getTooltipContent( |
| 149 | + date: Date, |
| 150 | + variant: DateTimeProps['variant'], |
| 151 | + tooltip: DateTimeProps['tooltip'], |
| 152 | + options: FormatterOptions, |
| 153 | + timeZone: string |
| 154 | +): React.ReactNode { |
| 155 | + // Detailed variant - show all time formats |
| 156 | + if (variant === 'detailed') { |
| 157 | + const utcTime = formatUTCDate(date); |
| 158 | + const timezoneTime = formatTimezoneDate(date, timeZone); |
| 159 | + const relativeTime = formatRelativeDate(date, options); |
| 160 | + const timestamp = getTimestamp(date); |
| 161 | + |
| 162 | + const rows = [ |
| 163 | + { label: 'UTC', value: utcTime }, |
| 164 | + { label: timeZone.replace('_', ' '), value: timezoneTime }, |
| 165 | + { label: 'Relative', value: relativeTime }, |
| 166 | + { label: 'Timestamp', value: timestamp }, |
| 167 | + ]; |
| 168 | + |
| 169 | + return ( |
| 170 | + <div className="space-y-2 text-xs"> |
| 171 | + {rows.map((row) => ( |
| 172 | + <div key={row.label} className="flex items-center justify-between gap-2"> |
| 173 | + <span className="font-medium">{row.label}</span> |
| 174 | + <span className="mx-1 flex-1 border-b border-dotted border-current/50" /> |
| 175 | + <span className="text-right">{row.value}</span> |
| 176 | + </div> |
| 177 | + ))} |
| 178 | + </div> |
| 179 | + ); |
| 180 | + } |
| 181 | + |
| 182 | + // Explicit timezone mode |
| 183 | + if (tooltip === 'timezone') { |
| 184 | + return ( |
| 185 | + <p> |
| 186 | + {timeZone.replace('_', ' ')} ({getTimezoneAbbreviation(date, timeZone)}) |
| 187 | + </p> |
| 188 | + ); |
| 189 | + } |
| 190 | + |
| 191 | + // Alternate mode - show opposite format |
| 192 | + if (tooltip === 'alternate') { |
| 193 | + if (variant === 'relative') { |
| 194 | + return formatAbsoluteDate(date, options); |
| 195 | + } |
| 196 | + if (variant === 'absolute' || variant === 'both') { |
| 197 | + return formatRelativeDate(date, options); |
| 198 | + } |
| 199 | + } |
| 200 | + |
| 201 | + // Auto mode - intelligent defaults |
| 202 | + if (tooltip === 'auto' || tooltip === true) { |
| 203 | + switch (variant) { |
| 204 | + case 'relative': |
| 205 | + // Show absolute date for relative time |
| 206 | + return formatAbsoluteDate(date, options); |
| 207 | + |
| 208 | + case 'both': |
| 209 | + case 'absolute': |
| 210 | + default: |
| 211 | + // Show timezone info for absolute dates |
| 212 | + return ( |
| 213 | + <p> |
| 214 | + {timeZone.replace('_', ' ')} ({getTimezoneAbbreviation(date, timeZone)}) |
| 215 | + </p> |
| 216 | + ); |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + return null; |
| 221 | +} |
0 commit comments