fitst commit

This commit is contained in:
Samandar Turgunboyev
2026-01-28 18:26:50 +05:00
parent 166a55b1e9
commit 124798419b
196 changed files with 26627 additions and 421 deletions

View File

@@ -0,0 +1,19 @@
import httpClient from '@/api/httpClient';
import { API_URLS } from '@/api/URLs';
import { AxiosResponse } from 'axios';
import { AnnouncementDetailBody, AnnouncementListBody } from './type';
export const announcement_api = {
async list(params: {
page: number;
page_size: number;
}): Promise<AxiosResponse<AnnouncementListBody>> {
const res = await httpClient.get(API_URLS.DASHBOARD_ADS, { params });
return res;
},
async detail(id: number): Promise<AxiosResponse<AnnouncementDetailBody>> {
const res = await httpClient.get(API_URLS.DASHBOARD_ADS_DETAIL(id));
return res;
},
};

View File

@@ -0,0 +1,54 @@
export interface AnnouncementListBody {
status: boolean;
data: {
links: {
previous: string | null;
next: string | null;
};
total_items: number;
total_pages: number;
page_size: number;
current_page: number;
results: AnnouncementListBodyRes[];
};
}
export interface AnnouncementListBodyRes {
id: number;
title: string;
description: string;
total_view_count: number;
files: [
{
file: string;
}
];
status: 'pending' | 'paid' | 'verified' | 'canceled';
types: {
id: number;
name: string;
icon_name: string;
}[];
created_at: string;
}
export interface AnnouncementDetailBody {
status: boolean;
data: {
id: number;
title: string;
description: string;
total_view_count: number;
files: {
id: number;
file: string;
}[];
status: 'pending' | 'paid' | 'verified' | 'canceled';
types: {
id: number;
name: string;
icon_name: string;
}[];
created_at: string;
};
}

View File

@@ -0,0 +1,38 @@
import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FAFBFF',
},
loader: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
card: {
flex: 1,
borderRadius: 12,
overflow: 'hidden',
backgroundColor: '#fff',
},
media: {
width: '100%',
height: 120,
},
content: {
padding: 10,
},
title: {
fontWeight: '600',
fontSize: 14,
},
desc: {
fontSize: 12,
color: '#666',
},
close: {
fontSize: 22,
alignSelf: 'flex-end',
},
});

View File

