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