228 lines
8.0 KiB
TypeScript
228 lines
8.0 KiB
TypeScript
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 },
|
|
});
|