@@ -0,0 +1,227 @@
import { useTheme } from '@/components/ThemeContext';
import { Ionicons } from '@expo/vector-icons';
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
import { useQuery } from '@tanstack/react-query';
import { ResizeMode, Video } from 'expo-av';
import { LinearGradient } from 'expo-linear-gradient';
import { Package, PlayCircle } from 'lucide-react-native';
import React, { useCallback, useRef, useState } from 'react';
import { Dimensions, Image, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { announcement_api } from '../lib/api';
import { AnnouncementListBodyRes } from '../lib/type';
const { width, height } = Dimensions.get('window');
const cardWidth = (width - 44) / 2;
export default function AnnouncementCard({
announcement,
}: {
announcement: AnnouncementListBodyRes;
}) {
const [sheetOpen, setSheetOpen] = useState(false);
const bottomSheetRef = useRef<BottomSheetModal>(null);
const file = announcement.files?.[0]?.file;
const isVideo = file?.endsWith('.mp4');
const { isDark } = useTheme();
const theme = {
cardBg: isDark ? '#1e293b' : '#ffffff',
cardBorder: isDark ? '#334155' : '#e2e8f0',
mediaBg: isDark ? '#0f172a' : '#f1f5f9',
text: isDark ? '#f1f5f9' : '#0f172a',
textSecondary: isDark ? '#cbd5e1' : '#64748b',
textTertiary: isDark ? '#94a3b8' : '#94a3b8',
sheetBg: isDark ? '#0f172a' : '#ffffff',
indicator: isDark ? '#94a3b8' : '#cbd5e1',
shadow: isDark ? '#000' : '#64748b',
error: '#ef4444',
placeholder: isDark ? '#cbd5e1' : '#94a3b8',
};
const renderBackdrop = useCallback(
(props: any) => (
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.4} />
),
[]
);
const openSheet = () => {
bottomSheetRef.current?.present();
setSheetOpen(true);
};
const { data, isLoading, isError } = useQuery({
queryKey: ['announcement_detail', announcement.id],
queryFn: () => announcement_api.detail(announcement.id),
enabled: sheetOpen,
});
const detail = data?.data?.data;
const files = detail?.files || [];
return (
<>
{/* Card */}
<Pressable
style={[styles.card, { backgroundColor: theme.cardBg, shadowColor: theme.shadow }]}
onPress={openSheet}
android_ripple={{ color: 'rgba(99, 102, 241, 0.1)' }}
>
<View style={[styles.mediaContainer, { backgroundColor: theme.mediaBg }]}>
{file ? (
<>
<Image source={{ uri: file }} style={styles.image} resizeMode="cover" />
{isVideo && (
<View style={styles.videoIconOverlay}>
<PlayCircle size={36} color="white" fill="rgba(0,0,0,0.35)" />
</View>
)}
</>
) : (
<View style={styles.placeholder}>
<Package size={40} color={theme.placeholder} />
</View>
)}
<LinearGradient colors={['transparent', 'rgba(0,0,0,0.6)']} style={styles.gradient} />
</View>
<View style={styles.content}>
<Text style={[styles.title, { color: theme.text }]} numberOfLines={2}>
{announcement.title}
</Text>
<Text style={[styles.desc, { color: theme.textSecondary }]} numberOfLines={2}>
{announcement.description}
</Text>
<View style={styles.footer}>
<Ionicons name="time-outline" size={14} color={theme.textTertiary} />
<Text style={[styles.date, { color: theme.textTertiary }]}>
{new Date(announcement.created_at).toLocaleDateString('uz-UZ')}
</Text>
</View>
</View>
</Pressable>
<BottomSheetModal
ref={bottomSheetRef}
index={0}
snapPoints={['70%', '95%']}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ backgroundColor: theme.indicator, width: 50 }}
backgroundStyle={{ backgroundColor: theme.sheetBg }}
enablePanDownToClose
>
<BottomSheetScrollView contentContainerStyle={styles.sheetContent}>
{isLoading && (
<Text style={[styles.loading, { color: theme.textSecondary }]}>Yuklanmoqda...</Text>
)}
{isError && <Text style={[styles.error, { color: theme.error }]}>Xatolik yuz berdi</Text>}
{detail && (
<>
{/* Media carousel */}
{files.length > 0 && (
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
style={styles.carousel}
>
{files.map((f) => {
const fileIsVideo = f.file?.endsWith('.mp4');
return (
<View key={f.id} style={styles.sheetMediaContainer}>
{fileIsVideo ? (
<Video
source={{ uri: f.file }}
style={styles.media}
useNativeControls
resizeMode={ResizeMode.CONTAIN}
/>
) : (
<Image source={{ uri: f.file }} style={styles.media} />
)}
</View>
);
})}
</ScrollView>
)}
{/* Title */}
<Text style={[styles.sheetTitle, { color: theme.text }]}>{detail.title}</Text>
{/* Meta */}
<View style={styles.metaRow}>
<View style={styles.metaItem}>
<Ionicons name="calendar-outline" size={16} color={theme.textTertiary} />
<Text style={[styles.metaText, { color: theme.textTertiary }]}>
{new Date(detail.created_at).toLocaleDateString()}
</Text>
</View>
<View style={styles.metaItem}>
<Ionicons name="eye-outline" size={16} color={theme.textTertiary} />
<Text style={[styles.metaText, { color: theme.textTertiary }]}>
{detail.total_view_count} ko'rildi
</Text>
</View>
</View>
{/* Description */}
<Text style={[styles.sheetDesc, { color: theme.textSecondary }]}>
{detail.description}
</Text>
</>
)}
</BottomSheetScrollView>
</BottomSheetModal>
</>
);
}
const styles = StyleSheet.create({
card: {
width: cardWidth,
borderRadius: 16,
overflow: 'hidden',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 3,
marginBottom: 16,
},
mediaContainer: {
width: '100%',
height: 160,
position: 'relative',
},
image: { width: '100%', height: '100%' },
videoIconOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.25)',
},
placeholder: { flex: 1, alignItems: 'center', justifyContent: 'center' },
gradient: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 60 },
content: { padding: 12 },
title: { fontSize: 15, fontWeight: '700', marginBottom: 4 },
desc: { fontSize: 13, marginBottom: 6 },
footer: { flexDirection: 'row', alignItems: 'center', gap: 4 },
date: { fontSize: 12 },
// BottomSheet styles
sheetContent: { padding: 20, gap: 12 },
carousel: { marginBottom: 12 },
sheetMediaContainer: { width: width - 40, height: 200, marginRight: 12 },
media: { width: '100%', height: '100%', borderRadius: 16 },
sheetTitle: { fontSize: 18, fontWeight: '700' },
metaRow: { flexDirection: 'row', gap: 16, marginVertical: 8 },
metaItem: { flexDirection: 'row', alignItems: 'center', gap: 6 },
metaText: { fontSize: 14 },
sheetDesc: { fontSize: 14, lineHeight: 20 },
loading: { fontSize: 16 },
error: { fontSize: 16 },
});

