1(function () { 2 "use strict"; 3 4 var setupEl = document.getElementById("setup"); 5 var countdownEl = document.getElementById("countdown"); 6 7 /* ---------- helpers ---------- */ 8 9 // Parse "YYYY-MM-DDTHH:MM" (optionally with :SS) as LOCAL time. 10 // Returns a Date or null if malformed. 11 function parseLocal(str) { 12 if (!str) return null; 13 var m = String(str).match( 14 /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/ 15 ); 16 if (!m) return null; 17 var d = new Date( 18 +m[1], +m[2] - 1, +m[3], +m[4], +m[5], m[6] ? +m[6] : 0 19 ); 20 return isNaN(d.getTime()) ? null : d; 21 } 22 23 function pad(n) { return n < 10 ? "0" + n : "" + n; } 24 25 // Format a Date as a local "YYYY-MM-DDTHH:MM" string for the URL. 26 function toLocalParam(d) { 27 return ( 28 d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + 29 "T" + pad(d.getHours()) + ":" + pad(d.getMinutes()) 30 ); 31 } 32 33 function buildShareURL(target, title) { 34 var url = new URL(location.href); 35 url.search = ""; 36 url.hash = ""; 37 url.searchParams.set("to", toLocalParam(target)); 38 if (title) url.searchParams.set("title", title); 39 return url.toString(); 40 } 41 42 /* =================================================================== 43 SETUP MODE 44 =================================================================== */ 45 46 function showSetup(prefill) { 47 countdownEl.hidden = true; 48 setupEl.hidden = false; 49 50 var titleInput = document.getElementById("title-input"); 51 var dateInput = document.getElementById("date-input"); 52 var timeInput = document.getElementById("time-input"); 53 54 // sensible near-future default: tomorrow at 12:00 (local) 55 var def = prefill || (function () { 56 var d = new Date(); 57 d.setDate(d.getDate() + 1); 58 d.setHours(12, 0, 0, 0); 59 return d; 60 })(); 61 62 dateInput.value = toLocalParam(def).slice(0, 10); 63 timeInput.value = toLocalParam(def).slice(11, 16); 64 65 var presetsEl = document.getElementById("presets"); 66 presetsEl.innerHTML = ""; 67 PRESETS.forEach(function (p) { 68 var btn = document.createElement("button"); 69 btn.type = "button"; 70 btn.className = "preset"; 71 btn.textContent = p.label; 72 btn.addEventListener("click", function () { 73 var d = p.date(); 74 if (!d) return; 75 var s = toLocalParam(d); 76 dateInput.value = s.slice(0, 10); 77 timeInput.value = s.slice(11, 16); 78 if (p.title) titleInput.value = p.title; 79 }); 80 presetsEl.appendChild(btn); 81 }); 82 83 document.getElementById("setup-form").addEventListener("submit", function (e) { 84 e.preventDefault(); 85 var target = parseLocal(dateInput.value + "T" + timeInput.value); 86 if (!target) return; 87 var title = titleInput.value.trim(); 88 var url = buildShareURL(target, title); 89 // Navigate to the shareable URL â app re-renders in countdown mode. 90 location.href = url; 91 }); 92 } 93 94 // Next occurrence of a given weekday (0=Sun..6=Sat) at hh:mm. 95 // If today matches and we're still before the time, use today. 96 function nextWeekday(targetDay, hh, mm) { 97 var d = new Date(); 98 var add = (targetDay - d.getDay() + 7) % 7; 99 if (add === 0) { 100 var passed = d.getHours() > hh || (d.getHours() === hh && d.getMinutes() >= mm); 101 if (passed) add = 7; 102 } 103 d.setDate(d.getDate() + add);
104 d.setHours(hh, mm, 0, 0); 105 return d; 106 } 107 108 function inMinutes(n) { 109 var d = new Date(); 110 d.setMinutes(d.getMinutes() + n, 0, 0); 111 return d; 112 } 113 114 function todayAt(hh, mm) { 115 var d = new Date(); 116 d.setHours(hh, mm, 0, 0); 117 if (d.getTime() <= Date.now()) d.setDate(d.getDate() + 1); // roll to tomorrow if past 118 return d; 119 } 120 121 // Data-driven presets. `title` (optional) prefills the title field. 122 var PRESETS = [ 123 // --- useful --- 124 { label: "New Year", title: "New Year ð", date: function () { 125 return new Date(new Date().getFullYear() + 1, 0, 1, 0, 0, 0, 0); } }, 126 { label: "This Friday 5pm", title: "Weekend!", date: function () { 127 return nextWeekday(5, 17, 0); } }, 128 { label: "This weekend", title: "Weekend!", date: function () { 129 return nextWeekday(6, 0, 0); } }, 130 { label: "Monday 9am", title: "Back to it ð¼", date: function () { 131 return nextWeekday(1, 9, 0); } }, 132 { label: "Tomorrow 9am", title: "", date: function () { 133 var d = new Date(); d.setDate(d.getDate() + 1); d.setHours(9, 0, 0, 0); return d; } }, 134 { label: "In 1 hour", title: "", date: function () { return inMinutes(60); } }, 135 { label: "In 1 week", title: "", date: function () { 136 var d = new Date(); d.setDate(d.getDate() + 7); return d; } }, 137 { label: "End of month", title: "End of the month", date: function () { 138 var n = new Date(); return new Date(n.getFullYear(), n.getMonth() + 1, 1, 0, 0, 0, 0); } }, 139 140 // --- funny --- 141 { label: "Quittin' time ð»", title: "Quittin' time ð»", date: function () { 142 return todayAt(17, 0); } }, 143 { label: "Lunch ð", title: "Lunch ð", date: function () { return todayAt(12, 0); } }, 144 { label: "Coffee â", title: "Coffee time â", date: function () { return inMinutes(5); } }, 145 { label: "Friday night ðª©", title: "Friday night ðª©", date: function () { 146 return nextWeekday(5, 20, 0); } }, 147 { label: "Nap o'clock ð´", title: "Nap o'clock ð´", date: function () { return inMinutes(20); } } 148 ]; 149 150 /* =================================================================== 151 COUNTDOWN MODE 152 =================================================================== */ 153 154 var timer = null; 155 156 function showCountdown(target, title) { 157 setupEl.hidden = true; 158 countdownEl.hidden = false; 159 160 var titleEl = document.getElementById("cd-title"); 161 var statusEl = document.getElementById("cd-status"); 162 var grid = document.getElementById("cd-grid"); 163 var celebrate = document.getElementById("celebrate"); 164 165 var elDays = document.getElementById("days"); 166 var elHours = document.getElementById("hours"); 167 var elMins = document.getElementById("minutes"); 168 var elSecs = document.getElementById("seconds"); 169 170 titleEl.textContent = title || "Easycountdown"; 171 grid.classList.add("live"); 172 173 var celebrated = false; 174 // If the target is already well in the past on load, skip celebration 175 // and go straight to a "time since" count-up. 176 var countingUp = target.getTime() - Date.now() < -3000; 177 if (countingUp) { 178 statusEl.hidden = false; 179 statusEl.textContent = "Time since"; 180 } 181 182 function render() { 183 var now = Date.now(); 184 var diff = target.getTime() - now; 185 186 // Reached zero â celebrate, and stay here permanently. 187 if (diff <= 0 && !countingUp) { 188 if (!celebrated) { 189 celebrated = true; 190 grid.hidden = true; 191 statusEl.hidden = true; 192 celebrate.hidden = false; 193 document.title = "It's here! ð"; 194 launchConfetti(); 195 if (timer) clearInterval(timer); // nothing left to tick 196 } 197 return; // persistent celebration â no flip to count-up 198 } 199 200 var ms = Math.abs(countingUp ? now - target.getTime() : diff); 201 var totalSec = Math.floor(ms / 1000); 202 var days = Math.floor(totalSec / 86400); 203 var hours = Math.floor((totalSec % 86400) / 3600); 204 var mins = Math.floor((totalSec % 3600) / 60); 205 var secs = totalSec % 60; 206 207 elDays.textContent = days; 208 elHours.textContent = pad(hours); 209 elMins.textContent = pad(mins); 210 elSecs.textContent = pad(secs); 211 212 document.title = countingUp 213 ? "+" + days + "d " + pad(hours) + ":" + pad(mins) + ":" + pad(secs) 214 : days + "d " + pad(hours) + ":" + pad(mins) + ":" + pad(secs) + " · Easycountdown"; 215 } 216 217 render(); 218 if (timer) clearInterval(timer); 219 timer = setInterval(render, 1000); 220 221 /* actions */ 222 var copyBtn = document.getElementById("copy-btn"); 223 copyBtn.addEventListener("click", function () { 224 var link = location.href; 225 var done = function () { 226 copyBtn.classList.add("copied"); 227 copyBtn.textContent = "Copied!"; 228 setTimeout(function () { 229 copyBtn.classList.remove("copied"); 230 copyBtn.textContent = "Copy link"; 231 }, 1600); 232 }; 233 if (navigator.clipboard && navigator.clipboard.writeText) { 234 navigator.clipboard.writeText(link).then(done, function () { fallbackCopy(link, done); }); 235 } else { 236 fallbackCopy(link, done); 237 } 238 }); 239 240 document.getElementById("new-btn").addEventListener("click", function () { 241 location.href = location.pathname; 242 }); 243 } 244 245 function fallbackCopy(text, done) { 246 var ta = document.createElement("textarea"); 247 ta.value = text; 248 ta.style.position = "fixed"; 249 ta.style.opacity = "0"; 250 document.body.appendChild(ta); 251 ta.select(); 252 try { document.execCommand("copy"); done(); } catch (e) {} 253 document.body.removeChild(ta); 254 } 255 256 /* =================================================================== 257 CONFETTI (light, dependency-free) 258 =================================================================== */ 259 260 function launchConfetti() { 261 var canvas = document.getElementById("confetti"); 262 var ctx = canvas.getContext("2d"); 263 var W, H; 264 function resize() { 265 W = canvas.width = window.innerWidth; 266 H = canvas.height = window.innerHeight; 267 } 268 resize(); 269 window.addEventListener("resize", resize); 270 271 var colors = ["#ff5c39", "#ffb199", "#ffffff", "#ffd166", "#06d6a0"]; 272 var pieces = []; 273 for (var i = 0; i < 140; i++) { 274 pieces.push({ 275 x: Math.random() * W, 276 y: Math.random() * -H, 277 r: 4 + Math.random() * 6, 278 c: colors[(Math.random() * colors.length) | 0], 279 vx: -1.5 + Math.random() * 3, 280 vy: 2 + Math.random() * 4, 281 rot: Math.random() * Math.PI, 282 vr: -0.2 + Math.random() * 0.4 283 }); 284 } 285 286 var start = performance.now(); 287 function frame(t) { 288 ctx.clearRect(0, 0, W, H);
289 var elapsed = t - start; 290 pieces.forEach(function (p) { 291 p.x += p.vx; 292 p.y += p.vy; 293 p.rot += p.vr; 294 ctx.save(); 295 ctx.translate(p.x, p.y); 296 ctx.rotate(p.rot); 297 ctx.fillStyle = p.c; 298 ctx.globalAlpha = elapsed > 4000 ? Math.max(0, 1 - (elapsed - 4000) / 1500) : 1; 299 ctx.fillRect(-p.r / 2, -p.r / 2, p.r, p.r * 0.6); 300 ctx.restore(); 301 }); 302 if (elapsed < 5500) { 303 requestAnimationFrame(frame); 304 } else { 305 ctx.clearRect(0, 0, W, H); 306 } 307 } 308 requestAnimationFrame(frame); 309 } 310 311 /* =================================================================== 312 ROUTER 313 =================================================================== */ 314 315 function init() { 316 var params = new URLSearchParams(location.search); 317 var to = params.get("to"); 318 var title = params.get("title") || ""; 319 320 if (to) { 321 var target = parseLocal(to); 322 if (target) { 323 showCountdown(target, title); 324 return; 325 } 326 // malformed â fall through to setup, prefilling title if present 327 } 328 showSetup(null); 329 if (title) { 330 var ti = document.getElementById("title-input"); 331 if (ti) ti.value = title; 332 } 333 } 334 335 init(); 336})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.