Time Zones Are Hard - https://tz.rita.moe
at main 11 kB view raw
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 &copy; <?= 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>