View File

@@ -0,0 +1,125 @@
import { Ionicons } from '@expo/vector-icons';
import BottomSheet, { BottomSheetScrollView } from '@gorhom/bottom-sheet';
import { useQuery } from '@tanstack/react-query';
import { ResizeMode, Video } from 'expo-av';
import React, { useMemo, useRef } from 'react';
import { Dimensions, Image, ScrollView, StyleSheet, Text, View } from 'react-native';
import { announcement_api } from '../lib/api';
const { width, height } = Dimensions.get('window');
export default function AnnouncementDetailSheet({ id }: { id: number }) {
const bottomSheetRef = useRef<BottomSheet>(null);
// Sheet sizes
const snapPoints = useMemo(() => [height * 0.3, height * 0.7], []);
const { data, isLoading, isError } = useQuery({
queryKey: ['announcement_detail', id],
queryFn: () => announcement_api.detail(id),
enabled: !!id,
});
const announcement = data?.data?.data;
const files = announcement?.files || [];
return (
<BottomSheet
ref={bottomSheetRef}
index={0}
snapPoints={snapPoints}
handleIndicatorStyle={{ backgroundColor: '#94a3b8' }}
backgroundStyle={{ backgroundColor: '#0f172a', borderRadius: 24 }}
>
<BottomSheetScrollView contentContainerStyle={styles.contentContainer}>
{isLoading && <Text style={styles.loading}>Yuklanmoqda...</Text>}
{isError && <Text style={styles.error}>Xatolik yuz berdi</Text>}
{announcement && (
<>
{/* Carousel */}
{files.length > 0 && (
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
style={styles.carousel}
>
{files.map((file) => {
const isVideo = file.file?.endsWith('.mp4');
return (
<View key={file.id} style={styles.mediaContainer}>
{isVideo ? (
<Video
source={{ uri: file.file }}
style={styles.media}
useNativeControls
resizeMode={ResizeMode.CONTAIN}
/>
) : (
<Image source={{ uri: file.file }} style={styles.media} />
)}
</View>
);
})}
</ScrollView>
)}
{/* Title */}
<Text style={styles.title}>{announcement.title}</Text>
{/* Meta */}
<View style={styles.metaRow}>
<View style={styles.metaItem}>
<Ionicons name="calendar-outline" size={16} color="#94a3b8" />
<Text style={styles.metaText}>
{new Date(announcement.created_at).toLocaleDateString()}
</Text>
</View>
<View style={styles.metaItem}>
<Ionicons name="eye-outline" size={16} color="#94a3b8" />
<Text style={styles.metaText}>{announcement.total_view_count} ko'rildi</Text>
</View>
</View>
{/* Status */}
{/* <View style={styles.statusBadge}>
<Text style={styles.statusText}>
{announcement.status === 'pending' && 'Kutilmoqda'}
{announcement.status === 'paid' && "To'langan"}
{announcement.status === 'verified' && 'Tasdiqlangan'}
{announcement.status === 'canceled' && 'Bekor qilingan'}
</Text>
</View> */}
{/* Description */}
<Text style={styles.desc}>{announcement.description}</Text>
</>
)}
</BottomSheetScrollView>
</BottomSheet>
);
}
const styles = StyleSheet.create({
contentContainer: { padding: 20, gap: 12 },
carousel: { marginBottom: 12 },
mediaContainer: { width: width - 40, height: 200, marginRight: 12 },
media: { width: '100%', height: '100%', borderRadius: 16 },
title: { fontSize: 18, fontWeight: '700', color: '#f1f5f9' },
metaRow: { flexDirection: 'row', gap: 16, marginVertical: 8 },
metaItem: { flexDirection: 'row', alignItems: 'center', gap: 6 },
metaText: { color: '#94a3b8', fontSize: 14 },
statusBadge: {
alignSelf: 'flex-start',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
backgroundColor: '#2563eb',
marginBottom: 12,
},
statusText: { color: '#f1f5f9', fontWeight: '600' },
desc: { color: '#cbd5f5', fontSize: 14, lineHeight: 20 },
loading: { color: '#cbd5f5', fontSize: 16 },
error: { color: '#ef4444', fontSize: 16 },
});

