Files
info-target-mobile/screens/profile/ui/MyServices.tsx
Samandar Turgunboyev ab363ca3b9 bug fixed
2026-03-02 13:22:55 +05:00

304 lines
10 KiB
TypeScript

import { useTheme } from '@/components/ThemeContext';
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Image as ExpoImage } from 'expo-image';
import { useRouter } from 'expo-router';
import { ArrowLeft, Edit2, Package, Plus, Trash2 } from 'lucide-react-native';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Alert,
FlatList,
Pressable,
StyleSheet,
Text,
View,
} from 'react-native';
import { RefreshControl } from 'react-native-gesture-handler';
import { user_api } from '../lib/api';
const PAGE_SIZE = 10;
export default function MyServicesScreen() {
const router = useRouter();
const queryClient = useQueryClient();
const { isDark } = useTheme();
const { t } = useTranslation();
/* ================= QUERY ================= */
const { data, isLoading, isError, fetchNextPage, hasNextPage, isRefetching } = useInfiniteQuery({
queryKey: ['my_services'],
queryFn: async ({ pageParam = 1 }) => {
const res = await user_api.my_sevices({
page: pageParam,
my: true,
page_size: PAGE_SIZE,
});
const d = res?.data?.data;
return {
results: d?.results ?? [],
current_page: d?.current_page ?? 1,
total_pages: d?.total_pages ?? 1,
};
},
getNextPageParam: (lastPage) =>
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
initialPageParam: 1,
});
const allServices = data?.pages.flatMap((p) => p.results) ?? [];
const { mutate } = useMutation({
mutationFn: (id: number) => user_api.delete_service(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['my_services'] });
},
});
const handleDelete = (id: number) => {
Alert.alert(t("Xizmatni o'chirish"), t("Rostdan ham bu xizmatni o'chirmoqchimisiz?"), [
{ text: t('Bekor qilish'), style: 'cancel' },
{
text: t("O'chirish"),
style: 'destructive',
onPress: () => mutate(id),
},
]);
};
const onRefresh = () => {
queryClient.refetchQueries({ queryKey: ['my_services'] });
};
if (isLoading) {
return (
<View style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<View style={[styles.topHeader, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
</Pressable>
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
{t('Xizmatlar')}
</Text>
<Pressable onPress={() => router.push('/profile/products/add')}>
<Plus color="#3b82f6" />
</Pressable>
</View>
<ActivityIndicator size={'large'} />
</View>
);
}
if (isError) {
return (
<View style={[styles.center, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<Text style={styles.error}>{t('Xatolik yuz berdi')}</Text>
</View>
);
}
return (
<View style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
{/* HEADER */}
<View style={[styles.topHeader, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
</Pressable>
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
{t('Xizmatlar')}
</Text>
<Pressable onPress={() => router.push('/profile/products/add')}>
<Plus color="#3b82f6" />
</Pressable>
</View>
{/* LIST */}
<FlatList
data={allServices}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={[styles.list, { flexGrow: 1 }]}
onEndReached={() => hasNextPage && fetchNextPage()}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={onRefresh}
colors={['#2563eb']}
tintColor="#2563eb"
/>
}
renderItem={({ item }) => {
const fileUrl = item.files?.length > 0 ? item.files[0]?.file : null;
return (
<Pressable
style={[
styles.card,
{
backgroundColor: isDark ? '#1e293b' : '#ffffff',
shadowColor: isDark ? '#000' : '#0f172a',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: isDark ? 0.3 : 0.08,
shadowRadius: 12,
elevation: 4,
},
]}
onPress={() =>
router.push({ pathname: `/profile/products/edit/[id]`, params: { id: item.id } })
}
>
{/* MEDIA */}
<View
style={[styles.mediaContainer, { backgroundColor: isDark ? '#334155' : '#e2e8f0' }]}
>
{fileUrl ? (
<ExpoImage
source={{ uri: fileUrl }}
style={styles.media}
contentFit="cover"
transition={200}
/>
) : (
<View style={styles.videoPlaceholder}>
<Text style={{ color: isDark ? '#64748b' : '#94a3b8' }}>{t("Media yo'q")}</Text>
</View>
)}
</View>
{/* CONTENT */}
<View style={styles.actions}>
<Pressable
style={styles.actionButton}
onPress={(e) => {
e.stopPropagation();
router.push({
pathname: `/profile/products/edit/[id]`,
params: { id: item.id },
});
}}
>
<Edit2 size={18} color="#3b82f6" />
</Pressable>
<Pressable
style={styles.actionButton}
onPress={(e) => {
e.stopPropagation();
handleDelete(item.id);
}}
>
<Trash2 size={18} color="#ef4444" />
</Pressable>
</View>
<View style={styles.contentContainer}>
<Text style={[styles.title, { color: isDark ? '#f8fafc' : '#0f172a' }]}>
{item.title}
</Text>
<Text
style={[styles.description, { color: isDark ? '#94a3b8' : '#64748b' }]}
numberOfLines={2}
>
{item.description}
</Text>
<View style={styles.categoriesContainer}>
{item.category.map((category, index) => (
<View
key={index}
style={[
styles.categoryChip,
{ backgroundColor: isDark ? '#334155' : '#e2e8f0' },
]}
>
<Text
style={[styles.categoryText, { color: isDark ? '#94a3b8' : '#64748b' }]}
>
{category.name}
</Text>
</View>
))}
</View>
</View>
</Pressable>
);
}}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Package size={64} color={isDark ? '#334155' : '#cbd5e1'} />
<Text style={[styles.emptyText, { color: isDark ? '#64748b' : '#94a3b8' }]}>
{t("Hozircha xizmatlar yo'q")}
</Text>
<Pressable
style={styles.emptyButton}
onPress={() => router.push('/profile/products/add')}
>
<Plus size={20} color="#ffffff" />
<Text style={styles.emptyButtonText}>{t("Xizmat qo'shish")}</Text>
</Pressable>
</View>
}
/>
</View>
);
}
/* ================= STYLES ================= */
const styles = StyleSheet.create({
container: { flex: 1 },
topHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
padding: 16,
alignItems: 'center',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
headerTitle: { fontSize: 18, fontWeight: '700', flex: 1, marginLeft: 10 },
list: { padding: 16, gap: 16, paddingBottom: 30 },
card: { borderRadius: 20, overflow: 'hidden' },
mediaContainer: { width: '100%', height: 200 },
media: { width: '100%', height: '100%' },
videoPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center' },
contentContainer: { padding: 20, gap: 12 },
title: { fontSize: 18, fontWeight: '700' as const },
description: { fontSize: 15, lineHeight: 22 },
categoriesContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
categoryChip: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
},
categoryText: { fontSize: 13, fontWeight: '500' as const },
emptyContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 80, gap: 16, flex: 1 },
emptyText: { fontSize: 17, fontWeight: '600' as const },
emptyButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
backgroundColor: '#3b82f6',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 12,
marginTop: 8,
},
emptyButtonText: { fontSize: 16, fontWeight: '600' as const, color: '#ffffff' },
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
loading: { fontSize: 16, fontWeight: '500' },
error: { color: '#ef4444', fontSize: 16, fontWeight: '500' },
actions: {
flexDirection: 'row',
position: 'absolute',
right: 5,
top: 5,
gap: 8,
},
actionButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#ffffff',
alignItems: 'center',
justifyContent: 'center',
},
});