diff --git a/package-lock.json b/package-lock.json index b1f5b69e..ca07deb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13667,9 +13667,9 @@ "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==" }, "node_modules/joi": { - "version": "17.12.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz", - "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==", + "version": "17.12.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", + "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx index c496d43f..de7b449d 100644 --- a/src/app/(tabs)/_layout.tsx +++ b/src/app/(tabs)/_layout.tsx @@ -55,7 +55,6 @@ function TabNav() { headerShown: false, tabBarLabel: 'Home', tabBarIcon: ({ color }) => HomeIcon({ color }), - // tabBarLabelStyle: { borderTopWidth: 12, paddingTop: 12 }, }} /> + ); } diff --git a/src/app/(tabs)/author/index.tsx b/src/app/(tabs)/author/index.tsx index 12c5bc73..38b8b88e 100644 --- a/src/app/(tabs)/author/index.tsx +++ b/src/app/(tabs)/author/index.tsx @@ -14,6 +14,7 @@ import { } from '../../../queries/authors'; import { Author, StoryPreview } from '../../../queries/types'; import globalStyles from '../../../styles/globalStyles'; +import * as cheerio from 'cheerio'; function AuthorScreen() { const [authorInfo, setAuthorInfo] = useState(); @@ -26,41 +27,39 @@ function AuthorScreen() { useEffect(() => { setLoading(true); (async () => { - const storyData: StoryPreview[] = await fetchAuthorStoryPreviews( - parseInt(author as string, 10), - ); - const authorData: Author = await fetchAuthor( - parseInt(author as string, 10), - ); try { - setAuthorInfo(authorData); - console.log('TESTING AUTHOR INFO QUERY OUTPUT:', authorInfo); - } catch (error) { - console.log( - `There was an error while trying to output authorinfo ${error}`, + const storyData: StoryPreview[] = await fetchAuthorStoryPreviews( + parseInt(author as string, 10), ); - } - try { + const authorData: Author = await fetchAuthor( + parseInt(author as string, 10), + ); + + // Assuming these setters do not throw, but if they do, they're caught by the catch block + setAuthorInfo(authorData); setAuthorStoryPreview(storyData); - console.log('TESTING STORY PREVIEW INFO QUERY OUTPUT:', storyData); } catch (error) { - console.log( - `There was an error while trying to output author story preview info ${error}`, - ); + console.error('There was an error while fetching data:', error); + } finally { + setLoading(false); } - })().then(() => { - setLoading(false); - }); + })(); }, [author]); + const getTextFromHtml = (text: string) => { + return cheerio.load(text).text().trim(); + }; + return ( - + {isLoading ? ( ) : ( router.back()} /> @@ -77,7 +76,7 @@ function AuthorScreen() { numberOfLines={2} style={globalStyles.h1} > - {authorInfo.name} + {getTextFromHtml(authorInfo.name)} {authorInfo?.pronouns && ( @@ -92,7 +91,9 @@ function AuthorScreen() { {authorInfo?.bio && ( <> - {decode(authorInfo.bio)} + + {getTextFromHtml(authorInfo.bio)} + )} @@ -105,7 +106,7 @@ function AuthorScreen() { Artist's Statement - {decode(authorInfo.artist_statement)} + {getTextFromHtml(authorInfo.artist_statement)} @@ -135,6 +136,9 @@ function AuthorScreen() { } /> ))} + + {/* View so there's space between the tab bar and the stories */} + )} diff --git a/src/app/(tabs)/genre/_layout.tsx b/src/app/(tabs)/genre/_layout.tsx new file mode 100644 index 00000000..d021a461 --- /dev/null +++ b/src/app/(tabs)/genre/_layout.tsx @@ -0,0 +1,11 @@ +import { Stack } from 'expo-router'; + +function StackLayout() { + return ( + + + + ); +} + +export default StackLayout; diff --git a/src/app/(tabs)/genre/index.tsx b/src/app/(tabs)/genre/index.tsx new file mode 100644 index 00000000..025680e5 --- /dev/null +++ b/src/app/(tabs)/genre/index.tsx @@ -0,0 +1,351 @@ +import { useLocalSearchParams, router } from 'expo-router'; +import { useEffect, useState, useMemo, ReactNode } from 'react'; +import { + ActivityIndicator, + ScrollView, + View, + Text, + FlatList, +} from 'react-native'; +import { 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'; + +import styles from './styles'; +import BackButton from '../../../components/BackButton/BackButton'; +import { fetchGenreStoryById } from '../../../queries/genres'; +import { fetchStoryPreviewByIds } from '../../../queries/stories'; +import { StoryPreview, GenreStories } from '../../../queries/types'; +import globalStyles from '../../../styles/globalStyles'; +import PreviewCard from '../../../components/PreviewCard/PreviewCard'; + +function GenreScreen() { + const [genreStoryData, setGenreStoryData] = useState(); + const [genreStoryIds, setGenreStoryIds] = useState([]); + const [subgenres, setSubgenres] = useState([]); + const [allStoryPreviews, setAllStoryPreviews] = useState([]); + const [filteredStoryPreviews, setFilteredStoryPreviews] = useState< + StoryPreview[] + >([]); + const [selectedSubgenre, setSelectedSubgenre] = useState(''); + const [mainGenre, setMainGenre] = useState(''); + const [isLoading, setLoading] = useState(true); + const [toneFilterOptions, setToneFilterOptions] = useState([]); + const [topicFilterOptions, setTopicFilterOptions] = useState([]); + const [selectedTonesForFiltering, setSelectedTonesForFiltering] = useState< + string[] + >([]); + const [selectedTopicsForFiltering, setSelectedTopicsForFiltering] = useState< + string[] + >([]); + const { genreId, genreType, genreName } = useLocalSearchParams<{ + genreId: string; + genreType: GenreType; + genreName: string; + }>(); + + 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 filteredPreviews = allStoryPreviews.filter( + preview => checkTopic(preview) && checkTone(preview), + ); + setFilteredStoryPreviews(filteredPreviews); + }, [selectedTopicsForFiltering, selectedTonesForFiltering]); + + function getAllStoryIds(genreStories: GenreStories[]): string[] { + return genreStories + .map(story => story.genre_story_previews) + .flat() + .filter(story => story !== null); + } + + function filterStoriesBySubgenreName( + subgenreName: string, + stories: GenreStories[], + ): string[] { + const matchingGenreStory = stories.find( + subgenre => subgenre.subgenre_name === subgenreName, + ); + + return matchingGenreStory?.genre_story_previews ?? []; + } + + function getSubgenres(stories: GenreStories[]): string[] { + const subgenres = stories.map(subgenre => subgenre.subgenre_name); + return ['All', ...subgenres]; + } + + function filterBySubgenre(subgenre: string) { + setLoading(true); + setSelectedSubgenre(subgenre); + if (!genreStoryData) { + setLoading(false); + return []; + } + + if (subgenre === 'All') { + setGenreStoryIds(getAllStoryIds(genreStoryData)); + } else { + const filteredStoryIds = filterStoriesBySubgenreName( + subgenre, + genreStoryData, + ); + + setGenreStoryIds(filteredStoryIds); + setToneFilterOptions([]); + setTopicFilterOptions([]); + setLoading(false); + } + } + + useEffect(() => { + const getGenre = async () => { + setLoading(true); + + const genreStoryData: GenreStories[] = await fetchGenreStoryById( + parseInt(genreId as string, 10), + ); + + setGenreStoryData(genreStoryData); + setMainGenre(genreStoryData[0].parent_name); + setSubgenres(getSubgenres(genreStoryData)); + + if (genreType == GenreType.PARENT) { + setSelectedSubgenre('All'); //if user clicks see all, selected should be 'ALL' + setGenreStoryIds(getAllStoryIds(genreStoryData)); + } else if (genreType == GenreType.SUBGENRE) { + setSelectedSubgenre(genreName || ''); //if user clicks a specific genre, selected should be genreName + + const filteredStoryIds = filterStoriesBySubgenreName( + genreName || '', + genreStoryData, + ); + setGenreStoryIds(filteredStoryIds); + + setLoading(false); + } + }; + getGenre(); + }, [genreName]); + + useEffect(() => { + const showAllStoryPreviews = async () => { + setLoading(true); + + const previews: StoryPreview[] = await fetchStoryPreviewByIds( + genreStoryIds as any, + ); + + const tones: string[] = previews + .reduce((acc: string[], current: StoryPreview) => { + return acc.concat(current.tone); + }, [] as string[]) + .filter(tone => tone !== null); + const topics: string[] = previews + .reduce((acc: string[], current: StoryPreview) => { + return acc.concat(current.topic); + }, [] as string[]) + .filter(topic => topic !== null); + + setAllStoryPreviews(previews.flat()); + setFilteredStoryPreviews(previews.flat()); + setTopicFilterOptions([...new Set(topics)]); + setToneFilterOptions([...new Set(tones)]); + + setLoading(false); + }; + + if (genreStoryIds.length > 0) { + showAllStoryPreviews(); + } + }, [genreStoryIds]); + + const renderGenreScrollSelector = () => { + return ( + + {subgenres.map((subgenre, index) => ( + filterBySubgenre(subgenre)} //onPress will trigger the filterBySubgenre function + style={{ paddingHorizontal: 20, paddingTop: 5 }} + key={index} + > + + {subgenre} + + + ))} + + ); + }; + + const renderGenreHeading = () => { + return ( + + + {selectedSubgenre === 'All' ? mainGenre : selectedSubgenre} + + {/* */} + {/* {' '} */} + {/* Subheading about{' '} */} + {/* {selectedSubgenre === 'All' ? mainGenre : selectedSubgenre} */} + {/* ...Include Later? */} + {/* */} + + ); + }; + + 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 ( + + Sorry! + + There are no stories under this Genre or Subgenre. Please continue to + search for other stories + + + ); + }; + + const renderStories = () => { + return ( + ( + { + router.push({ + pathname: '/story', + params: { storyId: item.id.toString() }, + }); + }} + /> + )} + /> + ); + }; + + return ( + + + + + router.push({ + pathname: '/search', + }) + } + /> + + {useMemo(renderGenreHeading, [selectedSubgenre, mainGenre])} + {useMemo(renderGenreScrollSelector, [subgenres, selectedSubgenre])} + + + + {renderFilterDropdown( + 'Tone', + selectedTonesForFiltering, + toneFilterOptions, + setSelectedTonesForFiltering, + )} + {renderFilterDropdown( + 'Topic', + selectedTopicsForFiltering, + topicFilterOptions, + setSelectedTopicsForFiltering, + )} + + + {genreStoryIds.length === 0 && !isLoading ? ( + renderNoStoryText() + ) : ( + <> + + {isLoading ? ( + + + + ) : ( + renderStories() + )} + + + )} + + + ); +} + +export enum GenreType { + PARENT = 'parent', + SUBGENRE = 'subgenre', +} + +export default GenreScreen; diff --git a/src/app/(tabs)/genre/styles.tsx b/src/app/(tabs)/genre/styles.tsx new file mode 100644 index 00000000..1869ac12 --- /dev/null +++ b/src/app/(tabs)/genre/styles.tsx @@ -0,0 +1,72 @@ +import { StyleSheet } from 'react-native'; + +import colors from '../../../styles/colors'; + +const styles = StyleSheet.create({ + textSelected: { + color: '#EB563B', + textDecorationLine: 'underline', + }, + container: { + paddingHorizontal: 24, + width: '100%', + marginTop: 24, + flex: 1, + }, + + flatListStyle: { + paddingTop: 15, + }, + scrollViewContainer: { + marginVertical: 15, + width: '100%', + }, + noStoriesText: { + fontSize: 20, + color: '#EB563B', + }, + noStoriesText2: { + fontSize: 13, + }, + renderStories: { + paddingBottom: 10, + flex: 1, + }, + headerContainer: {}, + dropdown: { + borderColor: '#797979', + flexGrow: 0, + flexShrink: 0, + borderWidth: 1.5, + borderRadius: 7, + width: 140, + height: 30, + color: '#797979', + }, + dropdownContainer: { + marginTop: 20, + marginBottom: 20, + flexDirection: 'row', + justifyContent: 'flex-start', + }, + firstDropdown: { + marginRight: 10, + }, + secondDropdown: { + marginLeft: 10, + }, + icon: { + marginRight: 5, + }, + iconStyle: { + width: 20, + height: 20, + }, + itemContainer: {}, + placeholderStyle: { + color: colors.darkGrey, + marginLeft: 45, + }, +}); + +export default styles; diff --git a/src/app/(tabs)/home/index.tsx b/src/app/(tabs)/home/index.tsx index 871b0eed..81341d77 100644 --- a/src/app/(tabs)/home/index.tsx +++ b/src/app/(tabs)/home/index.tsx @@ -1,6 +1,12 @@ import { router } from 'expo-router'; import { useEffect, useState } from 'react'; -import { Pressable, ScrollView, Text, View } from 'react-native'; +import { + ActivityIndicator, + Pressable, + ScrollView, + Text, + View, +} from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; @@ -54,15 +60,16 @@ function HomeScreen() { }); }, [user]); + if (loading) { + return ; + } return ( - {loading && ( - - Loading - - )} + Library ); diff --git a/src/app/(tabs)/search/index.tsx b/src/app/(tabs)/search/index.tsx index fd64c5b7..a5114727 100644 --- a/src/app/(tabs)/search/index.tsx +++ b/src/app/(tabs)/search/index.tsx @@ -9,6 +9,7 @@ import { Text, ScrollView, Pressable, + TouchableOpacity, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -22,13 +23,14 @@ import { fetchAllStoryPreviews } from '../../../queries/stories'; import { StoryPreview, RecentSearch, Genre } from '../../../queries/types'; import colors from '../../../styles/colors'; import globalStyles from '../../../styles/globalStyles'; +import { GenreType } from '../genre'; const getRecentSearch = async () => { try { const jsonValue = await AsyncStorage.getItem('GWN_RECENT_SEARCHES_ARRAY'); return jsonValue != null ? JSON.parse(jsonValue) : []; } catch (error) { - console.log(error); + console.error(error); } }; @@ -37,7 +39,7 @@ const setRecentSearch = async (searchResult: RecentSearch[]) => { const jsonValue = JSON.stringify(searchResult); await AsyncStorage.setItem('GWN_RECENT_SEARCHES_ARRAY', jsonValue); } catch (error) { - console.log(error); + console.error(error); } }; @@ -46,7 +48,7 @@ const getRecentStory = async () => { const jsonValue = await AsyncStorage.getItem('GWN_RECENT_STORIES_ARRAY'); return jsonValue != null ? JSON.parse(jsonValue) : []; } catch (error) { - console.log(error); + console.error(error); } }; @@ -55,7 +57,7 @@ const setRecentStory = async (recentStories: StoryPreview[]) => { const jsonValue = JSON.stringify(recentStories); await AsyncStorage.setItem('GWN_RECENT_STORIES_ARRAY', jsonValue); } catch (error) { - console.log(error); + console.error(error); } }; @@ -69,20 +71,24 @@ function SearchScreen() { const [showGenreCarousals, setShowGenreCarousals] = useState(true); const [showRecents, setShowRecents] = useState(false); const [recentlyViewed, setRecentlyViewed] = useState([]); + const genreColors = [colors.citrus, colors.lime, colors.lilac]; useEffect(() => { (async () => { - const data: StoryPreview[] = await fetchAllStoryPreviews(); - setAllStories(data); - const genreData: Genre[] = await fetchGenres(); - setAllGenres(genreData); - setRecentSearches(await getRecentSearch()); - setRecentlyViewed(await getRecentStory()); + fetchAllStoryPreviews().then((stories: StoryPreview[]) => + setAllStories(stories), + ); + fetchGenres().then((genres: Genre[]) => setAllGenres(genres)); + getRecentSearch().then((searches: RecentSearch[]) => + setRecentSearches(searches), + ); + getRecentStory().then((viewed: StoryPreview[]) => + setRecentlyViewed(viewed), + ); })(); }, []); const getColor = (index: number) => { - const genreColors = [colors.citrus, colors.lime, colors.lilac]; return genreColors[index % genreColors.length]; }; @@ -92,12 +98,14 @@ function SearchScreen() { setSearchResults([]); return; } + const updatedData = allStories.filter((item: StoryPreview) => { const title = `${item.title.toUpperCase()})`; const author = `${item.author_name.toUpperCase()})`; const text_data = text.toUpperCase(); return title.indexOf(text_data) > -1 || author.indexOf(text_data) > -1; }); + setSearch(text); setSearchResults(updatedData); setShowGenreCarousals(false); @@ -175,7 +183,7 @@ function SearchScreen() { return ( {allGenres.map((genre, index) => ( - + {genre.parent_name} - See All + { + router.push({ + pathname: '/genre', + params: { + genreId: genre.parent_id.toString(), + genreType: GenreType.PARENT, + genreName: genre.parent_name, + }, + }); + }} + > + See All + null} + pressFunction={() => { + router.push({ + pathname: '/genre', + params: { + genreId: genre.parent_id.toString(), + genreType: GenreType.SUBGENRE, + genreName: subgenre.name, + }, + }); + }} /> ))} - + ))} ) : ( diff --git a/src/app/(tabs)/settings/styles.tsx b/src/app/(tabs)/settings/styles.tsx index a5eceadc..cded918b 100644 --- a/src/app/(tabs)/settings/styles.tsx +++ b/src/app/(tabs)/settings/styles.tsx @@ -10,6 +10,7 @@ export default StyleSheet.create({ flex: 1, backgroundColor: 'white', paddingHorizontal: 24, + paddingBottom: 60, }, button: { marginBottom: 32, diff --git a/src/app/(tabs)/story/index.tsx b/src/app/(tabs)/story/index.tsx index 73cbdfef..2387f2f1 100644 --- a/src/app/(tabs)/story/index.tsx +++ b/src/app/(tabs)/story/index.tsx @@ -65,7 +65,7 @@ function StoryScreen() { }; return ( - + {isLoading ? ( ) : ( diff --git a/src/app/(tabs)/story/styles.ts b/src/app/(tabs)/story/styles.ts index 7e6e2c8c..66932cf5 100644 --- a/src/app/(tabs)/story/styles.ts +++ b/src/app/(tabs)/story/styles.ts @@ -3,10 +3,6 @@ import colors from '../../../styles/colors'; const styles = StyleSheet.create({ container: { - flex: 1, - backgroundColor: 'white', - alignItems: 'flex-start', - justifyContent: 'flex-start', paddingLeft: 24, paddingRight: 24, paddingTop: 48, diff --git a/src/components/GenreCard/GenreCard.tsx b/src/components/GenreCard/GenreCard.tsx index 86b9f552..37bb5360 100644 --- a/src/components/GenreCard/GenreCard.tsx +++ b/src/components/GenreCard/GenreCard.tsx @@ -9,6 +9,7 @@ import styles from './styles'; type GenreCardProps = { subgenres: string; + subgenre_id: number; cardColor: string; pressFunction: (event: GestureResponderEvent) => void; }; diff --git a/src/components/GenreStoryPreviewCard/GenreStoryPreviewCard.tsx b/src/components/GenreStoryPreviewCard/GenreStoryPreviewCard.tsx new file mode 100644 index 00000000..00b413da --- /dev/null +++ b/src/components/GenreStoryPreviewCard/GenreStoryPreviewCard.tsx @@ -0,0 +1,90 @@ +import { + GestureResponderEvent, + Text, + Image, + View, + TouchableOpacity, +} from 'react-native'; + +import styles from './styles'; +import globalStyles from '../../styles/globalStyles'; + +type GenreStoryPreviewCardProps = { + topic: string[]; + tone: string[]; + genreMedium: string[]; + allTags: string[]; + authorName: string; + storyImage: string; + authorImage: string; + storyTitle: string; + excerpt: { html: string }; + pressFunction: (event: GestureResponderEvent) => void; +}; + +function GenreStoryPreviewCard({ + topic, + tone, + genreMedium, + allTags, + authorName, + storyImage, + authorImage, + storyTitle, + excerpt, + pressFunction, +}: GenreStoryPreviewCardProps) { + return ( + + + + + + {storyTitle} + + + + + + + + + {authorName} + + + + {excerpt.html.slice(3, -3)} + + + + + + + + {genreMedium[0]} + + + {genreMedium[1]} + + + + + + {allTags.length} more{' '} + {allTags.length === 1 ? 'tag' : 'tags'} + + + + + + + + ); +} + +export default GenreStoryPreviewCard; diff --git a/src/components/GenreStoryPreviewCard/styles.ts b/src/components/GenreStoryPreviewCard/styles.ts new file mode 100644 index 00000000..a40bb58e --- /dev/null +++ b/src/components/GenreStoryPreviewCard/styles.ts @@ -0,0 +1,121 @@ +import { StyleSheet } from 'react-native'; + +import colors from '../../styles/colors'; + +const styles = StyleSheet.create({ + card: { + flexDirection: 'column', + justifyContent: 'flex-end', + backgroundColor: 'white', + borderRadius: 6, + marginTop: 8, + marginBottom: 8, + shadowColor: 'black', + shadowOffset: { width: 1, height: 3 }, + shadowOpacity: 0.5, + elevation: 10, + paddingRight: 30, + marginRight: 30, + width: '98%', + marginHorizontal: '0.75%', + }, + top: { + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + bottom: { + flex: 1, + flexDirection: 'column', + justifyContent: 'flex-start', + alignItems: 'flex-start', + height: 48, + backgroundColor: colors.lightGrey, + overflow: 'hidden', + borderRadius: 6, + paddingHorizontal: 12, + paddingTop: 8, + }, + image: { + height: 106, + width: 106, + backgroundColor: colors.lilac, + borderRadius: 4, + marginBottom: 12, + marginTop: 12, + }, + author: { + marginLeft: 8, + }, + authorImage: { + height: 22, + width: 22, + backgroundColor: colors.gwnOrange, + borderRadius: 22 / 2, + }, + cardTextContainer: { + flex: 1, + marginLeft: 16, + marginTop: 12, + marginBottom: 8, + }, + authorContainer: { + flexDirection: 'row', + alignItems: 'flex-start', + marginBottom: 10, + }, + title: { + marginBottom: 8, + fontSize: 22, + }, + tags: { + paddingHorizontal: 8, + paddingVertical: 4, + backgroundColor: '#EBEBEB', + borderRadius: 10, + width: 'auto', + marginRight: 8, + marginBottom: 10, + }, + tagsContainer: { + flexDirection: 'row', + justifyContent: 'flex-start', + + alignItems: 'center', + flexWrap: 'wrap', + }, + horizontalLine: { + borderBottomColor: '#EBEBEB', + borderBottomWidth: 1, + marginTop: 5, + marginBottom: 10, + }, + cardContainer2: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + }, + authorandText: { + flex: 1, + flexDirection: 'column', + marginLeft: 10, + marginTop: -10, + }, + subtext: { + color: '#797979', + fontSize: 15, + }, + tagSubtext: { + color: '#797979', + }, + tagParent: { + flex: 1, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + flexWrap: 'wrap', + }, +}); + +export default styles; diff --git a/src/components/PreviewCard/PreviewCard.tsx b/src/components/PreviewCard/PreviewCard.tsx index 98253bb7..84910f88 100644 --- a/src/components/PreviewCard/PreviewCard.tsx +++ b/src/components/PreviewCard/PreviewCard.tsx @@ -10,6 +10,9 @@ import { import styles from './styles'; import globalStyles from '../../styles/globalStyles'; +const placeholderImage = + 'https://gwn-uploads.s3.amazonaws.com/wp-content/uploads/2021/10/10120952/Girls-Write-Now-logo-avatar.png'; + type PreviewCardProps = { title: string; image: string; @@ -38,7 +41,10 @@ function PreviewCard({ - + diff --git a/src/queries/genres.tsx b/src/queries/genres.tsx index 4710cbcd..ca648b71 100644 --- a/src/queries/genres.tsx +++ b/src/queries/genres.tsx @@ -1,5 +1,4 @@ -// eslint-disable-next-line import/namespace -import { Genre } from './types'; +import { Genre, GenreStories } from './types'; import supabase from '../utils/supabase'; export async function fetchGenres(): Promise { @@ -12,3 +11,21 @@ export async function fetchGenres(): Promise { return data; } } + +export async function fetchGenreStoryById( + parent_id: number, +): Promise { + const { data, error } = await supabase.rpc( + 'fetch_genre_and_subgenre_stories', + { + genre_parent_id: parent_id, + }, + ); + if (error) { + throw new Error( + `An error occured when trying to fetch all genres story previews ${error}`, + ); + } else { + return data; + } +} diff --git a/src/queries/stories.tsx b/src/queries/stories.tsx index 86747ad9..9b8e833b 100644 --- a/src/queries/stories.tsx +++ b/src/queries/stories.tsx @@ -98,3 +98,19 @@ export async function fetchStoryPreviewById( return data; } } + +export async function fetchStoryPreviewByIds( + storyIds: number[], +): Promise { + const { data, error } = await supabase.rpc('curr_story_preview_by_ids', { + input_ids: storyIds, + }); + if (error) { + console.log(error); + throw new Error( + `An error occured when trying to fetch story preview by IDs: ${error}`, + ); + } else { + return data; + } +} diff --git a/src/styles/globalStyles.ts b/src/styles/globalStyles.ts index 12e17283..3a104cf0 100644 --- a/src/styles/globalStyles.ts +++ b/src/styles/globalStyles.ts @@ -10,6 +10,15 @@ export default StyleSheet.create({ justifyContent: 'flex-start', paddingHorizontal: 24, }, + + tabBarContainer: { + flex: 1, + backgroundColor: 'white', + alignItems: 'flex-start', + justifyContent: 'flex-start', + paddingHorizontal: 24, + paddingBottom: 60, + }, authContainer: { marginHorizontal: 38, flex: 1, diff --git a/src/utils/FilterContext.tsx b/src/utils/FilterContext.tsx new file mode 100644 index 00000000..b488e0dd --- /dev/null +++ b/src/utils/FilterContext.tsx @@ -0,0 +1,186 @@ +import React, { + createContext, + useContext, + useEffect, + useMemo, + useReducer, +} from 'react'; +import supabase from './supabase'; + +type FilterAction = + | { type: 'SET_TAGS'; tags: TagFilter[] } + | { type: 'TOGGLE_FILTER'; id: number } + | { type: 'SET_FILTER'; id: number; value: boolean } + | { type: 'CLEAR_ALL'; category: string } + | { type: 'TOGGLE_MAIN_GENRE'; mainGenreId: number }; + +export type FilterDispatch = React.Dispatch; + +export type TagFilter = { + id: number; + name: string; + category: string; + active: boolean; + parent: number | null; +}; + +type ParentFilter = { children: TagFilter[] } & TagFilter; + +export interface FilterState { + filters: Map; + isLoading: boolean; + dispatch: FilterDispatch; +} + +const FilterContext = createContext({} as FilterState); + +const mapParentsAndChildren = ( + filters: Map, + func: (filter: TagFilter) => TagFilter, +) => { + return new Map( + Array.from(filters).map(([id, parent]) => { + return [ + id, + { + ...func(parent), + children: parent.children.map(func), + } as ParentFilter, + ]; + }), + ); +}; + +export const useFilterReducer = () => + useReducer( + (prevState: FilterState, action: FilterAction) => { + switch (action.type) { + case 'SET_TAGS': + const nestedFilters = new Map(); + action.tags + .filter(filter => filter.parent === null) + .map(parentFilter => { + nestedFilters.set(parentFilter.id, { + ...parentFilter, + children: [], + } as ParentFilter); + }); + + action.tags.map(childFilter => { + if (childFilter.parent) { + nestedFilters.get(childFilter.parent)?.children.push(childFilter); + } + }); + + return { + ...prevState, + filters: nestedFilters, + isLoading: false, + }; + case 'SET_FILTER': + return { + ...prevState, + filters: mapParentsAndChildren(prevState.filters, fitler => + fitler.id == action.id + ? { ...fitler, active: action.value } + : fitler, + ), + }; + case 'TOGGLE_FILTER': + return { + ...prevState, + filters: mapParentsAndChildren(prevState.filters, fitler => + fitler.id == action.id + ? { ...fitler, active: !fitler.active } + : fitler, + ), + }; + case 'CLEAR_ALL': + return { + ...prevState, + filters: mapParentsAndChildren(prevState.filters, filter => + filter.category == action.category + ? { ...filter, active: false } + : filter, + ), + }; + case 'TOGGLE_MAIN_GENRE': + const parentGenre = prevState.filters.get(action.mainGenreId); + const newActiveState = !parentGenre?.active; + + const updatedFilters = mapParentsAndChildren( + prevState.filters, + tag => + tag.parent == action.mainGenreId || tag.id == action.mainGenreId + ? { ...tag, active: newActiveState } + : tag, + ); + + return { + ...prevState, + filters: updatedFilters, + }; + default: + return prevState; + } + }, + { + filters: new Map(), + isLoading: true, + dispatch: () => null, + }, + ); + +export function useFilter() { + const value = useContext(FilterContext); + if (process.env.NODE_ENV !== 'production') { + if (!value) { + throw new Error( + 'useFilter must be wrapped in a ', + ); + } + } + + return value; +} + +export function FilterContextProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [filterState, dispatch] = useFilterReducer(); + + const getTags = async () => { + const { data } = await supabase.from('tags').select(`*`); + + return data?.map(entry => { + const { category, id, name, parent_id } = entry; + return { + id, + name, + category, + parent: parent_id, + active: false, + } as TagFilter; + }); + }; + + useEffect(() => { + getTags().then(tags => dispatch({ type: 'SET_TAGS', tags: tags ?? [] })); + }, []); + + const filterContextValue = useMemo( + () => ({ + ...filterState, + dispatch, + }), + [filterState], + ); + + return ( + + {children} + + ); +}