Studi Kasus #18: Aplikasi Wisata Indonesia – Fitur Pencarian (Search)
Halo, calon full-stack developer! 👋
Di Studi Kasus #17, kita telah menambahkan filter berbasis tab di halaman utama. Sekarang kita akan menambahkan fitur pencarian (search) agar pengguna bisa mencari tempat wisata, penginapan, atau transportasi berdasarkan nama atau lokasi. Fitur ini akan memfilter item yang ditampilkan secara real-time saat pengguna mengetik di kotak pencarian.
Fitur yang akan dibuat:
- Search bar di bagian atas halaman utama.
- Pencarian berdasarkan nama dan lokasi (teks bebas).
- Filter diterapkan pada kategori yang sedang aktif (Semua, Wisata, Penginapan, Transportasi).
- Hasil pencarian ditampilkan dalam grid yang sama.
- Jika tidak ada hasil, tampilkan pesan "Tidak ada hasil ditemukan".
Kita akan melakukan filter di sisi klien (client-side) karena data tidak terlalu besar. Untuk skala lebih besar, sebaiknya menggunakan server-side search, tapi untuk pembelajaran ini client-side sudah cukup. Mari kita mulai! 🚀
Langkah 1: Menambahkan State untuk Search Query
Buka file src/pages/Home.jsx. Kita akan menambahkan state baru untuk menyimpan kata kunci pencarian:
const [searchQuery, setSearchQuery] = useState('');
Tempatkan state ini di atas state lainnya (misal setelah activeCategory).
🔍 Langkah 2: Membuat Komponen Search Bar
Tambahkan elemen input di dalam JSX, di atas tab navigasi. Kita juga akan menambahkan ikon search (opsional).
<div className="search-container">
<input
type="text"
placeholder="Cari berdasarkan nama atau lokasi..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
</div>
Letakkan kode ini setelah <h1> dan sebelum <div className="category-tabs">.
Langkah 3: Menambahkan CSS untuk Search Bar
Buka file src/pages/Home.css dan tambahkan gaya berikut:
.search-container {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.search-input {
width: 100%;
max-width: 500px;
padding: 12px 20px;
border: 2px solid #ecf0f1;
border-radius: 30px;
font-size: 1em;
outline: none;
transition: border-color 0.3s;
}
.search-input:focus {
border-color: #e67e22;
}
Ini akan membuat search bar terlihat rapi dan responsif.
Langkah 4: Membuat Fungsi Filter
Kita akan membuat fungsi filterItems yang menerima array item dan mengembalikan item yang cocok dengan searchQuery (berdasarkan nama dan lokasi). Pencarian bersifat case-insensitive.
Tambahkan fungsi ini di dalam komponen Home (setelah state dan sebelum fungsi-fungsi fetch):
const filterItems = (items) => {
if (!searchQuery.trim()) return items; // jika query kosong, kembalikan semua
const query = searchQuery.toLowerCase();
return items.filter(item =>
item.name.toLowerCase().includes(query) ||
(item.location & && item.location.toLowerCase().includes(query))
);
};
Perhatikan: properti location mungkin ada di semua entitas (destinasi, penginapan, transportasi). Kita asumsikan semua memiliki location.
Langkah 5: Memodifikasi Fungsi renderItems
Kita perlu mengubah fungsi renderItems agar menerapkan filter sebelum menampilkan card. Untuk kategori 'all', kita filter gabungan data. Untuk kategori spesifik, kita filter data kategori tersebut.
Ubah fungsi renderItems menjadi:
const renderItems = () => {
if (activeCategory === 'all') {
const allItems = [
...destinations.map(item => ({ ...item, categoryType: 'destination' })),
...accommodations.map(item => ({ ...item, categoryType: 'accommodation' })),
...transportations.map(item => ({ ...item, categoryType: 'transportation' }))
];
const filtered = filterItems(allItems);
return filtered.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') {
const filtered = filterItems(destinations);
return filtered.map(item => (
<DestinationCard key={item.id} destination={item} />
));
} else if (activeCategory === 'accommodations') {
const filtered = filterItems(accommodations);
return filtered.map(item => (
<AccommodationCard key={item.id} accommodation={item} />
));
} else if (activeCategory === 'transportations') {
const filtered = filterItems(transportations);
return filtered.map(item => (
<TransportationCard key={item.id} transportation={item} />
));
}
};
Perubahan: kita panggil filterItems pada array sebelum dipetakan ke card.
Langkah 6: Menampilkan Pesan Jika Tidak Ada Hasil
Kita sudah memiliki kondisi untuk menampilkan pesan jika tidak ada data. Namun karena filter bisa menyebabkan tidak ada hasil, kita perlu memastikan pesan muncul dengan benar. Di bagian akhir JSX, kita sudah punya:
{!isLoading() && !hasError() && renderItems().length === 0 && (
<p className="no-data">Tidak ada data untuk ditampilkan.</p>
)}
Ini sudah cukup karena renderItems() akan mengembalikan array kosong jika filter tidak cocok. Jadi pesan akan muncul.
Kita bisa mengganti teks menjadi "Tidak ada hasil ditemukan" jika diinginkan.
Kode Lengkap Home.jsx Setelah Modifikasi
Berikut potongan kode Home.jsx yang sudah dimodifikasi (bagian-bagian penting). Pastikan untuk mengintegrasikan dengan kode sebelumnya.
import React, { useState, useEffect } from 'react';
// ... import lainnya
const Home = () => {
const [activeCategory, setActiveCategory] = useState('all');
const [searchQuery, setSearchQuery] = useState(''); // baru
// ... state lainnya
// ... fungsi fetch (sama seperti sebelumnya)
const filterItems = (items) => {
if (!searchQuery.trim()) return items;
const query = searchQuery.toLowerCase();
return items.filter(item =>
item.name.toLowerCase().includes(query) ||
(item.location & && item.location.toLowerCase().includes(query))
);
};
const renderItems = () => {
// ... seperti di atas
};
// ... fungsi isLoading, hasError
return (
<div className="home">
<h1>Jelajahi Wisata Indonesia</h1>
{/* Search Bar */}
<div className="search-container">
<input
type="text"
placeholder="Cari berdasarkan nama atau lokasi..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
</div>
{/* Tab navigasi */}
<div className="category-tabs">
{/* ... */}
</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 hasil ditemukan.</p>
)}
</div>
</div>
);
};
export default Home;
Langkah 7: Menguji Fitur Pencarian
Jalankan aplikasi. Di halaman utama, kamu akan melihat search bar di atas tab. Coba ketik nama atau lokasi (misal "Bali" atau "Pantai"). Hasil akan langsung terfilter saat kamu mengetik. Coba juga ganti tab, pencarian akan tetap berfungsi pada kategori yang aktif.
Pastikan data yang kamu miliki mengandung kata kunci tersebut. Jika tidak ada hasil, akan muncul pesan "Tidak ada hasil ditemukan".
Penanganan Kinerja (Opsional)
Filter dijalankan setiap kali pengguna mengetik. Untuk data yang sangat besar, ini bisa menyebabkan lag. Kita bisa menambahkan debounce untuk menunda eksekusi filter hingga pengguna berhenti mengetik. Namun untuk dataset kecil, ini tidak diperlukan. Jika ingin menambahkan debounce, kamu bisa menggunakan lodash.debounce atau membuat sendiri dengan setTimeout.
Langkah Selanjutnya
Di Studi Kasus #19, kita akan mulai membuat halaman admin untuk mengelola data (CRUD) dengan autentikasi. Kita akan menggunakan token JWT yang sudah disiapkan sebelumnya.
Pastikan fitur pencarian berjalan dengan baik. Jika ada kendala, periksa kembali konsol browser untuk error.
Sampai jumpa di tutorial berikutnya!
Ditulis dengan ❤️ untuk para pengembang yang ingin membangun aplikasi wisata.