Pinned 2026 toolchains (Go 1.26, Rust 1.98/edition 2024, Python 3.14 + uv, Node 24, Zig 0.16, NixOS 26.05), postgres 18 / mongo 8, lockfiles built from, non-root runtimes, .dockerignore, per-project LICENSE, READMEs with the git.devai.io clone line, checkout@v7 CI. Security fixes in the legacy Rust APIs (any-password login, self-assigned admin, hard-coded JWT secret), JWT alg/exp/sub enforcement across the blog series, safe markdown links in the frontends, and many smaller bugs — every project was built, run and exercised end to end. Adds scripts/publish.sh + a CI publish job that splits every folder into its own repo at git.devai.io/templates/<folder>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01128fhuZbgivaSJvtMf4s1G
65 lines
2.3 KiB
JavaScript
65 lines
2.3 KiB
JavaScript
// Each "image" is a title and a CSS gradient, so there are no files to download.
|
|
const slides = [
|
|
{ title: "Sunrise", gradient: "linear-gradient(135deg, #f97316, #fbbf24)" },
|
|
{ title: "Ocean", gradient: "linear-gradient(135deg, #0ea5e9, #22d3ee)" },
|
|
{ title: "Forest", gradient: "linear-gradient(135deg, #16a34a, #84cc16)" },
|
|
{ title: "Berry", gradient: "linear-gradient(135deg, #db2777, #a855f7)" },
|
|
{ title: "Midnight", gradient: "linear-gradient(135deg, #1e293b, #4f46e5)" },
|
|
];
|
|
|
|
const slideshow = document.getElementById("slideshow");
|
|
const slide = document.getElementById("slide");
|
|
const slideTitle = document.getElementById("slideTitle");
|
|
const dotsBox = document.getElementById("dots");
|
|
|
|
// The whole slideshow is this one number: which slide we're on. Every button
|
|
// just changes `index` and calls show() to redraw.
|
|
let index = 0;
|
|
|
|
const dots = slides.map((_, i) => {
|
|
const dot = document.createElement("button");
|
|
dot.className = "dot";
|
|
dot.type = "button";
|
|
dot.setAttribute("aria-label", `Go to slide ${i + 1}`);
|
|
dot.addEventListener("click", () => show(i));
|
|
dotsBox.append(dot);
|
|
return dot;
|
|
});
|
|
|
|
function show(i) {
|
|
index = i;
|
|
slide.style.background = slides[index].gradient;
|
|
slideTitle.textContent = slides[index].title;
|
|
dots.forEach((dot, d) => {
|
|
dot.classList.toggle("active", d === index);
|
|
dot.setAttribute("aria-current", String(d === index));
|
|
});
|
|
}
|
|
|
|
// % (remainder) wraps the index around the ends: from the last slide, +1 lands
|
|
// on 0. Adding slides.length first keeps it positive, so -1 from 0 wraps too.
|
|
function move(step) {
|
|
show((index + step + slides.length) % slides.length);
|
|
}
|
|
|
|
document.getElementById("prev").addEventListener("click", () => move(-1));
|
|
document.getElementById("next").addEventListener("click", () => move(1));
|
|
|
|
let timer = null;
|
|
function play() {
|
|
clearInterval(timer);
|
|
timer = setInterval(() => move(1), 4000);
|
|
}
|
|
function pause() {
|
|
clearInterval(timer);
|
|
}
|
|
|
|
// Auto-play stops while the pointer or keyboard focus is on the slideshow, so
|
|
// it never moves out from under someone who is reading or clicking.
|
|
slideshow.addEventListener("mouseenter", pause);
|
|
slideshow.addEventListener("mouseleave", play);
|
|
slideshow.addEventListener("focusin", pause);
|
|
slideshow.addEventListener("focusout", play);
|
|
|
|
show(0);
|
|
play();
|