From 5e81a14ebb936df6ec80ecdedae8ee49c9b61927 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 3 May 2025 16:07:38 +0530 Subject: [PATCH] Remove search components: EmptyResults, SearchBar, RecentSearches, ResultsCarousel, SkeletonLoader, and SearchResultItem. Refactor SearchScreen to integrate their functionality directly, enhancing code organization and reducing component complexity. --- src/components/search/EmptyResults.tsx | 54 --- src/components/search/README.md | 34 -- src/components/search/RecentSearches.tsx | 75 ---- src/components/search/ResultsCarousel.tsx | 62 ---- src/components/search/SearchBar.tsx | 84 ----- src/components/search/SearchResultItem.tsx | 75 ---- src/components/search/SkeletonLoader.tsx | 108 ------ src/components/search/index.ts | 6 - src/screens/SearchScreen.tsx | 396 ++++++++++++++++++--- 9 files changed, 354 insertions(+), 540 deletions(-) delete mode 100644 src/components/search/EmptyResults.tsx delete mode 100644 src/components/search/README.md delete mode 100644 src/components/search/RecentSearches.tsx delete mode 100644 src/components/search/ResultsCarousel.tsx delete mode 100644 src/components/search/SearchBar.tsx delete mode 100644 src/components/search/SearchResultItem.tsx delete mode 100644 src/components/search/SkeletonLoader.tsx delete mode 100644 src/components/search/index.ts diff --git a/src/components/search/EmptyResults.tsx b/src/components/search/EmptyResults.tsx deleted file mode 100644 index 81524e8..0000000 --- a/src/components/search/EmptyResults.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { View, Text, StyleSheet } from 'react-native'; -import { MaterialIcons } from '@expo/vector-icons'; -import { colors } from '../../styles'; - -interface EmptyResultsProps { - isDarkMode?: boolean; -} - -const EmptyResults: React.FC = ({ isDarkMode = true }) => { - return ( - - - - No results found - - - Try different keywords or check your spelling - - - ); -}; - -const styles = StyleSheet.create({ - emptyContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - paddingHorizontal: 32, - }, - emptyText: { - fontSize: 18, - fontWeight: 'bold', - marginTop: 16, - marginBottom: 8, - }, - emptySubtext: { - fontSize: 14, - textAlign: 'center', - lineHeight: 20, - }, -}); - -export default EmptyResults; \ No newline at end of file diff --git a/src/components/search/README.md b/src/components/search/README.md deleted file mode 100644 index e941eee..0000000 --- a/src/components/search/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Search Components - -This directory contains modular components used in the SearchScreen. - -## Components - -- **SearchBar**: Input field with search icon and clear button -- **SkeletonLoader**: Loading animation shown while searching -- **RecentSearches**: Shows recent search history -- **ResultsCarousel**: Horizontal scrolling list of search results by category -- **SearchResultItem**: Individual content card in the search results -- **EmptyResults**: Displayed when no search results are found - -## Usage - -```jsx -import { - SearchBar, - SkeletonLoader, - RecentSearches, - ResultsCarousel, - EmptyResults -} from '../components/search'; - -// Use components in your screen... -``` - -## Refactoring Benefits - -- Improved code organization -- Smaller, reusable components -- Better separation of concerns -- Easier maintenance and testing -- Reduced file size of main screen component \ No newline at end of file diff --git a/src/components/search/RecentSearches.tsx b/src/components/search/RecentSearches.tsx deleted file mode 100644 index 7308b40..0000000 --- a/src/components/search/RecentSearches.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; -import { MaterialIcons } from '@expo/vector-icons'; -import { colors } from '../../styles'; - -interface RecentSearchesProps { - searches: string[]; - onSearchSelect: (search: string) => void; - isDarkMode?: boolean; -} - -const RecentSearches: React.FC = ({ - searches, - onSearchSelect, - isDarkMode = true, -}) => { - if (searches.length === 0) return null; - - return ( - - - Recent Searches - - {searches.map((search, index) => ( - onSearchSelect(search)} - > - - - {search} - - - ))} - - ); -}; - -const styles = StyleSheet.create({ - recentSearchesContainer: { - paddingHorizontal: 0, - paddingBottom: 16, - }, - carouselTitle: { - fontSize: 18, - fontWeight: '700', - color: colors.white, - marginBottom: 12, - paddingHorizontal: 16, - }, - recentSearchItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 10, - paddingHorizontal: 16, - }, - recentSearchIcon: { - marginRight: 12, - }, - recentSearchText: { - fontSize: 16, - flex: 1, - }, -}); - -export default RecentSearches; \ No newline at end of file diff --git a/src/components/search/ResultsCarousel.tsx b/src/components/search/ResultsCarousel.tsx deleted file mode 100644 index d24d670..0000000 --- a/src/components/search/ResultsCarousel.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { View, Text, StyleSheet, FlatList } from 'react-native'; -import { colors } from '../../styles'; -import { StreamingContent } from '../../services/catalogService'; -import SearchResultItem from './SearchResultItem'; - -interface ResultsCarouselProps { - title: string; - items: StreamingContent[]; - onItemPress: (item: StreamingContent) => void; - isDarkMode?: boolean; -} - -const ResultsCarousel: React.FC = ({ - title, - items, - onItemPress, - isDarkMode = true, -}) => { - if (items.length === 0) return null; - - return ( - - - {title} ({items.length}) - - ( - - )} - keyExtractor={item => `${item.type}-${item.id}`} - horizontal - showsHorizontalScrollIndicator={false} - contentContainerStyle={styles.horizontalListContent} - /> - - ); -}; - -const styles = StyleSheet.create({ - carouselContainer: { - marginBottom: 24, - }, - carouselTitle: { - fontSize: 18, - fontWeight: '700', - color: colors.white, - marginBottom: 12, - paddingHorizontal: 16, - }, - horizontalListContent: { - paddingHorizontal: 16, - paddingRight: 8, - }, -}); - -export default ResultsCarousel; \ No newline at end of file diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx deleted file mode 100644 index 889e537..0000000 --- a/src/components/search/SearchBar.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import { View, TextInput, TouchableOpacity, StyleSheet } from 'react-native'; -import { MaterialIcons } from '@expo/vector-icons'; -import { colors } from '../../styles'; - -interface SearchBarProps { - query: string; - onChangeQuery: (text: string) => void; - onClear: () => void; - autoFocus?: boolean; -} - -const SearchBar: React.FC = ({ - query, - onChangeQuery, - onClear, - autoFocus = true -}) => { - return ( - - - - {query.length > 0 && ( - - - - )} - - ); -}; - -const styles = StyleSheet.create({ - searchBar: { - flexDirection: 'row', - alignItems: 'center', - borderRadius: 24, - paddingHorizontal: 16, - height: 48, - }, - searchIcon: { - marginRight: 12, - }, - searchInput: { - flex: 1, - fontSize: 16, - height: '100%', - }, - clearButton: { - padding: 4, - }, -}); - -export default SearchBar; \ No newline at end of file diff --git a/src/components/search/SearchResultItem.tsx b/src/components/search/SearchResultItem.tsx deleted file mode 100644 index 33dc73a..0000000 --- a/src/components/search/SearchResultItem.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import { View, Text, StyleSheet, TouchableOpacity, Dimensions } from 'react-native'; -import { Image } from 'expo-image'; -import { colors } from '../../styles'; -import { StreamingContent } from '../../services/catalogService'; - -const { width } = Dimensions.get('window'); -const HORIZONTAL_ITEM_WIDTH = width * 0.3; -const HORIZONTAL_POSTER_HEIGHT = HORIZONTAL_ITEM_WIDTH * 1.5; - -const PLACEHOLDER_POSTER = 'https://placehold.co/300x450/222222/CCCCCC?text=No+Poster'; - -interface SearchResultItemProps { - item: StreamingContent; - onPress: (item: StreamingContent) => void; - isDarkMode?: boolean; -} - -const SearchResultItem: React.FC = ({ - item, - onPress, - isDarkMode = true -}) => { - return ( - onPress(item)} - > - - - - - {item.name} - - - ); -}; - -const styles = StyleSheet.create({ - horizontalItem: { - width: HORIZONTAL_ITEM_WIDTH, - marginRight: 12, - }, - horizontalItemPosterContainer: { - width: HORIZONTAL_ITEM_WIDTH, - height: HORIZONTAL_POSTER_HEIGHT, - borderRadius: 8, - overflow: 'hidden', - backgroundColor: colors.darkBackground, - marginBottom: 8, - }, - horizontalItemPoster: { - width: '100%', - height: '100%', - }, - horizontalItemTitle: { - fontSize: 14, - fontWeight: '500', - lineHeight: 18, - textAlign: 'left', - }, -}); - -export default SearchResultItem; \ No newline at end of file diff --git a/src/components/search/SkeletonLoader.tsx b/src/components/search/SkeletonLoader.tsx deleted file mode 100644 index 0608d32..0000000 --- a/src/components/search/SkeletonLoader.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import { View, StyleSheet, Animated } from 'react-native'; -import { colors } from '../../styles'; - -const POSTER_WIDTH = 90; -const POSTER_HEIGHT = 135; - -const SkeletonLoader: React.FC = () => { - const pulseAnim = React.useRef(new Animated.Value(0)).current; - - React.useEffect(() => { - const pulse = Animated.loop( - Animated.sequence([ - Animated.timing(pulseAnim, { - toValue: 1, - duration: 1000, - useNativeDriver: true, - }), - Animated.timing(pulseAnim, { - toValue: 0, - duration: 1000, - useNativeDriver: true, - }), - ]) - ); - pulse.start(); - return () => pulse.stop(); - }, [pulseAnim]); - - const opacity = pulseAnim.interpolate({ - inputRange: [0, 1], - outputRange: [0.3, 0.7], - }); - - const renderSkeletonItem = () => ( - - - - - - - - - - - ); - - return ( - - {[...Array(5)].map((_, index) => ( - - {index === 0 && ( - - )} - {renderSkeletonItem()} - - ))} - - ); -}; - -const styles = StyleSheet.create({ - skeletonContainer: { - padding: 16, - }, - skeletonVerticalItem: { - flexDirection: 'row', - marginBottom: 16, - }, - skeletonPoster: { - width: POSTER_WIDTH, - height: POSTER_HEIGHT, - borderRadius: 8, - backgroundColor: colors.darkBackground, - }, - skeletonItemDetails: { - flex: 1, - marginLeft: 16, - justifyContent: 'center', - }, - skeletonMetaRow: { - flexDirection: 'row', - gap: 8, - marginTop: 8, - }, - skeletonTitle: { - height: 20, - width: '80%', - marginBottom: 8, - backgroundColor: colors.darkBackground, - borderRadius: 4, - }, - skeletonMeta: { - height: 14, - width: '30%', - backgroundColor: colors.darkBackground, - borderRadius: 4, - }, - skeletonSectionHeader: { - height: 24, - width: '40%', - backgroundColor: colors.darkBackground, - marginBottom: 16, - borderRadius: 4, - }, -}); - -export default SkeletonLoader; \ No newline at end of file diff --git a/src/components/search/index.ts b/src/components/search/index.ts deleted file mode 100644 index 0bd40d4..0000000 --- a/src/components/search/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { default as SearchBar } from './SearchBar'; -export { default as SkeletonLoader } from './SkeletonLoader'; -export { default as RecentSearches } from './RecentSearches'; -export { default as SearchResultItem } from './SearchResultItem'; -export { default as ResultsCarousel } from './ResultsCarousel'; -export { default as EmptyResults } from './EmptyResults'; \ No newline at end of file diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx index 5a4c375..63459f8 100644 --- a/src/screens/SearchScreen.tsx +++ b/src/screens/SearchScreen.tsx @@ -3,32 +3,95 @@ import { View, Text, StyleSheet, - Keyboard, + TextInput, + FlatList, + TouchableOpacity, + ActivityIndicator, + useColorScheme, SafeAreaView, StatusBar, - ScrollView, + Keyboard, Dimensions, + ScrollView, + Animated as RNAnimated, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import debounce from 'lodash/debounce'; +import { MaterialIcons } from '@expo/vector-icons'; import { colors } from '../styles'; import { catalogService, StreamingContent } from '../services/catalogService'; +import { Image } from 'expo-image'; +import debounce from 'lodash/debounce'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import Animated, { FadeIn, FadeOut, SlideInRight } from 'react-native-reanimated'; import { RootStackParamList } from '../navigation/AppNavigator'; import { logger } from '../utils/logger'; -import { - SearchBar, - SkeletonLoader, - RecentSearches, - ResultsCarousel, - EmptyResults -} from '../components/search'; +const { width } = Dimensions.get('window'); +const HORIZONTAL_ITEM_WIDTH = width * 0.3; +const HORIZONTAL_POSTER_HEIGHT = HORIZONTAL_ITEM_WIDTH * 1.5; +const POSTER_WIDTH = 90; +const POSTER_HEIGHT = 135; const RECENT_SEARCHES_KEY = 'recent_searches'; const MAX_RECENT_SEARCHES = 10; -const SearchScreen: React.FC = () => { +const PLACEHOLDER_POSTER = 'https://placehold.co/300x450/222222/CCCCCC?text=No+Poster'; + +const SkeletonLoader = () => { + const pulseAnim = React.useRef(new RNAnimated.Value(0)).current; + + React.useEffect(() => { + const pulse = RNAnimated.loop( + RNAnimated.sequence([ + RNAnimated.timing(pulseAnim, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + }), + RNAnimated.timing(pulseAnim, { + toValue: 0, + duration: 1000, + useNativeDriver: true, + }), + ]) + ); + pulse.start(); + return () => pulse.stop(); + }, [pulseAnim]); + + const opacity = pulseAnim.interpolate({ + inputRange: [0, 1], + outputRange: [0.3, 0.7], + }); + + const renderSkeletonItem = () => ( + + + + + + + + + + + ); + + return ( + + {[...Array(5)].map((_, index) => ( + + {index === 0 && ( + + )} + {renderSkeletonItem()} + + ))} + + ); +}; + +const SearchScreen = () => { const navigation = useNavigation>(); const isDarkMode = true; const [query, setQuery] = useState(''); @@ -117,13 +180,65 @@ const SearchScreen: React.FC = () => { loadRecentSearches(); }; - const handleRecentSearchSelect = (search: string) => { - setQuery(search); - Keyboard.dismiss(); + const renderRecentSearches = () => { + if (!showRecent || recentSearches.length === 0) return null; + + return ( + + + Recent Searches + + {recentSearches.map((search, index) => ( + { + setQuery(search); + Keyboard.dismiss(); + }} + > + + + {search} + + + ))} + + ); }; - const handleItemPress = (item: StreamingContent) => { - navigation.navigate('Metadata', { id: item.id, type: item.type }); + const renderHorizontalItem = ({ item }: { item: StreamingContent }) => { + return ( + { + navigation.navigate('Metadata', { id: item.id, type: item.type }); + }} + > + + + + + {item.name} + + + ); }; const movieResults = useMemo(() => { @@ -150,17 +265,70 @@ const SearchScreen: React.FC = () => { Search - + + + + {query.length > 0 && ( + + + + )} + {searching ? ( ) : searched && !hasResultsToShow ? ( - + + + + No results found + + + Try different keywords or check your spelling + + ) : ( { keyboardShouldPersistTaps="handled" onScrollBeginDrag={Keyboard.dismiss} > - {showRecent && ( - - )} + {!query.trim() && renderRecentSearches()} {movieResults.length > 0 && ( - + + Movies ({movieResults.length}) + `movie-${item.id}`} + horizontal + showsHorizontalScrollIndicator={false} + contentContainerStyle={styles.horizontalListContent} + /> + )} {seriesResults.length > 0 && ( - + + TV Shows ({seriesResults.length}) + `series-${item.id}`} + horizontal + showsHorizontalScrollIndicator={false} + contentContainerStyle={styles.horizontalListContent} + /> + )} @@ -217,12 +389,152 @@ const styles = StyleSheet.create({ color: colors.white, letterSpacing: 0.5, }, + searchBar: { + flexDirection: 'row', + alignItems: 'center', + borderRadius: 24, + paddingHorizontal: 16, + height: 48, + }, + searchIcon: { + marginRight: 12, + }, + searchInput: { + flex: 1, + fontSize: 16, + height: '100%', + }, + clearButton: { + padding: 4, + }, scrollView: { flex: 1, }, scrollViewContent: { paddingBottom: 20, }, + carouselContainer: { + marginBottom: 24, + }, + carouselTitle: { + fontSize: 18, + fontWeight: '700', + color: colors.white, + marginBottom: 12, + paddingHorizontal: 16, + }, + horizontalListContent: { + paddingHorizontal: 16, + paddingRight: 8, + }, + horizontalItem: { + width: HORIZONTAL_ITEM_WIDTH, + marginRight: 12, + }, + horizontalItemPosterContainer: { + width: HORIZONTAL_ITEM_WIDTH, + height: HORIZONTAL_POSTER_HEIGHT, + borderRadius: 8, + overflow: 'hidden', + backgroundColor: colors.darkBackground, + marginBottom: 8, + }, + horizontalItemPoster: { + width: '100%', + height: '100%', + }, + horizontalItemTitle: { + fontSize: 14, + fontWeight: '500', + lineHeight: 18, + textAlign: 'left', + }, + recentSearchesContainer: { + paddingHorizontal: 0, + paddingBottom: 16, + }, + recentSearchItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 10, + paddingHorizontal: 16, + }, + recentSearchIcon: { + marginRight: 12, + }, + recentSearchText: { + fontSize: 16, + flex: 1, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + marginTop: 16, + fontSize: 16, + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 32, + }, + emptyText: { + fontSize: 18, + fontWeight: 'bold', + marginTop: 16, + marginBottom: 8, + }, + emptySubtext: { + fontSize: 14, + textAlign: 'center', + lineHeight: 20, + }, + skeletonContainer: { + padding: 16, + }, + skeletonVerticalItem: { + flexDirection: 'row', + marginBottom: 16, + }, + skeletonPoster: { + width: POSTER_WIDTH, + height: POSTER_HEIGHT, + borderRadius: 8, + backgroundColor: colors.darkBackground, + }, + skeletonItemDetails: { + flex: 1, + marginLeft: 16, + justifyContent: 'center', + }, + skeletonMetaRow: { + flexDirection: 'row', + gap: 8, + marginTop: 8, + }, + skeletonTitle: { + height: 20, + width: '80%', + marginBottom: 8, + backgroundColor: colors.darkBackground, + borderRadius: 4, + }, + skeletonMeta: { + height: 14, + width: '30%', + backgroundColor: colors.darkBackground, + borderRadius: 4, + }, + skeletonSectionHeader: { + height: 24, + width: '40%', + backgroundColor: colors.darkBackground, + marginBottom: 16, + borderRadius: 4, + }, }); export default SearchScreen; \ No newline at end of file