Studi Kasus #16: Aplikasi Wisata Indonesia – Membuat Halaman Transportasi dan Detailnya


Studi Kasus #16: Aplikasi Wisata Indonesia – Membuat Halaman Transportasi dan Detailnya

Halo, calon full-stack developer!

Di Studi Kasus #15, kita telah membuat halaman untuk penginapan. Sekarang kita akan membuat halaman yang serupa untuk transportasi (transportations). Transportasi bisa berupa layanan travel, rental mobil, bus pariwisata, dll. Polanya hampir sama, hanya berbeda pada endpoint API dan beberapa penyesuaian konten.

Fitur yang akan dibuat:

  • Halaman daftar transportasi dengan tampilan grid kartu.
  • Setiap kartu menampilkan foto cover, nama, lokasi (atau rute), dan deskripsi singkat.
  • Halaman detail transportasi yang menampilkan informasi lengkap: deskripsi, fasilitas, galeri foto, dan paket harga (jenis layanan).
  • Mengambil data dari endpoint /transportations, /galleries/transportation/:id, dan /packages/transportation/:id.

Kita akan memanfaatkan komponen dan pola yang sudah dibuat sebelumnya. Mari kita mulai! 🚀

Langkah 1: Menambahkan Service untuk Transportasi

Pertama, pastikan kita memiliki service untuk mengambil data transportasi. Buat file src/services/transportationService.js jika belum ada:

import API from './api';

export const getTransportations = async () => {
  try {
    const response = await API.get('/transportations');
    return response.data;
  } catch (error) {
    console.error('Gagal mengambil transportasi:', error);
    throw error;
  }
};

export const getTransportationById = async (id) => {
  try {
    const response = await API.get(`/transportations/${id}`);
    return response.data;
  } catch (error) {
    console.error('Gagal mengambil transportasi:', error);
    throw error;
  }
};

Service untuk galeri dan paket sudah kita buat sebelumnya. Kita akan gunakan fungsi yang sama dengan parameter entityType = 'transportation'.

Langkah 2: Membuat Komponen Card untuk Transportasi

Buat file src/components/TransportationCard.jsx:

import React from 'react';
import { Link } from 'react-router-dom';
import './TransportationCard.css';

const TransportationCard = ({ transportation }) => {
  const imageUrl = transportation.cover_image 
    ? `${import.meta.env.VITE_UPLOAD_URL}/${transportation.cover_image}`
    : 'https://via.placeholder.com/300x200?text=No+Image';

  const shortDescription = transportation.description 
    ? transportation.description.substring(0, 100) + '...'
    : 'Tidak ada deskripsi';

  return (
    <Link to={`/transportation/${transportation.id}`} className="card-link">
      <div className="transportation-card">
        <img src={imageUrl} alt={transportation.name} className="card-image" />
        <div className="card-content">
          <h3>{transportation.name}</h3>
          <p className="location">📍 {transportation.location}</p>
          <p className="description">{shortDescription}</p>
        </div>
      </div>
    </Link>
  );
};

export default TransportationCard;

Buat file CSS src/components/TransportationCard.css (bisa mirip dengan card sebelumnya):

.transportation-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  transition: transform 0.2s;
  background: white;
}

.transportation-card:hover {
  transform: scale(1.02);
  box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}

.card-image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.card-content {
  padding: 15px;
}

.card-content h3 {
  margin: 0 0 10px 0;
  color: #333;
}

.location {
  color: #666;
  font-size: 0.9em;
  margin: 5px 0;
}

.description {
  color: #777;
  font-size: 0.9em;
  line-height: 1.4;
}

.card-link {
  text-decoration: none;
  color: inherit;
}

Langkah 3: Membuat Halaman Daftar Transportasi

Buat file src/pages/Transportations.jsx:

import React, { useState, useEffect } from 'react';
import { getTransportations } from '../services/transportationService';
import TransportationCard from '../components/TransportationCard';
import './Transportations.css';

