Files
info-target-mobile/screens/profile/ui/NotificationTab.tsx
Samandar Turgunboyev d747c72c8d complated
2026-02-17 10:46:57 +05:00

419 lines
10 KiB
TypeScript

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>
{notifications.some((n) => !n.is_read) && (
<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>
<FlatList
data={notifications}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
renderItem={({ item, index }) => <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}
/>
</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,
},
/* 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',
},
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,
minWidth: 80,
alignItems: 'center',
justifyContent: 'center',
},
markAllText: {
fontSize: 13,
fontWeight: '600',
},
});