From 136af9caf27a392192846725da7fa790124342f7 Mon Sep 17 00:00:00 2001 From: Hafid Nur <73023445+hafidnrzs@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:45:00 +0700 Subject: [PATCH] feat(backoffice): add searchable city filter - add component for city filter - apply to user management and team management pages --- .../_components/modal-team-detail-new.tsx | 12 +- .../_components/submission-modal.tsx | 12 +- .../app/(protected)/hackathon-teams/page.tsx | 34 +- .../app/(protected)/hackathon-users/page.tsx | 55 +- .../src/components/city-filter-select.tsx | 142 +++++ apps/backoffice/src/constants/cities.ts | 518 ++++++++++++++++++ 6 files changed, 713 insertions(+), 60 deletions(-) create mode 100644 apps/backoffice/src/components/city-filter-select.tsx create mode 100644 apps/backoffice/src/constants/cities.ts diff --git a/apps/backoffice/src/app/(protected)/hackathon-teams/_components/modal-team-detail-new.tsx b/apps/backoffice/src/app/(protected)/hackathon-teams/_components/modal-team-detail-new.tsx index 1173250..23423aa 100644 --- a/apps/backoffice/src/app/(protected)/hackathon-teams/_components/modal-team-detail-new.tsx +++ b/apps/backoffice/src/app/(protected)/hackathon-teams/_components/modal-team-detail-new.tsx @@ -167,14 +167,12 @@ const ModalTeamDetail: FC = ({ isOpen, onClose, team }) => {

- + + {/* Content */} @@ -600,7 +598,7 @@ const ModalTeamDetail: FC = ({ isOpen, onClose, team }) => { {/* Delete Confirmation Modal */} {showDeleteConfirm && ( -
+
diff --git a/apps/backoffice/src/app/(protected)/hackathon-teams/_components/submission-modal.tsx b/apps/backoffice/src/app/(protected)/hackathon-teams/_components/submission-modal.tsx index 62af54c..673f0a4 100644 --- a/apps/backoffice/src/app/(protected)/hackathon-teams/_components/submission-modal.tsx +++ b/apps/backoffice/src/app/(protected)/hackathon-teams/_components/submission-modal.tsx @@ -39,7 +39,7 @@ const SubmissionModal: FC = ({ }; return ( -
+
{/* Header */}
@@ -56,14 +56,12 @@ const SubmissionModal: FC = ({

- + +
{/* Content */} diff --git a/apps/backoffice/src/app/(protected)/hackathon-teams/page.tsx b/apps/backoffice/src/app/(protected)/hackathon-teams/page.tsx index 6e80a36..a03a9a0 100644 --- a/apps/backoffice/src/app/(protected)/hackathon-teams/page.tsx +++ b/apps/backoffice/src/app/(protected)/hackathon-teams/page.tsx @@ -1,6 +1,8 @@ import { FC, ReactElement, useState, useMemo, useCallback } from 'react'; import ModalTeamDetail from './_components/modal-team-detail-new'; import SubmissionModal from './_components/submission-modal'; +import { CityFilterSelect } from '../../../components/city-filter-select'; +import INDONESIAN_CITIES from '../../../constants/cities'; import { BackofficeWrapper, DataTable, @@ -58,7 +60,8 @@ interface TeamType { } // Move mock data outside component to prevent recreation -const cities = ['Jakarta', 'Bandung', 'Surabaya', 'Medan', 'Yogyakarta']; +// Sample data for popular cities from the INDONESIAN_CITIES constant +const cities = INDONESIAN_CITIES.slice(0, 20); // Use first 20 cities for variety const teamNames = [ 'Innovators', 'Hackers', @@ -255,7 +258,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => { if ( !team.name.toLowerCase().includes(searchTerm) && !team.city.toLowerCase().includes(searchTerm) && - !team.description?.toLowerCase().includes(searchTerm) && !leaderName.toLowerCase().includes(searchTerm) && !memberNames.toLowerCase().includes(searchTerm) ) { @@ -496,7 +498,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
setCityFilter(e.target.value)} - > - - {cities.map((city) => ( - - ))} - -
+ {/* Member Count Filter */}
diff --git a/apps/backoffice/src/app/(protected)/hackathon-users/page.tsx b/apps/backoffice/src/app/(protected)/hackathon-users/page.tsx index 500f9eb..31257bc 100644 --- a/apps/backoffice/src/app/(protected)/hackathon-users/page.tsx +++ b/apps/backoffice/src/app/(protected)/hackathon-users/page.tsx @@ -14,7 +14,7 @@ import { FilterOutlined, PlusOutlined, } from '@ant-design/icons'; -// Removed unused SearchOutlined icon after schema revision +import { CityFilterSelect } from '../../../components/city-filter-select'; // Define interface outside component interface UserType { @@ -89,7 +89,7 @@ export const HackathonUsersPage: FC = (): ReactElement => { // Advanced filtering states const [statusFilter, setStatusFilter] = useState('all'); - const [locationFilter, setLocationFilter] = useState('all'); + const [cityFilter, setCityFilter] = useState('all'); const [skillsFilter, setSkillsFilter] = useState([]); // Constants @@ -123,8 +123,8 @@ export const HackathonUsersPage: FC = (): ReactElement => { if (user.is_active !== isActive) return false; } - // Location filter - if (locationFilter !== 'all' && user.location !== locationFilter) { + // City filter + if (cityFilter !== 'all' && user.location !== cityFilter) { return false; } @@ -138,7 +138,7 @@ export const HackathonUsersPage: FC = (): ReactElement => { return true; }); - }, [statusFilter, locationFilter, skillsFilter]); + }, [statusFilter, cityFilter, skillsFilter]); // Memoize columns to prevent recreation on every render const columns: ColumnDef[] = useMemo( @@ -315,22 +315,25 @@ export const HackathonUsersPage: FC = (): ReactElement => {
- {/* Location Filter */} -
- - -
+ {/* City Filter */} + + {cityFilter !== 'all' && ( + + Location: {cityFilter} + + + )} {/* Skills Filter with Icon */}
@@ -378,7 +381,7 @@ export const HackathonUsersPage: FC = (): ReactElement => { {/* Active filters display */} {(skillsFilter.length > 0 || statusFilter !== 'all' || - locationFilter !== 'all') && ( + cityFilter !== 'all') && (
Active filters: @@ -396,11 +399,11 @@ export const HackathonUsersPage: FC = (): ReactElement => { )} {/* Location filter badge */} - {locationFilter !== 'all' && ( + {cityFilter !== 'all' && ( - Location: {locationFilter} + City: {cityFilter} + )} +
+ + {isOpen && ( +
+ {/* All Cities Option */} +
handleSelectCity('all')} + className={`px-3 py-2 cursor-pointer hover:bg-neutral-50 border-b border-neutral-100 ${ + value === 'all' + ? 'bg-primary-50 text-primary-700 font-medium' + : 'text-neutral-900' + }`} + > + {allOptionLabel} +
+ + {/* Filtered Cities */} + {filteredCities.length > 0 ? ( +
+ {filteredCities.slice(0, 100).map((city) => ( +
handleSelectCity(city)} + className={`px-3 py-2 cursor-pointer hover:bg-neutral-50 text-sm ${ + value === city + ? 'bg-primary-50 text-primary-700 font-medium' + : 'text-neutral-700' + }`} + > + {city} +
+ ))} + {filteredCities.length > 100 && ( +
+ Showing first 100 results. Continue typing to refine... +
+ )} +
+ ) : searchQuery ? ( +
+ No cities found matching "{searchQuery}" +
+ ) : null} +
+ )} +
+ ); +}; diff --git a/apps/backoffice/src/constants/cities.ts b/apps/backoffice/src/constants/cities.ts new file mode 100644 index 0000000..9047bc0 --- /dev/null +++ b/apps/backoffice/src/constants/cities.ts @@ -0,0 +1,518 @@ +const INDONESIAN_CITIES: string[] = [ + 'Aceh Selatan', + 'Aceh Tenggara', + 'Aceh Timur', + 'Aceh Tengah', + 'Aceh Barat', + 'Aceh Besar', + 'Pidie', + 'Aceh Utara', + 'Simeulue', + 'Aceh Singkil', + 'Bireuen', + 'Aceh Barat Daya', + 'Gayo Lues', + 'Aceh Jaya', + 'Nagan Raya', + 'Aceh Tamiang', + 'Bener Meriah', + 'Pidie Jaya', + 'Kota Banda Aceh', + 'Kota Sabang', + 'Kota Lhokseumawe', + 'Kota Langsa', + 'Kota Subulussalam', + 'Tapanuli Tengah', + 'Tapanuli Utara', + 'Tapanuli Selatan', + 'Nias', + 'Langkat', + 'Karo', + 'Deli Serdang', + 'Simalungun', + 'Asahan', + 'Labuhanbatu', + 'Dairi', + 'Toba', + 'Mandailing Natal', + 'Nias Selatan', + 'Pakpak Bharat', + 'Humbang Hasundutan', + 'Samosir', + 'Serdang Bedagai', + 'Batu Bara', + 'Padang Lawas Utara', + 'Padang Lawas', + 'Labuhanbatu Selatan', + 'Labuhanbatu Utara', + 'Nias Utara', + 'Nias Barat', + 'Kota Medan', + 'Kota Pematangsiantar', + 'Kota Sibolga', + 'Kota Tanjung Balai', + 'Kota Binjai', + 'Kota Tebing Tinggi', + 'Kota Padangsidimpuan', + 'Kota Gunungsitoli', + 'Pesisir Selatan', + 'Solok', + 'Sijunjung', + 'Tanah Datar', + 'Padang Pariaman', + 'Agam', + 'Lima Puluh Kota', + 'Pasaman', + 'Kepulauan Mentawai', + 'Dharmasraya', + 'Solok Selatan', + 'Pasaman Barat', + 'Kota Padang', + 'Kota Solok', + 'Kota Sawahlunto', + 'Kota Padang Panjang', + 'Kota Bukittinggi', + 'Kota Payakumbuh', + 'Kota Pariaman', + 'Kampar', + 'Indragiri Hulu', + 'Bengkalis', + 'Indragiri Hilir', + 'Pelalawan', + 'Rokan Hulu', + 'Rokan Hilir', + 'Siak', + 'Kuantan Singingi', + 'Kepulauan Meranti', + 'Kota Pekanbaru', + 'Kota Dumai', + 'Kerinci', + 'Merangin', + 'Sarolangun', + 'Batanghari', + 'Muaro Jambi', + 'Tanjung Jabung Barat', + 'Tanjung Jabung Timur', + 'Bungo', + 'Tebo', + 'Kota Jambi', + 'Kota Sungai Penuh', + 'Ogan Komering Ulu', + 'Ogan Komering Ilir', + 'Muara Enim', + 'Lahat', + 'Musi Rawas', + 'Musi Banyuasin', + 'Banyuasin', + 'Ogan Komering Ulu Timur', + 'Ogan Komering Ulu Selatan', + 'Ogan Ilir', + 'Empat Lawang', + 'Penukal Abab Lematang Ilir', + 'Musi Rawas Utara', + 'Kota Palembang', + 'Kota Pagar Alam', + 'Kota Lubuk Linggau', + 'Kota Prabumulih', + 'Bengkulu Selatan', + 'Rejang Lebong', + 'Bengkulu Utara', + 'Kaur', + 'Seluma', + 'Muko Muko', + 'Lebong', + 'Kepahiang', + 'Bengkulu Tengah', + 'Kota Bengkulu', + 'Lampung Selatan', + 'Lampung Tengah', + 'Lampung Utara', + 'Lampung Barat', + 'Tulang Bawang', + 'Tanggamus', + 'Lampung Timur', + 'Way Kanan', + 'Pesawaran', + 'Pringsewu', + 'Mesuji', + 'Tulang Bawang Barat', + 'Pesisir Barat', + 'Kota Bandar Lampung', + 'Kota Metro', + 'Bangka', + 'Belitung', + 'Bangka Selatan', + 'Bangka Tengah', + 'Bangka Barat', + 'Belitung Timur', + 'Kota Pangkal Pinang', + 'Bintan', + 'Karimun', + 'Natuna', + 'Lingga', + 'Kepulauan Anambas', + 'Kota Batam', + 'Kota Tanjung Pinang', + 'Kepulauan Seribu', + 'Kota Jakarta Pusat', + 'Kota Jakarta Utara', + 'Kota Jakarta Barat', + 'Kota Jakarta Selatan', + 'Kota Jakarta Timur', + 'Bogor', + 'Sukabumi', + 'Cianjur', + 'Bandung', + 'Garut', + 'Tasikmalaya', + 'Ciamis', + 'Kuningan', + 'Cirebon', + 'Majalengka', + 'Sumedang', + 'Indramayu', + 'Subang', + 'Purwakarta', + 'Karawang', + 'Bekasi', + 'Bandung Barat', + 'Pangandaran', + 'Kota Bogor', + 'Kota Sukabumi', + 'Kota Bandung', + 'Kota Cirebon', + 'Kota Bekasi', + 'Kota Depok', + 'Kota Cimahi', + 'Kota Tasikmalaya', + 'Kota Banjar', + 'Cilacap', + 'Banyumas', + 'Purbalingga', + 'Banjarnegara', + 'Kebumen', + 'Purworejo', + 'Wonosobo', + 'Magelang', + 'Boyolali', + 'Klaten', + 'Sukoharjo', + 'Wonogiri', + 'Karanganyar', + 'Sragen', + 'Grobogan', + 'Blora', + 'Rembang', + 'Pati', + 'Kudus', + 'Jepara', + 'Demak', + 'Semarang', + 'Temanggung', + 'Kendal', + 'Batang', + 'Pekalongan', + 'Pemalang', + 'Tegal', + 'Brebes', + 'Kota Magelang', + 'Kota Surakarta', + 'Kota Salatiga', + 'Kota Semarang', + 'Kota Pekalongan', + 'Kota Tegal', + 'Kulon Progo', + 'Bantul', + 'Gunungkidul', + 'Sleman', + 'Kota Yogyakarta', + 'Pacitan', + 'Ponorogo', + 'Trenggalek', + 'Tulungagung', + 'Blitar', + 'Kediri', + 'Malang', + 'Lumajang', + 'Jember', + 'Banyuwangi', + 'Bondowoso', + 'Situbondo', + 'Probolinggo', + 'Pasuruan', + 'Sidoarjo', + 'Mojokerto', + 'Jombang', + 'Nganjuk', + 'Madiun', + 'Magetan', + 'Ngawi', + 'Bojonegoro', + 'Tuban', + 'Lamongan', + 'Gresik', + 'Bangkalan', + 'Sampang', + 'Pamekasan', + 'Sumenep', + 'Kota Kediri', + 'Kota Blitar', + 'Kota Malang', + 'Kota Probolinggo', + 'Kota Pasuruan', + 'Kota Mojokerto', + 'Kota Madiun', + 'Kota Surabaya', + 'Kota Batu', + 'Pandeglang', + 'Lebak', + 'Tangerang', + 'Serang', + 'Kota Tangerang', + 'Kota Cilegon', + 'Kota Serang', + 'Kota Tangerang Selatan', + 'Jembrana', + 'Tabanan', + 'Badung', + 'Gianyar', + 'Klungkung', + 'Bangli', + 'Karangasem', + 'Buleleng', + 'Kota Denpasar', + 'Lombok Barat', + 'Lombok Tengah', + 'Lombok Timur', + 'Sumbawa', + 'Dompu', + 'Bima', + 'Sumbawa Barat', + 'Lombok Utara', + 'Kota Mataram', + 'Kota Bima', + 'Kupang', + 'Timor Tengah Selatan', + 'Timor Tengah Utara', + 'Belu', + 'Alor', + 'Flores Timur', + 'Sikka', + 'Ende', + 'Ngada', + 'Manggarai', + 'Sumba Timur', + 'Sumba Barat', + 'Lembata', + 'Rote Ndao', + 'Manggarai Barat', + 'Nagekeo', + 'Sumba Tengah', + 'Sumba Barat Daya', + 'Manggarai Timur', + 'Sabu Raijua', + 'Malaka', + 'Kota Kupang', + 'Sambas', + 'Mempawah', + 'Sanggau', + 'Ketapang', + 'Sintang', + 'Kapuas Hulu', + 'Bengkayang', + 'Landak', + 'Sekadau', + 'Melawi', + 'Kayong Utara', + 'Kubu Raya', + 'Kota Pontianak', + 'Kota Singkawang', + 'Kotawaringin Barat', + 'Kotawaringin Timur', + 'Kapuas', + 'Barito Selatan', + 'Barito Utara', + 'Katingan', + 'Seruyan', + 'Sukamara', + 'Lamandau', + 'Gunung Mas', + 'Pulang Pisau', + 'Murung Raya', + 'Barito Timur', + 'Kota Palangkaraya', + 'Tanah Laut', + 'Kotabaru', + 'Banjar', + 'Barito Kuala', + 'Tapin', + 'Hulu Sungai Selatan', + 'Hulu Sungai Tengah', + 'Hulu Sungai Utara', + 'Tabalong', + 'Tanah Bumbu', + 'Balangan', + 'Kota Banjarmasin', + 'Kota Banjarbaru', + 'Paser', + 'Kutai Kartanegara', + 'Berau', + 'Kutai Barat', + 'Kutai Timur', + 'Penajam Paser Utara', + 'Mahakam Ulu', + 'Kota Balikpapan', + 'Kota Samarinda', + 'Kota Bontang', + 'Bulungan', + 'Malinau', + 'Nunukan', + 'Tana Tidung', + 'Kota Tarakan', + 'Bolaang Mongondow', + 'Minahasa', + 'Kepulauan Sangihe', + 'Kepulauan Talaud', + 'Minahasa Selatan', + 'Minahasa Utara', + 'Minahasa Tenggara', + 'Bolaang Mongondow Utara', + 'Kepulauan Siau Tagulandang Biaro (Sitaro)', + 'Bolaang Mongondow Timur', + 'Bolaang Mongondow Selatan', + 'Kota Manado', + 'Kota Bitung', + 'Kota Tomohon', + 'Kota Kotamobagu', + 'Banggai', + 'Poso', + 'Donggala', + 'Toli Toli', + 'Buol', + 'Morowali', + 'Banggai Kepulauan', + 'Parigi Moutong', + 'Tojo Una Una', + 'Sigi', + 'Banggai Laut', + 'Morowali Utara', + 'Kota Palu', + 'Kepulauan Selayar', + 'Bulukumba', + 'Bantaeng', + 'Jeneponto', + 'Takalar', + 'Gowa', + 'Sinjai', + 'Bone', + 'Maros', + 'Pangkajene Kepulauan', + 'Barru', + 'Soppeng', + 'Wajo', + 'Sidenreng Rappang', + 'Pinrang', + 'Enrekang', + 'Luwu', + 'Tana Toraja', + 'Luwu Utara', + 'Luwu Timur', + 'Toraja Utara', + 'Kota Makassar', + 'Kota Pare Pare', + 'Kota Palopo', + 'Kolaka', + 'Konawe', + 'Muna', + 'Buton', + 'Konawe Selatan', + 'Bombana', + 'Wakatobi', + 'Kolaka Utara', + 'Konawe Utara', + 'Buton Utara', + 'Kolaka Timur', + 'Konawe Kepulauan', + 'Muna Barat', + 'Buton Tengah', + 'Buton Selatan', + 'Kota Kendari', + 'Kota Bau Bau', + 'Gorontalo', + 'Boalemo', + 'Bone Bolango', + 'Pahuwato', + 'Gorontalo Utara', + 'Kota Gorontalo', + 'Pasangkayu (Mamuju Utara)', + 'Mamuju', + 'Mamasa', + 'Polewali Mandar', + 'Majene', + 'Mamuju Tengah', + 'Maluku Tengah', + 'Maluku Tenggara', + 'Kepulauan Tanimbar (Maluku Tenggara Barat)', + 'Buru', + 'Seram Bagian Timur', + 'Seram Bagian Barat', + 'Kepulauan Aru', + 'Maluku Barat Daya', + 'Buru Selatan', + 'Kota Ambon', + 'Kota Tual', + 'Halmahera Barat', + 'Halmahera Tengah', + 'Halmahera Utara', + 'Halmahera Selatan', + 'Kepulauan Sula', + 'Halmahera Timur', + 'Pulau Morotai', + 'Pulau Taliabu', + 'Kota Ternate', + 'Kota Tidore Kepulauan', + 'Jayapura', + 'Kepulauan Yapen', + 'Biak Numfor', + 'Sarmi', + 'Keerom', + 'Waropen', + 'Supiori', + 'Mamberamo Raya', + 'Kota Jayapura', + 'Manokwari', + 'Fak Fak', + 'Teluk Bintuni', + 'Teluk Wondama', + 'Kaimana', + 'Manokwari Selatan', + 'Pegunungan Arfak', + 'Merauke', + 'Boven Digoel', + 'Mappi', + 'Asmat', + 'Nabire', + 'Puncak Jaya', + 'Paniai', + 'Mimika', + 'Puncak', + 'Dogiyai', + 'Intan Jaya', + 'Deiyai', + 'Jayawijaya', + 'Pegunungan Bintang', + 'Yahukimo', + 'Tolikara', + 'Mamberamo Tengah', + 'Yalimo', + 'Lanny Jaya', + 'Nduga', + 'Sorong', + 'Sorong Selatan', + 'Raja Ampat', + 'Tambrauw', + 'Maybrat', + 'Kota Sorong', +]; + +export default INDONESIAN_CITIES;