feat: bun app foundation, sample data, shared credits/audio, mockup stubs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-23 18:19:36 +02:00
1 parent d52ac63afe
commit 10ce858b31
42 files changed
+839

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
import type { Song } from "../data/types";
export function createAudioSlot(song: Song): HTMLElement {
const root = document.createElement("div");
root.className = "audio-slot";
if (!song.audio) {
root.classList.add("audio-slot--empty");
root.textContent = "No audio file for this song yet.";
return root;
}
const audio = document.createElement("audio");
audio.controls = true;
audio.src = song.audio;
root.append(audio);
for (const part of song.parts) {
if (part.startSec === undefined) continue;
const btn = document.createElement("button");
btn.type = "button";
btn.className = "audio-slot__seek";
btn.dataset.start = String(part.startSec);
btn.textContent = part.name;
btn.addEventListener("click", () => {
audio.currentTime = part.startSec!;
void audio.play?.();
});
root.append(btn);
}
return root;
}
+9
View File
@@ -0,0 +1,9 @@
.fan-credits { padding: 1rem 1.5rem; font-size: 0.8rem; opacity: 0.75; text-align: center; }
.fan-credits p { margin: 0; }
.audio-slot { padding: 0.75rem; border: 1px solid currentColor; border-radius: 6px; }
.audio-slot--empty { opacity: 0.6; border-style: dashed; }
.audio-slot audio { width: 100%; }
.audio-slot__seek { margin: 0.25rem 0.25rem 0 0; }
.video-cards { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.video-card { display: block; padding: 0.5rem 0.75rem; border: 1px solid currentColor; border-radius: 6px; color: inherit; text-decoration: none; }
.video-card:hover { opacity: 0.8; }
+13
View File
@@ -0,0 +1,13 @@
export const FAN_NOTICE =
"This is an unofficial fan project, not affiliated with or endorsed by the creator of EPIC: The Musical. " +
"With thanks to Jorge Rivera-Herrans, creator of EPIC.";
export function mountCredits(target?: Element): HTMLElement {
const el = (target ?? document.querySelector("[data-fan-credits]") ?? document.body.appendChild(document.createElement("footer"))) as HTMLElement;
el.setAttribute("data-fan-credits", "");
el.classList.add("fan-credits");
const p = document.createElement("p");
p.textContent = FAN_NOTICE;
el.replaceChildren(p);
return el;
}
+16
View File
@@ -0,0 +1,16 @@
import type { Song } from "../data/types";
export function createVideoCards(song: Song): HTMLElement {
const root = document.createElement("div");
root.className = "video-cards";
for (const v of song.videos) {
const a = document.createElement("a");
a.className = "video-card";
a.href = v.url;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.textContent = v.title;
root.append(a);
}
return root;
}