Refresh gallery metadata after thumbnail generation so new thumb URLs are available immediately, and lazy-load gallery thumbnails with IntersectionObserver to avoid fetching all images on initial page load. Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
3.1 KiB
Plaintext
129 lines
3.1 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>
|
|
}
|
|
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"
|
|
>
|
|
×
|
|
</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"
|
|
>
|
|
← 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"
|
|
}
|
|
|
|
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)
|
|
}
|