View File

@@ -0,0 +1,84 @@
import { ThemedText } from '@/components/themed-text';
import { styles } from '@/screens/welcome/styles/welcomeStyle';
import LanguageSelect from '@/screens/welcome/ui/LanguageSelect';
import AntDesign from '@expo/vector-icons/AntDesign';
import { useRouter } from 'expo-router';
import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Animated, TouchableOpacity, View } from 'react-native';
export default function AnnouncementHeader({ menuOpen }: { onMenuPress: any; menuOpen: boolean }) {
const { t } = useTranslation();
const router = useRouter();
const rotateAnim = useRef(new Animated.Value(0)).current;
const scaleAnim = useRef(new Animated.Value(1)).current;
useEffect(() => {
if (menuOpen) {
Animated.parallel([
Animated.spring(rotateAnim, {
toValue: 1,
tension: 100,
friction: 8,
useNativeDriver: true,
}),
Animated.sequence([
Animated.timing(scaleAnim, {
toValue: 0.8,
duration: 100,
useNativeDriver: true,
}),
Animated.spring(scaleAnim, {
toValue: 1,
tension: 100,
friction: 5,
useNativeDriver: true,
}),
]),
]).start();
} else {
Animated.parallel([
Animated.spring(rotateAnim, {
toValue: 0,
tension: 100,
friction: 8,
useNativeDriver: true,
}),
Animated.sequence([
Animated.timing(scaleAnim, {
toValue: 0.8,
duration: 100,
useNativeDriver: true,
}),
Animated.spring(scaleAnim, {
toValue: 1,
tension: 100,
friction: 5,
useNativeDriver: true,
}),
]),
]).start();
}
}, [menuOpen]);
return (
<View style={styles.header}>
<View style={styles.headerContent}>
<View style={styles.logoBox}>
<TouchableOpacity onPress={() => router.back()}>
<AntDesign name="left" size={18} color="black" />
</TouchableOpacity>
<View style={styles.logoCircle}>
<ThemedText style={styles.logoText}>IT</ThemedText>
</View>
<ThemedText style={styles.brandText}>{t('common.target')}</ThemedText>
</View>
<View style={styles.headerRight}>
<LanguageSelect />
</View>
</View>
</View>
);
}

