1<?php
2function getOffset($tz) {
3 $tz = new DateTime('now', new DateTimeZone($tz));
4 $offset = $tz->format('T');
5 return str_starts_with($offset, '-') || str_starts_with($offset, '+') ? 'UTC' . $offset : $offset;
6}
7
8function toTZ($dt, $tz, $format='Y-m-d H:i') {
9 $tz = new DateTimeZone($tz);
10 return $dt->setTimezone($tz)->format($format);
11}
12
13$popTZ = [
14 'America/Los_Angeles',
15 'America/New_York',
16 'Europe/London',
17 'Europe/Paris',
18 'Asia/Dubai',
19 'Asia/Bangkok',
20 'Asia/Tokyo',
21 'Australia/Sydney',
22 'UTC'
23];
24
25// User sets timezone cookie
26if (isset($_POST['user-tz'])) {
27 // Just check it's valid
28 if (in_array($_POST['user-tz'], timezone_identifiers_list(DateTimeZone::ALL))) {
29 setcookie('user-tz', $_POST['user-tz'], time() + 60 * 60 * 24 * 365);
30 $_COOKIE['user-tz'] = $_POST['user-tz'];
31 }
32}
33
34// User sets 24 hours
35if (isset($_POST['user-24'])) {
36 setcookie('user-24', '1', time() + 60 * 60 * 24 * 365);
37 $_COOKIE['user-24'] = '1';
38}
39
40// If had JS enabled but doesn't now, set has-js to 0
41if (isset($_POST['no-js'])) {
42 setcookie('has-js', '0', time() + 60 * 60 * 24 * 365);
43 $_COOKIE['has-js'] = '0';
44}
45
46// Micro-routing
47if (isset($_POST['datetime']) && isset($_POST['timezone'])) {
48 // -- Redirect to submitted date
49 $postTZ = $_POST['timezone'];
50
51 // Handle if timezone is an offset
52 if (is_numeric($_POST['timezone'])) {
53 $isNeg = str_starts_with($postTZ, '-');
54 $h = str_pad(abs(intdiv($postTZ, 60)), 2, '0', STR_PAD_LEFT);
55 $m = str_pad($postTZ % 60, 2, '0', STR_PAD_LEFT);
56 $postTZ = ($isNeg ? '-' : '+') . $h . $m;
57 }
58
59 // Make our date object
60 $dateObj = new DateTime($_POST['datetime'], new DateTimeZone($postTZ));
61
62 // Redirect, with the "+" replaced by "_"
63 header('Location: /' . str_replace('+', '_', $dateObj->format('c')));
64 exit;
65} else if ($_SERVER['REQUEST_URI'] !== '/') {
66 // Remove leading "/"
67 $req = substr($_SERVER['REQUEST_URI'], 1);
68
69 // -- Show date infos
70 // First check if date is following the format
71 $re = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[\_|-]\d{2}:\d{2}$/';
72 if (!preg_match($re, $req)) {
73 http_response_code(404);
74 $error = 'Date is not valid, wrong format, or file not found.';
75 } else {
76 // Make our date object, convert back "_" to "+"
77 $dt = new DateTime(str_replace('_', '+', $req));
78 }
79}
80?>
81<!DOCTYPE html>
82<!--
83 Source code: https://git.rita.moe/kody/tz
84 License: MIT
85-->
86<html lang="en">
87<head>
88 <meta charset="UTF-8">
89 <meta name="viewport" content="width=device-width, initial-scale=1.0">
90 <title>Time Zones Are Hard<?= isset($req) ? ' - ' . str_replace('_', '+', $req) : '' ?></title>
91 <link rel="stylesheet" href="/css/styles.css">
92<?php if (isset($dt)) { ?>
93 <meta name="robots" content="noindex,noarchive">
94 <script src="/js/dayjs.min.js"></script>
95 <script src="/js/relativeTime.js"></script>
96 <script>
97 dayjs.extend(window.dayjs_plugin_relativeTime)
98 </script>
99<?php } ?>
100 <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png">
101 <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png">
102 <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png">
103 <link rel="manifest" href="/site.webmanifest">
104 <link rel="mask-icon" href="/img/safari-pinned-tab.svg" color="#111111">
105 <meta name="msapplication-TileColor" content="#111111">
106 <meta name="theme-color" content="#111111">
107</head>
108<body>
109 <header>
110 <h1><a href="/"><?= file_get_contents('./img/clock.svg'); ?> Time Zones Are Hard</a></h1>
111 </header>
112
113 <main>
114<?php if (isset($error)) { ?>
115 <h2 class="error">Error: <?= $error ?></h2>
116 <form action="/" method="post">
117 <button type="submit">Go back home</button>
118 </form>
119<?php } else if (isset($dt)) { ?>
120 <h2 class="local">
121 <noscript>
122<?php
123 if (isset($_COOKIE['user-tz'])) {
124 $hm = $_COOKIE['user-24'] === '1' ? 'H:i' : 'h:i a';
125 // I put spaces just so it looks nice in the source code
126 echo ' ' . toTZ($dt, $_COOKIE['user-tz'], 'l \t\h\e jS \o\f F, Y \a\t ' . $hm) . PHP_EOL;
127 } else if ($_COOKIE['has-js'] !== '1') {
128?>
129 Sorry, we use JavaScript to detect your local time.<br/>
130 Enable it or select your time zone:<br/>
131 <form method="post">
132 <select required id="user-tz" name="user-tz">
133 <option selected disabled>(pick one)</option>
134 <optgroup label="Popular">
135<?php foreach($popTZ as $tz) { ?>
136 <option value="<?= $tz ?>">
137 <?= str_replace('_', ' ', $tz) ?> (<?= getOffset($tz) ?>)
138 </option>
139<?php } ?>
140 </optgroup>
141 <optgroup label="All">
142<?php foreach(timezone_identifiers_list(DateTimeZone::ALL) as $tz) { ?>
143 <option value="<?= $tz ?>">
144 <?= str_replace('_', ' ', $tz) ?> (<?= getOffset($tz) ?>)
145 </option>
146<?php } ?>
147 </optgroup>
148 </select>
149 <button type="submit">Submit</button><br/>
150 <input type="checkbox" id="user-24" name="user-24"/>
151 <label for="user-24"><span>24h format</span></label>
152 </form>
153<?php } else { ?>
154 Did you turn off Javascript?<br/>
155 <form method="post">
156 <input type="hidden" name="no-js" value="1"/>
157 <button type="submit">Reload to no-JS</button>
158 </form>
159<?php } ?>
160 </noscript>
161 </h2>
162
163 <br/>
164
165 <h3>In popular time zones</h3>
166 <div class="tz-table">
167<?php foreach($popTZ as $tz) { ?>
168 <div class="tz-table-element">
169 <strong><?= $tz === 'UTC' ? 'UTC' : str_replace('_', ' ', explode('/', $tz, 2)[1]) . ' (' . getOffset($tz) . ')' ?></strong>
170 <br/>
171 <span><?= toTZ($dt, $tz) ?></span>
172 </div>
173<?php } ?>
174 </div>
175
176 <div class="share">
177 <button type="button" onclick="shareURL()">Share</button>
178 <input type="hidden" id="url" value="https://tz.kdy.ch<?= $_SERVER['REQUEST_URI'] ?>"/>
179 </div>
180
181 <script>
182 // Make our day.js object
183 const djs = dayjs('<?= $dt->format('c') ?>')
184
185 // Use the browser's formating to local time
186 const localDT = djs.toDate().toLocaleString(undefined, {
187 dateStyle: 'full',
188 timeStyle: 'short'
189 })
190
191 // Get the relative time
192 const relative = djs.fromNow()
193
194 // Display
195 document.querySelector('.local').innerHTML = localDT + '<br/><span>(' + relative + ')</span>'
196
197 // Display share button
198 document.querySelector('.share').style.display = 'block'
199
200 // Our share URL function
201 function shareURL () {
202 if (navigator.canShare) {
203 // Use native navigator share
204 navigator.share({
205 url: 'https://tz.kdy.ch<?= $_SERVER['REQUEST_URI'] ?>'
206 })
207 } else if (navigator.clipboard) {
208 // Use Clipboard API
209 navigator.clipboard.writeText('https://tz.kdy.ch<?= $_SERVER['REQUEST_URI'] ?>')
210 .then(() => {
211 alert('URL copied to clipboard!')
212 })
213 .catch((err) => {
214 console.log('Clipboard API failed, fallback.', err)
215 // Use old input copy trick
216 commandCopy()
217 })
218 } else {
219 // Use old input copy trick
220 commandCopy()
221 }
222 }
223
224 // Copy content from the hidden input
225 function commandCopy () {
226 shareTextInput = document.querySelector('#url')
227 shareTextInput.focus()
228 shareTextInput.select()
229
230 if (document.execCommand('copy')) {
231 alert('URL copied to clipboard!')
232 } else {
233 // Fallback to prompt with URL
234 prompt('Share this URL:', document.querySelector('#url').value)
235 }
236 }
237 </script>
238<?php } else { ?>
239 <p>
240 Fill out this form to make a page that shows a set date and time in the visitor's time zone.<br/>
241 It will also show the time in other popular time zones.
242 </p>
243
244 <form action="/" method="post">
245 <label for="datetime">Date and Time:</label>
246 <input required type="datetime-local" id="datetime" name="datetime"/>
247
248 <br/>
249
250 <label for="timezone">Time Zone:</label>
251 <select required id="timezone" name="timezone">
252 <option selected disabled>(pick one)</option>
253 <optgroup label="Popular">
254<?php foreach($popTZ as $tz) { ?>
255 <option value="<?= $tz ?>">
256 <?= str_replace('_', ' ', $tz) ?> (<?= getOffset($tz) ?>)
257 </option>
258<?php } ?>
259 </optgroup>
260 <optgroup label="All">
261<?php foreach(timezone_identifiers_list(DateTimeZone::ALL) as $tz) { ?>
262 <option value="<?= $tz ?>">
263 <?= str_replace('_', ' ', $tz) ?> (<?= getOffset($tz) ?>)
264 </option>
265<?php } ?>
266 </optgroup>
267 </select>
268
269 <br/>
270
271 <button type="submit">Submit</button>
272 </form>
273
274 <script>
275 // Get a date object
276 const dt = new Date()
277
278 // Set the user's offset and enable option
279 const userEl = document.querySelector('option[disabled]')
280 userEl.value = dt.getTimezoneOffset() * -1
281 userEl.innerHTML = '(use my timezone)'
282 userEl.disabled = false
283 userEl.selected = true
284 </script>
285<?php } ?>
286 </main>
287
288 <footer>
289 © <?= date('Y') ?> Kody - Made with 🍮<br/>
290 <a href="https://thenounproject.com/icon/wall-clock-5456766/" target="_blank" title="Wall Clock Icon" rel="noopener">Wall Clock by Basicon</a> from Noun Project (CC BY 3.0)
291 </footer>
292<?php if ($_COOKIE['has-js'] !== '1') { ?>
293
294 <script>
295 // Save on the document size if user has JavaScript
296 const date = new Date()
297 date.setTime(date.getTime() + (1000 * 60 * 60 * 24 * 365))
298 document.cookie = 'has-js=1; expires=' + date.toUTCString() + '; path=/'
299 </script>
300<?php } ?>
301</body>
302</html>