fitst commit
This commit is contained in:
302
screens/profile/ui/MyServices.tsx
Normal file
302
screens/profile/ui/MyServices.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { useGlobalRefresh } from '@/components/ui/RefreshContext';
|
||||
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 { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { user_api } from '../lib/api';
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
export default function MyServicesScreen() {
|
||||
const router = useRouter();
|
||||
const { onRefresh, refreshing } = useGlobalRefresh();
|
||||
const queryClient = useQueryClient();
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
/* ================= QUERY ================= */
|
||||
const { data, isLoading, isError, fetchNextPage, hasNextPage } = 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),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SafeAreaView 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'} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View style={[styles.center, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
<Text style={styles.error}>{t('Xatolik yuz berdi')}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView 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}
|
||||
onEndReached={() => hasNextPage && fetchNextPage()}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
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>
|
||||
}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
/* ================= 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 },
|
||||
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 },
|
||||
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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user