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
13function setUserCookie($name, $value) {
14 setcookie($name, $value, time() + 60 * 60 * 24 * 365, '/');
15 $_COOKIE[$name] = $value;
16}
17
18$popTZ = [
19 'America/Los_Angeles',
20 'America/New_York',
21 'Europe/London',
22 'Europe/Paris',
23 'Asia/Dubai',
24 'Asia/Jakarta',
25 'Asia/Tokyo',
26 'Australia/Sydney',
27 'UTC'
28];
29
30// User sets timezone cookie
31if (isset($_POST['user-tz']) &&
32 in_array($_POST['user-tz'], timezone_identifiers_list(DateTimeZone::ALL))) {
33 setUserCookie('user-tz', $_POST['user-tz']);
34}
35
36// User sets 24 hours
37if (isset($_POST['user-24'])) {
38 setUserCookie('user-24', '1');
39}
40
41// If had JS enabled but doesn't now, set has-js to 0
42if (isset($_POST['no-js'])) {
43 setUserCookie('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} elseif ($_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/Rita/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 } elseif (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 } elseif ($_COOKIE['has-js'] !== '1') {
128?>
129 Please select your local time zone <small>(or enable JavaScript)</small>:<br/>
130 <form method="post">
131 <select required id="user-tz" name="user-tz">
132 <option selected disabled>(pick one)</option>
133 <optgroup label="Popular">
134<?php foreach($popTZ as $tz) { ?>
135 <option value="<?= $tz ?>">
136 <?= str_replace('_', ' ', $tz) ?> (<?= getOffset($tz) ?>)
137 </option>
138<?php } ?>
139 </optgroup>
140 <optgroup label="All">
141<?php foreach(timezone_identifiers_list(DateTimeZone::ALL) as $tz) { ?>
142 <option value="<?= $tz ?>">
143 <?= str_replace('_', ' ', $tz) ?> (<?= getOffset($tz) ?>)
144 </option>
145<?php } ?>
146 </optgroup>
147 </select>
148 <br/>
149 <input type="checkbox" id="user-24" name="user-24"/>
150 <label for="user-24"><span>24h format</span></label>
151 <br/>
152 <button type="submit">Submit</button>
153 </form>
154<?php } else { ?>
155 Did you turn off Javascript?<br/>
156 <form method="post">
157 <input type="hidden" name="no-js" value="1"/>
158 <button type="submit">Reload to no-JS</button>
159 </form>
160<?php } ?>
161 </noscript>
162 </h2>
163
164 <br/>
165
166 <h3>In popular time zones</h3>
167 <div class="tz-table">
168<?php foreach($popTZ as $tz) { ?>
169 <div class="tz-table-element">
170 <strong><?= $tz === 'UTC' ? 'UTC' : str_replace('_', ' ', explode('/', $tz, 2)[1]) . ' (' . getOffset($tz) . ')' ?></strong>
171 <br/>
172 <span><?= toTZ($dt, $tz) ?></span>
173 </div>
174<?php } ?>
175 </div>
176
177 <div class="share">
178 <input type="hidden" id="url" value="https://tz.rita.moe<?= $_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 const shareButton = document.createElement('button')
199 shareButton.type = 'button'
200 shareButton.textContent = 'Share'
201 shareButton.onclick = shareURL
202 document.querySelector('.share').appendChild(shareButton)
203
204 // Our share URL function
205 function shareURL () {
206 if (navigator.canShare) {
207 // Use native navigator share
208 navigator.share({
209 url: 'https://tz.rita.moe<?= $_SERVER['REQUEST_URI'] ?>'
210 })
211 } else if (navigator.clipboard) {
212 // Use Clipboard API
213 navigator.clipboard.writeText('https://tz.rita.moe<?= $_SERVER['REQUEST_URI'] ?>')
214 .then(() => {
215 alert('URL copied to clipboard!')
216 })
217 .catch((err) => {
218 console.log('Clipboard API failed, fallback.', err)
219 // Use old input copy trick
220 commandCopy()
221 })
222 } else {
223 // Use old input copy trick
224 commandCopy()
225 }
226 }
227
228 // Copy content from the hidden input
229 function commandCopy () {
230 shareTextInput = document.querySelector('#url')
231 shareTextInput.focus()
232 shareTextInput.select()
233
234 if (document.execCommand('copy')) {
235 alert('URL copied to clipboard!')
236 } else {
237 // Fallback to prompt with URL
238 prompt('Share this URL:', document.querySelector('#url').value)
239 }
240 }
241 </script>
242<?php } else { ?>
243 <p>
244 Fill out this form to make a page that shows a set date and time in the visitor's time zone.<br/>
245 It will also show the time in other popular time zones.
246 </p>
247
248 <form action="/" method="post">
249 <noscript>
250 <small>(If you only see a plain input, use the "YYYY-MM-DD HH:MM" format.)</small>
251 <br/>
252 </noscript>
253 <label for="datetime">Date and Time:</label>
254 <input required type="datetime-local" id="datetime" name="datetime"/>
255
256 <br/>
257
258 <label for="timezone">Time Zone:</label>
259 <select required id="timezone" name="timezone">
260 <option selected disabled>(pick one)</option>
261 <optgroup label="Popular">
262<?php foreach($popTZ as $tz) { ?>
263 <option value="<?= $tz ?>">
264 <?= str_replace('_', ' ', $tz) ?> (<?= getOffset($tz) ?>)
265 </option>
266<?php } ?>
267 </optgroup>
268 <optgroup label="All">
269<?php foreach(timezone_identifiers_list(DateTimeZone::ALL) as $tz) { ?>
270 <option value="<?= $tz ?>">
271 <?= str_replace('_', ' ', $tz) ?> (<?= getOffset($tz) ?>)
272 </option>
273<?php } ?>
274 </optgroup>
275 </select>
276
277 <br/>
278
279 <button type="submit">Submit</button>
280 </form>
281
282 <script>
283 // Get a date object
284 const dt = new Date()
285
286 // Set the user's offset and enable option
287 const userEl = document.querySelector('option[disabled]')
288 userEl.value = dt.getTimezoneOffset() * -1
289 userEl.innerHTML = '(use my timezone)'
290 userEl.disabled = false
291 userEl.selected = true
292 </script>
293<?php } ?>
294 </main>
295
296 <footer>
297 © <?= date('Y') ?> rita.moe - Made with 🍮<br/>
298 <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)
299 </footer>
300<?php if ($_COOKIE['has-js'] !== '1') { ?>
301
302 <script>
303 // Save on the document size if user has JavaScript
304 const date = new Date()
305 date.setTime(date.getTime() + (1000 * 60 * 60 * 24 * 365))
306 document.cookie = 'has-js=1; expires=' + date.toUTCString() + '; path=/'
307 </script>
308<?php } ?>
309</body>
310</html>