Studi Kasus #14: Aplikasi Wisata Indonesia – Membuat Halaman Detail Tempat Wisata


Studi Kasus #14: Aplikasi Wisata Indonesia – Membuat Halaman Detail Tempat Wisata

Halo, calon full-stack developer! 

Di Studi Kasus #13, kita telah membuat halaman beranda yang menampilkan daftar destinasi dalam bentuk kartu. Sekarang saatnya kita membuat halaman detail yang akan muncul ketika pengguna mengklik salah satu kartu. Halaman ini akan menampilkan informasi lengkap tentang destinasi, termasuk deskripsi, fasilitas, galeri foto, dan paket harga yang tersedia.

Fitur yang akan dibuat:

  • Mengambil ID dari URL menggunakan useParams.
  • Mengambil data destinasi, galeri, dan paket dari API.
  • Menampilkan loading dan error state.
  • Menampilkan informasi utama: nama, lokasi, deskripsi, fasilitas.
  • Menampilkan galeri foto dalam bentuk grid.
  • Menampilkan paket harga dalam bentuk tabel atau kartu.

Mari kita mulai! 

Langkah 1: Menambahkan Service untuk Galeri dan Paket

Kita perlu membuat fungsi untuk mengambil galeri dan paket berdasarkan entity type dan ID. Buat file src/services/galleryService.js:

import API from './api';

export const getGalleriesByEntity = async (entityType, entityId) => {
  try {
    const response = await API.get(`/galleries/${entityType}/${entityId}`);
    return response.data;
  } catch (error) {
    console.error('Gagal mengambil galeri:', error);
    throw error;
  }
};

Buat file src/services/packageService.js:

import API from './api';

export const getPackagesByEntity = async (entityType, entityId) => {
  try {
    const response = await API.get(`/packages/${entityType}/${entityId}`);
    return response.data;
  } catch (error) {
    console.error('Gagal mengambil paket:', error);
    throw error;
  }
};

Pastikan destinationService.js sudah memiliki fungsi getDestinationById (seperti di tutorial sebelumnya). Jika belum, tambahkan.

📄 Langkah 2: Membuat Halaman Detail Destinasi

Buat file src/pages/DestinationDetail.jsx. Kita akan menggunakan useParams dari react-router-dom untuk mengambil ID dari URL.

import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { getDestinationById } from '../services/destinationService';
import { getGalleriesByEntity } from '../services/galleryService';
import { getPackagesByEntity } from '../services/packageService';
import './DestinationDetail.css';

