Santa's Village — Google Santa Tracker Google Santa Tracker }(); js/countdown-bar-2018.js added +73 /* * Countdown fallback for the 2018 Santa Tracker bar. * The 2018 app already draws the pink bar with a DAYS / HRS / MIN / SEC * countdown, but that countdown depends on Google's old live-data service. * If the built-in countdown stays hidden, this adds an identical-looking one * to the bar that counts down locally to Santa's takeoff. */ (function () { // Santa took off at 10:00 UTC on 24 December. Date is mocked to 2018 above. var TAKEOFF = Date.UTC(2018, 11, 24, 10, 0, 0); var LABELS = ['Days', 'Hrs', 'Min', 'Sec']; var CSS = 'countdown-timer[hidden]{display:none!important}' + '#cd2018{display:flex;align-items:center;margin:0 8px 0 auto;flex:none;' + 'font-family:Roboto,sans-serif;color:#fff;pointer-events:none}' + '#cd2018[hidden]{display:none}' + '#cd2018 .u{display:flex;flex-direction:column;align-items:center;min-width:30px;margin-left:10px}' + '#cd2018 .n{font-size:20px;font-weight:700;line-height:18px;letter-spacing:.5px}' + '#cd2018 .l{font-size:8px;font-weight:800;line-height:10px;letter-spacing:.5px;text-transform:uppercase}' + '@media (max-width:480px){#cd2018 .u{margin-left:6px;min-width:24px}#cd2018 .n{font-size:16px}}'; function findDeep(root, selector) { var hit = root.querySelector(selector); if (hit) return hit; var all = root.querySelectorAll('*'); for (var i = 0; i < all.length; i++) { if (all[i].shadowRoot && (hit = findDeep(all[i].shadowRoot, selector))) return hit; } return null; } function pad(n) { return (n < 10 ? '0' : '') + n; } function secondsLeft() { return Math.max(0, Math.floor((TAKEOFF - Date.now()) / 1000)); } var box = null, nums = []; function build() { box = document.createElement('div'); box.id = 'cd2018'; box.setAttribute('aria-hidden', 'true'); nums = LABELS.map(function (label) { var u = document.createElement('div'); u.className = 'u'; var n = document.createElement('span'); n.className = 'n'; var l = document.createElement('span'); l.className = 'l'; l.textContent = label; u.appendChild(n); u.appendChild(l); box.appendChild(u); return n; }); } function update() { var chrome = findDeep(document, 'santa-chrome'); var root = chrome && chrome.shadowRoot; var title = root && root.getElementById('title'); if (!title) return; var native = title.querySelector('countdown-timer'); var nativeShowing = native && !native.hidden && native.offsetWidth > 0; var left = secondsLeft(); if (!box) build(); if (box.parentNode !== title) { var style = document.createElement('style'); style.textContent = CSS; title.appendChild(style); title.appendChild(box); } box.hidden = nativeShowing || left === 0; var parts = [Math.floor(left / 86400), Math.floor(left / 3600) % 24, Math.floor(left / 60) % 60, left % 60]; for (var i = 0; i < 4; i++) nums[i].textContent = pad(parts[i]); } setInterval(update, 1000); })(); js/unlock-all-games.js added +17 /* * Unlocks every Santa's Village game and activity right away, instead of * waiting for each one's December 2018 launch date. Uses the 2018 app's own * built-in "unlock all houses" switch once the app has loaded. */ (function () { var tries = 0; var timer = setInterval(function () { var app = document.getElementById('santaApp'); if (app && typeof app.unlockAllHouses === 'function') { app.unlockAllHouses(); clearInterval(timer); } else if (++tries > 240) { clearInterval(timer); } }, 250); })(); netlify.toml added +16 [build] publish = "." # Open the site straight into the Map Quiz ("mercator") scene [[redirects]] from = "/" to = "/mercator.html" status = 302 force = true # Santa Tracker routes scenes by path (e.g. /mercator.html, /village.html). # Serve the app shell for any path that isn't a real file so these never 404. [[redirects]] from = "/*" to = "/index.html" status = 200 netlify/functions/santa-api.mjs added +91 // Serves Santa Tracker live data in the format the 2018 web app expects. // // Google's santa-api still answers, but it now returns a newer format and // real (present-day) times, and its route data is for a later year. The 2018 // app runs on a simulated 2018 clock, so this function: // ?op=info -> 2018-style /info response on the simulated 2018 clock // ?op=route -> Google's route, shifted so takeoff is 24 Dec 2018 10:00 UTC const GOOGLE_INFO = "https://santa-api.appspot.com/info?client=web&language=en"; const TAKEOFF_2018 = Date.UTC(2018, 11, 24, 10, 0, 0); const SIM_YEAR = 2018; function simulatedNow() { const d = new Date(); d.setUTCFullYear(SIM_YEAR); return d.getTime(); } function json(body, cacheControl) { return new Response(JSON.stringify(body), { headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...(cacheControl ? { "netlify-cdn-cache-control": cacheControl } : {}), }, }); } async function googleRouteUrl() { const res = await fetch(GOOGLE_INFO + "&rand=" + Math.random()); if (!res.ok) throw new Error("santa-api info " + res.status); const info = await res.json(); const route = Array.isArray(info.route) ? info.route[0] : info.route; if (!route) throw new Error("no route in santa-api info"); return route; } async function shiftedRoute() { const res = await fetch(await googleRouteUrl()); if (!res.ok) throw new Error("route " + res.status); const data = await res.json(); const destinations = data.destinations || []; if (!destinations.length) throw new Error("empty route"); const delta = TAKEOFF_2018 - destinations[0].departure; const shift = (t) => (typeof t === "number" ? t + delta : t); return { destinations: destinations.map((d) => ({ ...d, arrival: shift(d.arrival), departure: shift(d.departure), // The 2018 app expects a Panoramio photo list (a retired Google service); // an empty list keeps its city-photo panel from crashing. details: d.details ? { panoramio: [], ...d.details } : d.details, })), stream: (data.stream || []).map((s) => ({ ...s, timestamp: shift(s.timestamp) })), }; } export default async (req, context) => { const op = new URL(req.url).searchParams.get("op") || "info"; if (op === "route") { try { // Cache the (large) route at the CDN; keep serving it if Google is down. return json(await shiftedRoute(), "public, durable, s-maxage=86400, stale-while-revalidate=2592000, stale-if-error=2592000"); } catch (err) { console.error("route failed:", err.message); return json({ destinations: [] }); } } // Preview mode (?preview=tracker on the page) sends its own simulated time, // limited to the 2018 Christmas Eve flight window. const requested = Number(new URL(req.url).searchParams.get("now")); const previewNow = requested >= TAKEOFF_2018 - 86400000 && requested <= TAKEOFF_2018 + 2 * 86400000 ? requested : null; const geo = context.geo || {}; const info = { status: "OK", now: previewNow ?? simulatedNow(), timeOffset: 0, refresh: 60000, route: "/.netlify/functions/santa-api?op=route&v=2", }; if (isFinite(geo.latitude) && isFinite(geo.longitude)) { info.location = geo.latitude + "," + geo.longitude; } return json(info); };