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