complated

This commit is contained in:
Samandar Turgunboyev
2026-02-17 10:46:57 +05:00
parent 754f11804a
commit d747c72c8d
71 changed files with 917 additions and 397 deletions

View File

@@ -18,16 +18,15 @@ export interface AnnouncementListBodyRes {
title: string;
description: string;
total_view_count: number;
files: [
{
file: string;
}
];
files: {
id: number;
file: string;
}[];
status: 'pending' | 'paid' | 'verified' | 'canceled';
types: {
id: number;
name: string;
icon_name: string;
icon_name: string | null;
}[];
created_at: string;
}

View File

@@ -1,31 +1,86 @@
import { useTheme } from '@/components/ThemeContext';
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import { useVideoPlayer, VideoPlayer, VideoView } from 'expo-video';
import { Play } from 'lucide-react-native';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, Animated, RefreshControl, StyleSheet, Text, View } from 'react-native';
import {
ActivityIndicator,
Animated,
RefreshControl,
StyleSheet,
Text,
TouchableOpacity,
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';
function VideoCard({ player }: { player: VideoPlayer }) {
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
const subscription = player.addListener('playingChange', (state) => {
setIsPlaying(state.isPlaying);
});
return () => {
subscription.remove();
};
}, [player]);
return (
<View>
<VideoView player={player} style={styles.video} contentFit="contain" />
{!isPlaying && (
<View style={styles.playOverlay}>
<TouchableOpacity
style={styles.playButton}
onPress={() => {
player.play();
setIsPlaying(true);
}}
>
<Play color="white" size={26} fill="black" />
</TouchableOpacity>
</View>
)}
</View>
);
}
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 { t, i18n } = useTranslation();
const userLang = i18n.language.startsWith('ru')
? 'ru'
: i18n.language.startsWith('en')
? 'en'
: 'uz';
const [selectedLang, setSelectedLang] = useState<'uz' | 'ru' | 'en'>(userLang);
const theme = {
background: isDark ? '#0f172a' : '#f8fafc',
primary: '#2563eb',
text: isDark ? '#f8fafc' : '#0f172a',
loaderBg: isDark ? '#0f172a' : '#ffffff',
};
// Announcements query
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 });
const res = await announcement_api.list({
page: pageParam,
page_size: 10,
});
return res.data.data;
},
getNextPageParam: (lastPage) =>
@@ -36,8 +91,6 @@ export default function DashboardScreen() {
const allAnnouncements = data?.pages.flatMap((p) => p.results) ?? [];
useEffect(() => {
setAnnouncements(allAnnouncements);
fadeAnim.setValue(0);
Animated.timing(fadeAnim, {
toValue: 1,
@@ -46,6 +99,44 @@ export default function DashboardScreen() {
}).start();
}, [allAnnouncements]);
// Announcement videos
const videos = {
uz: require('@/assets/announcements-video/video_uz.webm'),
ru: require('@/assets/announcements-video/video_ru.webm'),
en: require('@/assets/announcements-video/video_en.webm'),
};
// Government videos: faqat RU mavjud
const govermentVideos: Partial<Record<'uz' | 'ru' | 'en', any>> = {
ru: require('@/assets/goverment/video_ru.webm'),
};
// Update selected language
useEffect(() => {
const lang = i18n.language.startsWith('ru')
? 'ru'
: i18n.language.startsWith('en')
? 'en'
: 'uz';
setSelectedLang(lang);
}, [i18n.language]);
// 🔹 Hooks: conditional emas, har doim chaqiriladi
const player = useVideoPlayer(videos[selectedLang], (player) => {
player.loop = false;
player.volume = 1;
player.muted = false;
});
const govermentVideoSource = govermentVideos[selectedLang] ?? null;
const player2 = useVideoPlayer(govermentVideoSource, (player) => {
if (!govermentVideoSource) return; // no video, do nothing
player.loop = false;
player.volume = 1;
player.muted = false;
});
const onRefresh = () => {
queryClient.refetchQueries({ queryKey: ['announcements_list'] });
};
@@ -54,6 +145,21 @@ export default function DashboardScreen() {
if (hasNextPage) fetchNextPage();
};
const videoItems = [
{ id: '1', player },
govermentVideoSource && { id: '2', player: player2 },
].filter(Boolean) as { id: string; player: VideoPlayer }[];
const renderVideoHeader = () => (
<View style={{ marginBottom: 8 }}>
{videoItems.map((item) => (
<View key={item.id} style={styles.videoContainer}>
<VideoCard player={item.player} />
</View>
))}
</View>
);
if (isLoading) {
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
@@ -66,33 +172,33 @@ export default function DashboardScreen() {
return (
<View style={[styles.container, { backgroundColor: theme.background }]}>
<Text style={{ color: 'white', fontSize: 20, marginBottom: 10 }}>
<Text style={{ color: theme.text, fontSize: 20, marginVertical: 16 }}>
{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} />
)}
<Animated.FlatList
style={{ flex: 1, opacity: fadeAnim }}
data={allAnnouncements}
keyExtractor={(item) => item.id.toString()}
numColumns={2}
columnWrapperStyle={styles.columnWrapper}
renderItem={({ item }) => <AnnouncementCard announcement={item} />}
ListHeaderComponent={renderVideoHeader}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={onRefresh}
colors={[theme.primary]}
tintColor={theme.primary}
progressBackgroundColor={theme.background}
/>
}
ListEmptyComponent={<EmptyState onRefresh={onRefresh} isRefreshing={isRefetching} />}
onEndReached={loadMore}
onEndReachedThreshold={0.3}
contentContainerStyle={{ paddingBottom: 80 }}
showsVerticalScrollIndicator={false}
/>
</View>
);
}
@@ -101,7 +207,6 @@ const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 16,
marginTop: 20,
},
loaderBox: {
flex: 1,
@@ -112,4 +217,33 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
gap: 12,
},
videoContainer: {
width: '100%',
height: 250,
marginBottom: 8,
borderRadius: 12,
overflow: 'hidden',
backgroundColor: '#000',
},
video: {
width: '100%',
height: '100%',
},
playOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
},
playButton: {
backgroundColor: 'white',
borderRadius: 50,
width: 48,
height: 48,
justifyContent: 'center',
alignItems: 'center',
},
});

View File

@@ -2,6 +2,7 @@ import { useTheme } from '@/components/ThemeContext';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
type Props = {
@@ -18,7 +19,7 @@ export default function EmptyState({
isRefreshing = false,
}: Props) {
const { isDark } = useTheme();
const { t } = useTranslation();
const theme = {
gradientColors: isDark
? (['#1e293b', '#334155'] as [string, string])
@@ -37,8 +38,8 @@ export default function EmptyState({
<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>
<Text style={[emptyStyles.title, { color: theme.title }]}>{t(title)}</Text>
<Text style={[emptyStyles.description, { color: theme.description }]}>{t(description)}</Text>
{onRefresh && (
<TouchableOpacity
@@ -52,7 +53,9 @@ export default function EmptyState({
) : (
<>
<Ionicons name="refresh" size={20} color={theme.buttonText} />
<Text style={[emptyStyles.refreshText, { color: theme.buttonText }]}>Yangilash</Text>
<Text style={[emptyStyles.refreshText, { color: theme.buttonText }]}>
{t('Yangilash')}
</Text>
</>
)}
</TouchableOpacity>