3939 flex-direction : column;
4040 }
4141
42+ .comparison-table {
43+ margin-top : 1rem ;
44+ }
45+
46+ .comparison-table [hidden ] {
47+ display : none;
48+ }
49+
50+ .comparison-table table {
51+ width : 100% ;
52+ border-collapse : collapse;
53+ }
54+
55+ .comparison-table th ,
56+ .comparison-table td {
57+ padding : 0.75rem 0.5rem ;
58+ border-bottom : 1px solid var (--surface-border , rgba (0 , 0 , 0 , 0.08 ));
59+ vertical-align : top;
60+ }
61+
62+ .comparison-table th {
63+ text-align : left;
64+ }
65+
66+ .comparison-label {
67+ font-weight : 600 ;
68+ }
69+
70+ .comparison-zone {
71+ font-size : 0.9rem ;
72+ color : var (--text-muted );
73+ margin-top : 0.25rem ;
74+ word-break : break-word;
75+ }
76+
77+ .comparison-select-wrapper select {
78+ width : 100% ;
79+ }
80+
81+ .comparison-controls {
82+ margin-top : 0.75rem ;
83+ }
84+
85+ .comparison-actions-header {
86+ width : 1% ;
87+ }
88+
89+ .comparison-actions-cell {
90+ text-align : right;
91+ }
92+
93+ .link-button {
94+ background : none;
95+ border : none;
96+ color : var (--link-color , var (--accent-color , # 0051ff ));
97+ cursor : pointer;
98+ padding : 0 ;
99+ font : inherit;
100+ text-decoration : underline;
101+ }
102+
103+ .link-button : hover ,
104+ .link-button : focus {
105+ text-decoration : none;
106+ }
107+
42108 @media (max-width : 720px ) {
43109 body {
44110 padding : 20px 16px 40px ;
@@ -55,6 +121,9 @@ <h1>What time is it for me?</h1>
55121 </ header >
56122
57123 < main >
124+ < section class ="surface tool-card " aria-live ="polite " aria-label ="Inferred timezone ">
125+ < p > Your inferred timezone: < span id ="inferred-timezone-value "> Detecting…</ span > </ p >
126+ </ section >
58127 < section class ="surface tool-card " aria-labelledby ="selection-heading ">
59128 < h2 id ="selection-heading "> Pick a moment</ h2 >
60129 < form id ="moment-form ">
@@ -78,6 +147,21 @@ <h2 id="selection-heading">Pick a moment</h2>
78147 < h2 id ="comparison-heading "> Comparison</ h2 >
79148 < p id ="comparison-output " class ="lead "> Choose a date, time, and timezone to see how it translates to your
80149 current timezone.</ p >
150+ < div id ="comparison-table " class ="comparison-table " hidden >
151+ < table >
152+ < thead >
153+ < tr >
154+ < th scope ="col "> Timezone</ th >
155+ < th scope ="col "> Local time</ th >
156+ < th scope ="col " class ="comparison-actions-header " aria-label ="Actions "> </ th >
157+ </ tr >
158+ </ thead >
159+ < tbody id ="comparison-rows "> </ tbody >
160+ </ table >
161+ < div class ="comparison-controls ">
162+ < button type ="button " id ="add-comparison-row "> Add timezone</ button >
163+ </ div >
164+ </ div >
81165 </ section >
82166
83167 < section class ="surface tool-card " aria-labelledby ="share-heading ">
@@ -99,12 +183,20 @@ <h2 id="share-heading">Shareable link</h2>
99183 const datetimeInput = document . getElementById ( 'datetime-input' ) ;
100184 const timezoneSelect = document . getElementById ( 'timezone-select' ) ;
101185 const comparisonOutput = document . getElementById ( 'comparison-output' ) ;
186+ const comparisonTable = document . getElementById ( 'comparison-table' ) ;
187+ const comparisonRowsContainer = document . getElementById ( 'comparison-rows' ) ;
188+ const addComparisonRowButton = document . getElementById ( 'add-comparison-row' ) ;
102189 const copyButton = document . getElementById ( 'copy-link-button' ) ;
103190 const copyStatus = document . getElementById ( 'copy-status' ) ;
104191 const shareableUrlInput = document . getElementById ( 'shareable-url' ) ;
192+ const inferredTimezoneValue = document . getElementById ( 'inferred-timezone-value' ) ;
105193
106194 const resolvedZone = Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone ;
107- const localZone = resolvedZone || DateTime . local ( ) . zoneName || 'UTC' ;
195+ const fallbackZone = resolvedZone || DateTime . local ( ) . zoneName || 'UTC' ;
196+ let localZone = fallbackZone ;
197+ let localRowEntry = null ;
198+ let timezoneManuallySet = false ;
199+ const comparisonRows = [ ] ;
108200
109201 function ensureOptionForZone ( zone ) {
110202 const exists = Array . from ( timezoneSelect . options ) . some ( option => option . value === zone ) ;
@@ -144,6 +236,126 @@ <h2 id="share-heading">Shareable link</h2>
144236 ensureOptionForZone ( localZone ) ;
145237 }
146238
239+ function createTimezoneSelectElement ( ) {
240+ const select = document . createElement ( 'select' ) ;
241+ select . className = 'comparison-timezone-select' ;
242+ select . innerHTML = timezoneSelect . innerHTML ;
243+ return select ;
244+ }
245+
246+ function updateInferredTimezoneDisplay ( zone ) {
247+ inferredTimezoneValue . textContent = zone || 'Unavailable' ;
248+ }
249+
250+ function addLocalComparisonRow ( ) {
251+ const row = document . createElement ( 'tr' ) ;
252+ row . className = 'comparison-row comparison-row-fixed' ;
253+ row . dataset . rowType = 'local' ;
254+
255+ const labelCell = document . createElement ( 'th' ) ;
256+ labelCell . scope = 'row' ;
257+ labelCell . innerHTML = `<div class="comparison-label">Your timezone</div><div class="comparison-zone">${ localZone } </div>` ;
258+
259+ const timeCell = document . createElement ( 'td' ) ;
260+ timeCell . className = 'comparison-time' ;
261+
262+ const actionCell = document . createElement ( 'td' ) ;
263+ actionCell . className = 'comparison-actions-cell' ;
264+
265+ row . append ( labelCell , timeCell , actionCell ) ;
266+ comparisonRowsContainer . append ( row ) ;
267+
268+ localRowEntry = {
269+ getZone : ( ) => localZone ,
270+ timeCell,
271+ updateLabel ( zone ) {
272+ labelCell . innerHTML = `<div class="comparison-label">Your timezone</div><div class="comparison-zone">${ zone } </div>` ;
273+ }
274+ } ;
275+
276+ comparisonRows . push ( localRowEntry ) ;
277+ }
278+
279+ function addCustomComparisonRow ( initialZone ) {
280+ ensureOptionForZone ( initialZone ) ;
281+ const row = document . createElement ( 'tr' ) ;
282+ row . className = 'comparison-row comparison-row-custom' ;
283+
284+ const labelCell = document . createElement ( 'th' ) ;
285+ labelCell . scope = 'row' ;
286+ const select = createTimezoneSelectElement ( ) ;
287+ select . value = initialZone ;
288+ if ( initialZone && select . value !== initialZone ) {
289+ const option = document . createElement ( 'option' ) ;
290+ option . value = initialZone ;
291+ option . textContent = initialZone ;
292+ select . append ( option ) ;
293+ select . value = initialZone ;
294+ }
295+ const selectWrapper = document . createElement ( 'div' ) ;
296+ selectWrapper . className = 'comparison-select-wrapper' ;
297+ selectWrapper . append ( select ) ;
298+ labelCell . append ( selectWrapper ) ;
299+
300+ const timeCell = document . createElement ( 'td' ) ;
301+ timeCell . className = 'comparison-time' ;
302+
303+ const actionCell = document . createElement ( 'td' ) ;
304+ actionCell . className = 'comparison-actions-cell' ;
305+ const removeButton = document . createElement ( 'button' ) ;
306+ removeButton . type = 'button' ;
307+ removeButton . className = 'link-button' ;
308+ removeButton . textContent = 'Remove' ;
309+ removeButton . addEventListener ( 'click' , ( ) => {
310+ row . remove ( ) ;
311+ const index = comparisonRows . findIndex ( entry => entry . row === row ) ;
312+ if ( index >= 0 ) {
313+ comparisonRows . splice ( index , 1 ) ;
314+ }
315+ updateComparison ( true ) ;
316+ } ) ;
317+ actionCell . append ( removeButton ) ;
318+
319+ row . append ( labelCell , timeCell , actionCell ) ;
320+ comparisonRowsContainer . append ( row ) ;
321+
322+ const entry = {
323+ row,
324+ getZone : ( ) => select . value ,
325+ timeCell
326+ } ;
327+ comparisonRows . push ( entry ) ;
328+
329+ select . addEventListener ( 'change' , ( ) => updateComparison ( ) ) ;
330+ return row ;
331+ }
332+
333+ function updateLocalZone ( zone , { updateSelect = true , updateComparison = true } = { } ) {
334+ if ( ! zone ) {
335+ updateInferredTimezoneDisplay ( 'Unavailable' ) ;
336+ return ;
337+ }
338+
339+ const previousZone = localZone ;
340+ const zoneChanged = zone !== localZone ;
341+ localZone = zone ;
342+
343+ if ( localRowEntry ) {
344+ localRowEntry . updateLabel ( zone ) ;
345+ }
346+
347+ updateInferredTimezoneDisplay ( zone ) ;
348+ ensureOptionForZone ( zone ) ;
349+
350+ if ( updateSelect && ! timezoneManuallySet && ( timezoneSelect . value === previousZone || ! timezoneSelect . value ) ) {
351+ timezoneSelect . value = zone ;
352+ }
353+
354+ if ( updateComparison && ( zoneChanged || ! comparisonTable . hidden ) ) {
355+ updateComparison ( ) ;
356+ }
357+ }
358+
147359 function formatOffset ( minutes ) {
148360 const sign = minutes >= 0 ? '+' : '-' ;
149361 const absolute = Math . abs ( minutes ) ;
@@ -189,6 +401,7 @@ <h2 id="share-heading">Shareable link</h2>
189401
190402 if ( ! datetimeValue || ! selectedZone ) {
191403 comparisonOutput . textContent = 'Choose a date, time, and timezone to see how it translates to your current timezone.' ;
404+ comparisonTable . hidden = true ;
192405 shareableUrlInput . value = location . href ;
193406 return ;
194407 }
@@ -197,18 +410,32 @@ <h2 id="share-heading">Shareable link</h2>
197410
198411 if ( ! momentInSelectedZone . isValid ) {
199412 comparisonOutput . textContent = 'The selected date or timezone could not be parsed. Please check your inputs.' ;
413+ comparisonTable . hidden = true ;
200414 shareableUrlInput . value = location . href ;
201415 return ;
202416 }
203417
204- const momentInLocalZone = momentInSelectedZone . setZone ( localZone ) ;
205418 const selectedOffset = formatOffset ( momentInSelectedZone . offset ) ;
206- const localOffset = formatOffset ( momentInLocalZone . offset ) ;
207419
208420 const sourceText = `${ formatDateTime ( momentInSelectedZone ) } in ${ momentInSelectedZone . zoneName } (${ selectedOffset } )` ;
209- const localText = `${ formatDateTime ( momentInLocalZone ) } in ${ localZone } (${ localOffset } )` ;
421+ comparisonOutput . textContent = `${ sourceText } . Here's how that moment translates across the selected timezones:` ;
422+ comparisonTable . hidden = false ;
423+
424+ for ( const entry of comparisonRows ) {
425+ const zoneName = entry . getZone ( ) ;
426+ if ( ! zoneName ) {
427+ entry . timeCell . textContent = 'Pick a timezone to see the converted time.' ;
428+ continue ;
429+ }
210430
211- comparisonOutput . textContent = `${ sourceText } will be ${ localText } .` ;
431+ const zoned = momentInSelectedZone . setZone ( zoneName ) ;
432+ if ( ! zoned . isValid ) {
433+ entry . timeCell . textContent = 'Unable to resolve that timezone.' ;
434+ continue ;
435+ }
436+
437+ entry . timeCell . textContent = `${ formatDateTime ( zoned ) } (${ formatOffset ( zoned . offset ) } )` ;
438+ }
212439
213440 const shareUrl = buildShareableUrl ( momentInSelectedZone ) ;
214441 shareableUrlInput . value = shareUrl ;
@@ -275,24 +502,56 @@ <h2 id="share-heading">Shareable link</h2>
275502 if ( timezoneParam ) {
276503 ensureOptionForZone ( timezoneParam ) ;
277504 timezoneSelect . value = timezoneParam ;
505+ timezoneManuallySet = true ;
278506 }
279507
280508 if ( datetimeInput . value && timezoneSelect . value ) {
281509 updateComparison ( true ) ;
282510 }
283511 }
284512
513+ async function detectTimezoneFromIp ( ) {
514+ let detectedZone = null ;
515+ try {
516+ const response = await fetch ( 'https://worldtimeapi.org/api/ip' ) ;
517+ if ( response . ok ) {
518+ const data = await response . json ( ) ;
519+ if ( data && typeof data . timezone === 'string' && data . timezone ) {
520+ detectedZone = data . timezone ;
521+ }
522+ }
523+ } catch ( error ) {
524+ // Ignore network or parsing issues and fall back to browser detection
525+ }
526+
527+ if ( detectedZone ) {
528+ updateLocalZone ( detectedZone ) ;
529+ } else {
530+ updateLocalZone ( fallbackZone , { updateSelect : false , updateComparison : false } ) ;
531+ }
532+ }
533+
285534 populateTimezones ( ) ;
535+ addLocalComparisonRow ( ) ;
286536 timezoneSelect . value = localZone ;
287537 datetimeInput . value = DateTime . now ( ) . toISO ( { suppressSeconds : true , suppressMilliseconds : true } ) . slice ( 0 , 16 ) ;
288538 updateComparison ( true ) ;
289539
290540 applyQueryParameters ( ) ;
541+ detectTimezoneFromIp ( ) ;
291542
292543 datetimeInput . addEventListener ( 'input' , ( ) => updateComparison ( ) ) ;
293544 datetimeInput . addEventListener ( 'change' , ( ) => updateComparison ( ) ) ;
294- timezoneSelect . addEventListener ( 'change' , ( ) => updateComparison ( ) ) ;
545+ timezoneSelect . addEventListener ( 'change' , ( ) => {
546+ timezoneManuallySet = true ;
547+ updateComparison ( ) ;
548+ } ) ;
295549 copyButton . addEventListener ( 'click' , copyLink ) ;
550+ addComparisonRowButton . addEventListener ( 'click' , ( ) => {
551+ const defaultZone = timezoneSelect . value || localZone ;
552+ addCustomComparisonRow ( defaultZone ) ;
553+ updateComparison ( ) ;
554+ } ) ;
296555 } ) ( ) ;
297556 </ script >
298557</ body >
0 commit comments