View File

@@ -0,0 +1,115 @@
import { useTheme } from '@/components/ThemeContext';
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, Animated, RefreshControl, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { announcement_api } from '../lib/api';
import { AnnouncementListBodyRes } from '../lib/type';
import AnnouncementCard from './AnnouncementCard';
import EmptyState from './EmptyState';
export default function DashboardScreen() {
const [announcements, setAnnouncements] = useState<AnnouncementListBodyRes[]>([]);
const queryClient = useQueryClient();
const fadeAnim = useRef(new Animated.Value(0)).current;
const { isDark } = useTheme();
const { t } = useTranslation();
const theme = {
background: isDark ? '#0f172a' : '#f8fafc',
primary: '#2563eb',
loaderBg: isDark ? '#0f172a' : '#ffffff',
};
const { data, isLoading, isRefetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['announcements_list'],
queryFn: async ({ pageParam = 1 }) => {
const res = await announcement_api.list({ page: pageParam, page_size: 10 });
return res.data.data;
},
getNextPageParam: (lastPage) =>
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
initialPageParam: 1,
});
const allAnnouncements = data?.pages.flatMap((p) => p.results) ?? [];
useEffect(() => {
setAnnouncements(allAnnouncements);
fadeAnim.setValue(0);
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
}, [allAnnouncements]);
const onRefresh = () => {
queryClient.refetchQueries({ queryKey: ['announcements_list'] });
};
const loadMore = () => {
if (hasNextPage) fetchNextPage();
};
if (isLoading) {
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<View style={[styles.loaderBox, { backgroundColor: theme.loaderBg }]}>
<ActivityIndicator size="large" color={theme.primary} />
</View>
</SafeAreaView>
);
}
return (
<View style={[styles.container, { backgroundColor: theme.background }]}>
<Text style={{ color: 'white', fontSize: 20, marginBottom: 10 }}>
{t("E'lonlar ro'yxati")}
</Text>
{announcements.length > 0 ? (
<Animated.FlatList
style={{ flex: 1 }}
data={announcements}
keyExtractor={(item) => item.id.toString()}
numColumns={2}
columnWrapperStyle={styles.columnWrapper}
renderItem={({ item }) => <AnnouncementCard announcement={item} />}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={onRefresh}
colors={[theme.primary]}
tintColor={theme.primary}
progressBackgroundColor={theme.background}
/>
}
onEndReached={loadMore}
onEndReachedThreshold={0.3}
showsVerticalScrollIndicator={false}
/>
) : (
<EmptyState onRefresh={onRefresh} isRefreshing={isRefetching} />
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 16,
marginTop: 20,
},
loaderBox: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
columnWrapper: {
justifyContent: 'space-between',
gap: 12,
},
});

View File

