11import { GlobeAltIcon , GlobeAmericasIcon } from "@heroicons/react/20/solid" ;
22import { Laptop } from "lucide-react" ;
3- import { Fragment , type ReactNode , useEffect , useState } from "react" ;
3+ import { Fragment , type ReactNode , useSyncExternalStore } from "react" ;
44import { CopyButton } from "./CopyButton" ;
55import { useLocales } from "./LocaleProvider" ;
66import { Paragraph } from "./Paragraph" ;
77import { SimpleTooltip } from "./Tooltip" ;
88
9+ // Cache the browser's local timezone - resolved once and reused
10+ let cachedLocalTimeZone : string | null = null ;
11+
12+ function getLocalTimeZone ( ) : string {
13+ if ( cachedLocalTimeZone === null ) {
14+ cachedLocalTimeZone = Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone ;
15+ }
16+ return cachedLocalTimeZone ;
17+ }
18+
19+ // For SSR compatibility: returns "UTC" on server, actual timezone on client
20+ function subscribeToTimeZone ( ) {
21+ // No-op - timezone doesn't change
22+ return ( ) => { } ;
23+ }
24+
25+ function getTimeZoneSnapshot ( ) : string {
26+ return getLocalTimeZone ( ) ;
27+ }
28+
29+ function getServerTimeZoneSnapshot ( ) : string {
30+ return "UTC" ;
31+ }
32+
33+ /**
34+ * Hook to get the browser's local timezone.
35+ * Uses useSyncExternalStore for SSR compatibility - returns "UTC" on server,
36+ * actual timezone on client. The timezone is cached and only resolved once.
37+ */
38+ export function useLocalTimeZone ( ) : string {
39+ return useSyncExternalStore ( subscribeToTimeZone , getTimeZoneSnapshot , getServerTimeZoneSnapshot ) ;
40+ }
41+
942type DateTimeProps = {
1043 date : Date | string ;
1144 timeZone ?: string ;
@@ -28,15 +61,10 @@ export const DateTime = ({
2861 hour12 = true ,
2962} : DateTimeProps ) => {
3063 const locales = useLocales ( ) ;
31- const [ localTimeZone , setLocalTimeZone ] = useState < string > ( "UTC" ) ;
64+ const localTimeZone = useLocalTimeZone ( ) ;
3265
3366 const realDate = typeof date === "string" ? new Date ( date ) : date ;
3467
35- useEffect ( ( ) => {
36- const resolvedOptions = Intl . DateTimeFormat ( ) . resolvedOptions ( ) ;
37- setLocalTimeZone ( resolvedOptions . timeZone ) ;
38- } , [ ] ) ;
39-
4068 const tooltipContent = (
4169 < TooltipContent
4270 realDate = { realDate }
@@ -128,38 +156,23 @@ export function formatDateTimeISO(date: Date, timeZone: string): string {
128156}
129157
130158// New component that only shows date when it changes
131- export const SmartDateTime = ( { date, previousDate = null , timeZone = "UTC" , hour12 = true } : DateTimeProps ) => {
159+ export const SmartDateTime = ( { date, previousDate = null , hour12 = true } : DateTimeProps ) => {
132160 const locales = useLocales ( ) ;
161+ const localTimeZone = useLocalTimeZone ( ) ;
133162 const realDate = typeof date === "string" ? new Date ( date ) : date ;
134163 const realPrevDate = previousDate
135164 ? typeof previousDate === "string"
136165 ? new Date ( previousDate )
137166 : previousDate
138167 : null ;
139168
140- // Initial formatted values
141- const initialTimeOnly = formatTimeOnly ( realDate , timeZone , locales , hour12 ) ;
142- const initialWithDate = formatSmartDateTime ( realDate , timeZone , locales , hour12 ) ;
143-
144- // State for the formatted time
145- const [ formattedDateTime , setFormattedDateTime ] = useState < string > (
146- realPrevDate && isSameDay ( realDate , realPrevDate ) ? initialTimeOnly : initialWithDate
147- ) ;
148-
149- useEffect ( ( ) => {
150- const resolvedOptions = Intl . DateTimeFormat ( ) . resolvedOptions ( ) ;
151- const userTimeZone = resolvedOptions . timeZone ;
152-
153- // Check if we should show the date
154- const showDatePart = ! realPrevDate || ! isSameDay ( realDate , realPrevDate ) ;
169+ // Check if we should show the date
170+ const showDatePart = ! realPrevDate || ! isSameDay ( realDate , realPrevDate ) ;
155171
156- // Format with appropriate function
157- setFormattedDateTime (
158- showDatePart
159- ? formatSmartDateTime ( realDate , userTimeZone , locales , hour12 )
160- : formatTimeOnly ( realDate , userTimeZone , locales , hour12 )
161- ) ;
162- } , [ locales , realDate , realPrevDate , hour12 ] ) ;
172+ // Format with appropriate function
173+ const formattedDateTime = showDatePart
174+ ? formatSmartDateTime ( realDate , localTimeZone , locales , hour12 )
175+ : formatTimeOnly ( realDate , localTimeZone , locales , hour12 ) ;
163176
164177 return < Fragment > { formattedDateTime . replace ( / \s / g, String . fromCharCode ( 32 ) ) } </ Fragment > ;
165178} ;
@@ -174,7 +187,12 @@ function isSameDay(date1: Date, date2: Date): boolean {
174187}
175188
176189// Format with date and time
177- function formatSmartDateTime ( date : Date , timeZone : string , locales : string [ ] , hour12 : boolean = true ) : string {
190+ function formatSmartDateTime (
191+ date : Date ,
192+ timeZone : string ,
193+ locales : string [ ] ,
194+ hour12 : boolean = true
195+ ) : string {
178196 return new Intl . DateTimeFormat ( locales , {
179197 month : "short" ,
180198 day : "numeric" ,
@@ -189,7 +207,12 @@ function formatSmartDateTime(date: Date, timeZone: string, locales: string[], ho
189207}
190208
191209// Format time only
192- function formatTimeOnly ( date : Date , timeZone : string , locales : string [ ] , hour12 : boolean = true ) : string {
210+ function formatTimeOnly (
211+ date : Date ,
212+ timeZone : string ,
213+ locales : string [ ] ,
214+ hour12 : boolean = true
215+ ) : string {
193216 return new Intl . DateTimeFormat ( locales , {
194217 hour : "2-digit" ,
195218 minute : "numeric" ,
@@ -210,19 +233,14 @@ export const DateTimeAccurate = ({
210233 hour12 = true ,
211234} : DateTimeProps ) => {
212235 const locales = useLocales ( ) ;
213- const [ localTimeZone , setLocalTimeZone ] = useState < string > ( "UTC" ) ;
236+ const localTimeZone = useLocalTimeZone ( ) ;
214237 const realDate = typeof date === "string" ? new Date ( date ) : date ;
215238 const realPrevDate = previousDate
216239 ? typeof previousDate === "string"
217240 ? new Date ( previousDate )
218241 : previousDate
219242 : null ;
220243
221- useEffect ( ( ) => {
222- const resolvedOptions = Intl . DateTimeFormat ( ) . resolvedOptions ( ) ;
223- setLocalTimeZone ( resolvedOptions . timeZone ) ;
224- } , [ ] ) ;
225-
226244 // Smart formatting based on whether date changed
227245 const formattedDateTime = hideDate
228246 ? formatTimeOnly ( realDate , localTimeZone , locales , hour12 )
@@ -253,7 +271,12 @@ export const DateTimeAccurate = ({
253271 ) ;
254272} ;
255273
256- function formatDateTimeAccurate ( date : Date , timeZone : string , locales : string [ ] , hour12 : boolean = true ) : string {
274+ function formatDateTimeAccurate (
275+ date : Date ,
276+ timeZone : string ,
277+ locales : string [ ] ,
278+ hour12 : boolean = true
279+ ) : string {
257280 const formattedDateTime = new Intl . DateTimeFormat ( locales , {
258281 month : "short" ,
259282 day : "numeric" ,
@@ -269,21 +292,21 @@ function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[],
269292 return formattedDateTime ;
270293}
271294
272- export const DateTimeShort = ( { date, timeZone = "UTC" , hour12 = true } : DateTimeProps ) => {
295+ export const DateTimeShort = ( { date, hour12 = true } : DateTimeProps ) => {
273296 const locales = useLocales ( ) ;
297+ const localTimeZone = useLocalTimeZone ( ) ;
274298 const realDate = typeof date === "string" ? new Date ( date ) : date ;
275- const initialFormattedDateTime = formatDateTimeShort ( realDate , timeZone , locales , hour12 ) ;
276- const [ formattedDateTime , setFormattedDateTime ] = useState < string > ( initialFormattedDateTime ) ;
277-
278- useEffect ( ( ) => {
279- const resolvedOptions = Intl . DateTimeFormat ( ) . resolvedOptions ( ) ;
280- setFormattedDateTime ( formatDateTimeShort ( realDate , resolvedOptions . timeZone , locales , hour12 ) ) ;
281- } , [ locales , realDate , hour12 ] ) ;
299+ const formattedDateTime = formatDateTimeShort ( realDate , localTimeZone , locales , hour12 ) ;
282300
283301 return < Fragment > { formattedDateTime . replace ( / \s / g, String . fromCharCode ( 32 ) ) } </ Fragment > ;
284302} ;
285303
286- function formatDateTimeShort ( date : Date , timeZone : string , locales : string [ ] , hour12 : boolean = true ) : string {
304+ function formatDateTimeShort (
305+ date : Date ,
306+ timeZone : string ,
307+ locales : string [ ] ,
308+ hour12 : boolean = true
309+ ) : string {
287310 const formattedDateTime = new Intl . DateTimeFormat ( locales , {
288311 hour : "numeric" ,
289312 minute : "numeric" ,
0 commit comments