Studi Kasus #17: Aplikasi Wisata Indonesia – Filter Berdasarkan Kategori


Studi Kasus #17: Aplikasi Wisata Indonesia – Filter Berdasarkan Kategori

Halo, calon full-stack developer!

Di Studi Kasus #16, kita telah membuat halaman terpisah untuk destinasi, penginapan, dan transportasi. Namun, akan lebih baik jika di halaman utama pengguna bisa melihat semua kategori sekaligus dengan filter tab atau dropdown. Di tutorial ini, kita akan menambahkan filter berdasarkan kategori di halaman utama (Home) menggunakan tab sederhana. Pengguna bisa memilih "Semua", "Wisata", "Penginapan", atau "Transportasi", dan halaman akan menampilkan item yang sesuai.

Fitur yang akan dibuat:

  • Tab navigasi untuk memilih kategori.
  • Saat tab diklik, aplikasi akan mengambil data dari endpoint yang sesuai.
  • Menampilkan item dalam bentuk grid menggunakan card yang sudah kita buat sebelumnya.
  • Indikator loading dan error untuk setiap tab.

Kita akan memanfaatkan komponen card yang sudah ada (DestinationCard, AccommodationCard, TransportationCard) dan service yang sudah dibuat. Mari kita mulai!

Langkah 1: Menyiapkan State dan Fungsi Fetch

Kita akan memodifikasi halaman Home.jsx. Pertama, kita perlu state untuk menyimpan kategori aktif, data, loading, dan error. Kita juga akan membuat fungsi untuk mengambil data berdasarkan kategori.

Buka src/pages/Home.jsx dan ubah menjadi seperti berikut:

import React, { useState, useEffect } from 'react';
import { getDestinations } from '../services/destinationService';
import { getAccommodations } from '../services/accommodationService';
import { getTransportations } from '../services/transportationService';
import DestinationCard from '../components/DestinationCard';
import AccommodationCard from '../components/AccommodationCard';
import TransportationCard from '../components/TransportationCard';
import './Home.css';

const Home = () => {
  // State untuk kategori aktif
  const [activeCategory, setActiveCategory] = useState('all'); // 'all', 'destinations', 'accommodations', 'transportations'
  
  // State untuk data masing-masing kategori (disimpan terpisah agar tidak perlu fetch ulang)
  const [destinations, setDestinations] = useState([]);
  const [accommodations, setAccommodations] = useState([]);
  const [transportations, setTransportations] = useState([]);
  
  // State loading dan error per kategori
  const [loading, setLoading] = useState({
    destinations: false,
    accommodations: false,
    transportations: false
  });
  const [error, setError] = useState({
    destinations: null,
    accommodations: null,
    transportations: null
  });

  // Fetch data saat komponen pertama dimuat (untuk semua kategori)
  useEffect(() => {
    fetchDestinations();
    fetchAccommodations();
    fetchTransportations();
  }, []);

  const fetchDestinations = async () => {
    setLoading(prev => ({ ...prev, destinations: true }));
    try {
      const data = await getDestinations();
      setDestinations(data);
    } catch (err) {
      setError(prev => ({ ...prev, destinations: 'Gagal memuat destinasi' }));
      console.error(err);
    } finally {
      setLoading(prev => ({ ...prev, destinations: false }));
    }
  };

  const fetchAccommodations = async () => {
    setLoading(prev => ({ ...prev, accommodations: true }));
    try {
      const data = await getAccommodations();
      setAccommodations(data);
    } catch (err) {
      setError(prev => ({ ...prev, accommodations: 'Gagal memuat penginapan' }));
      console.error(err);
    } finally {
      setLoading(prev => ({ ...prev, accommodations: false }));
    }
  };

  const fetchTransportations = async () => {
    setLoading(prev => ({ ...prev, transportations: true }));
    try {
      const data = await getTransportations();
      setTransportations(data);
    } catch (err) {
      setError(prev => ({ ...prev, transportations: 'Gagal memuat transportasi' }));
      console.error(err);
    } finally {
      setLoading(prev => ({ ...prev, transportations: false }));
    }
  };

  // Fungsi untuk merender card berdasarkan kategori aktif
  const renderItems = () => {
    if (activeCategory === 'all') {
      // Gabungkan semua data
      const allItems = [
        ...destinations.map(item => ({ ...item, categoryType: 'destination' })),
        ...accommodations.map(item => ({ ...item, categoryType: 'accommodation' })),
        ...transportations.map(item => ({ ...item, categoryType: 'transportation' }))
      ];
      return allItems.map(item => {
        switch (item.categoryType) {
          case 'destination':
            return <DestinationCard key={`dest-${item.id}`} destination={item} />;
          case 'accommodation':
            return <AccommodationCard key={`acc-${item.id}`} accommodation={item} />;
          case 'transportation':
            return <TransportationCard key={`trans-${item.id}`} transportation={item} />;
          default:
            return null;
        }
      });
    } else if (activeCategory === 'destinations') {
      return destinations.map(item => (
        <DestinationCard key={item.id} destination={item} />
      ));
    } else if (activeCategory === 'accommodations') {
      return accommodations.map(item => (
        <AccommodationCard key={item.id} accommodation={item} />
      ));
    } else if (activeCategory === 'transportations') {
      return transportations.map(item => (
        <TransportationCard key={item.id} transportation={item} />
      ));
    }
  };

  // Menentukan apakah sedang loading untuk kategori aktif
  const isLoading = () => {
    if (activeCategory === 'all') {
      return loading.destinations || loading.accommodations || loading.transportations;
    } else {
      return loading[activeCategory];
    }
  };

  // Menentukan error untuk kategori aktif
  const hasError = () => {
    if (activeCategory === 'all') {
      return error.destinations || error.accommodations || error.transportations;
    } else {
      return error[activeCategory];
    }
  };

  return (
    <div className="home">
      <h1>Jelajahi Wisata Indonesia</h1>

      {/* Tab navigasi */}
      <div className="category-tabs">
        <button
          className={activeCategory === 'all' ? 'active' : ''}
          onClick={() => setActiveCategory('all')}
        >
          Semua
        </button>
        <button
          className={activeCategory === 'destinations' ? 'active' : ''}
          onClick={() => setActiveCategory('destinations')}
        >
          Wisata
        </button>
        <button
          className={activeCategory === 'accommodations' ? 'active' : ''}
          onClick={() => setActiveCategory('accommodations')}
        >
          Penginapan
        </button>
        <button
          className={activeCategory === 'transportations' ? 'active' : ''}
          onClick={() => setActiveCategory('transportations')}
        >
          Transportasi
        </button>
      </div>

      {/* Konten */}
      <div className="category-content">
        {isLoading() ? (
          <div className="loading">Memuat data...</div>
        ) : hasError() ? (
          <div className="error">Terjadi kesalahan. Silakan coba lagi.</div>
        ) : (
          <div className="items-grid">
            {renderItems()}
          </div>
        )}
        {!isLoading() & && !hasError() & && renderItems().length === 0 & && (
          <p className="no-data">Tidak ada data untuk ditampilkan.</p>
        )}
      </div>
    </div>
  );
};

