Files
website/app/templates/gallery.templ
Jimmy 3f5235daaf Improve gallery video UX and add upload-to-publish media workflow.
Stage raw files in upload/, publish with make sync-media/publish, and polish the lightbox: autoplay, remembered volume, Escape to close, and image/video icons without poster or caption clutter.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 23:55:43 +12:00

130 lines
3.4 KiB
Plaintext

package templates
import (
"fmt"
"time"
"technical.kiwi/website/internal/gallery"
)
templ GalleryGrid(images []gallery.Image) {
if len(images) == 0 {
<p class="gallery-empty">No images for this filter.</p>
} else {
for _, img := range images {
<button
type="button"
class="gallery-item"
hx-get={ fmt.Sprintf("/gallery/%s", gallery.URLPath(img.RelPath)) }
hx-target="#modal-root"
hx-swap="innerHTML"
aria-label={ fmt.Sprintf("Open %s", gallery.AlbumLabel(img.Album)) }
>
if img.ThumbURL != "" {
<img
src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="
data-src={ img.ThumbURL }
class="lazy-thumb"
alt={ fmt.Sprintf("%s — %s", gallery.AlbumLabel(img.Album), formatDate(img.Date)) }
loading="lazy"
decoding="async"
sizes="(max-width: 480px) 45vw, 160px"
/>
} else {
<span class="gallery-placeholder" aria-hidden="true"></span>
}
@GalleryMediaIcon(img.IsVideo)
if img.Collection != "" {
<span class="gallery-album">{ img.Collection }</span>
} else if img.Album != "" {
<span class="gallery-album">{ gallery.AlbumLabel(img.Album) }</span>
}
if !img.Date.IsZero() {
<span class="gallery-date">{ formatDate(img.Date) }</span>
}
</button>
}
}
}
templ GalleryMediaIcon(isVideo bool) {
<span class="gallery-media-icon" aria-hidden="true">
if isVideo {
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"></path>
</svg>
} else {
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"></path>
</svg>
}
</span>
}
templ ImageModal(img gallery.Image, prevPath, nextPath string) {
<div
class="modal-backdrop"
hx-on:click="if (event.target === this) this.remove()"
>
<div class="modal" role="dialog" aria-modal="true" aria-label={ modalAriaLabel(img) }>
<button
type="button"
class="modal-close"
onclick="document.getElementById('modal-root').innerHTML = ''"
aria-label="Close"
>
&times;
</button>
<figure class="modal-figure">
if img.IsVideo {
<div class="modal-video-stack">
<video src={ img.URL } controls autoplay playsinline preload="auto"></video>
</div>
} else {
<img src={ img.URL } alt={ img.Filename }/>
}
</figure>
<nav class="modal-nav" aria-label="Gallery navigation">
if prevPath != "" {
<button
type="button"
class="modal-nav-btn"
hx-get={ fmt.Sprintf("/gallery/%s", gallery.URLPath(prevPath)) }
hx-target="#modal-root"
hx-swap="innerHTML"
>
&larr; Previous
</button>
} else {
<span></span>
}
if nextPath != "" {
<button
type="button"
class="modal-nav-btn"
hx-get={ fmt.Sprintf("/gallery/%s", gallery.URLPath(nextPath)) }
hx-target="#modal-root"
hx-swap="innerHTML"
>
Next &rarr;
</button>
}
</nav>
</div>
</div>
}
func formatDate(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format("Jan 2006")
}
func modalAriaLabel(img gallery.Image) string {
if img.IsVideo {
return "Video viewer"
}
return "Image viewer"
}