From c06877c15368384596c9e253a688a1abfdc9446d Mon Sep 17 00:00:00 2001 From: Kyle Ramachandran <156966341+kylezryr@users.noreply.github.com> Date: Wed, 24 Apr 2024 01:57:11 -0700 Subject: [PATCH] [genre] Implement subgenre dropdowns for search (#89) * dropdowns implemented * finished subgenre dropdowns * Add scroll view to dropdowns * Ready for styling * Run prettier * Style clear filter button * App is done * Add color when filter selected * Run prettier --------- Co-authored-by: Kyle Ramachandran Co-authored-by: Aditya Pawar --- src/app/(tabs)/genre/index.tsx | 63 +---- src/app/(tabs)/search/index.tsx | 236 +++++++++++++++++- src/app/(tabs)/search/styles.ts | 47 +++- src/app/(tabs)/story/index.tsx | 9 +- .../FilterDropdown/FilterDropdown.tsx | 107 ++++++++ src/components/FilterDropdown/styles.ts | 38 +++ src/components/GenreCard/GenreCard.tsx | 5 +- src/components/GenreCard/styles.ts | 1 - 8 files changed, 436 insertions(+), 70 deletions(-) create mode 100644 src/components/FilterDropdown/FilterDropdown.tsx create mode 100644 src/components/FilterDropdown/styles.ts diff --git a/src/app/(tabs)/genre/index.tsx b/src/app/(tabs)/genre/index.tsx index b5d1df56..924cbabf 100644 --- a/src/app/(tabs)/genre/index.tsx +++ b/src/app/(tabs)/genre/index.tsx @@ -8,8 +8,6 @@ import { FlatList, } from 'react-native'; -import { Dropdown, MultiSelect } from 'react-native-element-dropdown'; -import { Icon } from 'react-native-elements'; import { TouchableOpacity } from 'react-native-gesture-handler'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -21,6 +19,7 @@ import { fetchGenreStoryById } from '../../../queries/genres'; import { fetchStoryPreviewByIds } from '../../../queries/stories'; import { StoryPreview, GenreStories } from '../../../queries/types'; import globalStyles from '../../../styles/globalStyles'; +import { FilterDropdown } from '../../../components/FilterDropdown/FilterDropdown'; function GenreScreen() { const [genreStoryData, setGenreStoryData] = useState(); @@ -217,42 +216,6 @@ function GenreScreen() { ); }; - const renderFilterDropdown = ( - placeholder: string, - value: string[], - data: string[], - setter: React.Dispatch>, - ) => { - return ( - { - return { label: topic, value: topic }; - })} - renderSelectedItem={() => } - maxHeight={400} - labelField="label" - valueField="value" - placeholder={placeholder} - renderRightIcon={() => } - onChange={item => { - if (item) { - setter(item); - } - }} - /> - ); - }; - const renderNoStoryText = () => { return ( @@ -306,18 +269,18 @@ function GenreScreen() { - {renderFilterDropdown( - 'Tone', - selectedTonesForFiltering, - toneFilterOptions, - setSelectedTonesForFiltering, - )} - {renderFilterDropdown( - 'Topic', - selectedTopicsForFiltering, - topicFilterOptions, - setSelectedTopicsForFiltering, - )} + + {genreStoryIds.length === 0 && !isLoading ? ( diff --git a/src/app/(tabs)/search/index.tsx b/src/app/(tabs)/search/index.tsx index 10496c53..8020b954 100644 --- a/src/app/(tabs)/search/index.tsx +++ b/src/app/(tabs)/search/index.tsx @@ -1,9 +1,8 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { SearchBar } from '@rneui/themed'; import { router } from 'expo-router'; -import { Fragment, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { - Button, FlatList, View, Text, @@ -14,7 +13,6 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; -import FilterModal from '../../../components/FilterModal/FilterModal'; import GenreCard from '../../../components/GenreCard/GenreCard'; import PreviewCard from '../../../components/PreviewCard/PreviewCard'; import RecentSearchCard from '../../../components/RecentSearchCard/RecentSearchCard'; @@ -29,6 +27,10 @@ import { import colors from '../../../styles/colors'; import globalStyles from '../../../styles/globalStyles'; import { GenreType } from '../genre'; +import { + FilterDropdown, + FilterSingleDropdown, +} from '../../../components/FilterDropdown/FilterDropdown'; const getRecentSearch = async () => { try { @@ -74,6 +76,9 @@ function SearchScreen() { const [searchResults, setSearchResults] = useState< StoryPreviewWithPreloadedReactions[] >([]); + const [unfilteredSearchResults, setUnfilteredSearchResults] = useState< + StoryPreviewWithPreloadedReactions[] + >([]); const [search, setSearch] = useState(''); const [filterVisible, setFilterVisible] = useState(false); const [recentSearches, setRecentSearches] = useState([]); @@ -81,20 +86,164 @@ function SearchScreen() { const [showRecents, setShowRecents] = useState(false); const [recentlyViewed, setRecentlyViewed] = useState([]); const genreColors = [colors.citrus, colors.lime, colors.lilac]; + const [toneFilterOptions, setToneFilterOptions] = useState([]); + const [topicFilterOptions, setTopicFilterOptions] = useState([]); + const [genreFilterOptions, setGenreFilterOptions] = useState([]); + const [selectedTonesForFiltering, setSelectedTonesForFiltering] = useState< + string[] + >([]); + const [selectedTopicsForFiltering, setSelectedTopicsForFiltering] = useState< + string[] + >([]); + const [ + selectedMultipleGenresForFiltering, + setSelectedMultipleGenresForFiltering, + ] = useState([]); + const [selectedGenre, setSelectedGenre] = useState(''); + + const populateFilterDropdowns = (stories: StoryPreview[]) => { + const tones: string[] = stories + .reduce((acc: string[], current: StoryPreview) => { + return acc.concat(current.tone); + }, [] as string[]) + .filter(tone => tone !== null); + const topics: string[] = stories + .reduce((acc: string[], current: StoryPreview) => { + return acc.concat(current.topic); + }, [] as string[]) + .filter(topic => topic !== null); + const genres: string[] = stories + .reduce((acc: string[], current: StoryPreview) => { + return acc.concat(current.genre_medium); + }, [] as string[]) + .filter(genre => genre !== null); + + setGenreFilterOptions([...new Set(genres)]); + setTopicFilterOptions([...new Set(topics)]); + setToneFilterOptions([...new Set(tones)]); + }; useEffect(() => { (async () => { - fetchAllStoryPreviews().then(stories => setAllStories(stories)); - fetchGenres().then((genres: Genre[]) => setAllGenres(genres)); + fetchAllStoryPreviews().then( + (stories: StoryPreviewWithPreloadedReactions[]) => { + setAllStories(stories); + const tones: string[] = stories + .reduce((acc: string[], current: StoryPreview) => { + return acc.concat(current.tone); + }, [] as string[]) + .filter(tone => tone !== null); + const topics: string[] = stories + .reduce((acc: string[], current: StoryPreview) => { + return acc.concat(current.topic); + }, [] as string[]) + .filter(topic => topic !== null); + + setTopicFilterOptions([...new Set(topics)]); + setToneFilterOptions([...new Set(tones)]); + }, + ); + + fetchGenres().then((genres: Genre[]) => { + setAllGenres(genres); + const genreOptions: string[] = genres + .reduce((acc: string[], current: Genre) => { + return acc.concat( + current.parent_name, + current.subgenres.map(subgenre => subgenre.name), + ); + }, [] as string[]) + .filter(genre => genre !== null); + setGenreFilterOptions([...new Set(genreOptions)]); + }); getRecentSearch().then((searches: RecentSearch[]) => setRecentSearches(searches), ); getRecentStory().then((viewed: StoryPreview[]) => setRecentlyViewed(viewed), ); - })(); + })().then(() => {}); }, []); + useEffect(() => { + search.length > 0 + ? populateFilterDropdowns(searchResults) + : populateFilterDropdowns(allStories); + }, [search]); + + useEffect(() => { + if (selectedGenre) { + if (search.length === 0) { + const subgenreNames = allGenres + .reduce((acc: string[], current: Genre) => { + return acc.concat(current.subgenres.map(subgenre => subgenre.name)); + }, [] as string[]) + .filter(genre => genre !== null); + + if (subgenreNames.includes(selectedGenre)) { + const genre = allGenres.filter(genre => + genre.subgenres + .map(subgenre => subgenre.name) + .includes(selectedGenre), + )[0]; + router.push({ + pathname: '/genre', + params: { + genreId: genre.parent_id.toString(), + genreType: GenreType.SUBGENRE, + genreName: selectedGenre, + }, + }); + } else { + const genre = allGenres.filter( + genre => genre.parent_name === selectedGenre, + )[0]; + router.push({ + pathname: '/genre', + params: { + genreId: genre.parent_id.toString(), + genreType: GenreType.PARENT, + genreName: genre.parent_name, + }, + }); + } + } + } + }, [selectedGenre]); + + useEffect(() => { + const checkTopic = (preview: StoryPreview): boolean => { + if (preview == null || preview.topic == null) return false; + if (selectedTopicsForFiltering.length == 0) return true; + else + return selectedTopicsForFiltering.every(t => preview.topic.includes(t)); + }; + const checkTone = (preview: StoryPreview): boolean => { + if (preview == null || preview.tone == null) return false; + if (selectedTonesForFiltering.length == 0) return true; + else + return selectedTonesForFiltering.every(t => preview.tone.includes(t)); + }; + const checkGenre = (preview: StoryPreview): boolean => { + if (preview == null || preview.genre_medium == null) return false; + if (selectedMultipleGenresForFiltering.length == 0) return true; + else + return selectedMultipleGenresForFiltering.every(t => + preview.genre_medium.includes(t), + ); + }; + + const filteredPreviews = unfilteredSearchResults.filter( + preview => + checkTopic(preview) && checkTone(preview) && checkGenre(preview), + ); + setSearchResults(filteredPreviews); + }, [ + selectedTopicsForFiltering, + selectedTonesForFiltering, + selectedMultipleGenresForFiltering, + ]); + const getColor = (index: number) => { return genreColors[index % genreColors.length]; }; @@ -103,6 +252,7 @@ function SearchScreen() { if (text === '') { setSearch(text); setSearchResults([]); + setUnfilteredSearchResults([]); return; } @@ -115,11 +265,13 @@ function SearchScreen() { setSearch(text); setSearchResults(updatedData); + setUnfilteredSearchResults(updatedData); setShowGenreCarousals(false); }; const handleCancelButtonPress = () => { setSearchResults([]); + setUnfilteredSearchResults([]); setShowGenreCarousals(true); setShowRecents(false); }; @@ -134,6 +286,12 @@ function SearchScreen() { setRecentStory([]); }; + const clearFilters = () => { + setSelectedMultipleGenresForFiltering([]); + setSelectedTonesForFiltering([]); + setSelectedTopicsForFiltering([]); + }; + const searchResultStacking = ( searchString: string, searchResults: number, @@ -230,6 +388,46 @@ function SearchScreen() { }} /> + {((search && searchResults.length > 0) || showGenreCarousals) && ( + + {search ? ( + + ) : ( + + )} + + + + + )} + {/* {search && ( */} {/* */} {/*