export default Home;

Penjelasan:

  • Kita menambahkan state activeCategory untuk melacak tab yang dipilih.
  • Data untuk masing-masing kategori disimpan terpisah agar tidak perlu fetch ulang saat berganti tab.
  • Fungsi fetchDestinations, fetchAccommodations, fetchTransportations dipanggil di useEffect saat komponen mount.
  • Fungsi renderItems mengembalikan array card berdasarkan kategori aktif. Untuk kategori 'all', kita gabungkan semua data dan tambahkan properti categoryType agar bisa memilih card yang tepat.
  • Fungsi isLoading dan hasError membantu menampilkan status yang sesuai.

Langkah 2: Menambahkan CSS untuk Tab dan Grid

Buka file src/pages/Home.css (atau buat jika belum ada) dan tambahkan gaya berikut:

.home {
  padding: 20px;
}

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

/* Tab styling */
.category-tabs {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-bottom: 30px;
  flex-wrap: wrap;
}

.category-tabs button {
  padding: 10px 20px;
  border: none;
  border-radius: 30px;
  background: #ecf0f1;
  color: #7f8c8d;
  font-size: 1em;
  cursor: pointer;
  transition: all 0.3s;
}

.category-tabs button:hover {
  background: #bdc3c7;
}

.category-tabs button.active {
  background: #e67e22;
  color: white;
}

/* Grid untuk item */
.items-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
}

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

.error {
  color: #e74c3c;
}

.no-data {
  color: #7f8c8d;
}

Gaya ini akan membuat tab terlihat rapi dan grid responsif.

Langkah 3: Menguji Filter

Jalankan backend dan frontend. Buka halaman utama (/). Seharusnya muncul tab dengan opsi. Klik masing-masing tab untuk melihat data yang sesuai.

  • Tab "Semua" akan menampilkan campuran destinasi, penginapan, dan transportasi (jika ada).
  • Tab "Wisata" hanya menampilkan destinasi.
  • Tab "Penginapan" hanya menampilkan penginapan.
  • Tab "Transportasi" hanya menampilkan transportasi.

Jika salah satu kategori belum memiliki data, akan tampil pesan "Tidak ada data untuk ditampilkan".

💡 Tips: Untuk memastikan semua kategori memiliki data, kamu bisa menambahkan beberapa data dummy melalui Postman atau phpMyAdmin.

Penanganan Loading dan Error yang Lebih Baik

Dalam implementasi di atas, kita menggunakan state loading dan error terpisah. Saat kategori "Semua", kita menunggu semua data selesai dimuat. Jika salah satu gagal, kita menampilkan error. Ini cukup untuk aplikasi sederhana. Jika ingin lebih halus, kita bisa menampilkan item yang berhasil dimuat meskipun ada yang gagal, tetapi untuk saat ini kita sederhanakan.

Kita juga bisa menambahkan tombol refresh per kategori jika diperlukan.

Langkah Selanjutnya

Di Studi Kasus #18, kita akan menambahkan fitur pencarian (search) di halaman utama, sehingga pengguna bisa mencari berdasarkan nama atau lokasi. Kita juga bisa menambahkan filter tambahan seperti berdasarkan harga atau fasilitas.

Pastikan fitur filter kategori berjalan dengan baik sebelum melanjutkan.

Sampai jumpa di tutorial berikutnya!


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

Lebih baru Lebih lama

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