[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

Updates

Login, register and those things

C3B 2e5adede 328fa55a

+973 -92
api/videoServices.ts api/feedServices.ts
+53 -6
app/(auth)/Login.tsx
··· 1 + import ActionButton from '@/components/global/ActionButton'; 2 + import InputArea from '@/components/global/InputArea'; 3 + import Logo from '@/components/global/Logo'; 1 4 import { ThemedText } from '@/components/ThemedText'; 2 - import React from 'react'; 3 - import { View, StyleSheet } from 'react-native'; 5 + import { Colors } from '@/constants/Colors'; 6 + import { Ionicons } from '@expo/vector-icons'; 7 + import { router } from 'expo-router'; 8 + import React, { useState } from 'react'; 9 + import { View, StyleSheet, useColorScheme, TouchableOpacity, SafeAreaView } from 'react-native'; 4 10 5 11 export default function Login() { 12 + 13 + useColorScheme(); 14 + const colorScheme = useColorScheme(); 6 15 7 16 const styles = StyleSheet.create({ 8 17 container: { 9 18 flex: 1, 10 19 alignItems: 'center', 11 20 justifyContent: 'center', 12 - backgroundColor: '#fff', 21 + backgroundColor: Colors[colorScheme ?? 'light'].background, 13 22 }, 14 23 text: { 15 24 fontSize: 18, ··· 18 27 }, 19 28 }); 20 29 30 + const [email, setEmail] = useState(''); 31 + 32 + const [password, setPassword] = useState(''); 33 + 21 34 return ( 22 - <View style={styles.container}> 23 - <ThemedText style={styles.text}>Hello from Login!</ThemedText> 24 - </View> 35 + <SafeAreaView style={styles.container}> 36 + <TouchableOpacity 37 + onPress={() => router.back()} 38 + style={{ position: 'absolute', top: 80, left: 20, zIndex: 1 }}> 39 + <Ionicons name="chevron-back" size={24} color={Colors[colorScheme ?? 'light'].text} /> 40 + </TouchableOpacity> 41 + <Logo size={14} color={Colors[colorScheme ?? 'light'].selectedIcon} style={{ marginBottom: 50 }} /> 42 + <View style={{ width: '80%' }}> 43 + <InputArea 44 + label='Email' 45 + placeholder='Enter your email' 46 + icon='mail' 47 + type='email' 48 + inputStyle={{ width: '100%' }} 49 + value={email} 50 + onChangeText={setEmail} 51 + style={{ width: "100%" }} 52 + /> 53 + <InputArea 54 + label='Password' 55 + placeholder='Enter your password' 56 + icon='lock-closed' 57 + type='password' 58 + inputStyle={{ width: '100%' }} 59 + value={password} 60 + onChangeText={setPassword} 61 + style={{ width: "100%", marginBottom: 30 }} 62 + /> 63 + 64 + <ActionButton 65 + title="Login" 66 + onPress={() => { 67 + 68 + }} 69 + /> 70 + </View> 71 + </SafeAreaView> 25 72 ); 26 73 }
+72 -8
app/(auth)/Register.tsx
··· 1 + import ActionButton from '@/components/global/ActionButton'; 2 + import InputArea from '@/components/global/InputArea'; 3 + import Logo from '@/components/global/Logo'; 1 4 import { ThemedText } from '@/components/ThemedText'; 2 - import React from 'react'; 3 - import { View, StyleSheet } from 'react-native'; 5 + import { Colors } from '@/constants/Colors'; 6 + import { Ionicons } from '@expo/vector-icons'; 7 + import { router } from 'expo-router'; 8 + import React, { useState } from 'react'; 9 + import { View, StyleSheet, useColorScheme, TouchableOpacity, SafeAreaView, Dimensions } from 'react-native'; 4 10 5 11 export default function Register() { 6 12 13 + useColorScheme(); 14 + const colorScheme = useColorScheme(); 15 + 7 16 const styles = StyleSheet.create({ 8 17 container: { 9 - flex: 1, 18 + height: Dimensions.get('screen').height, 10 19 alignItems: 'center', 11 20 justifyContent: 'center', 12 - backgroundColor: '#fff', 21 + backgroundColor: Colors[colorScheme ?? 'light'].background, 22 + // backgroundColor: 'blue', 23 + width: '100%', 13 24 }, 14 25 text: { 15 26 fontSize: 18, 16 27 fontWeight: 'bold', 17 - color: '#333', 18 28 }, 19 29 }); 20 30 31 + const [email, setEmail] = useState(''); 32 + 33 + const [password, setPassword] = useState(''); 34 + 35 + const [birthDate, setBirthDate] = useState(''); 36 + 21 37 return ( 22 - <View style={styles.container}> 23 - <ThemedText style={styles.text}>Hello from Register!</ThemedText> 24 - </View> 38 + <SafeAreaView style={styles.container}> 39 + <TouchableOpacity 40 + onPress={() => router.back()} 41 + style={{ position: 'absolute', top: 80, left: 20, zIndex: 1 }}> 42 + <Ionicons name="chevron-back" size={24} color={Colors[colorScheme ?? 'light'].text} /> 43 + </TouchableOpacity> 44 + <Logo size={14} color={Colors[colorScheme ?? 'light'].selectedIcon} style={{ marginBottom: 50 }} /> 45 + <View style={{ width: '100%', alignItems: 'center' }}> 46 + <InputArea 47 + label='Email' 48 + placeholder='Enter your email' 49 + icon='mail' 50 + type='email' 51 + inputStyle={{ width: '100%' }} 52 + value={email} 53 + onChangeText={setEmail} 54 + style={{ width: "90%" }} 55 + /> 56 + <InputArea 57 + label='Password' 58 + placeholder='Enter your password' 59 + icon='lock-closed' 60 + type='password' 61 + inputStyle={{ width: '90%' }} 62 + value={password} 63 + onChangeText={setPassword} 64 + style={{ width: "90%", marginBottom: 25 }} 65 + /> 66 + <InputArea 67 + label='Birth Date' 68 + placeholder='26/11/2002' 69 + icon='calendar' 70 + type='birthDate' 71 + format='mmddyyyy' 72 + inputStyle={{ width: '90%' }} 73 + value={birthDate} 74 + onChangeText={setBirthDate} 75 + style={{ width: "90%", marginBottom: 55 }} 76 + /> 77 + <ActionButton 78 + title="Next" 79 + onPress={() => { 80 + console.log(password) 81 + console.log(birthDate) 82 + console.log(email) 83 + 84 + }} 85 + width={'90%'} 86 + /> 87 + </View> 88 + </SafeAreaView> 25 89 ); 26 90 }
-3
app/(screens)/ProfileFeed.tsx
··· 20 20 const [currentVisibleIndex, setCurrentVisibleIndex] = useState(0); 21 21 22 22 const route = useRoute(); 23 - // Expect videoData as a JSON string and initialIndex as a string. 24 23 const params = route.params as { videoData?: string; initialIndex?: string }; 25 24 26 - // Parse initial index from params 27 25 const initialIndex = parseInt(params.initialIndex ?? "0", 10) || 0; 28 26 29 - // Use the passed videoData from the router parameter instead of fetching trending posts. 30 27 useEffect(() => { 31 28 if (params.videoData) { 32 29 try {
+177 -56
app/(tabs)/ProfileScreen.tsx
··· 18 18 import PlaceholderVideoDisplay from '@/components/Profile/PlaceholderVideoDisplay'; 19 19 import { did } from '@/constants/MockData'; 20 20 import { UserProps, PostProps } from '@/types/Interfaces'; 21 - import { useRouter } from 'expo-router'; 21 + import { router, useRouter } from 'expo-router'; 22 22 import { getProfile, getProfileMedia } from '@/api/profileServices'; 23 23 24 24 function padVideosWithPlaceholders( ··· 59 59 return [...videos, ...placeholders]; 60 60 } 61 61 62 - 63 62 export default function ProfileScreen() { 64 63 const colorScheme = useColorScheme(); 65 64 const route = useRouter(); 65 + 66 + // Ajuste aqui para os cenários que você quer simular: 67 + // - isLoggedIn = true/false 68 + // - isMine = true/false 69 + const isLoggedIn = true; // Se está logado ou não 70 + const isMine = true; // Se a conta do perfil atual é minha 66 71 67 72 const [userData, setUserData] = useState<UserProps | null>(null); 68 73 const [videoPosts, setVideoPosts] = useState<PostProps[]>([]); 69 74 70 75 useEffect(() => { 71 - const loadProfileData = async () => { 72 - try { 73 - const profileData = await getProfile(did); 74 - if (profileData) { 75 - setUserData({ 76 - id: profileData.did, 77 - did: profileData.did, 78 - displayName: profileData.displayName, 79 - handle: profileData.handle, 80 - description: profileData.description, 81 - avatar: profileData.avatar || '', 82 - banner: profileData.banner || '', 83 - followersCount: profileData.followersCount, 84 - followsCount: profileData.followsCount, 85 - postsCount: profileData.postsCount, 86 - associated: profileData.associated, 87 - joinedViaStarterPack: profileData.joinedViaStarterPack, 88 - indexedAt: profileData.indexedAt, 89 - createdAt: profileData.createdAt, 90 - viewer: profileData.viewer, 91 - labels: profileData.labels, 92 - pinnedPost: profileData.pinnedPost, 93 - }); 76 + if (isLoggedIn) { 77 + // Carrega dados de um perfil "real" 78 + const loadProfileData = async () => { 79 + try { 80 + const profileData = await getProfile(did); 81 + if (profileData) { 82 + setUserData({ 83 + id: profileData.did, 84 + did: profileData.did, 85 + displayName: profileData.displayName, 86 + handle: profileData.handle, 87 + description: profileData.description, 88 + avatar: profileData.avatar || '', 89 + banner: profileData.banner || '', 90 + followersCount: profileData.followersCount, 91 + followsCount: profileData.followsCount, 92 + postsCount: profileData.postsCount, 93 + associated: profileData.associated, 94 + joinedViaStarterPack: profileData.joinedViaStarterPack, 95 + indexedAt: profileData.indexedAt, 96 + createdAt: profileData.createdAt, 97 + viewer: profileData.viewer, 98 + labels: profileData.labels, 99 + pinnedPost: profileData.pinnedPost, 100 + }); 101 + } 102 + } catch (error) { 103 + console.error('Error loading profile:', error); 104 + } 105 + }; 106 + const loadVideoPosts = async () => { 107 + try { 108 + const mediaPosts = await getProfileMedia(did, 'video'); 109 + const posts = mediaPosts.map((item: PostProps) => item as PostProps); 110 + setVideoPosts(posts); 111 + } catch (error) { 112 + console.error('Error loading video posts:', error); 94 113 } 95 - } catch (error) { 96 - console.error('Error loading profile:', error); 97 - } 98 - }; 99 - 100 - const loadVideoPosts = async () => { 101 - try { 102 - const mediaPosts = await getProfileMedia(did, 'video'); 103 - const posts = mediaPosts.map((item: { post: PostProps }) => item.post as PostProps); 104 - setVideoPosts(posts); 105 - } catch (error) { 106 - console.error('Error loading video posts:', error); 107 - } 108 - }; 109 - 110 - loadProfileData(); 111 - loadVideoPosts(); 112 - }, []); 114 + }; 115 + loadProfileData(); 116 + loadVideoPosts(); 117 + } else { 118 + // Se não está logado, crie um "stub" de userData 119 + setUserData({ 120 + id: '', 121 + did: '', 122 + displayName: 'Login ou Registrar', 123 + handle: 'null', 124 + description: '', 125 + avatar: 'https://static.sprk.so/branding/default-profile.png?d', 126 + banner: '', 127 + followersCount: 0, 128 + followsCount: 0, 129 + postsCount: 0, 130 + indexedAt: '', 131 + createdAt: '', 132 + labels: [], 133 + }); 134 + } 135 + }, [isLoggedIn]); 113 136 114 137 const paddedVideoData = padVideosWithPlaceholders(videoPosts); 115 138 ··· 124 147 }); 125 148 } 126 149 127 - const isMine = true; 150 + function goTo(route: string) { 151 + route = route.toLowerCase(); 152 + if (route === 'register') { 153 + router.push('/(auth)/Register', { relativeToDirectory: true }); 154 + } else { 155 + router.push('/(auth)/Login', { relativeToDirectory: true }); 156 + } 157 + } 128 158 129 159 const styles = StyleSheet.create({ 130 160 container: { ··· 152 182 alignItems: 'center', 153 183 width: '100%', 154 184 }, 185 + profileHeaderNull: { 186 + alignItems: 'center', 187 + width: '100%', 188 + justifyContent: 'center', 189 + height: '70%', 190 + }, 155 191 profileContent: { 156 192 marginTop: 20, 157 193 }, ··· 180 216 gap: 10, 181 217 width: '100%', 182 218 }, 219 + profileActionButtonsVertical: { 220 + flexDirection: 'column', 221 + justifyContent: 'center', 222 + alignItems: 'center', 223 + gap: 10, 224 + width: '100%', 225 + }, 183 226 }); 184 227 185 228 return ( ··· 187 230 <ContentWrapper> 188 231 <ScrollView 189 232 contentContainerStyle={styles.scrollViewContent} 190 - showsVerticalScrollIndicator={false}> 233 + showsVerticalScrollIndicator={false} 234 + > 235 + {/* Navbar Superior */} 191 236 <View style={styles.profileNavbar}> 192 - <TouchableOpacity onPress={() => { }}> 237 + <TouchableOpacity onPress={() => {}}> 193 238 <Ionicons 194 - name="arrow-back" 239 + name="chevron-back" 195 240 size={24} 196 241 color={Colors[colorScheme ?? 'light'].text} 197 242 /> 198 243 </TouchableOpacity> 244 + 199 245 <ThemedText style={styles.profileTopText}> 200 - {userData?.displayName} 246 + {userData?.displayName ?? ''} 201 247 </ThemedText> 202 - <TouchableOpacity onPress={() => { }}> 248 + 249 + <TouchableOpacity onPress={() => {}}> 250 + {/* No seu caso, pode ser "ellipsis-horizontal" ou vazio */} 203 251 <Ionicons 204 252 name="ellipsis-horizontal" 205 253 size={24} ··· 207 255 /> 208 256 </TouchableOpacity> 209 257 </View> 258 + 259 + {/* Cabeçalho e foto do perfil */} 210 260 <View style={styles.profileHeader}> 211 261 {userData && <ProfilePicture userData={userData} />} 212 262 {userData && <ProfileInfo userData={userData} />} 213 - {!isMine && ( 214 - <ActionButton title="Follow" onPress={() => { }} width={250} /> 215 - )} 263 + 264 + {/* 265 + 4 CASOS (botões de ação): 266 + 267 + 1) [logado && minha conta]: Editar / Compartilhar 268 + 2) [logado && não é minha conta]: Seguir 269 + 3) [não logado && não é minha conta]: Perfil + Botão "Seguir" (redireciona para login) 270 + 4) [não logado && é minha conta]: Botões de "Login" / "Registrar" 271 + */} 216 272 { 217 - isMine && ( 273 + !isLoggedIn && isMine && ( 274 + // Não logado, mas é minha conta => mostrar Login / Register 275 + <View style={styles.profileActionButtonsVertical}> 276 + <ActionButton 277 + type="primary" 278 + title="Registrar" 279 + onPress={() => goTo('register')} 280 + width="60%" 281 + /> 282 + <ActionButton 283 + type="outline" 284 + title="Login" 285 + onPress={() => goTo('login')} 286 + width="60%" 287 + /> 288 + </View> 289 + ) 290 + } 291 + { 292 + !isLoggedIn && !isMine && ( 293 + // Não logado e não é minha conta => mostrar "Seguir" (redireciona pra login) 294 + <ActionButton 295 + type="primary" 296 + title="Follow" 297 + onPress={() => goTo('login')} 298 + width={250} 299 + /> 300 + ) 301 + } 302 + { 303 + isLoggedIn && !isMine && ( 304 + // Logado, mas não é minha conta => mostrar "Seguir" 305 + <ActionButton 306 + type="primary" 307 + title="Follow" 308 + onPress={() => { 309 + // Lógica real de seguir aqui 310 + console.log("Seguiu este perfil"); 311 + }} 312 + width={250} 313 + /> 314 + ) 315 + } 316 + { 317 + isLoggedIn && isMine && ( 318 + // Logado e é minha conta => mostrar editar e compartilhar 218 319 <View style={styles.profileActionButtons}> 219 - <ActionButton type={'secondary'} title="Edit Profile" onPress={() => { }} width={140} icon='create' /> 220 - <ActionButton type={'secondary'} title="" onPress={() => { }} width={70} icon='share-social' /> 320 + <ActionButton 321 + type="secondary" 322 + title="Edit Profile" 323 + onPress={() => { 324 + console.log("Editar perfil"); 325 + }} 326 + width={140} 327 + icon="create" 328 + /> 329 + <ActionButton 330 + type="secondary" 331 + title="" 332 + onPress={() => { 333 + console.log("Compartilhar perfil"); 334 + }} 335 + width={70} 336 + icon="share-social" 337 + /> 221 338 </View> 222 339 ) 223 340 } 224 341 </View> 342 + 343 + {/* Tabs (Ex: Videos e Fotos) */} 225 344 <View style={styles.profileContent}> 226 345 <View style={styles.profileTabs}> 227 346 <View style={styles.tabButton}> ··· 253 372 </ThemedText> 254 373 </View> 255 374 </View> 375 + 376 + {/* Grid de vídeos */} 256 377 <View style={styles.videoGrid}> 257 378 {paddedVideoData.map((item, index) => { 258 379 const key = item.uri ? item.uri : `fallback-${index}`; ··· 274 395 </ContentWrapper> 275 396 </SafeAreaView> 276 397 ); 277 - } 398 + }
+1 -1
app/(tabs)/SearchScreen.tsx
··· 7 7 import PlaceholderVideoDisplay from '@/components/Profile/PlaceholderVideoDisplay'; 8 8 import { Colors } from '@/constants/Colors'; 9 9 import { UserProps, PostProps } from '@/types/Interfaces'; 10 - import { fetchTrendingPosts } from '@/api/videoServices'; 10 + import { fetchTrendingPosts } from '@/api/feedServices'; 11 11 import { getProfile } from '@/api/profileServices'; 12 12 import { useRouter } from 'expo-router'; 13 13
+20
app/(tabs)/_layout.tsx
··· 15 15 const colorScheme = useColorScheme(); 16 16 17 17 const [userData, setUserData] = useState<UserProps | null>(null); 18 + const isLoggedIn = false; 19 + 18 20 19 21 useEffect(() => { 22 + if (isLoggedIn) { 20 23 const loadProfileData = async () => { 21 24 try { 22 25 const profileData = await getProfile(did); ··· 50 53 }; 51 54 52 55 loadProfileData(); 56 + } else { 57 + setUserData({ 58 + id: '', 59 + did: '', 60 + displayName: 'Login or Register', 61 + handle: 'null', 62 + description: '', 63 + avatar: 'https://static.sprk.so/branding/default-profile.png?d', 64 + banner: '', 65 + followersCount: 0, 66 + followsCount: 0, 67 + postsCount: 0, 68 + indexedAt: '', 69 + createdAt: '', 70 + labels: [], 71 + }); 72 + } 53 73 }, []); 54 74 55 75 return (
+1 -1
app/(tabs)/index.tsx
··· 10 10 import VideoTop from '@/components/Video/VideoTop'; 11 11 import ImageScreen from '@/components/Image/ImageScreen'; 12 12 import { PostProps } from '@/types/Interfaces'; 13 - import { fetchTrendingPosts } from '@/api/videoServices'; 13 + import { fetchTrendingPosts } from '@/api/feedServices'; 14 14 import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; 15 15 import { router } from 'expo-router'; 16 16
+5 -2
components/Profile/ProfileInfo.tsx
··· 72 72 <View style={styles.container}> 73 73 <View style={styles.nameContainer}> 74 74 <ThemedText type="defaultBold" style={styles.name}>{userData.displayName}</ThemedText> 75 - <ThemedText type="username" style={styles.handler}>@{userData.handle}</ThemedText> 75 + {userData.handle === 'null' ? null : 76 + <ThemedText type="username" style={styles.handler}>@{userData.handle}</ThemedText> 77 + } 76 78 </View> 77 - 79 + {userData.handle === 'null' ? null : 78 80 <View style={styles.statsContainer}> 79 81 <View style={styles.statItem}> 80 82 <ThemedText type="defaultBold" style={styles.statNumber}>{formatNumber(userData.followsCount ?? 0)}</ThemedText> ··· 89 91 <ThemedText type="default" style={styles.statLabel}>Likes</ThemedText> 90 92 </View> 91 93 </View> 94 + } 92 95 <View style={styles.bioContainer}> 93 96 <ThemedText type="default" style={styles.bio}>{userData.description}</ThemedText> 94 97 </View>
+1 -1
components/Video/VideoInfoOverlay.tsx
··· 5 5 import VideoBottom from './VideoBottom'; 6 6 import CommentsTray from './CommentsTray'; 7 7 import { VideoInfoOverlayProps } from '@/types/Interfaces'; 8 - import { fetchPostThread } from '@/api/videoServices'; 8 + import { fetchPostThread } from '@/api/feedServices'; 9 9 10 10 const VideoInfoOverlay: React.FC<VideoInfoOverlayProps> = ({ videoData }) => { 11 11 const [isCommentsVisible, setIsCommentsVisible] = useState(false);
+15 -11
components/Video/VideoTop.tsx
··· 181 181 const [customFeeds, setCustomFeeds] = useState<Array<{ 182 182 name: string; 183 183 enabled: boolean; 184 + selected: boolean; 184 185 origin: string; 185 186 weights: FeedWeights; 186 187 }>>([ 187 188 { 188 189 name: 'For You', 189 190 enabled: true, 191 + selected: true, 190 192 origin: 'spark', 191 193 weights: { 192 194 comedy: 50, ··· 198 200 { 199 201 name: 'Sports', 200 202 enabled: false, 203 + selected: false, 201 204 origin: 'spark', 202 205 weights: { 203 206 football: 50, ··· 209 212 { 210 213 name: 'Cute Cats', 211 214 enabled: false, 215 + selected: false, 212 216 origin: 'spark', 213 217 weights: { 214 218 kittens: 50, ··· 265 269 }; 266 270 267 271 const generateFeedTab = ( 268 - title: string 272 + title: string, 273 + selected: boolean, 274 + key: string, 269 275 ) => { 270 276 return ( 271 277 <TouchableOpacity> 272 - <ThemedText type='defaultBold' darkColor={Colors.dark.text} lightColor={Colors.dark.text} style={styles.text}>{title}</ThemedText> 278 + <ThemedText type='defaultBold' darkColor={Colors.dark.text} lightColor={Colors.dark.text} style={[styles.text, selected ? { color: Colors.light.background } : {}]}>{title}</ThemedText> 273 279 </TouchableOpacity> 274 280 ); 275 281 }; ··· 284 290 </TouchableOpacity> 285 291 </View> 286 292 <ScrollView style={styles.optionsContent}> 287 - 293 + <View style={styles.feedOptions}> 294 + <ThemedText type='subtitle' lightColor={Colors.dark.underlineColor} darkColor={Colors.light.underlineColor}>Content Types</ThemedText> 295 + {generateSwitch("#0085FF", "Videos", true, () => {}, "videos")} 296 + {generateSwitch("#0085FF", "Photos", true, () => {}, "photos")} 297 + </View> 288 298 <View style={styles.feedOptions}> 289 299 <ThemedText type='subtitle' lightColor={Colors.dark.underlineColor} darkColor={Colors.light.underlineColor}>Content Origin</ThemedText> 290 300 ··· 345 355 346 356 <ActionButton title='Add Custom Feed' onPress={handleAddCustomFeed} icon='add' width={'100%'}/> 347 357 </View> 348 - <View style={styles.feedOptions}> 349 - <ThemedText type='subtitle' lightColor={Colors.dark.underlineColor} darkColor={Colors.light.underlineColor}>Content Types</ThemedText> 350 - {generateSwitch("#0085FF", "Videos", true, () => {}, "videos")} 351 - {generateSwitch("#0085FF", "More Videos", true, () => {}, "moreVideos")} 352 - {generateSwitch("#0085FF", "Photos", true, () => {}, "photos")} 353 - {generateSwitch("#0085FF", "More Photos", true, () => {}, "morePhotos")} 354 - </View> 358 + 355 359 356 360 </ScrollView> 357 361 </SafeAreaView> ··· 359 363 { 360 364 customFeeds.map((feed, index) => ( 361 365 feed.enabled && 362 - generateFeedTab(feed.name) 366 + generateFeedTab(feed.name, feed.selected, `feedTab_${index}`) 363 367 )) 364 368 } 365 369
+1 -1
components/global/ActionButton.tsx
··· 52 52 color: Colors.dark.text, 53 53 }, 54 54 outlineText: { 55 - color: Colors[colorScheme ?? 'light'].tint, 55 + color: Colors[colorScheme ?? 'light'].text, 56 56 }, 57 57 }); 58 58
+135
components/global/BottomSlider.tsx
··· 1 + import React, { useEffect, useRef, useState } from 'react'; 2 + import { 3 + View, 4 + StyleSheet, 5 + useColorScheme, 6 + TouchableOpacity, 7 + Dimensions, 8 + SafeAreaView, 9 + ScrollView, 10 + Animated, 11 + StyleProp, 12 + ViewStyle, 13 + } from 'react-native'; 14 + import { ThemedText } from '../ThemedText'; 15 + import { Colors } from '@/constants/Colors'; 16 + import { Ionicons } from '@expo/vector-icons'; 17 + 18 + interface BottomSliderProps { 19 + visible: boolean; 20 + onClose?: () => void; 21 + title?: string; 22 + renderHeader?: () => React.ReactNode; 23 + animationDuration?: number; 24 + containerStyle?: StyleProp<ViewStyle>; 25 + headerStyle?: StyleProp<ViewStyle>; 26 + contentStyle?: StyleProp<ViewStyle>; 27 + children?: React.ReactNode; 28 + } 29 + 30 + const BottomSlider: React.FC<BottomSliderProps> = ({ 31 + visible, 32 + onClose, 33 + title = 'Content Settings', 34 + renderHeader, 35 + animationDuration = 150, 36 + containerStyle, 37 + headerStyle, 38 + contentStyle, 39 + children, 40 + }) => { 41 + const screenHeight = Dimensions.get('screen').height; 42 + const colorScheme: 'light' | 'dark' = useColorScheme() as 'light' | 'dark'; 43 + 44 + const translateY = useRef(new Animated.Value(screenHeight)).current; 45 + 46 + 47 + useEffect(() => { 48 + if (visible) { 49 + Animated.timing(translateY, { 50 + toValue: 0, 51 + duration: animationDuration, 52 + useNativeDriver: true, 53 + }).start(); 54 + } else { 55 + Animated.timing(translateY, { 56 + toValue: screenHeight, 57 + duration: animationDuration, 58 + useNativeDriver: true, 59 + }).start(({ finished }) => { 60 + if (finished && onClose) { 61 + onClose(); 62 + } 63 + }); 64 + } 65 + }, [visible, onClose, translateY, animationDuration, screenHeight]); 66 + 67 + const handleClose = () => { 68 + if (onClose) { 69 + onClose(); 70 + } 71 + }; 72 + 73 + const styles = StyleSheet.create({ 74 + wrapper: { 75 + position: 'absolute', 76 + width: '100%', 77 + height: '100%', 78 + zIndex: 999, 79 + }, 80 + sliderContainer: { 81 + backgroundColor: Colors[colorScheme ?? 'light'].background, 82 + width: '100%', 83 + height: screenHeight, 84 + transform: [{ translateY }], 85 + }, 86 + headerContainer: { 87 + width: '100%', 88 + flexDirection: 'row', 89 + justifyContent: 'space-between', 90 + alignItems: 'center', 91 + paddingHorizontal: 10, 92 + paddingTop: 10, 93 + }, 94 + closeButton: { 95 + padding: 5, 96 + borderRadius: 50, 97 + }, 98 + sliderContent: { 99 + width: '100%', 100 + height: '100%', 101 + padding: 10, 102 + marginBottom: 20, 103 + }, 104 + }); 105 + 106 + const defaultHeader = ( 107 + <View style={[styles.headerContainer, headerStyle]}> 108 + <ThemedText type="title">{title}</ThemedText> 109 + <TouchableOpacity style={styles.closeButton} onPress={handleClose}> 110 + <Ionicons 111 + name="close" 112 + size={24} 113 + color={Colors[colorScheme ?? 'light'].text} 114 + /> 115 + </TouchableOpacity> 116 + </View> 117 + ); 118 + 119 + const header = renderHeader ? renderHeader() : defaultHeader; 120 + 121 + return ( 122 + <View pointerEvents={visible ? 'auto' : 'none'} style={[styles.wrapper, containerStyle]}> 123 + <Animated.View style={styles.sliderContainer}> 124 + <SafeAreaView> 125 + {header} 126 + <ScrollView style={[styles.sliderContent, contentStyle]}> 127 + {children} 128 + </ScrollView> 129 + </SafeAreaView> 130 + </Animated.View> 131 + </View> 132 + ); 133 + }; 134 + 135 + export default BottomSlider;
+301
components/global/InputArea.tsx
··· 1 + import { Colors } from '@/constants/Colors'; 2 + import { Ionicons } from '@expo/vector-icons'; 3 + import React, { useState } from 'react'; 4 + import { 5 + View, 6 + TextInput, 7 + StyleSheet, 8 + Text, 9 + TouchableOpacity, 10 + useColorScheme, 11 + ViewStyle, 12 + TextStyle, 13 + KeyboardTypeOptions, 14 + Platform, 15 + Modal, 16 + Dimensions, 17 + SafeAreaView 18 + } from 'react-native'; 19 + import DateTimePicker from '@react-native-community/datetimepicker'; 20 + import { ThemedText } from '../ThemedText'; 21 + import BottomSlider from './BottomSlider'; 22 + import ActionButton from './ActionButton'; 23 + 24 + interface BaseInputAreaProps { 25 + placeholder: string; 26 + value: string; 27 + onChangeText: (text: string) => void; 28 + icon?: string; 29 + label?: string; 30 + error?: string; 31 + disabled?: boolean; 32 + style?: ViewStyle; 33 + inputStyle?: TextStyle; 34 + maxLength?: number; 35 + autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters'; 36 + } 37 + 38 + interface NonBirthDateProps extends BaseInputAreaProps { 39 + type?: 'text' | 'email' | 'password' | 'username'; 40 + format?: never; 41 + } 42 + 43 + interface BirthDateProps extends BaseInputAreaProps { 44 + type: 'birthDate'; 45 + format: 'ddmmyy' | 'mmddyy' | 'ddmmyyyy' | 'mmddyyyy'; 46 + } 47 + 48 + export type InputAreaProps = NonBirthDateProps | BirthDateProps; 49 + 50 + const InputArea: React.FC<InputAreaProps> = ({ 51 + placeholder, 52 + value, 53 + onChangeText, 54 + type = 'text', 55 + icon, 56 + label, 57 + error, 58 + disabled = false, 59 + style, 60 + inputStyle, 61 + maxLength, 62 + autoCapitalize = 'none', 63 + format, 64 + }) => { 65 + const colorScheme = useColorScheme(); 66 + const [secureTextEntry, setSecureTextEntry] = useState(type === 'password'); 67 + const [showDatePicker, setShowDatePicker] = useState(false); 68 + const [date, setDate] = useState(new Date()); 69 + 70 + const getKeyboardType = (): KeyboardTypeOptions => { 71 + switch (type) { 72 + case 'email': 73 + return 'email-address'; 74 + case 'username': 75 + case 'text': 76 + return 'default'; 77 + case 'birthDate': 78 + return 'numeric'; 79 + default: 80 + return 'default'; 81 + } 82 + }; 83 + 84 + const formatDate = (date: Date): string => { 85 + const day = date.getDate().toString().padStart(2, '0'); 86 + const month = (date.getMonth() + 1).toString().padStart(2, '0'); 87 + const fullYear = date.getFullYear().toString(); 88 + const year = fullYear.slice(2); 89 + 90 + switch (format) { 91 + case 'ddmmyy': 92 + return `${day}/${month}/${year}`; 93 + case 'mmddyy': 94 + return `${month}/${day}/${year}`; 95 + case 'ddmmyyyy': 96 + return `${day}/${month}/${fullYear}`; 97 + case 'mmddyyyy': 98 + return `${month}/${day}/${fullYear}`; 99 + default: 100 + return type === 'birthDate' ? `${day}/${month}/${fullYear}` : ''; 101 + } 102 + }; 103 + 104 + const handleDateChange = (event: any, selectedDate?: Date) => { 105 + const currentDate = selectedDate || date; 106 + 107 + if (Platform.OS === 'android') { 108 + setShowDatePicker(false); 109 + } 110 + 111 + setDate(currentDate); 112 + onChangeText(formatDate(currentDate)); 113 + }; 114 + 115 + const styles = StyleSheet.create({ 116 + container: { 117 + marginBottom: 16, 118 + }, 119 + labelText: { 120 + fontSize: 14, 121 + fontWeight: '600', 122 + marginBottom: 6, 123 + color: Colors[colorScheme ?? 'light'].text, 124 + }, 125 + inputContainer: { 126 + flexDirection: 'row', 127 + alignItems: 'center', 128 + borderWidth: 1, 129 + borderColor: error 130 + ? Colors[colorScheme ?? 'light'].error 131 + : Colors[colorScheme ?? 'light'].underlineColor, 132 + borderRadius: 8, 133 + paddingHorizontal: 12, 134 + backgroundColor: Colors[colorScheme ?? 'light'].background, 135 + height: 50, 136 + }, 137 + icon: { 138 + marginRight: 10, 139 + }, 140 + input: { 141 + flex: 1, 142 + color: Colors[colorScheme ?? 'light'].text, 143 + fontSize: 16, 144 + height: '100%', 145 + }, 146 + errorText: { 147 + fontSize: 12, 148 + color: Colors[colorScheme ?? 'light'].error, 149 + marginTop: 4, 150 + }, 151 + disabledInput: { 152 + opacity: 0.5, 153 + }, 154 + passwordIcon: { 155 + padding: 4, 156 + }, 157 + datePickerButton: { 158 + flex: 1, 159 + justifyContent: 'center', 160 + height: '40%', 161 + width: '90%', 162 + }, 163 + modalContent: { 164 + width: '100%', 165 + height: '100%', 166 + position: 'absolute', 167 + bottom: 0, 168 + zIndex: 1, 169 + }, 170 + }); 171 + 172 + const renderDatePicker = () => { 173 + if (Platform.OS === 'ios') { 174 + return ( 175 + showDatePicker && 176 + <SafeAreaView style={styles.modalContent}> 177 + <View style={{ 178 + position: 'absolute', 179 + bottom: -100, 180 + left: 0, 181 + width: '100%', 182 + height: Dimensions.get('screen').height / 3, 183 + backgroundColor: Colors[colorScheme ?? 'light'].background, 184 + justifyContent: 'center', 185 + alignItems: 'center', 186 + }}> 187 + <DateTimePicker 188 + value={date} 189 + mode="date" 190 + display="spinner" 191 + onChange={handleDateChange} 192 + maximumDate={new Date()} 193 + /> 194 + <ActionButton 195 + title="Done" 196 + onPress={() => setShowDatePicker(false)} 197 + width={'100%'} 198 + /> 199 + </View> 200 + </SafeAreaView> 201 + ); 202 + } 203 + 204 + return showDatePicker && ( 205 + <DateTimePicker 206 + value={date} 207 + mode="date" 208 + display="default" 209 + onChange={handleDateChange} 210 + maximumDate={new Date()} 211 + /> 212 + ); 213 + }; 214 + 215 + return ( 216 + <View style={[styles.container, style]}> 217 + {label && <ThemedText type='description'>{label}</ThemedText>} 218 + 219 + <View style={[styles.inputContainer, disabled && styles.disabledInput]}> 220 + {icon && ( 221 + <Ionicons 222 + name={icon as any} 223 + size={20} 224 + color={Colors[colorScheme ?? 'light'].icon} 225 + style={styles.icon} 226 + /> 227 + )} 228 + 229 + {type === 'birthDate' ? ( 230 + <TouchableOpacity 231 + style={styles.datePickerButton} 232 + onPress={() => !disabled && setShowDatePicker(true)} 233 + disabled={disabled} 234 + > 235 + <Text 236 + style={[ 237 + styles.input, 238 + inputStyle, 239 + !value && { color: Colors[colorScheme ?? 'light'].textGray } 240 + ]} 241 + > 242 + {value || placeholder} 243 + </Text> 244 + </TouchableOpacity> 245 + ) : ( 246 + <TextInput 247 + placeholder={placeholder} 248 + placeholderTextColor={Colors[colorScheme ?? 'light'].textGray} 249 + value={value} 250 + onChangeText={onChangeText} 251 + secureTextEntry={secureTextEntry && type === 'password'} 252 + keyboardType={getKeyboardType()} 253 + editable={!disabled} 254 + autoCapitalize={autoCapitalize} 255 + autoComplete={type === 'password' || (type as string) === 'birthDate' ? 'off' : 'name'} 256 + autoCorrect={false} 257 + maxLength={maxLength} 258 + style={[styles.input, inputStyle]} 259 + /> 260 + )} 261 + 262 + {type === 'password' && ( 263 + <TouchableOpacity 264 + onPress={() => setSecureTextEntry(!secureTextEntry)} 265 + style={styles.passwordIcon} 266 + > 267 + <Ionicons 268 + name={secureTextEntry ? 'eye-outline' : 'eye-off-outline'} 269 + size={20} 270 + color={Colors[colorScheme ?? 'light'].icon} 271 + /> 272 + </TouchableOpacity> 273 + )} 274 + 275 + {type === 'birthDate' && ( 276 + <TouchableOpacity 277 + onPress={() => !disabled && setShowDatePicker(true)} 278 + style={styles.passwordIcon} 279 + disabled={disabled} 280 + > 281 + <Ionicons 282 + name="calendar-outline" 283 + size={20} 284 + color={Colors[colorScheme ?? 'light'].icon} 285 + /> 286 + </TouchableOpacity> 287 + )} 288 + </View> 289 + 290 + {error && <Text style={styles.errorText}>{error}</Text>} 291 + {type === 'birthDate' && ( 292 + <View style={{ 293 + }}> 294 + {renderDatePicker()} 295 + </View> 296 + )} 297 + </View> 298 + ); 299 + }; 300 + 301 + export default InputArea;
+17
components/global/Logo.tsx
··· 1 + import { ThemedText } from '@/components/ThemedText'; 2 + import React from 'react'; 3 + import { View, StyleSheet, ViewStyle } from 'react-native'; 4 + import * as Svg from 'react-native-svg'; 5 + export default function Logo({ size, color, style }: { size: number; color?: string; style?: ViewStyle }) { 6 + 7 + return ( 8 + <Svg.Svg width={`${1437/size}`} height={`${1341/size}`} style={style} viewBox="0 0 1437 1341" fill="none"> 9 + <Svg.Path fillRule="evenodd" clipRule="evenodd" d="M756.32 171.886L840.203 51.8886C858.119 26.2589 893.398 20.0211 919.001 37.9561C944.604 55.891 950.835 91.2071 932.919 116.837L849.036 236.834L973.993 214.74C1004.77 209.299 1034.12 229.861 1039.55 260.666C1044.99 291.47 1024.45 320.853 993.676 326.294L868.719 348.388L988.591 432.359C1014.19 450.294 1020.42 485.611 1002.51 511.24C984.592 536.87 949.313 543.108 923.71 525.173L803.838 441.202L825.909 566.289C831.345 597.094 810.805 626.477 780.032 631.918C749.26 637.359 719.907 616.798 714.472 585.993L692.401 460.905L499.519 736.829C481.603 762.458 446.324 768.696 420.721 750.761C395.118 732.826 388.887 697.51 406.803 671.881L599.685 395.957L474.728 418.051C443.956 423.492 414.603 402.93 409.168 372.126C403.733 341.321 424.273 311.938 455.045 306.497L580.002 284.403L460.13 200.432C434.527 182.497 428.296 147.181 446.212 121.551C464.129 95.9214 499.408 89.6836 525.01 107.619L644.882 191.59L622.812 66.5017C617.376 35.6969 637.916 6.31383 668.689 0.872854C699.461 -4.56812 728.814 15.9933 734.249 46.7981L756.32 171.886Z" fill={color || "white"} /> 10 + <Svg.Path d="M147.126 1249.05C67.0321 1249.05 20.7011 1220.21 5.42172 1185.45C1.7251 1177.31 0 1168.93 0 1160.8C0 1135.65 18.976 1118.4 44.8524 1118.4C64.5677 1118.4 77.8755 1124.56 91.9227 1144.28C104.245 1161.78 124.946 1168.93 148.358 1168.93C179.656 1168.93 198.878 1156.11 198.878 1138.61C198.878 1121.85 185.078 1114.7 145.154 1107.55L109.42 1101.14C38.6913 1088.57 0 1052.82 0 997.602C0 926.113 61.6104 881 146.14 881C216.622 881 267.143 905.898 285.626 950.271C288.583 957.42 290.062 964.076 290.062 972.211C290.062 995.877 272.811 1011.9 246.688 1012.15C224.508 1012.15 210.461 1004.5 197.892 985.523C186.31 967.774 169.798 961.118 147.372 961.118C118.292 961.118 103.505 973.937 103.505 989.96C103.505 1005.98 118.785 1013.63 154.272 1020.04L190.006 1026.44C266.403 1040.25 302.384 1071.56 302.384 1129.24C302.384 1202.46 245.209 1249.05 147.126 1249.05Z" fill={color || "white"} /> 11 + <Svg.Path d="M386.174 1341C356.601 1341 335.653 1324.48 335.653 1288.74V1016.83C335.653 981.086 356.601 964.569 386.174 964.569C415.747 964.569 436.941 981.086 436.941 1017.08V1018.76C436.941 1019.33 437.4 1019.79 437.967 1019.79C438.388 1019.79 438.768 1019.53 438.925 1019.14C452.106 986.251 480.818 966.788 519.745 966.788C588.256 966.788 626.208 1016.83 626.208 1106.81C626.208 1196.54 588.502 1246.83 520.977 1246.83C481.121 1246.83 451.698 1227.9 438.964 1196.55C438.783 1196.1 438.349 1195.8 437.868 1195.8C437.22 1195.8 436.694 1196.33 436.694 1196.98V1288.74C436.694 1324.48 415.747 1341 386.174 1341ZM479.575 1168.93C506.191 1168.93 522.702 1144.77 522.702 1106.81C522.702 1069.09 505.944 1044.69 479.575 1044.69C453.206 1044.69 436.448 1069.09 436.201 1107.06C436.448 1145.27 452.959 1168.93 479.575 1168.93Z" fill={color || "white"} /> 12 + <Svg.Path d="M739.078 1246.83C689.543 1246.83 647.402 1212.81 647.402 1163.75C647.402 1111.49 687.572 1083.14 760.272 1078.71L806.634 1075.83C809.38 1075.66 810.753 1075.58 811.894 1075.25C815.808 1074.14 818.793 1070.96 819.659 1066.99C819.911 1065.83 819.911 1064.45 819.911 1061.7C819.911 1043.45 806.11 1033.59 787.134 1033.59C771.362 1033.59 763.476 1037.78 745.978 1053.81C734.149 1064.65 721.334 1070.08 706.301 1070.08C682.396 1070.08 665.392 1055.29 665.392 1034.33C665.392 1028.17 665.885 1024.23 667.363 1019.3C677.714 984.537 724.784 963.83 792.556 963.83C867.967 963.83 918.487 1001.55 918.487 1056.77V1198.27C918.487 1233.27 897.54 1249.05 868.46 1249.05C842.202 1249.05 823.62 1236.33 820.049 1207.59C819.966 1206.92 819.401 1206.4 818.727 1206.4C818.242 1206.4 817.796 1206.67 817.561 1207.1C803.07 1233.27 770.347 1246.83 739.078 1246.83ZM776.784 1179.29C799.21 1179.29 819.911 1166.71 819.911 1146.74C819.911 1141.25 819.911 1138.51 818.99 1136.4C817.773 1133.61 815.458 1131.45 812.594 1130.42C810.426 1129.65 807.69 1129.84 802.216 1130.22L777.03 1131.95C757.807 1133.43 746.964 1142.55 746.964 1155.87C746.964 1170.66 759.04 1179.29 776.784 1179.29Z" fill={color || "white"} /> 13 + <Svg.Path d="M1007.21 1249.05C976.155 1249.05 956.686 1231.3 956.686 1196.79V1015.11C956.686 982.318 976.401 964.569 1005.97 964.569C1034.31 964.569 1053.78 982.318 1053.78 1015.35V1024.15C1053.78 1024.73 1054.26 1025.21 1054.85 1025.21C1055.37 1025.21 1055.82 1024.82 1055.91 1024.31C1062.44 985.721 1080.36 966.788 1107.26 966.788C1117.37 966.788 1125.74 969.253 1131.91 973.937C1142.01 981.332 1147.18 994.398 1147.18 1013.13C1147.18 1029.16 1142.5 1041.24 1133.14 1049.62C1123.77 1058 1109.73 1062.44 1091.24 1063.18C1064.13 1064.16 1057.73 1073.04 1057.73 1102.86V1196.79C1057.73 1231.3 1038.26 1249.05 1007.21 1249.05Z" fill={color || "white"} /> 14 + <Svg.Path d="M1220.38 1249.05C1190.81 1249.05 1169.86 1232.53 1169.86 1196.79V933.262C1169.86 897.517 1190.81 881 1220.38 881C1249.95 881 1270.9 897.517 1270.9 933.508V1065.25C1270.9 1066.01 1271.52 1066.63 1272.28 1066.63C1272.66 1066.63 1273.02 1066.47 1273.28 1066.2L1349.27 986.016C1363.56 970.979 1375.39 964.569 1391.16 964.569C1417.78 964.569 1437 983.304 1437 1009.19C1437 1025.21 1432.07 1034.83 1416.79 1049.12L1383.69 1079.99C1376.92 1086.31 1373.54 1089.46 1373.13 1093.61C1372.72 1097.76 1375.43 1101.51 1380.84 1109.03L1420.24 1163.75C1431.82 1180.02 1435.28 1188.9 1435.28 1201.96C1435.28 1230.31 1415.56 1249.05 1386.23 1249.05C1362.33 1249.05 1349.51 1240.17 1332.02 1212.07L1303.85 1166.46C1299.92 1160.1 1297.95 1156.92 1295.25 1155.52C1293.02 1154.37 1290.47 1154.01 1288.02 1154.5C1285.03 1155.1 1282.26 1157.61 1276.73 1162.64C1274.55 1164.63 1273.46 1165.62 1272.68 1166.81C1272.04 1167.8 1271.56 1168.89 1271.26 1170.03C1270.9 1171.4 1270.9 1172.88 1270.9 1175.83V1196.79C1270.9 1232.53 1249.95 1249.05 1220.38 1249.05Z" fill={color || "white"} /> 15 + </Svg.Svg> 16 + ); 17 + }
+2
constants/Colors.ts
··· 16 16 heartColor: '#E60A41', 17 17 followColor: '#75D400', 18 18 commentColor: '#0D7EE1', 19 + error: '#FF0000', 19 20 }, 20 21 dark: { 21 22 tint: tintColorDark, ··· 31 32 heartColor: '#FF1E1E', 32 33 followColor: '#3AE866', 33 34 commentColor: '#13CCFF', 35 + error: '#FF0000', 34 36 }, 35 37 };
+169 -1
package-lock.json
··· 10 10 "dependencies": { 11 11 "@expo/vector-icons": "^14.0.2", 12 12 "@gorhom/bottom-sheet": "^5.1.1", 13 + "@react-native-community/datetimepicker": "8.2.0", 13 14 "@react-native-community/slider": "4.5.5", 14 15 "@react-navigation/bottom-tabs": "^7.2.0", 15 16 "@react-navigation/drawer": "^7.1.1", ··· 39 40 "react-native-reanimated": "~3.16.1", 40 41 "react-native-safe-area-context": "4.12.0", 41 42 "react-native-screens": "~4.4.0", 43 + "react-native-svg": "15.8.0", 42 44 "react-native-video": "^6.10.0", 43 45 "react-native-videoeditorsdk": "^3.3.0", 44 46 "react-native-web": "~0.19.13", ··· 3355 3357 "react": "^16.8 || ^17.0 || ^18.0" 3356 3358 } 3357 3359 }, 3360 + "node_modules/@react-native-community/datetimepicker": { 3361 + "version": "8.2.0", 3362 + "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.2.0.tgz", 3363 + "integrity": "sha512-qrUPhiBvKGuG9Y+vOqsc56RPFcHa1SU2qbAMT0hfGkoFIj3FodE0VuPVrEa8fgy7kcD5NQmkZIKgHOBLV0+hWg==", 3364 + "license": "MIT", 3365 + "dependencies": { 3366 + "invariant": "^2.2.4" 3367 + }, 3368 + "peerDependencies": { 3369 + "expo": ">=50.0.0", 3370 + "react": "*", 3371 + "react-native": "*", 3372 + "react-native-windows": "*" 3373 + }, 3374 + "peerDependenciesMeta": { 3375 + "expo": { 3376 + "optional": true 3377 + }, 3378 + "react-native-windows": { 3379 + "optional": true 3380 + } 3381 + } 3382 + }, 3358 3383 "node_modules/@react-native-community/slider": { 3359 3384 "version": "4.5.5", 3360 3385 "resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-4.5.5.tgz", ··· 4938 4963 "node": ">=0.6" 4939 4964 } 4940 4965 }, 4966 + "node_modules/boolbase": { 4967 + "version": "1.0.0", 4968 + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 4969 + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", 4970 + "license": "ISC" 4971 + }, 4941 4972 "node_modules/bplist-creator": { 4942 4973 "version": "0.0.7", 4943 4974 "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", ··· 5718 5749 "hyphenate-style-name": "^1.0.3" 5719 5750 } 5720 5751 }, 5752 + "node_modules/css-select": { 5753 + "version": "5.1.0", 5754 + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 5755 + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 5756 + "license": "BSD-2-Clause", 5757 + "dependencies": { 5758 + "boolbase": "^1.0.0", 5759 + "css-what": "^6.1.0", 5760 + "domhandler": "^5.0.2", 5761 + "domutils": "^3.0.1", 5762 + "nth-check": "^2.0.1" 5763 + }, 5764 + "funding": { 5765 + "url": "https://github.com/sponsors/fb55" 5766 + } 5767 + }, 5768 + "node_modules/css-tree": { 5769 + "version": "1.1.3", 5770 + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", 5771 + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", 5772 + "license": "MIT", 5773 + "dependencies": { 5774 + "mdn-data": "2.0.14", 5775 + "source-map": "^0.6.1" 5776 + }, 5777 + "engines": { 5778 + "node": ">=8.0.0" 5779 + } 5780 + }, 5781 + "node_modules/css-tree/node_modules/source-map": { 5782 + "version": "0.6.1", 5783 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 5784 + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 5785 + "license": "BSD-3-Clause", 5786 + "engines": { 5787 + "node": ">=0.10.0" 5788 + } 5789 + }, 5790 + "node_modules/css-what": { 5791 + "version": "6.1.0", 5792 + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 5793 + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 5794 + "license": "BSD-2-Clause", 5795 + "engines": { 5796 + "node": ">= 6" 5797 + }, 5798 + "funding": { 5799 + "url": "https://github.com/sponsors/fb55" 5800 + } 5801 + }, 5721 5802 "node_modules/cssom": { 5722 5803 "version": "0.5.0", 5723 5804 "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", ··· 5977 6058 "node": ">=8" 5978 6059 } 5979 6060 }, 6061 + "node_modules/dom-serializer": { 6062 + "version": "2.0.0", 6063 + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 6064 + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 6065 + "license": "MIT", 6066 + "dependencies": { 6067 + "domelementtype": "^2.3.0", 6068 + "domhandler": "^5.0.2", 6069 + "entities": "^4.2.0" 6070 + }, 6071 + "funding": { 6072 + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 6073 + } 6074 + }, 6075 + "node_modules/domelementtype": { 6076 + "version": "2.3.0", 6077 + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 6078 + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 6079 + "funding": [ 6080 + { 6081 + "type": "github", 6082 + "url": "https://github.com/sponsors/fb55" 6083 + } 6084 + ], 6085 + "license": "BSD-2-Clause" 6086 + }, 5980 6087 "node_modules/domexception": { 5981 6088 "version": "4.0.0", 5982 6089 "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", ··· 5988 6095 }, 5989 6096 "engines": { 5990 6097 "node": ">=12" 6098 + } 6099 + }, 6100 + "node_modules/domhandler": { 6101 + "version": "5.0.3", 6102 + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 6103 + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 6104 + "license": "BSD-2-Clause", 6105 + "dependencies": { 6106 + "domelementtype": "^2.3.0" 6107 + }, 6108 + "engines": { 6109 + "node": ">= 4" 6110 + }, 6111 + "funding": { 6112 + "url": "https://github.com/fb55/domhandler?sponsor=1" 6113 + } 6114 + }, 6115 + "node_modules/domutils": { 6116 + "version": "3.2.2", 6117 + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", 6118 + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", 6119 + "license": "BSD-2-Clause", 6120 + "dependencies": { 6121 + "dom-serializer": "^2.0.0", 6122 + "domelementtype": "^2.3.0", 6123 + "domhandler": "^5.0.3" 6124 + }, 6125 + "funding": { 6126 + "url": "https://github.com/fb55/domutils?sponsor=1" 5991 6127 } 5992 6128 }, 5993 6129 "node_modules/dotenv": { ··· 6094 6230 "version": "4.5.0", 6095 6231 "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 6096 6232 "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 6097 - "dev": true, 6098 6233 "engines": { 6099 6234 "node": ">=0.12" 6100 6235 }, ··· 9622 9757 "node": ">=0.10" 9623 9758 } 9624 9759 }, 9760 + "node_modules/mdn-data": { 9761 + "version": "2.0.14", 9762 + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", 9763 + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", 9764 + "license": "CC0-1.0" 9765 + }, 9625 9766 "node_modules/memoize-one": { 9626 9767 "version": "5.2.1", 9627 9768 "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", ··· 10405 10546 "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", 10406 10547 "engines": { 10407 10548 "node": ">=4" 10549 + } 10550 + }, 10551 + "node_modules/nth-check": { 10552 + "version": "2.1.1", 10553 + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 10554 + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 10555 + "license": "BSD-2-Clause", 10556 + "dependencies": { 10557 + "boolbase": "^1.0.0" 10558 + }, 10559 + "funding": { 10560 + "url": "https://github.com/fb55/nth-check?sponsor=1" 10408 10561 } 10409 10562 }, 10410 10563 "node_modules/nullthrows": { ··· 11429 11582 "dependencies": { 11430 11583 "react-freeze": "^1.0.0", 11431 11584 "warn-once": "^0.1.0" 11585 + }, 11586 + "peerDependencies": { 11587 + "react": "*", 11588 + "react-native": "*" 11589 + } 11590 + }, 11591 + "node_modules/react-native-svg": { 11592 + "version": "15.8.0", 11593 + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.8.0.tgz", 11594 + "integrity": "sha512-KHJzKpgOjwj1qeZzsBjxNdoIgv2zNCO9fVcoq2TEhTRsVV5DGTZ9JzUZwybd7q4giT/H3RdtqC3u44dWdO0Ffw==", 11595 + "license": "MIT", 11596 + "dependencies": { 11597 + "css-select": "^5.1.0", 11598 + "css-tree": "^1.1.3", 11599 + "warn-once": "0.1.1" 11432 11600 }, 11433 11601 "peerDependencies": { 11434 11602 "react": "*",
+3 -1
package.json
··· 49 49 "react-native-videoeditorsdk": "^3.3.0", 50 50 "react-native-web": "~0.19.13", 51 51 "react-native-webview": "13.12.5", 52 - "@react-native-community/slider": "4.5.5" 52 + "@react-native-community/slider": "4.5.5", 53 + "react-native-svg": "15.8.0", 54 + "@react-native-community/datetimepicker": "8.2.0" 53 55 }, 54 56 "devDependencies": { 55 57 "@babel/core": "^7.25.2",