PageSourceSearch

https://easycountdown.netlify.app/script.js

js easycountdown.netlify.app collected 2026-10-03 10:40:31 UTC 11,346 bytes, 336 lines download raw bytes

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.