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