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