@@ -0,0 +1,136 @@
import { useTheme } from '@/components/ThemeContext';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import React from 'react';
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
type Props = {
title?: string;
description?: string;
onRefresh?: () => void;
isRefreshing?: boolean;
};
export default function EmptyState({
title = 'Maʼlumot topilmadi',
description = 'Hozircha hech qanday eʼlon mavjud emas',
onRefresh,
isRefreshing = false,
}: Props) {
const { isDark } = useTheme();
const theme = {
gradientColors: isDark
? (['#1e293b', '#334155'] as [string, string])
: (['#EEF2FF', '#E0E7FF'] as [string, string]),
iconColor: '#6366F1',
title: isDark ? '#f8fafc' : '#1F2937',
description: isDark ? '#94a3b8' : '#6B7280',
buttonBg: '#6366F1',
buttonText: '#ffffff',
dotColor: '#6366F1',
};
return (
<View style={emptyStyles.container}>
<LinearGradient colors={theme.gradientColors} style={emptyStyles.iconContainer}>
<Ionicons name="megaphone-outline" size={64} color={theme.iconColor} />
</LinearGradient>
<Text style={[emptyStyles.title, { color: theme.title }]}>{title}</Text>
<Text style={[emptyStyles.description, { color: theme.description }]}>{description}</Text>
{onRefresh && (
<TouchableOpacity
style={[emptyStyles.refreshBtn, { backgroundColor: theme.buttonBg }]}
onPress={onRefresh}
disabled={isRefreshing}
activeOpacity={0.8}
>
{isRefreshing ? (
<ActivityIndicator color={theme.buttonText} size="small" />
) : (
<>
<Ionicons name="refresh" size={20} color={theme.buttonText} />
<Text style={[emptyStyles.refreshText, { color: theme.buttonText }]}>Yangilash</Text>
</>
)}
</TouchableOpacity>
)}
<View style={emptyStyles.decoration}>
<View style={[emptyStyles.dot, { backgroundColor: theme.dotColor }]} />
<View
style={[emptyStyles.dot, emptyStyles.dotMedium, { backgroundColor: theme.dotColor }]}
/>
<View
style={[emptyStyles.dot, emptyStyles.dotSmall, { backgroundColor: theme.dotColor }]}
/>
</View>
</View>
);
}
const emptyStyles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 32,
},
iconContainer: {
width: 120,
height: 120,
borderRadius: 60,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 24,
},
title: {
fontSize: 20,
fontWeight: '700',
textAlign: 'center',
marginBottom: 8,
},
description: {
fontSize: 15,
textAlign: 'center',
lineHeight: 22,
marginBottom: 24,
},
refreshBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 12,
shadowColor: '#6366F1',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 5,
minWidth: 140,
justifyContent: 'center',
},
refreshText: {
fontSize: 16,
fontWeight: '600',
},
decoration: {
flexDirection: 'row',
gap: 8,
marginTop: 32,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
dotMedium: {
opacity: 0.6,
},
dotSmall: {
opacity: 0.3,
},
});

View File

@@ -0,0 +1,84 @@
import { Ionicons } from '@expo/vector-icons';
import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
export default function PaginationLite({ currentPage, totalPages, onChange }: any) {
const canGoPrev = currentPage > 1;
const canGoNext = currentPage < totalPages;
return (
<View style={paginationStyles.container}>
<Pressable
disabled={!canGoPrev}
onPress={() => onChange(currentPage - 1)}
style={[paginationStyles.btn, !canGoPrev && paginationStyles.btnDisabled]}
>
<Ionicons name="chevron-back" size={20} color={canGoPrev ? '#6366F1' : '#D1D5DB'} />
</Pressable>
<View style={paginationStyles.indicator}>
<Text style={paginationStyles.currentPage}>{currentPage}</Text>
<Text style={paginationStyles.separator}>/</Text>
<Text style={paginationStyles.totalPages}>{totalPages}</Text>
</View>
<Pressable
disabled={!canGoNext}
onPress={() => onChange(currentPage + 1)}
style={[paginationStyles.btn, !canGoNext && paginationStyles.btnDisabled]}
>
<Ionicons name="chevron-forward" size={20} color={canGoNext ? '#6366F1' : '#D1D5DB'} />
</Pressable>
</View>
);
}
const paginationStyles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: 16,
paddingVertical: 20,
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#F0F2F8',
},
btn: {
width: 44,
height: 44,
borderRadius: 12,
backgroundColor: '#F9FAFB',
borderWidth: 1,
borderColor: '#E5E7EB',
justifyContent: 'center',
alignItems: 'center',
},
btnDisabled: {
backgroundColor: '#F3F4F6',
borderColor: '#E5E7EB',
},
indicator: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingHorizontal: 20,
paddingVertical: 10,
backgroundColor: '#EEF2FF',
borderRadius: 12,
},
currentPage: {
fontSize: 18,
fontWeight: '700',
color: '#6366F1',
},
separator: {
fontSize: 16,
color: '#9CA3AF',
},
totalPages: {
fontSize: 16,
fontWeight: '600',
color: '#6B7280',
},
});