feat: реализован профиль пользователя с публичными сейвами
This commit is contained in:
184
apps/frontend/app/user/[slug].tsx
Normal file
184
apps/frontend/app/user/[slug].tsx
Normal file
@ -0,0 +1,184 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
RefreshControl,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { useVideoPlayer, VideoView } from 'expo-video';
|
||||
import { Text, View } from '@/components/Themed';
|
||||
import { savesApi } from '@/lib/api';
|
||||
import type { SaveListItem } from '@shared-types';
|
||||
import { useColorScheme } from '@/components/useColorScheme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import { File, ArrowLeft, Image as ImageIcon } from 'lucide-react-native';
|
||||
|
||||
export default function UserProfileScreen() {
|
||||
const { slug } = useLocalSearchParams<{ slug: string }>();
|
||||
const [saves, setSaves] = useState<SaveListItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const router = useRouter();
|
||||
const colorScheme = useColorScheme();
|
||||
const colors = Colors[colorScheme ?? 'light'];
|
||||
|
||||
const loadSaves = async () => {
|
||||
if (!slug) return;
|
||||
|
||||
try {
|
||||
const data = await savesApi.getPublicSavesByUser(slug);
|
||||
setSaves(data);
|
||||
} catch (error: any) {
|
||||
Alert.alert('Ошибка', error.message || 'Не удалось загрузить сейвы');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (slug) {
|
||||
loadSaves();
|
||||
}
|
||||
}, [slug]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setRefreshing(true);
|
||||
loadSaves();
|
||||
};
|
||||
|
||||
const handleSavePress = (save: SaveListItem) => {
|
||||
router.push(`/save/${save.id}`);
|
||||
};
|
||||
|
||||
// Компонент для элемента списка
|
||||
const SaveItem = React.memo(({ item }: { item: SaveListItem }) => {
|
||||
if (!item) return null;
|
||||
|
||||
const itemColorScheme = useColorScheme();
|
||||
const itemColors = Colors[itemColorScheme ?? 'light'];
|
||||
|
||||
const videoPlayer = useVideoPlayer(
|
||||
item.type === 'video' ? item.url : null,
|
||||
(player) => {
|
||||
player.muted = true; // Без звука в списке
|
||||
player.loop = true; // Зацикливание для превью
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className="border rounded-xl mb-3 overflow-hidden"
|
||||
style={{ borderColor: itemColors.tabIconDefault }}
|
||||
onPress={() => handleSavePress(item)}
|
||||
>
|
||||
<View className="flex-row p-3">
|
||||
<View className="w-24 h-24 rounded-lg overflow-hidden mr-3">
|
||||
{item.type === 'image' || item.type === 'gif' ? (
|
||||
<Image
|
||||
source={{ uri: item.url }}
|
||||
className="w-full h-full"
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : item.type === 'video' ? (
|
||||
<VideoView
|
||||
player={videoPlayer}
|
||||
className="w-full h-full bg-black"
|
||||
contentFit="cover"
|
||||
nativeControls={false}
|
||||
fullscreenOptions={{ enable: false }}
|
||||
/>
|
||||
) : (
|
||||
<View className="w-full h-full justify-center items-center" style={{ backgroundColor: itemColors.tabIconDefault }}>
|
||||
<File size={32} color={itemColors.text} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-base font-semibold mb-1" numberOfLines={1}>
|
||||
{item.name || 'Без названия'}
|
||||
</Text>
|
||||
{item.description && (
|
||||
<Text
|
||||
className="text-sm mb-2"
|
||||
style={{ color: itemColors.tabIconDefault }}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.description}
|
||||
</Text>
|
||||
)}
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<View className="flex-row flex-wrap gap-1.5 mb-2 items-center">
|
||||
{item.tags.slice(0, 3).map((tag, idx) => (
|
||||
<View
|
||||
key={idx}
|
||||
className="px-2 py-1 rounded"
|
||||
style={{ backgroundColor: itemColors.tint + '20' }}
|
||||
>
|
||||
<Text className="text-xs" style={{ color: itemColors.tint }}>
|
||||
{tag}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
{item.tags.length > 3 && (
|
||||
<Text className="text-xs" style={{ color: itemColors.tabIconDefault }}>
|
||||
+{item.tags.length - 3}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<Text className="text-xs" style={{ color: itemColors.tabIconDefault }}>
|
||||
{new Date(item.createdAt).toLocaleDateString('ru-RU')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="flex-1 justify-center items-center">
|
||||
<ActivityIndicator size="large" color={colors.tint} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1">
|
||||
<View className="flex-row justify-between items-center p-4 pt-16">
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
className="p-2"
|
||||
>
|
||||
<ArrowLeft size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text className="text-3xl font-bold flex-1 text-center">Публичные сейвы</Text>
|
||||
<View className="w-10" />
|
||||
</View>
|
||||
|
||||
{saves.length === 0 ? (
|
||||
<View className="flex-1 justify-center items-center p-10">
|
||||
<ImageIcon size={64} color={colors.tabIconDefault} />
|
||||
<Text className="text-lg mt-4 mb-2 text-center font-semibold">Нет публичных сейвов</Text>
|
||||
<Text className="text-sm text-center" style={{ color: colors.tabIconDefault }}>
|
||||
У этого пользователя пока нет публичных сейвов
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={saves}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={({ item }) => <SaveItem item={item} />}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||
}
|
||||
contentContainerStyle={{ padding: 16 }}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user