307 lines
13 KiB
TypeScript
307 lines
13 KiB
TypeScript
import AppHeader from "@/components/AppHeader";
|
|
import BorderBottomItem from "@/components/borderBottomItem";
|
|
import ImageUser from "@/components/imageNew";
|
|
import InputSearch from "@/components/inputSearch";
|
|
import Text from '@/components/Text';
|
|
import { ColorsStatus } from "@/constants/ColorsStatus";
|
|
import { ConstEnv } from "@/constants/ConstEnv";
|
|
import Styles from "@/constants/Styles";
|
|
import { apiGetSearch } from "@/lib/api";
|
|
import { useAuthSession } from "@/providers/AuthProvider";
|
|
import { useTheme } from "@/providers/ThemeProvider";
|
|
import { AntDesign, Feather, MaterialIcons } from "@expo/vector-icons";
|
|
import { router, Stack } from "expo-router";
|
|
import React, { useState } from "react";
|
|
import { RefreshControl, SafeAreaView, ScrollView, TouchableOpacity, View } from "react-native";
|
|
import Toast from "react-native-toast-message";
|
|
|
|
type PropsUser = {
|
|
id: string
|
|
name: string
|
|
email: string
|
|
position: string
|
|
group: string
|
|
img: string
|
|
}
|
|
|
|
type PropProject = {
|
|
id: string
|
|
title: string
|
|
group: string
|
|
}
|
|
|
|
type PropDivisi = {
|
|
id: string
|
|
name: string
|
|
desc: string
|
|
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[]>([])
|
|
const [dataDivisi, setDataDivisi] = useState<PropDivisi[]>([])
|
|
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 })
|
|
if (hasil.success) {
|
|
setDataUser(hasil.data.user)
|
|
setDataDivisi(hasil.data.division)
|
|
setDataProject(hasil.data.project)
|
|
} else {
|
|
return Toast.show({ type: 'small', text1: hasil.message })
|
|
}
|
|
} else {
|
|
setDataUser([])
|
|
setDataDivisi([])
|
|
setDataProject([])
|
|
}
|
|
} catch (error: any) {
|
|
console.error(error);
|
|
const message = error?.response?.data?.message || "Gagal melakukan pencarian"
|
|
Toast.show({ type: 'small', text1: message })
|
|
}
|
|
}
|
|
|
|
const handleRefresh = async () => {
|
|
setRefreshing(true)
|
|
handleSearch(search)
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
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.flex1]}>
|
|
{/* Search bar */}
|
|
<View style={[Styles.ph15, { paddingTop: 15 }]}>
|
|
<InputSearch onChange={handleSearch} />
|
|
</View>
|
|
|
|
{/* 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>
|
|
)
|
|
}
|