Santa's Village — Google Santa TrackerGoogle 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);
};