repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

2.4 KB · 80 lines · JavaScript Raw History
 1// The home page: one huge washed-out word behind the hero, cycling every two
 2// seconds. Each change is an old word falling out downward while the new one
 3// drops in from above.
 4
 5import { enter, exit } from "../transition.js";
 6
 7const WORDS = ["AI Agents", "Automation", "DevOps", "Architecture"];
 8const CYCLE_MS = 2000;
 9const TRANSITION_MS = 1000;
10
11const CLASSES = {
12  appear: {
13    from: "index-wordAppear",
14    active: "index-wordAppearActive",
15    done: "index-wordAppearDone",
16  },
17  enter: {
18    from: "index-wordEnter",
19    active: "index-wordEnterActive",
20    done: "index-wordEnterDone",
21  },
22  exit: {
23    from: "index-wordExit",
24    active: "index-wordExitActive",
25    clear: ["index-wordEnterDone", "index-wordAppearDone"],
26  },
27};
28
29export const initHome = () => {
30  const container = document.querySelector(".index-words");
31  const hero = document.querySelector(".index-hero");
32  const image = document.querySelector(".index-imageWrapper img");
33
34  // The hero animation is held until the image is there, so the text does not
35  // fade up over an empty rectangle. Both paths release the loader, including
36  // the failure path: a broken image must not strand the page behind it.
37  if (image) {
38    const ready = () => {
39      if (hero) hero.style.animationPlayState = "running";
40      window.dispatchEvent(new Event("loaderReady"));
41    };
42    if (image.complete) ready();
43    else {
44      image.addEventListener("load", ready, { once: true });
45      image.addEventListener("error", ready, { once: true });
46    }
47  }
48
49  if (!container) return;
50
51  const makeWord = (text) => {
52    const el = document.createElement("h3");
53    el.className = "index-word";
54    el.textContent = text;
55    return el;
56  };
57
58  let index = 0;
59  let current = makeWord(WORDS[index]);
60  container.appendChild(current);
61  enter(current, CLASSES.appear, TRANSITION_MS);
62
63  window.setInterval(() => {
64    // setInterval keeps firing in a hidden tab and requestAnimationFrame does
65    // not, and enter() and exit() both defer their work behind it, so without
66    // this a backgrounded tab piles up words and bursts them all on return.
67    if (document.hidden) return;
68
69    const outgoing = current;
70    index = (index + 1) % WORDS.length;
71
72    const incoming = makeWord(WORDS[index]);
73    container.appendChild(incoming);
74    current = incoming;
75    enter(incoming, CLASSES.enter, TRANSITION_MS);
76
77    exit(outgoing, CLASSES.exit, TRANSITION_MS, (el) => el.remove());
78  }, CYCLE_MS);
79};