Files
website/app/templates/gallery.templ
jimmy 45b31be9a7 Add gallery admin and video media support.
This updates gallery handling to support video playback with generated poster thumbnails, adds authenticated admin upload/delete flows, and improves dev/runtime behavior including reliable thumbnail generation and media-safe response handling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 23:01:02 +12:00

127 lines
3.0 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={ img.ThumbURL }
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>
}
if img.IsVideo {
<span class="gallery-video-badge" aria-hidden="true">Video</span>
}
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 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 {
<video src={ img.URL } controls playsinline preload="metadata"></video>
} else {
<img src={ img.URL } alt={ img.Filename }/>
}
if !img.Date.IsZero() {
<figcaption>{ modalCaption(img) }</figcaption>
}
</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"
}
func modalCaption(img gallery.Image) string {
if img.Collection != "" {
return img.Collection + " — " + formatDate(img.Date)
}
if img.Album != "" {
return gallery.AlbumLabel(img.Album) + " — " + formatDate(img.Date)
}
return formatDate(img.Date)
}