feat: redesign halaman pencarian dengan filter tabs, section badge, dan card style
This commit is contained in:
@@ -9,13 +9,12 @@ import Styles from "@/constants/Styles";
|
||||
import { apiGetSearch } from "@/lib/api";
|
||||
import { useAuthSession } from "@/providers/AuthProvider";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { AntDesign, MaterialIcons } from "@expo/vector-icons";
|
||||
import { AntDesign, Feather, MaterialIcons } from "@expo/vector-icons";
|
||||
import { router, Stack } from "expo-router";
|
||||
import React, { useState } from "react";
|
||||
import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
||||
import { RefreshControl, SafeAreaView, ScrollView, TouchableOpacity, View } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
// ... types ...
|
||||
type PropsUser = {
|
||||
id: string
|
||||
name: string
|
||||
@@ -38,6 +37,27 @@ type PropDivisi = {
|
||||
group: string
|
||||
}
|
||||
|
||||
type FilterType = "all" | "member" | "division" | "project"
|
||||
|
||||
function SectionHeader({ label, count, colors }: { label: string; count: number; colors: any }) {
|
||||
return (
|
||||
<View style={[Styles.rowItemsCenter, Styles.mb08]}>
|
||||
<Text style={{ fontSize: 11, fontWeight: '600', color: colors.dimmed, letterSpacing: 0.8, textTransform: 'uppercase' }}>
|
||||
{label}
|
||||
</Text>
|
||||
<View style={{
|
||||
marginLeft: 6,
|
||||
backgroundColor: colors.icon + '25',
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 7,
|
||||
paddingVertical: 1,
|
||||
}}>
|
||||
<Text style={{ fontSize: 11, color: colors.dimmed, fontWeight: '600' }}>{count}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Search() {
|
||||
const { token, decryptToken } = useAuthSession()
|
||||
const [dataUser, setDataUser] = useState<PropsUser[]>([])
|
||||
@@ -45,11 +65,16 @@ export default function Search() {
|
||||
const [dataProject, setDataProject] = useState<PropProject[]>([])
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
const [activeFilter, setActiveFilter] = useState<FilterType>("all")
|
||||
const { colors } = useTheme();
|
||||
|
||||
const totalResults = dataUser.length + dataDivisi.length + dataProject.length
|
||||
const hasSearch = search.length >= 3
|
||||
|
||||
async function handleSearch(cari: string) {
|
||||
try {
|
||||
setSearch(cari)
|
||||
setActiveFilter("all")
|
||||
if (cari.length >= 3) {
|
||||
const user = await decryptToken(String(token?.current))
|
||||
const hasil = await apiGetSearch({ text: cari, user: user })
|
||||
@@ -58,7 +83,7 @@ export default function Search() {
|
||||
setDataDivisi(hasil.data.division)
|
||||
setDataProject(hasil.data.project)
|
||||
} else {
|
||||
return Toast.show({ type: 'small', text1: hasil.message, })
|
||||
return Toast.show({ type: 'small', text1: hasil.message })
|
||||
}
|
||||
} else {
|
||||
setDataUser([])
|
||||
@@ -68,15 +93,10 @@ export default function Search() {
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const message = error?.response?.data?.message || "Gagal melakukan pencarian"
|
||||
|
||||
Toast.show({
|
||||
type: 'small',
|
||||
text1: message
|
||||
})
|
||||
Toast.show({ type: 'small', text1: message })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
handleSearch(search)
|
||||
@@ -84,114 +104,203 @@ export default function Search() {
|
||||
setRefreshing(false)
|
||||
};
|
||||
|
||||
const filters: { key: FilterType; label: string; count: number }[] = [
|
||||
{ key: "all", label: "Semua", count: totalResults },
|
||||
{ key: "member", label: "Anggota", count: dataUser.length },
|
||||
{ key: "division", label: "Divisi", count: dataDivisi.length },
|
||||
{ key: "project", label: "Kegiatan", count: dataProject.length },
|
||||
]
|
||||
|
||||
const showUser = activeFilter === "all" || activeFilter === "member"
|
||||
const showDivision = activeFilter === "all" || activeFilter === "division"
|
||||
const showProject = activeFilter === "all" || activeFilter === "project"
|
||||
|
||||
const activeFilterEmpty =
|
||||
(activeFilter === "member" && dataUser.length === 0) ||
|
||||
(activeFilter === "division" && dataDivisi.length === 0) ||
|
||||
(activeFilter === "project" && dataProject.length === 0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerTitle: 'Pencarian',
|
||||
headerTitleAlign: 'center',
|
||||
header: () => (
|
||||
<AppHeader title="Pencarian" showBack={true} onPressLeft={() => router.back()} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<View style={[Styles.p15]}>
|
||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerTitle: 'Pencarian',
|
||||
headerTitleAlign: 'center',
|
||||
header: () => (
|
||||
<AppHeader title="Pencarian" showBack={true} onPressLeft={() => router.back()} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<View style={[Styles.flex1]}>
|
||||
{/* Search bar */}
|
||||
<View style={[Styles.ph15, { paddingTop: 15 }]}>
|
||||
<InputSearch onChange={handleSearch} />
|
||||
{
|
||||
dataProject.length + dataDivisi.length + dataUser.length > 0
|
||||
?
|
||||
<ScrollView
|
||||
style={[Styles.h100]}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.icon}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{
|
||||
dataUser.length > 0 &&
|
||||
<View style={[Styles.mv05, Styles.p10]}>
|
||||
<Text>ANGGOTA</Text>
|
||||
{
|
||||
dataUser.map((item, index) => (
|
||||
<BorderBottomItem
|
||||
key={index}
|
||||
borderType="bottom"
|
||||
icon={<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} />}
|
||||
title={item.name}
|
||||
subtitle={`${item.group}-${item.position}`}
|
||||
onPress={() => {
|
||||
router.push(`/member/${item.id}`)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
}
|
||||
|
||||
{
|
||||
dataDivisi.length > 0 &&
|
||||
<View style={[Styles.mv05, Styles.p10]}>
|
||||
<Text>DIVISI</Text>
|
||||
{
|
||||
dataDivisi.map((item, index) => (
|
||||
<BorderBottomItem
|
||||
key={index}
|
||||
borderType="bottom"
|
||||
icon={
|
||||
<View style={[Styles.iconContent, ColorsStatus.primary]}>
|
||||
<MaterialIcons name="group" size={25} color="white" />
|
||||
</View>
|
||||
}
|
||||
title={item.name}
|
||||
subtitle={item.group}
|
||||
onPress={() => {
|
||||
router.push(`/division/${item.id}`)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
dataProject.length > 0 &&
|
||||
<View style={[Styles.mv05, Styles.p10]}>
|
||||
<Text>KEGIATAN</Text>
|
||||
{
|
||||
dataProject.map((item, index) => (
|
||||
<BorderBottomItem
|
||||
key={index}
|
||||
borderType="bottom"
|
||||
icon={
|
||||
<View style={[Styles.iconContent, ColorsStatus.primary]}>
|
||||
<AntDesign name="areachart" size={25} color="white" />
|
||||
</View>
|
||||
}
|
||||
title={item.title}
|
||||
subtitle={item.group}
|
||||
onPress={() => {
|
||||
router.push(`/project/${item.id}`)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
}
|
||||
</ScrollView>
|
||||
:
|
||||
<View style={[Styles.contentItemCenter, Styles.mt10]}>
|
||||
<Text style={[Styles.textInformation, { color: colors.icon }]}>Tidak ada data</Text>
|
||||
</View>
|
||||
}
|
||||
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</>
|
||||
|
||||
{/* Filter tabs */}
|
||||
{hasSearch && totalResults > 0 && (
|
||||
<View style={{ marginTop: 10, flexShrink: 0 }}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={[Styles.ph15, { paddingRight: 5, alignItems: 'center' }]}
|
||||
>
|
||||
{filters.map((f) => {
|
||||
const isActive = activeFilter === f.key
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={f.key}
|
||||
onPress={() => setActiveFilter(f.key)}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: 20,
|
||||
marginRight: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: isActive ? colors.tabActive : colors.icon + '40',
|
||||
backgroundColor: isActive ? colors.tabActive + '20' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<Text style={{
|
||||
fontSize: 13,
|
||||
color: isActive ? colors.tabActive : colors.dimmed,
|
||||
fontWeight: isActive ? '600' : 'normal',
|
||||
}}>
|
||||
{f.label}
|
||||
</Text>
|
||||
{f.count > 0 && (
|
||||
<View style={{
|
||||
marginLeft: 5,
|
||||
backgroundColor: isActive ? colors.tabActive : colors.icon + '30',
|
||||
borderRadius: 10,
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 4,
|
||||
}}>
|
||||
<Text style={{ fontSize: 11, color: isActive ? 'white' : colors.dimmed, fontWeight: '600' }}>
|
||||
{f.count}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<View style={[Styles.flex1]}>
|
||||
{!hasSearch ? (
|
||||
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||
<Feather name="search" size={42} color={colors.icon + '40'} />
|
||||
<Text style={[Styles.mt10, { color: colors.dimmed, fontSize: 14 }]}>
|
||||
Ketik minimal 3 karakter untuk mencari
|
||||
</Text>
|
||||
</View>
|
||||
) : totalResults === 0 ? (
|
||||
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||
<Feather name="inbox" size={42} color={colors.icon + '40'} />
|
||||
<Text style={[Styles.mt10, { color: colors.dimmed, fontSize: 14 }]}>
|
||||
Tidak ada hasil untuk "{search}"
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
style={[Styles.flex1]}
|
||||
contentContainerStyle={[Styles.ph15, { paddingTop: 14, paddingBottom: 30 }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.icon}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/* Anggota */}
|
||||
{showUser && dataUser.length > 0 && (
|
||||
<View style={[Styles.mb15]}>
|
||||
<SectionHeader label="Anggota" count={dataUser.length} colors={colors} />
|
||||
{dataUser.map((item, index) => (
|
||||
<View key={index} style={index < dataUser.length - 1 ? Styles.mb05 : undefined}>
|
||||
<BorderBottomItem
|
||||
borderType="all"
|
||||
icon={<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} />}
|
||||
title={item.name}
|
||||
subtitle={`${item.group} · ${item.position}`}
|
||||
onPress={() => router.push(`/member/${item.id}`)}
|
||||
colorPress
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Divisi */}
|
||||
{showDivision && dataDivisi.length > 0 && (
|
||||
<View style={[Styles.mb15]}>
|
||||
<SectionHeader label="Divisi" count={dataDivisi.length} colors={colors} />
|
||||
{dataDivisi.map((item, index) => (
|
||||
<View key={index} style={index < dataDivisi.length - 1 ? Styles.mb05 : undefined}>
|
||||
<BorderBottomItem
|
||||
borderType="all"
|
||||
icon={
|
||||
<View style={[Styles.iconContent, ColorsStatus.primary]}>
|
||||
<MaterialIcons name="group" size={25} color="white" />
|
||||
</View>
|
||||
}
|
||||
title={item.name}
|
||||
subtitle={item.group}
|
||||
onPress={() => router.push(`/division/${item.id}`)}
|
||||
colorPress
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Kegiatan */}
|
||||
{showProject && dataProject.length > 0 && (
|
||||
<View style={[Styles.mb15]}>
|
||||
<SectionHeader label="Kegiatan" count={dataProject.length} colors={colors} />
|
||||
{dataProject.map((item, index) => (
|
||||
<View key={index} style={index < dataProject.length - 1 ? Styles.mb05 : undefined}>
|
||||
<BorderBottomItem
|
||||
borderType="all"
|
||||
icon={
|
||||
<View style={[Styles.iconContent, ColorsStatus.primary]}>
|
||||
<AntDesign name="areachart" size={25} color="white" />
|
||||
</View>
|
||||
}
|
||||
title={item.title}
|
||||
subtitle={item.group}
|
||||
onPress={() => router.push(`/project/${item.id}`)}
|
||||
colorPress
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Empty state untuk filter aktif */}
|
||||
{activeFilter !== "all" && activeFilterEmpty && (
|
||||
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||
<Feather name="inbox" size={42} color={colors.icon + '40'} />
|
||||
<Text style={[Styles.mt10, { color: colors.dimmed, fontSize: 14 }]}>
|
||||
Tidak ada hasil di kategori ini
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user