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