|
| 1 | +--- |
| 2 | +import { resolveSlideImageUrl } from "@/utils/images"; |
| 3 | +
|
| 4 | +interface Slide { |
| 5 | + image: string; |
| 6 | + timestamp?: number; |
| 7 | +} |
| 8 | +
|
| 9 | +interface Props { |
| 10 | + /** |
| 11 | + * Either a number of slides (will look for images 1.png through n.png) |
| 12 | + * or an array of slide objects with image paths and optional timestamps |
| 13 | + */ |
| 14 | + slides: number | Slide[]; |
| 15 | + /** |
| 16 | + * Base path for slide images (e.g., "/talks/codegen-in-rust") |
| 17 | + */ |
| 18 | + basePath: string; |
| 19 | + /** |
| 20 | + * ID for the talk (used for namespacing) |
| 21 | + */ |
| 22 | + talkId: string; |
| 23 | +} |
| 24 | +
|
| 25 | +const { slides, basePath, talkId } = Astro.props; |
| 26 | +
|
| 27 | +// Normalize slides to array format |
| 28 | +const slideList: Slide[] = typeof slides === "number" |
| 29 | + ? Array.from({ length: slides }, (_, i) => ({ |
| 30 | + image: `codegen-${i + 1}.png`, |
| 31 | + })) |
| 32 | + : slides; |
| 33 | +
|
| 34 | +// Resolve image URLs with local-first fallback |
| 35 | +// Priority: 1) Local file in /assets/, 2) R2 URL from manifest, 3) Fallback path |
| 36 | +const resolvedSlides = await Promise.all( |
| 37 | + slideList.map(async (slide) => { |
| 38 | + const resolvedUrl = await resolveSlideImageUrl(basePath, slide.image); |
| 39 | + // Fallback: if not resolved, try /assets/{basePath} (should rarely happen) |
| 40 | + const normalizedBase = basePath.startsWith("/") ? basePath.slice(1) : basePath; |
| 41 | + return { |
| 42 | + ...slide, |
| 43 | + url: resolvedUrl ?? `/assets/${normalizedBase}/${slide.image}`, |
| 44 | + }; |
| 45 | + }) |
| 46 | +); |
| 47 | +
|
| 48 | +const totalSlides = resolvedSlides.length; |
| 49 | +const containerId = `talk-slides-${talkId}`; |
| 50 | +--- |
| 51 | + |
| 52 | +<div id={containerId} class="talk-slides not-prose" tabindex="0"> |
| 53 | + <div class="slide-container bg-1 border-2 border-fg-2 relative" style="cursor: pointer;"> |
| 54 | + <img |
| 55 | + src={resolvedSlides[0].url} |
| 56 | + alt="Slide 1" |
| 57 | + class="slide-image w-full h-auto" |
| 58 | + /> |
| 59 | + </div> |
| 60 | + |
| 61 | + <div class="controls mt-2 flex items-center justify-between gap-2"> |
| 62 | + <button |
| 63 | + class="btn-prev bg-2 text-accent hocus:bg-0 hocus:invert px-1 py-0.5 border-2 border-fg-0 outline-none font-medium disabled:opacity-50 disabled:cursor-not-allowed" |
| 64 | + disabled |
| 65 | + > |
| 66 | + ← Prev |
| 67 | + </button> |
| 68 | + |
| 69 | + <div class="slide-counter text-fg-1 font-mono"> |
| 70 | + 1 / {totalSlides} |
| 71 | + </div> |
| 72 | + |
| 73 | + <button |
| 74 | + class="btn-next bg-2 text-accent hocus:bg-0 hocus:invert px-1 py-0.5 border-2 border-fg-0 outline-none font-medium disabled:opacity-50 disabled:cursor-not-allowed" |
| 75 | + > |
| 76 | + Next → |
| 77 | + </button> |
| 78 | + </div> |
| 79 | + |
| 80 | + <div class="keyboard-hints mt-1 text-fg-2 text-sm text-center"> |
| 81 | + <kbd class="px-1 bg-1 border border-fg-2">←</kbd> |
| 82 | + <kbd class="px-1 bg-1 border border-fg-2">→</kbd> |
| 83 | + <kbd class="px-1 bg-1 border border-fg-2">Space</kbd> |
| 84 | + to navigate • |
| 85 | + <kbd class="px-1 bg-1 border border-fg-2">Home</kbd> |
| 86 | + <kbd class="px-1 bg-1 border border-fg-2">End</kbd> |
| 87 | + to jump |
| 88 | + </div> |
| 89 | +</div> |
| 90 | + |
| 91 | +<script define:vars={{ containerId, resolvedSlides, totalSlides }}> |
| 92 | + const container = document.getElementById(containerId); |
| 93 | + if (!container) throw new Error(`Container ${containerId} not found`); |
| 94 | + |
| 95 | + const img = container.querySelector(".slide-image"); |
| 96 | + const counter = container.querySelector(".slide-counter"); |
| 97 | + const btnPrev = container.querySelector(".btn-prev"); |
| 98 | + const btnNext = container.querySelector(".btn-next"); |
| 99 | + const slideContainer = container.querySelector(".slide-container"); |
| 100 | + |
| 101 | + let currentSlide = 0; |
| 102 | + |
| 103 | + function updateSlide() { |
| 104 | + const slide = resolvedSlides[currentSlide]; |
| 105 | + img.src = slide.url; |
| 106 | + img.alt = `Slide ${currentSlide + 1}`; |
| 107 | + counter.textContent = `${currentSlide + 1} / ${totalSlides}`; |
| 108 | + btnPrev.disabled = currentSlide === 0; |
| 109 | + btnNext.disabled = currentSlide === totalSlides - 1; |
| 110 | + } |
| 111 | + |
| 112 | + function goToSlide(index) { |
| 113 | + currentSlide = Math.max(0, Math.min(index, totalSlides - 1)); |
| 114 | + updateSlide(); |
| 115 | + } |
| 116 | + |
| 117 | + // Button clicks |
| 118 | + btnPrev.addEventListener("click", () => goToSlide(currentSlide - 1)); |
| 119 | + btnNext.addEventListener("click", () => goToSlide(currentSlide + 1)); |
| 120 | + slideContainer.addEventListener("click", () => goToSlide(currentSlide + 1)); |
| 121 | + |
| 122 | + // Keyboard events |
| 123 | + container.addEventListener("keydown", (evt) => { |
| 124 | + if (evt.key === "ArrowRight" || evt.key === " ") { |
| 125 | + evt.preventDefault(); |
| 126 | + goToSlide(currentSlide + 1); |
| 127 | + } else if (evt.key === "ArrowLeft") { |
| 128 | + evt.preventDefault(); |
| 129 | + goToSlide(currentSlide - 1); |
| 130 | + } else if (evt.key === "Home") { |
| 131 | + evt.preventDefault(); |
| 132 | + goToSlide(0); |
| 133 | + } else if (evt.key === "End") { |
| 134 | + evt.preventDefault(); |
| 135 | + goToSlide(totalSlides - 1); |
| 136 | + } |
| 137 | + }); |
| 138 | +</script> |
| 139 | + |
| 140 | +<style> |
| 141 | + .talk-slides { |
| 142 | + max-width: 100%; |
| 143 | + margin: 2lh auto; |
| 144 | + } |
| 145 | + |
| 146 | + .slide-container { |
| 147 | + position: relative; |
| 148 | + aspect-ratio: 16 / 9; |
| 149 | + overflow: hidden; |
| 150 | + } |
| 151 | + |
| 152 | + .slide-image { |
| 153 | + position: absolute; |
| 154 | + top: 0; |
| 155 | + left: 0; |
| 156 | + width: 100%; |
| 157 | + height: 100%; |
| 158 | + object-fit: contain; |
| 159 | + } |
| 160 | + |
| 161 | + kbd { |
| 162 | + font-family: monospace; |
| 163 | + font-size: 0.875em; |
| 164 | + } |
| 165 | +</style> |
0 commit comments