419 lines
10 KiB
TypeScript
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',
|
|
},
|
|
});
|