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"))))
}