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>
130 lines
3.4 KiB
Plaintext
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"
|
|
>
|
|
×
|
|
</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"
|
|
>
|
|
← 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 →
|
|
</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"
|
|
}
|