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

512 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useTheme } from '@/components/ThemeContext';
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { router } from 'expo-router';
import { ArrowLeft } from 'lucide-react-native';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Animated,
FlatList,
Pressable,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { user_api } from '../lib/api';
import { NotificationListDataRes } from '../lib/type';
const PAGE_SIZE = 10;
export function NotificationTab() {
const { isDark } = useTheme();
const { t } = useTranslation();
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } =
useInfiniteQuery({
queryKey: ['notifications-list'],
queryFn: async ({ pageParam = 1 }) => {
const response = await user_api.notification_list({
page: pageParam,
page_size: PAGE_SIZE,
});
return response.data.data;
},
getNextPageParam: (lastPage) =>
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
initialPageParam: 1,
});
const notifications = data?.pages.flatMap((p) => p.results) ?? [];
const queryClient = useQueryClient();
const { mutate: markAllAsRead, isPending: isMarkingAllRead } = useMutation({
mutationFn: () => user_api.mark_all_as_read(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notifications-list'] });
queryClient.invalidateQueries({ queryKey: ['notification-list'] });
},
});
if (isLoading) {
return (
<View
style={[
styles.loadingContainer,
{
backgroundColor: isDark ? '#0f172a' : '#f8fafc',
},
]}
>
<View style={styles.loadingContent}>
<ActivityIndicator size="large" color="#3b82f6" />
</View>
</View>
);
}
if (isError) {
return (
<View style={[styles.errorContainer, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<View style={[styles.errorContent, { backgroundColor: isDark ? '#1e293b' : '#ffffff' }]}>
<Text style={styles.errorTitle}>{t('Xatolik yuz berdi')}</Text>
<Text style={[styles.errorMessage, { color: isDark ? '#cbd5e1' : '#64748b' }]}>
{t("Bildirishnomalarni yuklashda muammo bo'ldi")}
</Text>
<TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
<Text style={styles.retryButtonText}>{t('Qayta urinish')}</Text>
</TouchableOpacity>
</View>
</View>
);
}
return (
<View style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
</Pressable>
<Text style={[styles.headerTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
{t('Bildirishnomalar')}
</Text>
</View>
<FlatList
data={notifications}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
ListHeaderComponent={() => {
if (notifications.length === 0) {
return (
<View style={styles.emptyHeader}>
<Text
style={[
styles.emptyTitle,
{ color: isDark ? '#f1f5f9' : '#0f172a' },
]}
>
{t("Hozircha bildirishnomalar yo'q")}
</Text>
<Text
style={[
styles.emptyDesc,
{ color: isDark ? '#94a3b8' : '#64748b' },
]}
>
{t("Yangi xabarlar shu yerda paydo boladi")}
</Text>
</View>
);
}
if (notifications.some((n) => !n.is_read)) {
return (
<View style={styles.headerActions}>
<TouchableOpacity
style={[
styles.markAllButton,
{
backgroundColor: isDark ? '#1e293b' : '#e0f2fe',
borderColor: '#3b82f6',
},
]}
onPress={() => markAllAsRead()}
disabled={isMarkingAllRead}
>
{isMarkingAllRead ? (
<ActivityIndicator size="small" color="#3b82f6" />
) : (
<Text style={[styles.markAllText, { color: '#3b82f6' }]}>
{t("Barchasi o'qildi")}
</Text>
)}
</TouchableOpacity>
</View>
);
}
return (
<View style={styles.allReadContainer}>
<Text
style={[
styles.allReadText,
{ color: isDark ? '#94a3b8' : '#64748b' },
]}
>
{t("Barcha bildirishnomalar oqilgan")}
</Text>
</View>
);
}}
renderItem={({ item }) => <NotificationCard item={item} />}
onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}}
onEndReachedThreshold={0.5}
ListFooterComponent={
isFetchingNextPage ? (
<ActivityIndicator size="small" color="#3b82f6" style={{ marginVertical: 20 }} />
) : null
}
refreshing={isLoading}
onRefresh={refetch}
ListEmptyComponent={() =>
!isLoading && (
<View style={styles.emptyContainer}>
<Text style={[styles.emptyTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
{t("Hozircha bildirishnomalar yo'q")}
</Text>
</View>
)
}
/>
</View>
);
}
/* ---------------- CARD ---------------- */
function NotificationCard({ item }: { item: NotificationListDataRes }) {
const queryClient = useQueryClient();
const { isDark } = useTheme();
const { t } = useTranslation();
const [scaleAnim] = useState(new Animated.Value(1));
const { mutate } = useMutation({
mutationFn: (id: number) => user_api.is_ready_id(id),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ['notification-list'] });
queryClient.refetchQueries({ queryKey: ['notifications-list'] });
},
});
const handlePressIn = () => {
Animated.spring(scaleAnim, {
toValue: 0.96,
useNativeDriver: true,
}).start();
};
const handlePressOut = () => {
Animated.spring(scaleAnim, {
toValue: 1,
friction: 4,
tension: 50,
useNativeDriver: true,
}).start();
};
const handlePress = (id: number) => {
if (!item.is_read) {
mutate(id);
}
};
return (
<Animated.View
style={{
transform: [{ scale: scaleAnim }],
marginBottom: 16,
}}
>
<TouchableOpacity
activeOpacity={0.85}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onPress={() => handlePress(item.id)}
style={[
styles.card,
!item.is_read && styles.unreadCard,
{
backgroundColor: isDark ? '#283046' : '#e0f2fe',
borderColor: isDark ? '#3b82f6' : '#3b82f6',
},
]}
>
<View style={styles.cardContent}>
<View style={styles.cardHeader}>
<Text
style={[
styles.cardTitle,
!item.is_read && styles.unreadTitle,
{ color: isDark ? '#3b82f6' : '#1e40af' },
]}
numberOfLines={1}
>
{item.title}
</Text>
{!item.is_read && (
<View
style={[
styles.unreadIndicator,
{
backgroundColor: isDark ? '#3b82f6' : '#3b82f6',
},
]}
/>
)}
</View>
<Text
style={[
styles.cardMessage,
{
color: isDark ? '#cbd5e1' : '#64748b',
},
]}
numberOfLines={2}
>
{item.description}
</Text>
<Text style={[styles.cardTime, { color: isDark ? '#94a3b8' : '#94a3b8' }]}>
{formatDate(item.created_at, t)}
</Text>
</View>
</TouchableOpacity>
</Animated.View>
);
}
/* ---------------- HELPERS ---------------- */
function formatDate(date: string, t: any) {
const now = new Date();
const notifDate = new Date(date);
const diffMs = now.getTime() - notifDate.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Hozir';
if (diffMins < 60) return `${diffMins} ${t('daqiqa oldin')}`;
if (diffHours < 24) return `${diffHours} ${t('soat oldin')}`;
if (diffDays < 7) return `${diffDays} ${t('kun oldin')}`;
return notifDate.toLocaleDateString('uz-UZ', {
day: 'numeric',
month: 'short',
});
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
listContent: {
padding: 16,
paddingBottom: 32,
flexGrow: 1
},
/* Card Styles */
card: {
flexDirection: 'row',
padding: 16,
borderRadius: 24,
borderWidth: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.25,
shadowRadius: 12,
elevation: 8,
overflow: 'hidden',
},
unreadCard: {
shadowColor: '#3b82f6',
shadowOpacity: 0.3,
shadowRadius: 16,
elevation: 10,
},
cardContent: {
flex: 1,
justifyContent: 'center',
},
headerActions: {
marginBottom: 16,
alignItems: 'flex-end',
},
emptyHeader: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
allReadContainer: {
marginBottom: 16,
alignItems: 'center',
},
allReadText: {
fontSize: 14,
},
emptyTitle: {
fontSize: 18,
fontWeight: '700',
marginBottom: 8,
textAlign: 'center',
},
emptyDesc: {
fontSize: 14,
textAlign: 'center',
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 6,
},
cardTitle: {
flex: 1,
fontSize: 16.5,
fontWeight: '600',
letterSpacing: -0.1,
},
unreadTitle: {
fontWeight: '700',
// unread title rang
},
unreadIndicator: {
width: 10,
height: 10,
borderRadius: 5,
marginLeft: 8,
shadowColor: '#3b82f6',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.7,
shadowRadius: 6,
},
cardMessage: {
fontSize: 14.5,
lineHeight: 21,
marginBottom: 8,
},
cardTime: {
fontSize: 12.5,
fontWeight: '500',
opacity: 0.9,
},
/* Loading State */
loadingContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
loadingContent: {
alignItems: 'center',
padding: 32,
},
/* Error State */
errorContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 24,
},
errorContent: {
alignItems: 'center',
padding: 32,
borderRadius: 24,
maxWidth: 320,
},
errorTitle: {
color: '#ef4444',
fontSize: 20,
fontWeight: '700',
marginBottom: 8,
},
errorMessage: {
fontSize: 14,
textAlign: 'center',
marginBottom: 24,
lineHeight: 20,
},
retryButton: {
backgroundColor: '#3b82f6',
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
shadowColor: '#3b82f6',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 5,
},
retryButtonText: {
color: '#ffffff',
fontSize: 15,
fontWeight: '700',
},
/* Header */
header: {
padding: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
gap: 10,
elevation: 3,
},
headerTitle: {
fontSize: 18,
fontWeight: '700',
lineHeight: 24,
flex: 1,
},
markAllButton: {
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 8,
borderWidth: 1,
width: "auto",
alignItems: 'center',
justifyContent: 'center',
},
markAllText: {
fontSize: 13,
fontWeight: '600',
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 30,
},
});