From 20556231b9bd1e1bde3af078ce8ceeda9390d128 Mon Sep 17 00:00:00 2001 From: Mark Zheleznyakov Date: Wed, 19 Nov 2025 17:18:19 +0300 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=D0=AD=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/app.json | 67 +++-- apps/frontend/app/(tabs)/_layout.tsx | 34 +++ apps/frontend/app/(tabs)/index.tsx | 214 ++++++++++++++ apps/frontend/app/+html.tsx | 38 +++ apps/frontend/app/+not-found.tsx | 18 ++ apps/frontend/app/_layout.tsx | 92 +++++- apps/frontend/app/auth.tsx | 129 +++++++++ .../assets/fonts/SpaceMono-Regular.ttf | Bin 0 -> 93252 bytes apps/frontend/assets/images/adaptive-icon.png | Bin 0 -> 17547 bytes apps/frontend/assets/images/favicon.png | Bin 0 -> 1466 bytes apps/frontend/assets/images/icon.png | Bin 0 -> 22380 bytes apps/frontend/assets/images/splash-icon.png | Bin 0 -> 17547 bytes apps/frontend/components/Themed.tsx | 45 +++ apps/frontend/components/useColorScheme.ts | 1 + apps/frontend/config/api.ts | 7 + apps/frontend/constants/Colors.ts | 19 ++ apps/frontend/global.css | 2 +- apps/frontend/lib/api.ts | 162 +++++++++++ apps/frontend/lib/auth.tsx | 144 ++++++++++ apps/frontend/metro.config.js | 6 +- apps/frontend/nativewind-env.d.ts | 2 +- apps/frontend/package.json | 78 ++--- apps/frontend/tailwind.config.js | 6 +- apps/frontend/tsconfig.json | 25 +- bun.lock | 268 ++++++++++++------ package.json | 3 +- 26 files changed, 1177 insertions(+), 183 deletions(-) create mode 100644 apps/frontend/app/(tabs)/_layout.tsx create mode 100644 apps/frontend/app/(tabs)/index.tsx create mode 100644 apps/frontend/app/+html.tsx create mode 100644 apps/frontend/app/+not-found.tsx create mode 100644 apps/frontend/app/auth.tsx create mode 100644 apps/frontend/assets/fonts/SpaceMono-Regular.ttf create mode 100644 apps/frontend/assets/images/adaptive-icon.png create mode 100644 apps/frontend/assets/images/favicon.png create mode 100644 apps/frontend/assets/images/icon.png create mode 100644 apps/frontend/assets/images/splash-icon.png create mode 100644 apps/frontend/components/Themed.tsx create mode 100644 apps/frontend/components/useColorScheme.ts create mode 100644 apps/frontend/config/api.ts create mode 100644 apps/frontend/constants/Colors.ts create mode 100644 apps/frontend/lib/api.ts create mode 100644 apps/frontend/lib/auth.tsx diff --git a/apps/frontend/app.json b/apps/frontend/app.json index 8b48441..73b4e36 100644 --- a/apps/frontend/app.json +++ b/apps/frontend/app.json @@ -1,32 +1,39 @@ { - "expo": { - "name": "p1ctos4ve", - "slug": "p1ctos4ve", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/icon.png", - "userInterfaceStyle": "light", - "newArchEnabled": true, - "scheme": "p1ctos4ve", - "splash": { - "image": "./assets/splash-icon.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" - }, - "ios": { - "supportsTablet": true - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#ffffff" - }, - "edgeToEdgeEnabled": true, - "predictiveBackGestureEnabled": false - }, - "web": { - "favicon": "./assets/favicon.png", - "bundler": "metro" - } - } + "expo": { + "name": "frontend", + "slug": "frontend", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "frontend", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "splash": { + "image": "./assets/images/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/images/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "edgeToEdgeEnabled": true, + "predictiveBackGestureEnabled": false + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router" + ], + "experiments": { + "typedRoutes": true + } + } } diff --git a/apps/frontend/app/(tabs)/_layout.tsx b/apps/frontend/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..14326f3 --- /dev/null +++ b/apps/frontend/app/(tabs)/_layout.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Image as ImageIcon, Search } from 'lucide-react-native'; +import { Link, Tabs } from 'expo-router'; +import { Pressable } from 'react-native'; + +import Colors from '@/constants/Colors'; +import { useColorScheme } from '@/components/useColorScheme'; + +export default function TabLayout() { + const colorScheme = useColorScheme(); + + return ( + + , + }} + /> + , + }} + /> + + ); +} diff --git a/apps/frontend/app/(tabs)/index.tsx b/apps/frontend/app/(tabs)/index.tsx new file mode 100644 index 0000000..6d814ab --- /dev/null +++ b/apps/frontend/app/(tabs)/index.tsx @@ -0,0 +1,214 @@ +import React, { useEffect, useState } from 'react'; +import { + FlatList, + TouchableOpacity, + Image, + RefreshControl, + Alert, + ActivityIndicator, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { useVideoPlayer, VideoView } from 'expo-video'; +import { Text, View } from '@/components/Themed'; +import { useAuth } from '@/lib/auth'; +import { savesApi } from '@/lib/api'; +import type { SaveListItem } from '@shared-types'; +import { useColorScheme } from '@/components/useColorScheme'; +import Colors from '@/constants/Colors'; +import { File, Plus, LogOut, Image as ImageIcon } from 'lucide-react-native'; + +export default function SavesScreen() { + const [saves, setSaves] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const { user, signOut } = useAuth(); + const router = useRouter(); + const colorScheme = useColorScheme(); + const colors = Colors[colorScheme ?? 'light']; + + const loadSaves = async () => { + try { + const data = await savesApi.getMySaves(); + setSaves(data); + } catch (error: any) { + Alert.alert('Ошибка', error.message || 'Не удалось загрузить сейвы'); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + if (user) { + loadSaves(); + } + }, [user]); + + const handleRefresh = () => { + setRefreshing(true); + loadSaves(); + }; + + const handleAdd = () => { + router.push('/add'); + }; + + const handleSavePress = (save: SaveListItem) => { + router.push(`/save/${save.id}`); + }; + + const handleSignOut = async () => { + Alert.alert( + 'Выход', + 'Вы уверены, что хотите выйти?', + [ + { text: 'Отмена', style: 'cancel' }, + { + text: 'Выйти', + style: 'destructive', + onPress: async () => { + try { + await signOut(); + router.replace('/auth'); + } catch (error: any) { + Alert.alert('Ошибка', error.message); + } + }, + }, + ] + ); + }; + + // Компонент для элемента списка + const SaveItem = ({ item }: { item: SaveListItem }) => { + const itemColorScheme = useColorScheme(); + const itemColors = Colors[itemColorScheme ?? 'light']; + + const videoPlayer = useVideoPlayer( + item.type === 'video' ? item.url : null, + (player) => { + player.muted = true; + player.loop = true; + player.play(); + } + ); + + return ( + handleSavePress(item)} + > + + + {item.type === 'image' || item.type === 'gif' ? ( + + ) : item.type === 'video' ? ( + + ) : ( + + + + )} + + + + {item.name || 'Без названия'} + + {item.description && ( + + {item.description} + + )} + {item.tags.length > 0 && ( + + {item.tags.slice(0, 3).map((tag, idx) => ( + + + {tag} + + + ))} + {item.tags.length > 3 && ( + + +{item.tags.length - 3} + + )} + + )} + + {new Date(item.createdAt).toLocaleDateString('ru-RU')} + + + + + ); + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + Мои сейвы + + + + + + + + + + + {saves.length === 0 ? ( + + + Нет сохраненных файлов + + Добавить первый сейв + + + ) : ( + item.id.toString()} + renderItem={({ item }) => } + refreshControl={ + + } + contentContainerStyle={{ padding: 16 }} + /> + )} + + ); +} + diff --git a/apps/frontend/app/+html.tsx b/apps/frontend/app/+html.tsx new file mode 100644 index 0000000..cb31090 --- /dev/null +++ b/apps/frontend/app/+html.tsx @@ -0,0 +1,38 @@ +import { ScrollViewStyleReset } from 'expo-router/html'; + +// This file is web-only and used to configure the root HTML for every +// web page during static rendering. +// The contents of this function only run in Node.js environments and +// do not have access to the DOM or browser APIs. +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {/* + Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. + However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. + */} + + + {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} +