Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/TEMPLATES/tf_wrapper.html
Diff
/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/TEMPLATES/tf_wrapper.html
added on local at 2026-07-11 08:13:39
Added
+345
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 38df4e9c3a88
to 38df4e9c3a88
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.| 1 | <!doctype html> | |
| 2 | <html lang="en" class="$tvars.html_class"> | |
| 3 | <head> | |
| 4 | <meta charset="utf-8"> | |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| 6 | <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0"> | |
| 7 | <meta http-equiv="Pragma" content="no-cache"> | |
| 8 | <meta http-equiv="Expires" content="0"> | |
| 9 | <title>$tvars.title · $tvars.website_title</title> | |
| 10 | $tvars.meta_tags | |
| 11 | <link rel="stylesheet" href="$tvars.assets_css/site.css?v=2026-07-02-padfix"> | |
| 12 | $tvars.extra_css | |
| 13 | <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect x='0.75' y='0.75' width='30.5' height='30.5' rx='7.5' fill='%232c0508' stroke='white' stroke-width='1.5'/><text x='16' y='22' font-family='Arial,Helvetica,sans-serif' font-size='17' font-weight='700' fill='white' text-anchor='middle'>PT</text></svg>"> | |
| 14 | <script src="/assets/js/fingerprint.js" defer></script> | |
| 15 | </head> | |
| 16 | <body> | |
| 17 | $tvars.impersonation_banner | |
| 18 | <div class="app-shell"> | |
| 19 | <div class="sidebar-backdrop" id="sidebarBackdrop"></div> | |
| 20 | <aside class="sidebar"> | |
| 21 | <a class="sidebar-brand" href="/dashboard.cgi"> | |
| 22 | <div class="logo">PT</div> | |
| 23 | <div class="brand-text"> | |
| 24 | <div class="brand-name">$tvars.brand_name</div> | |
| 25 | <div class="brand-sub">Project Task Matrix</div> | |
| 26 | </div> | |
| 27 | </a> | |
| 28 | $tvars.sidebar | |
| 29 | </aside> | |
| 30 | <div class="app-main"> | |
| 31 | <header class="topbar"> | |
| 32 | <button type="button" class="topbar-iconbtn" id="sidebarToggle" data-tip="Collapse the left sidebar to icons-only. Click again to expand. We remember your preference per browser."> | |
| 33 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg> | |
| 34 | </button> | |
| 35 | <div class="topbar-title">$tvars.title</div> | |
| 36 | <div class="topbar-spacer"></div> | |
| 37 | <div class="topbar-search"> | |
| 38 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="20" y1="20" x2="16.65" y2="16.65"/></svg> | |
| 39 | <input type="text" id="globalSearch" placeholder="Search projects, tasks, people..." autocomplete="off"> | |
| 40 | </div> | |
| 41 | <div class="topbar-actions"> | |
| 42 | <div class="topbar-help-menu" tabindex="0"> | |
| 43 | <button type="button" class="topbar-pill tp-help" data-tip="**Help**\nGuides, support, and contact options."> | |
| 44 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3"/><path d="M12 17h0"/></svg> | |
| 45 | <span>Help</span> | |
| 46 | [if:$has_unread_support]<span class="tp-badge">$tvars.unread_support_count</span>[/if] | |
| 47 | <svg class="tp-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12"><polyline points="6 9 12 15 18 9"/></svg> | |
| 48 | </button> | |
| 49 | <div class="topbar-help-dropdown" role="menu"> | |
| 50 | <div class="thd-section">Resources</div> | |
| 51 | <a href="/help.cgi" role="menuitem"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="12" cy="12" r="10"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3"/><path d="M12 17h0"/></svg> Help center</a> | |
| 52 | <a href="/contact.cgi" role="menuitem"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg> Public contact form</a> | |
| 53 | <div class="thd-section">Talk to us</div> | |
| 54 | <a href="/support.cgi" role="menuitem" class="thd-primary"> | |
| 55 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg> | |
| 56 | Contact support | |
| 57 | [if:$has_unread_support]<span class="thd-badge">$tvars.unread_support_count</span>[/if] | |
| 58 | </a> | |
| 59 | </div> | |
| 60 | </div> | |
| 61 | [if:$is_admin] | |
| 62 | <a href="/admin_support.cgi" class="topbar-pill tp-support-queue [if:$has_unread_support]tp-attention[/if]" data-tip="**Support queue**\nCustomer support threads waiting for a reply. Pulses when there's something to handle."> | |
| 63 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg> | |
| 64 | <span>Support queue</span> | |
| 65 | [if:$has_unread_support]<span class="tp-badge">$tvars.unread_support_count</span>[/if] | |
| 66 | [if:$has_unread_support]<span class="tp-pulse-dot"></span>[/if] | |
| 67 | </a> | |
| 68 | [/if] | |
| 69 | <a href="/notifications.cgi" class="topbar-iconbtn" data-tip="**Notifications**\nMentions, task assignments, status changes, due-soon reminders.\nA red dot means you have unread ones."> | |
| 70 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg> | |
| 71 | [if:$has_unread_notifs]<span class="dot"></span>[/if] | |
| 72 | </a> | |
| 73 | <div class="profile-dropdown" id="profileDropdown"> | |
| 74 | <button type="button" class="profile-btn" onclick="document.getElementById('profileMenu').classList.toggle('open');"> | |
| 75 | <div class="avatar" style="background:$tvars.avatar_color">$tvars.initials</div> | |
| 76 | <div style="text-align:left;line-height:1.2"> | |
| 77 | <div style="font-size:13px;font-weight:600">$tvars.display_name</div> | |
| 78 | <div style="font-size:11px;color:var(--col-text-dim)">$tvars.company_name</div> | |
| 79 | </div> | |
| 80 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="6 9 12 15 18 9"/></svg> | |
| 81 | </button> | |
| 82 | <div class="profile-dropdown-menu" id="profileMenu"> | |
| 83 | <div style="padding:12px 12px 10px;border-bottom:1px solid var(--col-border);margin-bottom:6px"> | |
| 84 | <div style="font-weight:600;font-size:13.5px">$tvars.display_name</div> | |
| 85 | <div style="font-size:11.5px;color:var(--col-text-muted)">$tvars.email</div> | |
| 86 | <div style="margin-top:6px;display:flex;gap:6px"><span class="badge primary">$tvars.role_label</span><span class="badge">$tvars.company_plan_tier</span></div> | |
| 87 | </div> | |
| 88 | $tvars.dropdown_links | |
| 89 | <div class="profile-dropdown-divider"></div> | |
| 90 | <a href="/logout.cgi"><span class="dropdown-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></span> Sign out</a> | |
| 91 | </div> | |
| 92 | </div> | |
| 93 | </div> | |
| 94 | </header> | |
| 95 | <main class="app-content"> | |
| 96 | [if:$is_trialing] | |
| 97 | [if:$trial_tier_gentle] | |
| 98 | <div class="trial-banner trial-banner-gentle"> | |
| 99 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> | |
| 100 | <div>$tvars.trial_headline <a href="/billing.cgi">Pick a plan</a> any time to keep full access after $tvars.trial_ends_at.</div> | |
| 101 | </div> | |
| 102 | [/if] | |
| 103 | [if:$trial_tier_amber] | |
| 104 | <div class="trial-banner trial-banner-amber"> | |
| 105 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> | |
| 106 | <div><strong>$tvars.trial_headline</strong> After that, your workspace drops to Free (3 users, 1 project). <a href="/billing.cgi">Upgrade now →</a></div> | |
| 107 | </div> | |
| 108 | [/if] | |
| 109 | [if:$trial_tier_red] | |
| 110 | <div class="trial-banner trial-banner-red"> | |
| 111 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> | |
| 112 | <div><strong>$tvars.trial_headline</strong> Pick a plan today or your team drops to Free at $tvars.trial_ends_at. <a href="/billing.cgi">Upgrade now →</a></div> | |
| 113 | </div> | |
| 114 | [/if] | |
| 115 | [/if] | |
| 116 | [if:$needs_onboarding] | |
| 117 | <div class="trial-banner" style="background:linear-gradient(135deg, rgba(139,92,246,.18), rgba(90,169,255,.10));border-color:rgba(139,92,246,.4)"> | |
| 118 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg> | |
| 119 | <div><strong>Welcome!</strong> Take 1 minute to pick what to call your $term_projects_lower and $term_tasks_lower. <a href="/onboarding.cgi">Start setup →</a> · <a href="/onboarding.cgi?skip=1" style="color:var(--col-text-dim)">Skip for now</a></div> | |
| 120 | </div> | |
| 121 | [/if] | |
| 122 | $tvars.body | |
| 123 | </main> | |
| 124 | </div> | |
| 125 | </div> | |
| 126 | <script> | |
| 127 | // Sidebar scroll-position preservation. Saves the .sidebar scroll | |
| 128 | // offset to localStorage on scroll, restores it on page load -- so | |
| 129 | // clicking a nav item doesn't dump the user back at the top of the | |
| 130 | // menu after the new page renders. | |
| 131 | (function(){ | |
| 132 | var sb = document.querySelector('.sidebar'); | |
| 133 | if (!sb) return; | |
| 134 | var KEY = 'tf-sidebar-scroll'; | |
| 135 | try { var s = parseInt(localStorage.getItem(KEY), 10) || 0; if (s > 0) sb.scrollTop = s; } catch(e){} | |
| 136 | var t = null; | |
| 137 | sb.addEventListener('scroll', function(){ | |
| 138 | if (t) clearTimeout(t); | |
| 139 | t = setTimeout(function(){ try { localStorage.setItem(KEY, sb.scrollTop); } catch(e){} }, 80); | |
| 140 | }, { passive: true }); | |
| 141 | })(); | |
| 142 | </script> | |
| 143 | ||
| 144 | <script> | |
| 145 | // Sidebar collapse (desktop) / off-canvas drawer (mobile <= 760px). | |
| 146 | // Both modes persist across page loads via /save_pref.cgi: | |
| 147 | // sidebar_collapsed -- desktop icons-only mode | |
| 148 | // sidebar_mobile_open -- mobile drawer open | |
| 149 | // Server-side Wrapper.pm reads user_settings on every render and stamps | |
| 150 | // the same classes on <html> so the page boots in the saved state. | |
| 151 | (function(){ | |
| 152 | var btn = document.getElementById('sidebarToggle'); | |
| 153 | var backdrop = document.getElementById('sidebarBackdrop'); | |
| 154 | function isMobile(){ return window.matchMedia('(max-width: 760px)').matches; } | |
| 155 | function savePref(key, val){ | |
| 156 | try { | |
| 157 | fetch('/save_pref.cgi', { | |
| 158 | method: 'POST', | |
| 159 | headers:{'Content-Type':'application/x-www-form-urlencoded'}, | |
| 160 | body: key + '=' + val, | |
| 161 | credentials: 'same-origin', | |
| 162 | keepalive: true | |
| 163 | }); | |
| 164 | } catch (e) {} | |
| 165 | } | |
| 166 | function setMobileOpen(open){ | |
| 167 | document.documentElement.classList.toggle('sidebar-open', open); | |
| 168 | savePref('sidebar_mobile_open', open ? 1 : 0); | |
| 169 | } | |
| 170 | function closeDrawer(){ setMobileOpen(false); } | |
| 171 | ||
| 172 | if(btn){ | |
| 173 | btn.addEventListener('click', function(){ | |
| 174 | if (isMobile()) { | |
| 175 | var nowOpen = !document.documentElement.classList.contains('sidebar-open'); | |
| 176 | setMobileOpen(nowOpen); | |
| 177 | } else { | |
| 178 | document.documentElement.classList.toggle('sidebar-collapsed'); | |
| 179 | var v = document.documentElement.classList.contains('sidebar-collapsed') ? 1 : 0; | |
| 180 | savePref('sidebar_collapsed', v); | |
| 181 | } | |
| 182 | }); | |
| 183 | } | |
| 184 | if(backdrop){ backdrop.addEventListener('click', closeDrawer); } | |
| 185 | // Tapping a sidebar link on mobile closes the drawer (and persists). | |
| 186 | document.querySelectorAll('.sidebar a').forEach(function(a){ | |
| 187 | a.addEventListener('click', function(){ if (isMobile()) closeDrawer(); }); | |
| 188 | }); | |
| 189 | // If the viewport grows past mobile, drop the mobile-open state so the | |
| 190 | // desktop collapsed/expanded toggle takes over cleanly. Don't persist | |
| 191 | // here -- this is a transient viewport-change cleanup, the user didn't | |
| 192 | // tap close. | |
| 193 | window.addEventListener('resize', function(){ | |
| 194 | if (!isMobile()) document.documentElement.classList.remove('sidebar-open'); | |
| 195 | }); | |
| 196 | // Close profile menu on outside click. | |
| 197 | document.addEventListener('click', function(e){ | |
| 198 | var m = document.getElementById('profileMenu'); | |
| 199 | var d = document.getElementById('profileDropdown'); | |
| 200 | if(m && d && !d.contains(e.target)) m.classList.remove('open'); | |
| 201 | }); | |
| 202 | })(); | |
| 203 | </script> | |
| 204 | <script src="$tvars.assets_js/global-search.js?v=2026-06-12-1"></script> | |
| 205 | <script src="$tvars.assets_js/tooltips.js?v=20260610d"></script> | |
| 206 | <script src="$tvars.assets_js/color-picker.js?v=20260607a"></script> | |
| 207 | <script src="$tvars.assets_js/attachments.js?v=20260618-record"></script> | |
| 208 | ||
| 209 | <script> | |
| 210 | /* Web push: register the service worker on every page load (idempotent). | |
| 211 | Expose tfPush.* helpers for the prefs page to call. Permission is NOT | |
| 212 | auto-requested — the user opts in from /preferences.cgi. */ | |
| 213 | (function(){ | |
| 214 | if (!('serviceWorker' in navigator) || !('PushManager' in window)) { | |
| 215 | window.tfPush = { supported: false }; | |
| 216 | return; | |
| 217 | } | |
| 218 | function urlBase64ToUint8Array(s){ | |
| 219 | var pad = '='.repeat((4 - s.length % 4) % 4); | |
| 220 | var base64 = (s + pad).replace(/-/g, '+').replace(/_/g, '/'); | |
| 221 | var raw = atob(base64); | |
| 222 | var out = new Uint8Array(raw.length); | |
| 223 | for (var i=0;i<raw.length;i++) out[i] = raw.charCodeAt(i); | |
| 224 | return out; | |
| 225 | } | |
| 226 | function form(act, extra){ | |
| 227 | var fd = new FormData(); | |
| 228 | fd.append('act', act); | |
| 229 | if (extra) for (var k in extra) fd.append(k, extra[k]); | |
| 230 | return fd; | |
| 231 | } | |
| 232 | var swReg = null; | |
| 233 | navigator.serviceWorker.register('/sw.js?v=20260618-1', { scope: '/' }) | |
| 234 | .then(function(r){ swReg = r; }) | |
| 235 | .catch(function(){}); | |
| 236 | ||
| 237 | window.tfPush = { | |
| 238 | supported: true, | |
| 239 | permission: function(){ return Notification.permission; }, | |
| 240 | isSubscribed: function(){ | |
| 241 | if (!swReg) return Promise.resolve(false); | |
| 242 | return swReg.pushManager.getSubscription().then(function(s){ return !!s; }); | |
| 243 | }, | |
| 244 | enable: async function(){ | |
| 245 | if (Notification.permission === 'denied') { | |
| 246 | throw new Error('Notifications are blocked at the browser level. Open your browser settings to re-allow them.'); | |
| 247 | } | |
| 248 | var perm = await Notification.requestPermission(); | |
| 249 | if (perm !== 'granted') throw new Error('Permission denied.'); | |
| 250 | if (!swReg) swReg = await navigator.serviceWorker.ready; | |
| 251 | var resp = await fetch('/push_action.cgi?act=vapid_key', { credentials: 'include' }); | |
| 252 | var key = (await resp.json()).key; | |
| 253 | if (!key) throw new Error('Server has no VAPID key configured.'); | |
| 254 | var sub = await swReg.pushManager.subscribe({ | |
| 255 | userVisibleOnly: true, | |
| 256 | applicationServerKey: urlBase64ToUint8Array(key), | |
| 257 | }); | |
| 258 | var raw = sub.toJSON(); | |
| 259 | var keys = raw.keys || {}; | |
| 260 | var save = await fetch('/push_action.cgi', { | |
| 261 | method: 'POST', credentials: 'include', | |
| 262 | body: form('subscribe', { | |
| 263 | endpoint: raw.endpoint, | |
| 264 | p256dh: keys.p256dh || '', | |
| 265 | auth: keys.auth || '', | |
| 266 | ua: navigator.userAgent || '', | |
| 267 | }), | |
| 268 | }); | |
| 269 | var j = await save.json(); | |
| 270 | if (!j.ok) throw new Error(j.err || 'Subscribe failed'); | |
| 271 | return true; | |
| 272 | }, | |
| 273 | disable: async function(){ | |
| 274 | if (!swReg) swReg = await navigator.serviceWorker.ready; | |
| 275 | var sub = await swReg.pushManager.getSubscription(); | |
| 276 | if (sub) { | |
| 277 | try { | |
| 278 | await fetch('/push_action.cgi', { | |
| 279 | method: 'POST', credentials: 'include', | |
| 280 | body: form('unsubscribe', { endpoint: sub.endpoint }), | |
| 281 | }); | |
| 282 | } catch(_) {} | |
| 283 | await sub.unsubscribe(); | |
| 284 | } | |
| 285 | return true; | |
| 286 | }, | |
| 287 | test: async function(){ | |
| 288 | var r = await fetch('/push_action.cgi', { | |
| 289 | method: 'POST', credentials: 'include', | |
| 290 | body: form('test_send', {}), | |
| 291 | }); | |
| 292 | var j = await r.json(); | |
| 293 | if (!j.ok) throw new Error(j.err || 'Test send failed'); | |
| 294 | return true; | |
| 295 | }, | |
| 296 | }; | |
| 297 | })(); | |
| 298 | </script> | |
| 299 | ||
| 300 | $tvars.extra_js | |
| 301 | <!-- Viewer-local timestamp renderer (portfolio-canonical). | |
| 302 | Server emits `<span class="ts" data-ts="EPOCH" data-fmt="…">FALLBACK</span>`. | |
| 303 | Formats: datetime | date | date-short | time | relative | |
| 304 | Original text is the no-JS fallback. Re-run via window.tzLocalize() after AJAX. --> | |
| 305 | <script> | |
| 306 | (function(){ | |
| 307 | function fmt(el){ | |
| 308 | var e = parseInt(el.getAttribute('data-ts'), 10); | |
| 309 | if (!e) return; | |
| 310 | var d = new Date(e * 1000); | |
| 311 | var kind = el.getAttribute('data-fmt') || 'datetime'; | |
| 312 | var out; | |
| 313 | switch (kind) { | |
| 314 | case 'date': out = d.toLocaleDateString(undefined, {year:'numeric',month:'short',day:'numeric'}); break; | |
| 315 | case 'date-short': out = d.toLocaleDateString(undefined, {month:'short',day:'numeric'}); break; | |
| 316 | case 'time': out = d.toLocaleTimeString(undefined, {hour:'2-digit',minute:'2-digit'}); break; | |
| 317 | case 'relative': out = rel(e); break; | |
| 318 | default: out = d.toLocaleDateString(undefined, {year:'numeric',month:'short',day:'numeric'}) | |
| 319 | + ' ' + d.toLocaleTimeString(undefined, {hour:'2-digit',minute:'2-digit'}); | |
| 320 | } | |
| 321 | el.textContent = out; | |
| 322 | if (!el.title) el.title = d.toString(); | |
| 323 | } | |
| 324 | function rel(e){ | |
| 325 | var s = Math.floor(Date.now()/1000) - e, future = s < 0; s = Math.abs(s); | |
| 326 | var n, u; | |
| 327 | if (s < 60) { n = s; u = 'second'; } | |
| 328 | else if (s < 3600) { n = Math.floor(s/60); u = 'minute'; } | |
| 329 | else if (s < 86400) { n = Math.floor(s/3600); u = 'hour'; } | |
| 330 | else if (s < 2592000){ n = Math.floor(s/86400); u = 'day'; } | |
| 331 | else if (s < 31536000){n = Math.floor(s/2592000); u = 'month'; } | |
| 332 | else { n = Math.floor(s/31536000);u = 'year'; } | |
| 333 | var p = n === 1 ? '' : 's'; | |
| 334 | return future ? ('in ' + n + ' ' + u + p) : (n + ' ' + u + p + ' ago'); | |
| 335 | } | |
| 336 | function localize(root){ (root||document).querySelectorAll('span.ts[data-ts], time[data-ts]').forEach(fmt); } | |
| 337 | window.tzLocalize = localize; | |
| 338 | if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function(){ localize(); }); | |
| 339 | else localize(); | |
| 340 | window.addEventListener('ts-refresh', function(){ localize(); }); | |
| 341 | })(); | |
| 342 | </script> | |
| 343 | ||
| 344 | </body> | |
| 345 | </html> |