Course Goal: By the end of this course, you will build a fully functional, beautiful responsive image gallery using ONLY one HTML file, CSS, and pure JavaScript (no React, no frameworks).
Target student:
- Knows basic HTML (tags, attributes)
- Knows basic CSS (selectors, colors, flexbox/grid is a bonus)
- Has heard of JavaScript but never really used it
Final Project: A single-page image gallery that loads images from a JavaScript object/JSON, lets you click thumbnails to see a bigger version in a modal/lightbox, with next/prev buttons.
1–10 → JavaScript Fundamentals
11–18 → DOM Manipulation & Events
19–25 → Building the Image Gallery step-by-step
26–30 → Polish: modal, keyboard navigation, responsive design, comments & best practices
Here are the first 10 lessons with titles, goals, short explanation, and copy-pasteable code examples you can put directly use on the course website.
────────────────────────────────────
Goal: Run your first line of JavaScript.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript Course - Lesson 1</title>
</head>
<body>
<h1>My first JS page</h1>
<!-- Method 1: Inline (only for learning) -->
<script>
alert("Hello from JavaScript! 🎉");
</script>
<!-- Method 2: External file (recommended) -->
<script src="script.js"></script>
</body>
</html>Create a file called script.js in the same folder:
// script.js
console.log("Hello from external file!");Key takeaway: <script> tag or src attribute.
────────────────────────────────────
// Use const when value will NOT be reassigned
const studentName = "Alex";
// Use let when you WILL reassign
let score = 85;
score = 92;
// var is old – avoid it in new code
var oldWay = "please don't";Practice: Create 5 const and 3 let variables about yourself.
────────────────────────────────────
const name = "Maria"; // string
const age = 27; // number (no int/float difference)
const isStudent = true; // boolean
const nothing = null; // intentional empty value
let notAssigned; // undefined
const id = Symbol("id"); // unique identifier (advanced, just know it exists)
console.log(typeof age); // "number"Challenge: Use typeof on 8 different values.
────────────────────────────────────
const firstName = "Sam";
const city = "Berlin";
// Old way
console.log("Hello " + firstName + " from " + city + "!");
// Modern way (ES6+) – BACKTICKS!
console.log(`Hello ${firstName} from ${city}! 🎉`);
// Multi-line strings
const message = `
Dear ${firstName},
Welcome to the course!
`;
console.log(message);────────────────────────────────────
let likes = 42;
likes++; // 43
likes += 10; // 53
const price = 19.99;
const tax = price * 0.2;
const total = price + tax;
console.log(total.toFixed(2)); // "23.99"Common gotcha:
console.log(0.1 + 0.2); // 0.30000000000000004 ← floating point precision────────────────────────────────────
const fruits = ["apple", "banana", "orange"];
// Access by index (0-based)
console.log(fruits[1]); // "banana"
// Add to end
fruits.push("mango");
// Remove from end
fruits.pop();
// Add to beginning
fruits.unshift("kiwi");
// Length
console.log(fruits.length); // 4────────────────────────────────────
const person = {
name: "Luna",
age: 24,
city: "Tokyo",
hobbies: ["photography", "coding", "gaming"],
isDeveloper: true
};
// Dot notation
console.log(person.name);
// Bracket notation (useful when key has spaces or is dynamic)
console.log(person["city"]);
// Add or modify
person.age = 25;
person.country = "Japan";────────────────────────────────────
<script>
const temperature = 18;
if (temperature > 30) {
console.log("It's hot! 🌞");
} else if (temperature > 15) {
console.log("Perfect weather 😎");
} else {
console.log("A bit cold �");
}
// Ternary operator (shortcut)
const message = temperature > 20 ? "Warm" : "Cool";
console.log(message);
</script>Comparison operators: > < >= <= === !==
────────────────────────────────────
// for loop – most common
for (let i = 0; i < 5; i++) {
console.log(`Loop number ${i}`);
}
// Loop through array
const colors = ["red", "green", "blue"];
for (let i = 0; i < colors.length; i++) {
console.log(colors[i]);
}
// while loop
let count = 0;
while (count < 3) {
console.log("Hello");
count++;
}────────────────────────────────────
const pets = ["cat", "dog", "rabbit", "hamster"];
// Classic way
pets.forEach(function(pet) {
console.log(pet);
});
// Arrow function (ES6+) – you’ll see this everywhere
pets.forEach(pet => console.log(pet));
// for...of – cleanest for simple cases
for (const pet of pets) {
console.log(pet);
}Mini-project for lesson 10:
<h2>My Pets</h2>
<ul id="petList"></ul>
<script>
const pets = ["🐱 Cat", "🐶 Dog", "🐰 Rabbit"];
const list = document.getElementById("petList");
pets.forEach(pet => {
const li = document.createElement("li");
li.textContent = pet;
list.appendChild(li);
});
</script>Building Toward the Final Image Gallery (single HTML file, no frameworks)
Each lesson is ready to copy-paste as its own complete HTML page so students can open it instantly in the browser.
────────────────────────────────────
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lesson 11 – The DOM</title>
</head>
<body>
<h1 id="title">Welcome to the DOM</h1>
<p class="info">JavaScript can change everything you see here!</p>
<script>
// The entire page is represented as a tree → Document Object Model
console.log(document); // the whole document
console.log(document.body); // <body> element
console.log(document.getElementById("title")); // the h1
// Quick demo
alert("There are " + document.images.length + " images on this page right now.");
</script>
</body>
</html>Key takeaway: document is your entry point to the page.
────────────────────────────────────
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lesson 12 – Selectors</title>
<style>
.box { background: coral; padding: 20px; margin: 10px; }
.highlight { background: gold; }
</style>
</head>
<body>
<div class="box">Box 1</div>
<div class="box">Box 2</div>
<div class="box" id="special">Special Box</div>
<script>
// Same syntax as CSS selectors!
const special = document.querySelector("#special"); // first match
const firstBox = document.querySelector(".box"); // first .box
const allBoxes = document.querySelectorAll(".box"); // NodeList
special.classList.add("highlight");
allBoxes.forEach(box => box.textContent += " – selected!");
</script>
</body>
</html>────────────────────────────────────
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lesson 13 – Change Content</title>
</head>
<body>
<h1 id="greeting">Hello...</h1>
<div id="output"></div>
<script>
const greeting = document.getElementById("greeting");
// Safe – only text
greeting.textContent = "Hello <strong>You</strong>! (tags not rendered)";
// Dangerous if data comes from users, but great for HTML
greeting.innerHTML = "Hello <strong>You</strong>! (bold works)";
// Let's build a small list with innerHTML
const output = document.getElementById("output");
const fruits = ["Apple", " , "Banana " , "Mango " ];
let html = "<ul>";
fruits.forEach(fruit => html += `<li>${fruit}</li>`);
html += "</ul>";
output.innerHTML = html;
</script>
</body>
</html>────────────────────────────────────
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lesson 14 – Create & Remove</title>
<style> .card { border: 2px solid #333; margin: 10px; padding: 15px; display: inline-block; } </style>
</head>
<body>
<h2>My Dynamic Cards</h2>
<div id="container"></div>
<button id="add">Add Card</button>
<button id="clear">Clear All</button>
<script>
const container = document.getElementById("container");
const addBtn = document.getElementById("add");
const clearBtn = document.getElementById("clear");
let count = 1;
addBtn.addEventListener("click", () => {
const card = document.createElement("div");
card.className = "card";
card.innerHTML = `<strong>Card ${count}</strong><br>Created at ${new Date().toLocaleTimeString()}`;
container.appendChild(card);
count++;
});
clearBtn.addEventListener("click", () => {
container.innerHTML = ""; // fastest way to remove everything
});
</script>
</body>
</html>────────────────────────────────────
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lesson 15 – Events</title>
<style>
.box { width: 200px; height: 200px; background: lightblue; margin: 20px; }
.hover { background: hotpink; }
</style>
</head>
<body>
<div id="box" class="box"></div>
<p>Keys pressed: <span id="keys"></span></p>
<button id="clickMe">Click me!</button>
<script>
const box = document.getElementById("box");
const clickBtn = document.getElementById("clickMe");
const keysSpan = document.getElementById("keys");
// Click
clickBtn.addEventListener("click", () => alert("Button clicked!"));
// Mouse enter / leave
box.addEventListener("mouseenter", () => box.classList.add("hover"));
box.addEventListener("mouseleave", () => box.classList.remove("hover"));
// Keyboard
document.addEventListener("keydown", (event) => {
keysSpan.textContent += event.key + " ";
});
</script>
</body>
</html>────────────────────────────────────
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lesson 16 – Arrow Functions</title>
</head>
<body>
<button class="btn">Button 1</button>
<button class="btn">Button 2</button>
<button class="btn">Button 3</button>
<script>
// Traditional function → "this" changes!
// const buttons = document.querySelectorAll(".btn");
// buttons.forEach(function(btn) {
// btn.addEventListener("click", function() {
// this.style.background = "orange"; // "this" is the button (good!)
// });
// });
// Arrow function → cleaner and very common in modern code
document.querySelectorAll(".btn").forEach(btn => {
btn.addEventListener("click", () => {
btn.style.background = "limegreen";
btn.textContent = "Clicked!";
});
});
</script>
</body>
</html>────────────────────────────────────
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lesson 17 – JSON</title>
</head>
<body>
<pre id="output"></pre>
<script>
// This is valid JSON (string)
const jsonString = `{
"name": "Clara",
"age": 29,
"hobbies": ["climbing", "photography"],
"isDeveloper": true,
"address": null
}`;
// Convert JSON string → real JS object
const person = JSON.parse(jsonString);
console.log(person.name); // Clara
// Convert JS object → JSON string (e.g., to send to a server)
const newObject = { title: "My Cat", likes: 1024 };
const newJson = JSON.stringify(newObject);
console.log(newJson); // {"title":"My Cat","likes":1024}
// Show on page
document.getElementById("output").textContent =
`Name: ${person.name}\nHobbies: ${person.hobbies.join(", ")}`;
</script>
</body>
</html>────────────────────────────────────
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lesson 18 – Gallery Data</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 40px auto; }
pre { background: #f4f4f4; padding: 15px; border-radius: 8px; }
</style>
</head>
<body>
<h1>Image Gallery – Final Project Data</h1>
<p>We will store all images in one JavaScript array of objects:</p>
<pre id="data"></pre>
<script>
const galleryImages = [
{
id: 1,
title: "Sunset in Santorini",
thumbnail: "https://picsum.photos/300/200?random=1",
full: "https://picsum.photos/800/600?random=1",
description: "Beautiful Greek island sunset"
},
{
id: 2,
title: "Forest Path",
thumbnail: "https://picsum.photos/300/200?random=2",
full: "https://picsum.photos/800/600?random=2",
description: "Misty morning walk"
},
{
id: 3,
title: "City Nights",
thumbnail: "https://picsum.photos/300/200?random=3",
full: "https://picsum.photos/800/600?random=3",
description: "Neon lights in Tokyo"
}
// Students will add 5–10 more themselves
];
// Show the data nicely
document.getElementById("data").textContent = JSON.stringify(galleryImages, null, 2);
</script>
<h2>Next lessons (19–25):</h2>
<ol>
<li>Render thumbnails with .map() + innerHTML</li>
<li>Click thumbnail → open modal/lightbox</li>
<li>Next / Previous buttons in modal</li>
<li>Keyboard navigation (arrow keys, ESC)</li>
<li>Make it responsive with CSS Grid</li>
<li>Loading spinner & error handling</li>
<li>Final polish & comments</li>
</ol>
</body>
</html>One single HTML file · No frameworks · Fully working responsive image gallery with lightbox
Each lesson adds one major feature. By Lesson 25 you have the complete, beautiful final project.
Copy the code exactly — it works immediately when saved as index.html.
────────────────────────────────────
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lesson 19 – Render Thumbnails</title>
<style>
body { font-family: system-ui, sans-serif; margin: 40px; background: #f9f9f9; }
#gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
.thumb { border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: pointer; transition: transform .2s; }
.thumb:hover { transform: scale(1.98); }
.thumb img { width: 100%; height: 200px; object-fit: cover; }
.caption { padding: 10px; background: white; text-align: center; font-weight: bold; }
</style>
</head>
<body>
<h1>My JavaScript Image Gallery</h1>
<div id="gallery"></div>
<script>
const galleryImages = [
{id:1, title:"Santorini Sunset", thumb:"https://picsum.photos/400/300?random=1", full:"https://picsum.photos/1200/800?random=1", desc:"Golden hour in Greece"},
{id:2, title:"Forest Path", thumb:"https://picsum.photos/400/300?random=2", full:"https://picsum.photos/1200/800?random=2", desc:"Misty morning walk"},
{id:3, title:"Tokyo Nights", thumb:"https://picsum.photos/400/300?random=3", full:"https://picsum.photos/1200/800?random=3", desc:"Neon dreams"},
{id:4, title:"Mountain Lake", thumb:"https://picsum.photos/400/300?random=4", full:"https://picsum.photos/1200/800?random=4", desc:"Crystal clear water"},
{id:5, title:"Desert Dunes", thumb:"https://picsum.photos/400/300?random=5", full:"https://picsum.photos/1200/800?random=5", desc:"Endless sand waves"},
{id:6, title:"Northern Lights", thumb:"https://picsum.photos/400/300?random=6", full:"https://picsum.photos/1200/800?random=6", desc:"Aurora magic"}
];
const gallery = document.getElementById("gallery");
const html = galleryImages
.map(img => `
<figure class="thumb" data-id="${img.id}">
<img src="${img.thumb}" alt="${img.title}" loading="lazy">
<figcaption class="caption">${img.title}</figcaption>
</figure>
`)
.join("");
gallery.innerHTML = html;
</script>
</body>
</html>────────────────────────────────────
<!-- Add this just before closing </body> in the previous file -->
<div id="modal" class="modal">
<span class="close">×</span>
<img id="modalImage" src="" alt="">
<div id="caption"></div>
</div>
<style>
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); justify-content: center; align-items: center; }
.modal img { max-width: 90%; max-height: 80vh; border-radius: 12px; }
#caption { color: white; text-align: center; margin-top: 15px; font-size: 1.2rem; }
.close { position: absolute; top: 20px; right: 35px; color: white; font-size: 50px; cursor: pointer; }
</style>
<script>
const modal = document.getElementById("modal");
const modalImg = document.getElementById("modalImage");
const captionText = document.getElementById("caption");
document.querySelectorAll(".thumb").forEach(thumb => {
thumb.addEventListener("click", () => {
const id = Number(thumb.dataset.id);
const img = galleryImages.find(i => i.id === id);
modal.style.display = "flex";
modalImg.src = img.full;
captionText.textContent = img.desc;
document.body.style.overflow = "hidden"; // prevent scroll
});
});
// Close modal
document.querySelector(".close").addEventListener("click", () => {
modal.style.display = "none";
document.body.style.overflow = "auto";
});
// Click outside image → close
modal.addEventListener("click", e => {
if (e.target === modal) {
modal.style.display = "none";
document.body.style.overflow = "auto";
}
});
</script>────────────────────────────────────
<!-- Replace the modal HTML with this improved version -->
<div id="modal" class="modal">
<span class="close">×</span>
<button class="nav prev">‹</button>
<img id="modalImage" src="" alt="">
<button class="nav next">›</button>
<div id="caption"></div>
</div>
<style>
.nav { position: absolute; top: 50%; background: rgba(0,0,0,0.6); color: white; border: none; font-size: 3rem; padding: 10px 20px; cursor: pointer; border-radius: 8px; transform: translateY(-50%); }
.prev { left: 20px; }
.next { right: 20px; }
.nav:hover { background: rgba(0,0,0,0.9); }
</style>
<script>
let currentIndex = 0;
function openModal(index) {
currentIndex = index;
const img = galleryImages[index];
modal.style.display = "flex";
modalImg.src = img.full;
captionText.textContent = img.desc;
document.body.style.overflow = "hidden";
}
// Update click on thumbnails
document.querySelectorAll(".thumb").forEach((thumb, i) => {
thumb.addEventListener("click", () => openModal(i));
});
// Navigation
document.querySelector(".prev").addEventListener("click", () => {
currentIndex = (currentIndex - 1 + galleryImages.length) % galleryImages.length;
openModal(currentIndex);
});
document.querySelector(".next").addEventListener("click", () => {
currentIndex = (currentIndex + 1) % galleryImages.length;
openModal(currentIndex);
});
</script>────────────────────────────────────
<script>
document.addEventListener("keydown", e => {
if (modal.style.display === "none") return;
if (e.key === "ArrowRight") document.querySelector(".next").click();
if (e.key === "ArrowLeft") document.querySelector(".prev").click();
if (e.key === "Escape") document.querySelector(".close").click();
});
</script>────────────────────────────────────
<script>
function preloadAdjacent() {
const prev = (currentIndex - 1 + galleryImages.length) % galleryImages.length;
const next = (currentIndex + 1) % galleryImages.length;
new Image().src = galleryImages[prev].full;
new Image().src = galleryImages[next].full;
}
function openModal(index) {
currentIndex = index;
const img = galleryImages[index];
modal.style.display = "flex";
modalImg.src = img.full;
captionText.textContent = img.desc;
document.body.style.overflow = "hidden";
preloadAdjacent();
}
</script>────────────────────────────────────
<!-- Add inside modal, after <img> -->
<div id="loader" class="loader"></div>
<style>
.loader { display: none; border: 8px solid #f3f3f3; border-top: 8px solid #3498db; border-radius: 50%; width: 60px; height: 60px; animation: spin 1s linear infinite; position: absolute; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
<script>
modalImg.addEventListener("load", () => document.getElementById("loader").style.display = "none");
modalImg.addEventListener("error", () => {
modalImg.src = "https://via.placeholder.com/800x600?text=Image+Not+Found";
});
function openModal(index) {
// ... same as before
document.getElementById("loader").style.display = "block";
modalImg.src = ""; // reset
modalImg.src = img.full;
}
</script>────────────────────────────────────
Here is the complete final version — ready to ship!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS Image Gallery – Final Project</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; padding: 40px; background: #f0d1117; color: #c9d1d9; }
h1 { text-align: center; color: #58a6ff; }
#gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 24px; max-width: 1400px; margin: 0 auto; }
.thumb { border-radius: 16px; overflow: hidden; background: #161b22; box-shadow: 0 8px 24px rgba(0,0,0,0.4); transition: transform .3s, box-shadow .3s; cursor: pointer; }
.thumb:hover { transform: translateY(-8px); box-shadow: 0 20px 40px rgba(0,0,0,0.6); }
.thumb img { width: 100%; height: 220px; object-fit: cover; }
.caption { padding: 14px; text-align: center; font-weight: 600; font-size: 1.1rem; }
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); justify-content: center; align-items: center; flex-direction: column; }
.modal img { max-width: 94%; max-height: 80vh; border-radius: 16px; box-shadow: 0 0 40px rgba(0,0,0,0.8); }
#caption { margin-top: 20px; font-size: 1.4rem; max-width: 90%; }
.close { position: absolute; top: 30px; right: 40px; font-size: 60px; color: #fff; opacity: 0.8; cursor: pointer; }
.close:hover { opacity: 1; }
.nav { position: absolute; top: 50%; background: rgba(255,255,255,0.2); color: white; border: none; width: 60px; height: 60px; font-size: 40px; border-radius: 50%; cursor: pointer; transform: translateY(-50%); backdrop-filter: blur(10px); }
.prev { left: 30px; }
.next { right: 30px; }
.loader { border: 8px solid #f3f3f3; border-top: 8px solid #58a6ff; border-radius: 50%; width: 60px; height: 60px; animation: spin 1s linear infinite; margin-top: 20px; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<h1>My JavaScript Image Gallery</h1>
<div id="gallery"></div>
<div id="modal" class="modal">
<span class="close">×</span>
<button class="nav prev">‹</button>
<img id="modalImage" src="" alt="">
<button class="nav next">›</button>
<div id="loader" class="loader"></div>
<div id="caption"></div>
</div>
<script>
const galleryImages = [
{id:1, title:"Santorini Sunset", thumb:"https://picsum.photos/500/400?random=10", full:"https://picsum.photos/1600/1200?random=10", desc:"Golden hour in Greece"},
{id:2, title:"Misty Forest", thumb:"https://picsum.photos/500/400?random=11", full:"https://picsum.photos/1600/1200?random=11", desc:"Morning fog in the woods"},
{id:3, title:"Tokyo Neon", thumb:"https://picsum.photos/500/400?random=12", full:"https://picsum.photos/1600/1200?random=12", desc:"Night city vibes"},
{id:4, title:"Alpine Lake", thumb:"https://picsum.photos/500/400?random=13", full:"https://picsum.photos/1600/1200?random=13", desc:"Crystal clear mountain water"},
{id:5, title:"Sahara Dunes", thumb:"https://picsum.photos/500/400?random=14", full:"https://picsum.photos/1600/1200?random=14", desc:"Endless golden waves"},
{id:6, title:"Aurora Borealis", thumb:"https://picsum.photos/500/400?random=15", full:"https://picsum.photos/1600/1200?random=15", desc:"Nature's light show"},
{id:7, title:"Cherry Blossoms", thumb:"https://picsum.photos/500/400?random=16", full:"https://picsum.photos/1600/1200?random=16", desc:"Spring in Kyoto"},
{id:8, title:"Milky Way", thumb:"https://picsum.photos/500/400?random=17", full:"https://picsum.photos/1600/1200?random=17", desc:"Starry night sky"}
];
const gallery = document.getElementById("gallery");
const modal = document.getElementById("modal");
const modalImg = document.getElementById("modalImage");
const caption = document.getElementById("caption");
const loader = document.getElementById("loader");
let currentIndex = 0;
// Render thumbnails
gallery.innerHTML = galleryImages.map((img, i) => `
<figure class="thumb" data-index="${i}">
<img src="${img.thumb}" alt="${img.title}" loading="lazy">
<figcaption class="caption">${img.title}</figcaption>
</figure>
`).join("");
// Open modal
gallery.addEventListener("click", e => {
const thumb = e.target.closest(".thumb");
if (!thumb) return;
openModal(Number(thumb.dataset.index));
});
function openModal(index) {
currentIndex = index;
const img = galleryImages[index];
modal.style.display = "flex";
loader.style.display = "block";
modalImg.style.opacity = 0;
modalImg.src = img.full;
caption.textContent = img.desc;
document.body.style.overflow = "hidden";
preloadAdjacent();
}
function preloadAdjacent() {
const prev = (currentIndex - 1 + galleryImages.length) % galleryImages.length;
const next = (currentIndex + 1) % galleryImages.length;
new Image().src = galleryImages[prev].full;
new Image().src = galleryImages[next].full;
}
modalImg.onload = () => {
loader.style.display = "none";
modalImg.style.opacity = 1;
};
// Navigation
document.querySelector(".prev").onclick = () => openModal((currentIndex - 1 + galleryImages.length) % galleryImages.length);
document.querySelector(".next").onclick = () => openModal((currentIndex + 1) % galleryImages.length);
document.querySelector(".close").onclick = closeModal;
modal.onclick = e => { if (e.target === modal) closeModal(); }
function closeModal() {
modal.style.display = "none";
document.body.style.overflow = "auto";
}
// Keyboard
document.addEventListener("keydown", e => {
if (modal.style.display !== "flex") return;
if (e.key === "ArrowLeft") document.querySelector(".prev").click();
if (e.key === "ArrowRight") document.querySelector(".next").click();
if (e.key === "Escape") closeModal();
});
</script>
</body>
</html>