const Transportations = () => {
  const [transportations, setTransportations] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchTransportations = async () => {
      try {
        const data = await getTransportations();
        setTransportations(data);
      } catch (err) {
        setError('Gagal mengambil data transportasi.');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    fetchTransportations();
  }, []);

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

  if (error) {
    return <div className="error">{error}</div>;
  }

  return (
    <div className="transportations-page">
      <h1>Transportasi di Indonesia</h1>
      {transportations.length === 0 ? (
        <p>Belum ada data transportasi.</p>
      ) : (
        <div className="transportation-grid">
          {transportations.map((trans) => (
            <TransportationCard key={trans.id} transportation={trans} />
          ))}
        </div>
      )}
    </div>
  );
};

export default Transportations;

Buat file CSS src/pages/Transportations.css:

.transportations-page {
  padding: 20px;
}

.transportations-page h1 {
  text-align: center;
  margin-bottom: 30px;
  color: #2c3e50;
}

.transportation-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
}

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

.error {
  color: #e74c3c;
}

Langkah 4: Membuat Halaman Detail Transportasi

Buat file src/pages/TransportationDetail.jsx:

import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { getTransportationById } from '../services/transportationService';
import { getGalleriesByEntity } from '../services/galleryService';
import { getPackagesByEntity } from '../services/packageService';
import './TransportationDetail.css';

const TransportationDetail = () => {
  const { id } = useParams();
  const [transportation, setTransportation] = 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 {
        const transData = await getTransportationById(id);
        setTransportation(transData);

        const galleryData = await getGalleriesByEntity('transportation', id);
        setGalleries(galleryData);

        const packageData = await getPackagesByEntity('transportation', id);
        setPackages(packageData);
      } catch (err) {
        setError('Gagal memuat data transportasi.');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [id]);

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

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

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

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

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

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

      <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>

      <div className="detail-section">
        <h2>Paket Layanan & 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 TransportationDetail;

Buat file CSS src/pages/TransportationDetail.css (mirip dengan halaman detail sebelumnya, dengan warna aksen ungu):

.transportation-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 #9b59b6;
  padding-bottom: 10px;
  margin-bottom: 20px;
  color: #444;
}

.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);
}

.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: #9b59b6;
}

.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 5: Menambahkan Route

Di App.jsx, tambahkan route untuk halaman daftar transportasi dan detail transportasi:

import Transportations from './pages/Transportations';
import TransportationDetail from './pages/TransportationDetail';

// Di dalam router, tambahkan:
{
  path: '/transportations',
  element: <Transportations />,
},
{
  path: '/transportation/:id',
  element: <TransportationDetail />,
}

Contoh lengkap App.jsx setelah ditambahkan:

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

const router = createBrowserRouter([
  {
    element: <MainLayout />,
    children: [
      { path: '/', element: <Home /> },
      { path: '/destination/:id', element: <DestinationDetail /> },
      { path: '/accommodations', element: <Accommodations /> },
      { path: '/accommodation/:id', element: <AccommodationDetail /> },
      { path: '/transportations', element: <Transportations /> },
      { path: '/transportation/:id', element: <TransportationDetail /> },
    ],
  },
]);

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

export default App;

Jangan lupa untuk menambahkan link ke halaman daftar transportasi di navbar (misal di Navbar.jsx).

Langkah 6: Menguji Halaman

Pastikan backend berjalan dan memiliki data transportasi (transportations). Buka http://localhost:5173/transportations untuk melihat daftar transportasi. Klik salah satu kartu untuk menuju halaman detail.

Jika belum ada data, kamu bisa menambahkan melalui API menggunakan Postman (endpoint POST /api/transportations) dengan form-data (termasuk cover_image).

💡 Tips: Untuk memudahkan pengujian, kamu bisa menambahkan beberapa data dummy melalui phpMyAdmin atau langsung menggunakan Postman.

Penanganan Error dan Loading

Sama seperti halaman sebelumnya, kita sudah menambahkan state loading dan error. Pastikan untuk menguji skenario ketika backend mati atau ID tidak valid.

Langkah Selanjutnya

Di Studi Kasus #17, kita akan mulai menambahkan fitur filter berdasarkan kategori di halaman beranda, agar pengguna bisa melihat destinasi, penginapan, atau transportasi secara terpisah dengan mudah.

Pastikan halaman transportasi berfungsi dengan baik. Jika ada kendala, periksa kembali endpoint API dan pastikan service sudah benar.

Sampai jumpa di tutorial berikutnya!


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

Lebih baru Lebih lama

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