Studi Kasus #22: Aplikasi Wisata Indonesia – Menampilkan Galeri Foto dengan Lightbox
Halo, calon full-stack developer!
Di Studi Kasus #21, kita telah menambahkan state global dengan Context API untuk notifikasi dan autentikasi. Sekarang kita akan meningkatkan pengalaman pengguna pada halaman detail dengan menambahkan fitur lightbox pada galeri foto. Lightbox memungkinkan pengguna mengklik gambar galeri untuk melihatnya dalam ukuran besar, serta dapat bernavigasi ke gambar sebelumnya atau berikutnya.
Fitur yang akan dibuat:
- Saat gambar di grid galeri diklik, tampil modal dengan gambar besar.
- Modal memiliki tombol close, tombol prev, dan tombol next.
- Navigasi dapat dilakukan dengan keyboard (panah kiri/kanan, Escape untuk menutup).
- Klik di luar gambar juga menutup modal.
- Indikator halaman (misal "1/5").
Kita akan membuat komponen Lightbox sendiri tanpa library eksternal agar lebih ringan dan sesuai kebutuhan. Mari kita mulai!
Langkah 1: Membuat Komponen Lightbox
Buat file src/components/Lightbox.jsx. Komponen ini akan menerima props: images (array URL gambar), initialIndex (indeks gambar yang ditampilkan pertama), onClose (fungsi untuk menutup), dan isOpen (boolean).
import React, { useEffect, useState } from 'react';
import './Lightbox.css';
const Lightbox = ({ images, initialIndex = 0, onClose, isOpen }) => {
const [currentIndex, setCurrentIndex] = useState(initialIndex);
// Reset index saat initialIndex berubah (misal gambar berbeda)
useEffect(() => {
setCurrentIndex(initialIndex);
}, [initialIndex]);
// Handler untuk navigasi
const goToNext = () => {
setCurrentIndex((prev) => (prev + 1) % images.length);
};
const goToPrev = () => {
setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
};
// Keyboard events
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e) => {
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowRight') goToNext();
if (e.key === 'ArrowLeft') goToPrev();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose, goToNext, goToPrev]); // perhatikan dependencies
// Jika tidak open, tidak render
if (!isOpen) return null;
return (
<div className="lightbox-overlay" onClick={onClose}>
<div className="lightbox-content" onClick={(e) => e.stopPropagation()}>
<button className="lightbox-close" onClick={onClose}>×</button>
<button className="lightbox-nav lightbox-prev" onClick={goToPrev}>‹</button>
<button className="lightbox-nav lightbox-next" onClick={goToNext}>›</button>
<img src={images[currentIndex]} alt={`Gallery ${currentIndex + 1}`} className="lightbox-image" />
<div className="lightbox-counter">
{currentIndex + 1} / {images.length}
</div>
</div>
</div>
);
};
export default Lightbox;
Penjelasan:
- Komponen menerima
isOpenuntuk mengontrol tampilan. currentIndexdiatur dariinitialIndexdan berubah saat navigasi.- Fungsi
goToNextdangoToPrevmenggunakan modulus untuk looping. - Efek keyboard menambahkan event listener saat lightbox terbuka.
- Klik pada overlay menutup lightbox, tapi klik pada konten tidak (stopPropagation).
Langkah 2: Styling Lightbox
Buat file src/components/Lightbox.css:
.lightbox-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.lightbox-content {
position: relative;
max-width: 90%;
max-height: 90%;
}
.lightbox-image {
max-width: 100%;
max-height: 90vh;
border-radius: 4px;
box-shadow: 0 0 20px rgba(255,255,255,0.2);
}
.lightbox-close {
position: absolute;
top: -40px;
right: 0;
background: transparent;
border: none;
color: white;
font-size: 40px;
cursor: pointer;
padding: 0 10px;
line-height: 1;
}
.lightbox-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0,0,0,0.5);
border: none;
color: white;
font-size: 48px;
padding: 0 15px;
cursor: pointer;
border-radius: 4px;
transition: background 0.3s;
}
.lightbox-nav:hover {
background: rgba(0,0,0,0.8);
}
.lightbox-prev {
left: 10px;
}
.lightbox-next {
right: 10px;
}
.lightbox-counter {
position: absolute;
bottom: -30px;
left: 50%;
transform: translateX(-50%);
color: white;
font-size: 16px;
background: rgba(0,0,0,0.5);
padding: 5px 15px;
border-radius: 20px;
}
Langkah 3: Mengintegrasikan Lightbox ke Halaman Detail
Sekarang kita akan memodifikasi halaman detail (misal DestinationDetail.jsx) untuk menggunakan Lightbox. Kita akan menambahkan state untuk menyimpan gambar yang dipilih dan status open.
Buka file src/pages/DestinationDetail.jsx. Tambahkan state berikut di dalam komponen (setelah state lainnya):
const [lightboxOpen, setLightboxOpen] = useState(false);
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
Kemudian buat fungsi untuk membuka lightbox:
const openLightbox = (index) => {
setSelectedImageIndex(index);
setLightboxOpen(true);
};
Pada bagian render galeri, kita ubah setiap gambar agar bisa diklik:
<div className="gallery-grid">
{galleries.map((img, index) => (
<img
key={img.id}
src={`${uploadUrl}/${img.image_url}`}
alt={`Gallery ${img.id}`}
className="gallery-image"
onClick={() => openLightbox(index)}
/>
))}
</div>
Jangan lupa untuk mengimpor komponen Lightbox dan menempatkannya di dalam JSX, misalnya setelah bagian galeri atau di akhir komponen (sebelum penutup).
// Impor di bagian atas
import Lightbox from '../components/Lightbox';
// Di dalam return, setelah konten utama (atau di mana saja)
<Lightbox
images={galleries.map(img => `${uploadUrl}/${img.image_url}`)}
initialIndex={selectedImageIndex}
isOpen={lightboxOpen}
onClose={() => setLightboxOpen(false)}
/>
Perhatikan bahwa kita memetakan galleries menjadi array URL penuh untuk diberikan ke Lightbox.
Lakukan hal yang sama untuk halaman AccommodationDetail.jsx dan TransportationDetail.jsx.
Langkah 4: Menguji Fitur Lightbox
Jalankan aplikasi, buka halaman detail destinasi yang memiliki beberapa gambar galeri. Klik salah satu gambar. Seharusnya muncul lightbox dengan gambar besar. Coba navigasi dengan tombol panah atau keyboard. Tekan Escape untuk menutup. Klik di luar gambar juga harus menutup.
Pastikan tidak ada error di konsol.
Penanganan Jika Hanya Satu Gambar
Jika hanya ada satu gambar, tombol prev/next mungkin tetap muncul. Kita bisa menyembunyikannya jika images.length <= 1. Modifikasi komponen Lightbox:
{images.length > 1 && (
<button className="lightbox-nav lightbox-prev" onClick={goToPrev}>‹</button>
)}
{images.length > 1 && (
<button className="lightbox-nav lightbox-next" onClick={goToNext}>›</button>
)}
Juga, counter bisa ditampilkan meskipun hanya satu gambar.
🔍 Menggunakan Library Eksternal (Opsional)
Jika ingin fitur lebih lengkap (seperti zoom, swipe di mobile), kamu bisa menggunakan library seperti react-image-lightbox atau yet-another-react-lightbox. Cara install:
npm install yet-another-react-lightbox
Kemudian gunakan komponen yang disediakan. Namun untuk pembelajaran, membuat sendiri lebih baik agar memahami logikanya.
Langkah Selanjutnya
Di Studi Kasus #23, kita akan menambahkan fitur pencarian lanjutan (filter berdasarkan kategori, harga, dll.) dan mungkin mulai mempersiapkan deployment. Atau jika ingin, kita bisa membuat fitur pemesanan sederhana.
Pastikan lightbox berfungsi dengan baik di semua halaman detail.
Sampai jumpa di tutorial berikutnya!
Ditulis dengan ❤️ untuk para pengembang yang ingin membangun aplikasi wisata.