const DestinationDetail = () => {
  const { id } = useParams(); // ambil ID dari URL
  const [destination, setDestination] = useState(null);
  const [galleries, setGalleries] = useState([]);
  const [packages, setPackages] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        // Ambil data destinasi
        const destData = await getDestinationById(id);
        setDestination(destData);

        // Ambil galeri (entity_type = 'destination')
        const galleryData = await getGalleriesByEntity('destination', id);
        setGalleries(galleryData);

        // Ambil paket (entity_type = 'destination')
        const packageData = await getPackagesByEntity('destination', id);
        setPackages(packageData);
      } catch (err) {
        setError('Gagal memuat data destinasi.');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [id]);

  if (loading) {
    return <div className="loading">Memuat data...</div>;
  }

  if (error || !destination) {
    return <div className="error">{error || 'Destinasi tidak ditemukan'}</div>;
  }

  // Helper untuk URL gambar
  const uploadUrl = import.meta.env.VITE_UPLOAD_URL || 'http://localhost:3000/uploads';

  return (
    <div className="destination-detail">
      {/* Bagian header dengan gambar cover */}
      <div className="detail-header">
        <img 
          src={destination.cover_image ? `${uploadUrl}/${destination.cover_image}` : 'https://via.placeholder.com/1200x400?text=No+Image'} 
          alt={destination.name}
          className="cover-image"
        />
        <h1>{destination.name}</h1>
        <p className="location">📍 {destination.location}</p>
      </div>

      {/* Deskripsi dan fasilitas */}
      <div className="detail-section">
        <h2>Deskripsi</h2>
        <p>{destination.description || 'Tidak ada deskripsi.'}</p>
      </div>

      <div className="detail-section">
        <h2>Fasilitas</h2>
        {destination.facilities ? (
          <p>{destination.facilities}</p>
        ) : (
          <p>Tidak ada informasi fasilitas.</p>
        )}
      </div>

      {/* Galeri foto */}
      <div className="detail-section">
        <h2>Galeri Foto</h2>
        {galleries.length === 0 ? (
          <p>Belum ada foto galeri.</p>
        ) : (
          <div className="gallery-grid">
            {galleries.map((img) => (
              <img 
                key={img.id} 
                src={`${uploadUrl}/${img.image_url}`} 
                alt={`Gallery ${img.id}`}
                className="gallery-image"
              />
            ))}
          </div>
        )}
      </div>

      {/* Paket harga */}
      <div className="detail-section">
        <h2>Paket & Harga</h2>
        {packages.length === 0 ? (
          <p>Belum ada paket tersedia.</p>
        ) : (
          <div className="packages-grid">
            {packages.map((pkg) => (
              <div key={pkg.id} className="package-card">
                <h3>{pkg.name}</h3>
                {pkg.description < p < pkg.description > < < p < /p > }
                <p className="price">Rp {pkg.price.toLocaleString()}</p>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

export default DestinationDetail;

Penjelasan:

  • useParams mengambil parameter id dari URL (sesuai dengan route /destination/:id).
  • Kita menggunakan tiga state: destination, galleries, dan packages.
  • Di useEffect, kita memanggil ketiga API secara berurutan. Bisa juga menggunakan Promise.all untuk paralel, tapi di sini kita sederhanakan.
  • Kita menampilkan loading dan error.
  • Bagian-bagian halaman disusun dengan section yang jelas.

Langkah 3: Menambahkan CSS

Buat file src/pages/DestinationDetail.css:

.destination-detail {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.detail-header {
  position: relative;
  text-align: center;
  margin-bottom: 30px;
}

.cover-image {
  width: 100%;
  height: 400px;
  object-fit: cover;
  border-radius: 8px;
}

.detail-header h1 {
  margin-top: 20px;
  color: #333;
}

.location {
  color: #666;
  font-size: 1.1em;
}

.detail-section {
  margin-bottom: 40px;
}

.detail-section h2 {
  border-bottom: 2px solid #e67e22;
  padding-bottom: 10px;
  margin-bottom: 20px;
  color: #444;
}

/* Galeri grid */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 15px;
}

.gallery-image {
  width: 100%;
  height: 150px;
  object-fit: cover;
  border-radius: 4px;
  cursor: pointer;
  transition: transform 0.2s;
}

.gallery-image:hover {
  transform: scale(1.05);
}

/* Paket grid */
.packages-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
}

.package-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 15px;
  background: #f9f9f9;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.package-card h3 {
  margin-top: 0;
  color: #e67e22;
}

.package-card .price {
  font-size: 1.2em;
  font-weight: bold;
  color: #27ae60;
  margin: 10px 0 0;
}

.loading, .error {
  text-align: center;
  margin-top: 50px;
  font-size: 1.2em;
}

.error {
  color: #e74c3c;
}

🔗 Langkah 4: Menambahkan Route

Di App.jsx, pastikan kita sudah menambahkan route untuk halaman detail. Jika menggunakan createBrowserRouter, tambahkan di dalam children layout:

{
  path: '/destination/:id',
  element: <DestinationDetail />,
}

Contoh lengkap:

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import Home from './pages/Home';
import DestinationDetail from './pages/DestinationDetail';

const router = createBrowserRouter([
  {
    element: <MainLayout />,
    children: [
      { path: '/', element: <Home /> },
      { path: '/destination/:id', element: <DestinationDetail /> },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

export default App;

Langkah 5: Menguji Halaman Detail

Pastikan backend berjalan dan memiliki data destinasi dengan ID tertentu (misal ID 1). Jalankan frontend, lalu buka http://localhost:5173/destination/1. Halaman akan menampilkan detail destinasi, galeri, dan paket. Jika belum ada galeri atau paket, akan tampil pesan kosong.

Klik salah satu kartu di halaman beranda untuk navigasi ke halaman detail. Pastikan link di DestinationCard sudah benar.

💡 Tips: Jika gambar tidak muncul, periksa kembali URL upload dan pastikan file ada di folder backend/uploads.

Langkah 6: (Opsional) Menambahkan Lightbox untuk Galeri

Untuk meningkatkan pengalaman, kita bisa menambahkan lightbox (gambar tampil besar saat diklik). Instal library seperti react-image-lightbox atau buat sederhana dengan state. Contoh sederhana: tambahkan state selectedImage dan modal. Namun untuk menjaga tutorial tetap fokus, kita lewati dulu.

Penanganan Error

  • Jika ID tidak valid atau data tidak ditemukan, backend akan mengembalikan 404, dan kita tangkap di catch, menampilkan pesan error.
  • Jika salah satu API gagal (misal galeri), kita tetap ingin menampilkan data lain. Saat ini kita menggunakan satu try-catch yang akan menghentikan semua jika salah satu gagal. Untuk lebih baik, kita bisa memisahkan setiap panggilan API dengan try-catch masing-masing. Silakan modifikasi sesuai kebutuhan.

Langkah Selanjutnya

Di Studi Kasus #15, kita akan membuat halaman untuk penginapan (accommodations) dengan pola yang mirip, serta halaman transportasi. Kita juga bisa mulai memikirkan filter dan pencarian.

Pastikan halaman detail destinasi berfungsi dengan baik. Jika ada kendala, periksa kembali endpoint API dan data di database.

Sampai jumpa di tutorial berikutnya!


Ditulis dengan ❤️ untuk para pengembang yang ingin membangun aplikasi wisata.

Lebih baru Lebih lama

نموذج الاتصال