Skip to content

Commit 406566b

Browse files
Add timezone comparison link generator (#12)
1 parent f5a0319 commit 406566b

2 files changed

Lines changed: 294 additions & 0 deletions

File tree

what-time-is-it-for-me.docs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This tool lets you pick a date, time, and source timezone to generate a shareable link. Opening that link shows how the chosen moment translates into the visitor's current timezone, making it simple to coordinate across regions.

what-time-is-it-for-me.html

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>What time is it for me?</title>
7+
<link rel="stylesheet" href="styles.css">
8+
<style>
9+
body {
10+
max-width: 960px;
11+
margin: 0 auto;
12+
padding: 24px 20px 48px;
13+
}
14+
15+
main {
16+
display: grid;
17+
gap: 1.5rem;
18+
}
19+
20+
.tool-card {
21+
padding: clamp(1.25rem, 3vw, 2rem);
22+
}
23+
24+
.tool-actions {
25+
display: flex;
26+
flex-wrap: wrap;
27+
gap: 0.75rem;
28+
align-items: center;
29+
}
30+
31+
.status-message {
32+
font-size: 0.95rem;
33+
color: var(--text-muted);
34+
}
35+
36+
.inline-field {
37+
display: flex;
38+
flex-direction: column;
39+
}
40+
41+
@media (max-width: 720px) {
42+
body {
43+
padding: 20px 16px 40px;
44+
}
45+
}
46+
</style>
47+
</head>
48+
<body>
49+
<header>
50+
<h1>What time is it for me?</h1>
51+
<p class="lead">Pick a date, time, and timezone to create a shareable link that shows what that moment looks like from your timezone.</p>
52+
</header>
53+
54+
<main>
55+
<section class="surface tool-card" aria-labelledby="selection-heading">
56+
<h2 id="selection-heading">Pick a moment</h2>
57+
<form id="moment-form">
58+
<div class="form-group">
59+
<label for="datetime-input">Date and time</label>
60+
<input id="datetime-input" name="datetime" type="datetime-local" required>
61+
</div>
62+
<div class="form-group">
63+
<label for="timezone-select">Timezone</label>
64+
<select id="timezone-select" name="timezone" required>
65+
</select>
66+
</div>
67+
<div class="tool-actions">
68+
<button type="button" id="copy-link-button">Copy link with this moment</button>
69+
<span class="status-message" id="copy-status" role="status" aria-live="polite"></span>
70+
</div>
71+
</form>
72+
</section>
73+
74+
<section class="surface tool-card" aria-live="polite" aria-labelledby="comparison-heading">
75+
<h2 id="comparison-heading">Comparison</h2>
76+
<p id="comparison-output" class="lead">Choose a date, time, and timezone to see how it translates to your current timezone.</p>
77+
</section>
78+
79+
<section class="surface tool-card" aria-labelledby="share-heading">
80+
<h2 id="share-heading">Shareable link</h2>
81+
<p>Use the button above to copy a link that includes this moment as URL parameters. Anyone who opens the link will see what that time looks like from their own timezone.</p>
82+
<div class="form-group">
83+
<label for="shareable-url">Preview of copied link</label>
84+
<input id="shareable-url" type="url" readonly value="">
85+
</div>
86+
</section>
87+
</main>
88+
89+
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4/build/global/luxon.min.js" integrity="sha384-qbz0zdHqsBd0P3gGwICwbg0Ob9wZjC1IcWqbUb9SkSLyWFSbM8hwx2p/9DoUom4Q" crossorigin="anonymous"></script>
90+
<script>
91+
(function () {
92+
const { DateTime } = luxon;
93+
const datetimeInput = document.getElementById('datetime-input');
94+
const timezoneSelect = document.getElementById('timezone-select');
95+
const comparisonOutput = document.getElementById('comparison-output');
96+
const copyButton = document.getElementById('copy-link-button');
97+
const copyStatus = document.getElementById('copy-status');
98+
const shareableUrlInput = document.getElementById('shareable-url');
99+
100+
const resolvedZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
101+
const localZone = resolvedZone || DateTime.local().zoneName || 'UTC';
102+
103+
function ensureOptionForZone(zone) {
104+
const exists = Array.from(timezoneSelect.options).some(option => option.value === zone);
105+
if (!exists && zone) {
106+
const option = document.createElement('option');
107+
option.value = zone;
108+
option.textContent = zone;
109+
timezoneSelect.append(option);
110+
}
111+
}
112+
113+
function populateTimezones() {
114+
let zones = [];
115+
if (typeof Intl.supportedValuesOf === 'function') {
116+
try {
117+
zones = Intl.supportedValuesOf('timeZone');
118+
} catch (error) {
119+
// Ignore and fall back
120+
}
121+
}
122+
123+
if (!zones.length) {
124+
zones = [
125+
'UTC', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Madrid',
126+
'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles',
127+
'Asia/Tokyo', 'Asia/Shanghai', 'Asia/Kolkata', 'Australia/Sydney'
128+
];
129+
}
130+
131+
for (const zone of zones) {
132+
const option = document.createElement('option');
133+
option.value = zone;
134+
option.textContent = zone;
135+
timezoneSelect.append(option);
136+
}
137+
138+
ensureOptionForZone(localZone);
139+
}
140+
141+
function formatOffset(minutes) {
142+
const sign = minutes >= 0 ? '+' : '-';
143+
const absolute = Math.abs(minutes);
144+
const hours = String(Math.floor(absolute / 60)).padStart(2, '0');
145+
const mins = String(absolute % 60).padStart(2, '0');
146+
return `UTC${sign}${hours}:${mins}`;
147+
}
148+
149+
function formatDateTime(dt) {
150+
const dayNumber = Number(dt.toFormat('d'));
151+
const suffix = (function (n) {
152+
const tens = n % 100;
153+
if (tens >= 11 && tens <= 13) {
154+
return 'th';
155+
}
156+
switch (n % 10) {
157+
case 1: return 'st';
158+
case 2: return 'nd';
159+
case 3: return 'rd';
160+
default: return 'th';
161+
}
162+
})(dayNumber);
163+
164+
const formattedDate = dt.toFormat(`d'${suffix}' MMMM yyyy`);
165+
const formattedTime = dt.toFormat('h:mm a');
166+
return `${formattedDate}, ${formattedTime}`;
167+
}
168+
169+
function buildShareableUrl(dt) {
170+
if (!dt || !dt.isValid) {
171+
return location.href;
172+
}
173+
const url = new URL(location.href);
174+
url.searchParams.set('date', dt.toFormat('yyyy-LL-dd'));
175+
url.searchParams.set('time', dt.toFormat('HH:mm'));
176+
url.searchParams.set('timezone', dt.zoneName);
177+
return url.toString();
178+
}
179+
180+
function updateComparison(fromUrl = false) {
181+
const datetimeValue = datetimeInput.value;
182+
const selectedZone = timezoneSelect.value;
183+
184+
if (!datetimeValue || !selectedZone) {
185+
comparisonOutput.textContent = 'Choose a date, time, and timezone to see how it translates to your current timezone.';
186+
shareableUrlInput.value = location.href;
187+
return;
188+
}
189+
190+
const momentInSelectedZone = DateTime.fromISO(datetimeValue, { zone: selectedZone });
191+
192+
if (!momentInSelectedZone.isValid) {
193+
comparisonOutput.textContent = 'The selected date or timezone could not be parsed. Please check your inputs.';
194+
shareableUrlInput.value = location.href;
195+
return;
196+
}
197+
198+
const momentInLocalZone = momentInSelectedZone.setZone(localZone);
199+
const selectedOffset = formatOffset(momentInSelectedZone.offset);
200+
const localOffset = formatOffset(momentInLocalZone.offset);
201+
202+
const sourceText = `${formatDateTime(momentInSelectedZone)} in ${momentInSelectedZone.zoneName} (${selectedOffset})`;
203+
const localText = `${formatDateTime(momentInLocalZone)} in ${localZone} (${localOffset})`;
204+
205+
comparisonOutput.textContent = `${sourceText} will be ${localText}.`;
206+
207+
const shareUrl = buildShareableUrl(momentInSelectedZone);
208+
shareableUrlInput.value = shareUrl;
209+
copyStatus.textContent = '';
210+
211+
if (!fromUrl) {
212+
const url = new URL(location.href);
213+
url.searchParams.set('date', momentInSelectedZone.toFormat('yyyy-LL-dd'));
214+
url.searchParams.set('time', momentInSelectedZone.toFormat('HH:mm'));
215+
url.searchParams.set('timezone', momentInSelectedZone.zoneName);
216+
window.history.replaceState({}, '', url);
217+
}
218+
}
219+
220+
async function copyLink() {
221+
const datetimeValue = datetimeInput.value;
222+
const selectedZone = timezoneSelect.value;
223+
224+
if (!datetimeValue || !selectedZone) {
225+
copyStatus.textContent = 'Select a date, time, and timezone first.';
226+
return;
227+
}
228+
229+
const momentInSelectedZone = DateTime.fromISO(datetimeValue, { zone: selectedZone });
230+
if (!momentInSelectedZone.isValid) {
231+
copyStatus.textContent = 'Unable to copy—please check the selected date and timezone.';
232+
return;
233+
}
234+
235+
const shareUrl = buildShareableUrl(momentInSelectedZone);
236+
shareableUrlInput.value = shareUrl;
237+
238+
try {
239+
if (navigator.clipboard && navigator.clipboard.writeText) {
240+
await navigator.clipboard.writeText(shareUrl);
241+
} else {
242+
const textarea = document.createElement('textarea');
243+
textarea.value = shareUrl;
244+
textarea.setAttribute('readonly', '');
245+
textarea.style.position = 'absolute';
246+
textarea.style.left = '-9999px';
247+
document.body.append(textarea);
248+
textarea.select();
249+
document.execCommand('copy');
250+
textarea.remove();
251+
}
252+
copyStatus.textContent = 'Link copied to clipboard!';
253+
} catch (error) {
254+
copyStatus.textContent = 'Copy failed—select and copy the link manually.';
255+
}
256+
}
257+
258+
function applyQueryParameters() {
259+
const params = new URLSearchParams(window.location.search);
260+
const dateParam = params.get('date');
261+
const timeParam = params.get('time');
262+
const timezoneParam = params.get('timezone');
263+
264+
if (dateParam && timeParam) {
265+
const combined = `${dateParam}T${timeParam}`;
266+
datetimeInput.value = combined;
267+
}
268+
269+
if (timezoneParam) {
270+
ensureOptionForZone(timezoneParam);
271+
timezoneSelect.value = timezoneParam;
272+
}
273+
274+
if (datetimeInput.value && timezoneSelect.value) {
275+
updateComparison(true);
276+
}
277+
}
278+
279+
populateTimezones();
280+
timezoneSelect.value = localZone;
281+
datetimeInput.value = DateTime.now().toISO({ suppressSeconds: true, suppressMilliseconds: true }).slice(0, 16);
282+
updateComparison(true);
283+
284+
applyQueryParameters();
285+
286+
datetimeInput.addEventListener('input', () => updateComparison());
287+
datetimeInput.addEventListener('change', () => updateComparison());
288+
timezoneSelect.addEventListener('change', () => updateComparison());
289+
copyButton.addEventListener('click', copyLink);
290+
})();
291+
</script>
292+
</body>
293+
</html>

0 commit comments

Comments
 (0)