diff --git a/css/portfolio.css b/css/portfolio.css index 7cd72de..d25df4b 100644 --- a/css/portfolio.css +++ b/css/portfolio.css @@ -9,6 +9,7 @@ border-radius: var(--border-radius); transition: transform 0.3s, box-shadow 0.3s; position: relative; + cursor: pointer; } & > *:hover { @@ -41,3 +42,68 @@ grid-row: span 2; } } + +#lightbox { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + opacity: 0; + pointer-events: none; + transition: opacity 0.3s; + + &.open { + opacity: 1; + pointer-events: all; + } + + & > img { + max-width: min(90vw, 1200px); + max-height: 85vh; + border-radius: var(--small-radius); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + object-fit: contain; + } + + & > button { + position: absolute; + background: rgba(0, 0, 0, 0.5); + border: none; + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + width: 3rem; + height: 3rem; + border-radius: 50%; + transition: background 0.2s; + + &:hover { + background: rgba(0, 0, 0, 0.8); + } + + & svg { + width: 1.5rem; + height: 1.5rem; + } + } + + & #lb-close { + top: 1.5rem; + right: 1.5rem; + } + + & #lb-prev { + left: 1.5rem; + } + + & #lb-next { + right: 1.5rem; + } +} diff --git a/html/portfolio.templ b/html/portfolio.templ index 3d62e6c..b16717e 100644 --- a/html/portfolio.templ +++ b/html/portfolio.templ @@ -14,5 +14,6 @@ templ Portfolio(images []string) { + } } diff --git a/html/portfolio_templ.go b/html/portfolio_templ.go index f8162e9..84b2ab1 100644 --- a/html/portfolio_templ.go +++ b/html/portfolio_templ.go @@ -67,7 +67,7 @@ func Portfolio(images []string) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/js/portfolio.js b/js/portfolio.js new file mode 100644 index 0000000..c69ba3a --- /dev/null +++ b/js/portfolio.js @@ -0,0 +1,61 @@ +(function () { + var grid = document.getElementById('portfolio-grid'); + if (!grid) return; + + var items = Array.from(grid.querySelectorAll(':scope > div')); + if (!items.length) return; + + var lb = document.createElement('div'); + lb.id = 'lightbox'; + lb.innerHTML = + '' + + '' + + '' + + ''; + document.body.appendChild(lb); + + var lbImg = lb.querySelector('img'); + var current = -1; + + function open(idx) { + current = idx; + var img = items[current].querySelector('img'); + lbImg.src = img.src; + lbImg.alt = img.alt; + lb.classList.add('open'); + } + + function close() { + lb.classList.remove('open'); + current = -1; + } + + function prev() { + if (current < 0) return; + open((current - 1 + items.length) % items.length); + } + + function next() { + if (current < 0) return; + open((current + 1) % items.length); + } + + items.forEach(function (item, i) { + item.addEventListener('click', function () { open(i); }); + }); + + lb.querySelector('#lb-close').addEventListener('click', close); + lb.querySelector('#lb-prev').addEventListener('click', function (e) { e.stopPropagation(); prev(); }); + lb.querySelector('#lb-next').addEventListener('click', function (e) { e.stopPropagation(); next(); }); + + document.addEventListener('keydown', function (e) { + if (!lb.classList.contains('open')) return; + if (e.key === 'Escape') { close(); e.preventDefault(); } + if (e.key === 'ArrowLeft') { prev(); e.preventDefault(); } + if (e.key === 'ArrowRight') { next(); e.preventDefault(); } + }); + + lb.addEventListener('click', function (e) { + if (e.target === lb) close(); + }); +})(); diff --git a/main.go b/main.go index 9b0b069..9310c89 100644 --- a/main.go +++ b/main.go @@ -105,6 +105,7 @@ func main() { mergeStyles() utils.CopyFile("img/favicon.avif", "www/images/favicon.avif") utils.CopyFile("img/logo.svg", "www/images/logo.svg") + utils.CopyFile("js/portfolio.js", "www/portfolio.js") log.Println("listening on :8000") log.Fatal(http.ListenAndServe(":8000", http.FileServer(http.Dir("www")))) }