From 3d5abcc78a35d75384dc910876bd0650f42e13b6 Mon Sep 17 00:00:00 2001 From: emilysunaryo <99292788+emilysunaryo@users.noreply.github.com> Date: Tue, 23 Apr 2024 20:09:58 -0700 Subject: [PATCH 1/6] [reactions] Styling for reactions to stories (#91) * progress on database functions to pull genre information * pushing changes before rebase * ran prettier * finished styling genre preview card component * styling complete for genre screen, need to implement tone, topic dropdown selection logic * semi finished genre screen, need to debug drop down logic * figuring out filter context * reaction styling * Run prettier * Remove filter modal * Made reaction picker horizontal * Reactions implemented * Generalized pubsub * Fix reations * Fix home screen saved stories not working * Fix reaction bugs * Fix reaction bugs --------- Co-authored-by: Aditya Pawar <34043950+adityapawar1@users.noreply.github.com> Co-authored-by: Aditya Pawar --- app.json | 14 ++- package-lock.json | 55 ++++++++++++ package.json | 3 + src/app/(tabs)/genre/_layout.tsx | 10 ++- src/app/(tabs)/genre/index.tsx | 3 +- src/app/(tabs)/genre/styles.tsx | 2 +- src/app/(tabs)/home/index.tsx | 1 + src/app/(tabs)/search/index.tsx | 26 +++--- src/app/(tabs)/story/index.tsx | 4 + src/app/(tabs)/story/styles.ts | 11 +++ src/components/ContentCard/ContentCard.tsx | 6 +- src/components/FilterModal/ChildFilter.tsx | 27 ++++++ src/components/FilterModal/FilterModal.tsx | 90 +++++++++++-------- src/components/FilterModal/ParentFilter.tsx | 26 ++++++ src/components/PreviewCard/PreviewCard.tsx | 10 +-- .../ReactionDisplay/ReactionDisplay.tsx | 47 +++++++--- .../ReactionPicker/ReactionPicker.tsx | 81 +++++++++++++++++ src/components/ReactionPicker/styles.ts | 29 ++++++ .../SaveStoryButton/SaveStoryButton.tsx | 14 ++- src/queries/reactions.tsx | 11 +-- src/utils/PubSubContext.tsx | 50 ++++++----- 21 files changed, 406 insertions(+), 114 deletions(-) create mode 100644 src/components/FilterModal/ChildFilter.tsx create mode 100644 src/components/FilterModal/ParentFilter.tsx create mode 100644 src/components/ReactionPicker/ReactionPicker.tsx create mode 100644 src/components/ReactionPicker/styles.ts diff --git a/app.json b/app.json index 6a1cdecc..a874bffd 100644 --- a/app.json +++ b/app.json @@ -17,16 +17,24 @@ "supportsTablet": true }, "android": { + "package": "org.calblueprint.girlswritenow", "softwareKeyboardLayoutMode": "pan", "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" - }, - "softwareKeyboardLayoutMode": "pan" + } }, "web": { "favicon": "./assets/favicon.png" }, - "plugins": ["expo-router"] + "plugins": ["expo-router"], + "extra": { + "router": { + "origin": false + }, + "eas": { + "projectId": "12e1580c-e57a-466e-bc0d-c7a051565998" + } + } } } diff --git a/package-lock.json b/package-lock.json index da49b463..4bd8250b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,9 @@ "@emotion/styled": "^11.11.0", "@expo-google-fonts/manrope": "^0.2.3", "@expo/vector-icons": "^13.0.0", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@fortawesome/react-native-fontawesome": "^0.3.0", "@mui/icons-material": "^5.14.13", "@mui/material": "^5.14.13", "@mui/styled-engine-sc": "^6.0.0-alpha.1", @@ -3656,6 +3659,53 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", + "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", + "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", + "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-native-fontawesome": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-native-fontawesome/-/react-native-fontawesome-0.3.0.tgz", + "integrity": "sha512-wSfetdK4+b/pvPbM2v+bZ5hfNlwtk9l3QuJo59sbMrxJalfX7BuF2WsSIWMSxfWwSsbOtY4+TUs6uw/rE59NJA==", + "dependencies": { + "humps": "^2.0.1", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react-native": ">= 0.67", + "react-native-svg": ">= 11.x" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -12522,6 +12572,11 @@ "node": ">=10.17.0" } }, + "node_modules/humps": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", + "integrity": "sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==" + }, "node_modules/husky": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", diff --git a/package.json b/package.json index 45a2c09d..d058a0c8 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "@emotion/styled": "^11.11.0", "@expo-google-fonts/manrope": "^0.2.3", "@expo/vector-icons": "^13.0.0", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@fortawesome/react-native-fontawesome": "^0.3.0", "@mui/icons-material": "^5.14.13", "@mui/material": "^5.14.13", "@mui/styled-engine-sc": "^6.0.0-alpha.1", diff --git a/src/app/(tabs)/genre/_layout.tsx b/src/app/(tabs)/genre/_layout.tsx index d021a461..d309c307 100644 --- a/src/app/(tabs)/genre/_layout.tsx +++ b/src/app/(tabs)/genre/_layout.tsx @@ -1,10 +1,14 @@ import { Stack } from 'expo-router'; +import { FilterContextProvider } from '../../../utils/FilterContext'; + function StackLayout() { return ( - - - + + + + + ); } diff --git a/src/app/(tabs)/genre/index.tsx b/src/app/(tabs)/genre/index.tsx index 04483ea9..5bf00f55 100644 --- a/src/app/(tabs)/genre/index.tsx +++ b/src/app/(tabs)/genre/index.tsx @@ -7,13 +7,14 @@ import { Text, FlatList, } from 'react-native'; -import { MultiSelect } from 'react-native-element-dropdown'; +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'; import styles from './styles'; import BackButton from '../../../components/BackButton/BackButton'; + import PreviewCard from '../../../components/PreviewCard/PreviewCard'; import { fetchGenreStoryById } from '../../../queries/genres'; import { fetchStoryPreviewByIds } from '../../../queries/stories'; diff --git a/src/app/(tabs)/genre/styles.tsx b/src/app/(tabs)/genre/styles.tsx index 8d69590b..181292de 100644 --- a/src/app/(tabs)/genre/styles.tsx +++ b/src/app/(tabs)/genre/styles.tsx @@ -1,6 +1,7 @@ import { StyleSheet } from 'react-native'; import colors from '../../../styles/colors'; +import globalStyles from '../../../styles/globalStyles'; const styles = StyleSheet.create({ textSelected: { @@ -12,7 +13,6 @@ const styles = StyleSheet.create({ width: '100%', flex: 1, }, - flatListStyle: { paddingTop: 15, }, diff --git a/src/app/(tabs)/home/index.tsx b/src/app/(tabs)/home/index.tsx index 911f8a57..2c8c0e3e 100644 --- a/src/app/(tabs)/home/index.tsx +++ b/src/app/(tabs)/home/index.tsx @@ -7,6 +7,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; import ContentCard from '../../../components/ContentCard/ContentCard'; import PreviewCard from '../../../components/PreviewCard/PreviewCard'; + import { fetchUsername } from '../../../queries/profiles'; import { fetchFeaturedStoriesDescription, diff --git a/src/app/(tabs)/search/index.tsx b/src/app/(tabs)/search/index.tsx index 6163689c..10496c53 100644 --- a/src/app/(tabs)/search/index.tsx +++ b/src/app/(tabs)/search/index.tsx @@ -230,14 +230,14 @@ function SearchScreen() { }} /> - {search && ( - - + + + )} diff --git a/src/app/(tabs)/story/styles.ts b/src/app/(tabs)/story/styles.ts index 0a2dc686..9dd547c2 100644 --- a/src/app/(tabs)/story/styles.ts +++ b/src/app/(tabs)/story/styles.ts @@ -57,6 +57,17 @@ const styles = StyleSheet.create({ process: { marginBottom: 16, }, + backToTopButtonText: { + fontFamily: 'Manrope-Regular', + fontSize: 12, + fontWeight: '800', + textAlign: 'left', + color: 'black', + }, + bottomReactionContainer: { + flex: 1, + justifyContent: 'space-around', + }, }); export default styles; diff --git a/src/components/ContentCard/ContentCard.tsx b/src/components/ContentCard/ContentCard.tsx index b3c1d35f..181d0b9a 100644 --- a/src/components/ContentCard/ContentCard.tsx +++ b/src/components/ContentCard/ContentCard.tsx @@ -39,7 +39,7 @@ function ContentCard({ (async () => { const temp = await fetchAllReactionsToStory(id); if (temp != null) { - setReactions(temp.map(r => r.reaction)); + setReactions(temp); return; } @@ -76,8 +76,8 @@ function ContentCard({ - - + + diff --git a/src/components/FilterModal/ChildFilter.tsx b/src/components/FilterModal/ChildFilter.tsx new file mode 100644 index 00000000..3b93abd1 --- /dev/null +++ b/src/components/FilterModal/ChildFilter.tsx @@ -0,0 +1,27 @@ +import { CheckBox } from '@rneui/themed'; +import { memo } from 'react'; + +type ChildFilterProps = { + id: number; + name: string; + checked: boolean; + onPress: (id: number) => void; +}; + +function ChildFilter({ id, name, checked, onPress }: ChildFilterProps) { + return ( + onPress(id)} + iconType="material-community" + checkedIcon="checkbox-marked" + uncheckedIcon="checkbox-blank-outline" + checkedColor="black" + /> + ); +} + +export default memo(ChildFilter); diff --git a/src/components/FilterModal/FilterModal.tsx b/src/components/FilterModal/FilterModal.tsx index c2ded653..16fc7263 100644 --- a/src/components/FilterModal/FilterModal.tsx +++ b/src/components/FilterModal/FilterModal.tsx @@ -1,11 +1,14 @@ import { BottomSheet, CheckBox } from '@rneui/themed'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { View, Text, ScrollView, Pressable } from 'react-native'; +import { FlatList } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; -import 'react-native-gesture-handler'; +import ChildFilter from './ChildFilter'; +import ParentFilter from './ParentFilter'; import styles from './styles'; import Icon from '../../../assets/icons'; +import { TagFilter, useFilter } from '../../utils/FilterContext'; type FilterModalProps = { isVisible: boolean; @@ -13,33 +16,32 @@ type FilterModalProps = { title: string; }; +export enum CATEGORIES { + GENRE = 'genre-medium', + TOPIC = 'topic', + TONE = 'tone', +} + function FilterModal({ isVisible, setIsVisible, title }: FilterModalProps) { - const [checked1, toggleChecked1] = useState(false); - const [checked2, toggleChecked2] = useState(false); - const [checked3, toggleChecked3] = useState(false); + const { dispatch, filters } = useFilter(); - const genres = [ - { - title: 'Fiction', - state: checked1, - setState: toggleChecked1, + const toggleParentFilter = useCallback( + (id: number) => { + dispatch({ type: 'TOGGLE_MAIN_GENRE', mainGenreId: id }); }, - { - title: 'Erasure & Found Poetry', - state: checked2, - setState: toggleChecked2, - }, - { - title: 'Non-Fiction', - state: checked3, - setState: toggleChecked3, + [dispatch], + ); + + const toggleChildFilter = useCallback( + (id: number) => { + dispatch({ type: 'TOGGLE_FILTER', id }); }, - ]; + [dispatch], + ); return ( {title} - - {genres.map(item => { + { + const [_, parentFilter] = item; return ( - item.setState(!item.state)} - iconType="material-community" - checkedIcon="checkbox-marked" - uncheckedIcon="checkbox-blank-outline" - checkedColor="black" - /> + <> + + + { + return ( + + ); + }} + /> + ); - })} - + }} + /> diff --git a/src/components/FilterModal/ParentFilter.tsx b/src/components/FilterModal/ParentFilter.tsx new file mode 100644 index 00000000..d763e0a5 --- /dev/null +++ b/src/components/FilterModal/ParentFilter.tsx @@ -0,0 +1,26 @@ +import { CheckBox } from '@rneui/themed'; +import { memo } from 'react'; + +type ParentFilterProps = { + id: number; + name: string; + checked: boolean; + onPress: (id: number) => void; +}; + +function ParentFilter({ id, name, checked, onPress }: ParentFilterProps) { + return ( + onPress(id)} + iconType="material-community" + checkedIcon="checkbox-marked" + uncheckedIcon="checkbox-blank-outline" + checkedColor="black" + /> + ); +} + +export default memo(ParentFilter); diff --git a/src/components/PreviewCard/PreviewCard.tsx b/src/components/PreviewCard/PreviewCard.tsx index 71f3589b..53164d4e 100644 --- a/src/components/PreviewCard/PreviewCard.tsx +++ b/src/components/PreviewCard/PreviewCard.tsx @@ -1,6 +1,6 @@ import * as cheerio from 'cheerio'; import { Image } from 'expo-image'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { GestureResponderEvent, Pressable, @@ -24,7 +24,7 @@ type PreviewCardProps = { storyId: number; author: string; authorImage: string; - defaultSavedStoriesState?: boolean; + defaultSavedStoriesState?: boolean | null; excerpt: { html: string }; tags: string[]; reactions?: string[] | null; @@ -39,7 +39,7 @@ function PreviewCard({ authorImage, excerpt, tags, - defaultSavedStoriesState = false, + defaultSavedStoriesState = null, pressFunction, reactions: preloadedReactions = null, }: PreviewCardProps) { @@ -54,7 +54,7 @@ function PreviewCard({ (async () => { const temp = await fetchAllReactionsToStory(storyId); if (temp != null) { - setReactions(temp.map(r => r.reaction)); + setReactions(temp.filter(r => r != null)); return; } setReactions([]); @@ -101,7 +101,7 @@ function PreviewCard({ - + {(tags?.length ?? 0) > 0 && ( diff --git a/src/components/ReactionDisplay/ReactionDisplay.tsx b/src/components/ReactionDisplay/ReactionDisplay.tsx index 6b70eb1e..22f490b0 100644 --- a/src/components/ReactionDisplay/ReactionDisplay.tsx +++ b/src/components/ReactionDisplay/ReactionDisplay.tsx @@ -2,20 +2,27 @@ import { Text, View } from 'react-native'; import styles from './styles'; import Emoji from 'react-native-emoji'; import globalStyles from '../../styles/globalStyles'; +import { Channel, usePubSub } from '../../utils/PubSubContext'; +import { useEffect, useState } from 'react'; type ReactionDisplayProps = { reactions: (string | null)[]; + storyId: number; }; -function ReactionDisplay({ reactions }: ReactionDisplayProps) { - const cleanedReactions = reactions.filter(reaction => reaction !== null); - const reactionColors: Record = { - heart: '#FFCCCB', - clap: '#FFD580', - cry: '#89CFF0', - hugging_face: '#ffc3bf', - muscle: '#eddcf7', - }; +const reactionColors: Record = { + heart: '#FFCCCB', + clap: '#FFD580', + cry: '#89CFF0', + hugging_face: '#ffc3bf', + muscle: '#eddcf7', +}; + +function ReactionDisplay({ reactions, storyId }: ReactionDisplayProps) { + const { channels, getPubSubValue } = usePubSub(); + const [reactionCount, setReactionCount] = useState(0); + + const cleanedReactions = reactions.filter(reaction => reaction != null); const defaultColor = reactionColors['heart']; const setOfReactions = [...cleanedReactions]; setOfReactions.push('heart'); @@ -23,6 +30,24 @@ function ReactionDisplay({ reactions }: ReactionDisplayProps) { setOfReactions.push('muscle'); const reactionDisplay = [...new Set(setOfReactions)].slice(0, 3); + const serverReactionCount = cleanedReactions?.length ?? 0; + + useEffect(() => { + setReactionCount(serverReactionCount); + }, [reactions]); + + useEffect(() => { + const value = getPubSubValue(Channel.REACTIONS, storyId); + if (value == undefined) { + return; + } + + if (value) { + setReactionCount(serverReactionCount + 1); + } else { + setReactionCount(serverReactionCount); + } + }, [channels[Channel.REACTIONS][storyId]]); return ( {reactionDisplay.map(reaction => { - if (reaction === null) return; + if (reaction == null) return; return ( - {cleanedReactions?.length ?? 0} + {reactionCount} diff --git a/src/components/ReactionPicker/ReactionPicker.tsx b/src/components/ReactionPicker/ReactionPicker.tsx new file mode 100644 index 00000000..637a402e --- /dev/null +++ b/src/components/ReactionPicker/ReactionPicker.tsx @@ -0,0 +1,81 @@ +import { faFaceSmile } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { useEffect, useState } from 'react'; +import { View, TouchableOpacity } from 'react-native'; + +import styles from './styles'; +import Emoji from 'react-native-emoji'; +import { + addReactionToStory, + deleteReactionToStory, +} from '../../queries/reactions'; +import { useSession } from '../../utils/AuthContext'; +import { Channel, usePubSub } from '../../utils/PubSubContext'; + +type ReactionPickerProps = { + storyId: number; +}; + +const ReactionPicker = ({ storyId }: ReactionPickerProps) => { + const { user } = useSession(); + const { publish } = usePubSub(); + const [showReactions, setShowReactions] = useState(false); + const [currentReaction, setCurrentReaction] = useState(''); + + const toggleReactions = () => setShowReactions(!showReactions); + const reactionMapping: Record = { + heart: 2, + clap: 3, + muscle: 4, + cry: 5, + hugging_face: 6, + }; + + const handleReactionPress = (reactionName: string) => { + if (currentReaction == reactionName) { + removeReaction(reactionName); + } else { + addReaction(reactionName); + } + }; + + const addReaction = (reactionName: string) => { + setCurrentReaction(reactionName); + publish(Channel.REACTIONS, storyId, true); + + const reactionId = reactionMapping[reactionName]; + addReactionToStory(user?.id, storyId, reactionId); + }; + + const removeReaction = (reactionName: string) => { + setCurrentReaction(''); + publish(Channel.REACTIONS, storyId, false); + + const reactionId = reactionMapping[reactionName]; + deleteReactionToStory(user?.id, storyId, reactionId); + }; + + return ( + + + + + {showReactions && ( + <> + + {Object.keys(reactionMapping).map((reaction, i) => ( + handleReactionPress(reaction)} + > + + + ))} + + )} + + + ); +}; + +export default ReactionPicker; diff --git a/src/components/ReactionPicker/styles.ts b/src/components/ReactionPicker/styles.ts new file mode 100644 index 00000000..5a5a7052 --- /dev/null +++ b/src/components/ReactionPicker/styles.ts @@ -0,0 +1,29 @@ +import { StyleSheet } from 'react-native'; + +import colors from '../../styles/colors'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'flex-end', + // flexDirection: 'row' + }, + reactionView: { + borderRadius: 20, + padding: 10, + alignSelf: 'center', + marginBottom: 10, + }, + reactionsContainer: { + flexDirection: 'row', + gap: 5, + justifyContent: 'space-between', + padding: 10, + position: 'absolute', // Positioning the container above the toggle button + bottom: 50, + backgroundColor: '#D9D9D9', + borderRadius: 20, + }, +}); + +export default styles; diff --git a/src/components/SaveStoryButton/SaveStoryButton.tsx b/src/components/SaveStoryButton/SaveStoryButton.tsx index be44ebb9..307d1d7e 100644 --- a/src/components/SaveStoryButton/SaveStoryButton.tsx +++ b/src/components/SaveStoryButton/SaveStoryButton.tsx @@ -4,7 +4,7 @@ import { deleteUserStoryToReadingList, isStoryInReadingList, } from '../../queries/savedStories'; -import { usePubSub } from '../../utils/PubSubContext'; +import { Channel, usePubSub } from '../../utils/PubSubContext'; import { useSession } from '../../utils/AuthContext'; import { Image } from 'expo-image'; import { TouchableOpacity } from 'react-native-gesture-handler'; @@ -25,7 +25,7 @@ export default function SaveStoryButton({ const [storyIsSaved, setStoryIsSaved] = useState( defaultState, ); - const { channels, initializeChannel, publish } = usePubSub(); + const { publish, channels, getPubSubValue } = usePubSub(); useEffect(() => { if (defaultState != null) { @@ -34,20 +34,18 @@ export default function SaveStoryButton({ isStoryInReadingList(storyId, user?.id).then(storyInReadingList => { setStoryIsSaved(storyInReadingList); - initializeChannel(storyId); }); }, [storyId]); useEffect(() => { - // if another card updates this story, update it here also - if (typeof channels[storyId] !== 'undefined') { - setStoryIsSaved(channels[storyId] ?? false); + if (getPubSubValue(Channel.SAVED_STORIES, storyId) != null) { + setStoryIsSaved(getPubSubValue(Channel.SAVED_STORIES, storyId) ?? false); } - }, [channels[storyId]]); + }, [channels[Channel.SAVED_STORIES][storyId]]); const saveStory = async (saved: boolean) => { setStoryIsSaved(saved); - publish(storyId, saved); // update other cards with this story + publish(Channel.SAVED_STORIES, storyId, saved); // update other cards with this story if (saved) { await addUserStoryToReadingList(user?.id, storyId); diff --git a/src/queries/reactions.tsx b/src/queries/reactions.tsx index ffe68c40..f72c9e00 100644 --- a/src/queries/reactions.tsx +++ b/src/queries/reactions.tsx @@ -2,7 +2,7 @@ import { Reactions } from './types'; import supabase from '../utils/supabase'; export async function addReactionToStory( - input_profile_id: number, + input_profile_id: string | undefined, input_story_id: number, input_reaction_id: number, ): Promise { @@ -22,7 +22,7 @@ export async function addReactionToStory( } export async function deleteReactionToStory( - input_profile_id: number, + input_profile_id: string | undefined, input_story_id: number, input_reaction_id: number, ): Promise { @@ -43,17 +43,18 @@ export async function deleteReactionToStory( export async function fetchAllReactionsToStory( storyId: number, -): Promise { +): Promise { const { data, error } = await supabase.rpc('curr_get_reactions_for_story', { - input_story_id: storyId, + story_id: storyId, }); + if (error) { console.log(error); throw new Error( `An error occured when trying to fetch reactions to a story', ${error}`, ); } else { - return data as Reactions[]; + return data[0].reactions; } } diff --git a/src/utils/PubSubContext.tsx b/src/utils/PubSubContext.tsx index fbb9ada6..9079389f 100644 --- a/src/utils/PubSubContext.tsx +++ b/src/utils/PubSubContext.tsx @@ -1,9 +1,17 @@ -import React, { createContext, useContext, useMemo, useState } from 'react'; +import React, { createContext, useContext, useState } from 'react'; + +export enum Channel { + REACTIONS = 'reactions', + SAVED_STORIES = 'saved_stories', + FAVORITES = 'favorites', +} + +type channel = Record; export interface PubSubState { - channels: Record; - initializeChannel: (id: number) => void; - publish: (id: number, message: boolean) => void; + channels: Record; + publish: (channel: Channel, id: number, message: boolean) => void; + getPubSubValue: (channel: Channel, id: number) => boolean | undefined; } const BooleanPubSubContext = createContext({} as PubSubState); @@ -26,28 +34,26 @@ export function BooleanPubSubProvider({ }: { children: React.ReactNode; }) { - const [channels, setChannels] = useState>( - {}, - ); - - const initializeChannel = (id: number) => { - if (!(id in channels)) { - setChannels({ ...channels, [id]: undefined }); - } + const [channels, setChannels] = useState>({ + [Channel.FAVORITES]: {}, + [Channel.REACTIONS]: {}, + [Channel.SAVED_STORIES]: {}, + }); + + const publish = (channel: Channel, id: number, message: boolean) => { + let thisChannel = { ...channels[channel], [id]: message }; + setChannels({ ...channels, [channel]: thisChannel }); }; - const publish = (id: number, message: boolean) => { - setChannels({ ...channels, [id]: message }); + const getPubSubValue = (channel: Channel, id: number) => { + return channels[channel][id]; }; - const authContextValue = useMemo( - () => ({ - channels, - initializeChannel, - publish, - }), - [channels], - ); + const authContextValue = { + channels, + publish, + getPubSubValue, + }; return ( From 3148c48db1b18354e91cff4432e0611c33013d5a Mon Sep 17 00:00:00 2001 From: Aditya Pawar <34043950+adityapawar1@users.noreply.github.com> Date: Tue, 23 Apr 2024 21:21:26 -0700 Subject: [PATCH 2/6] Fix font on tab bar (#95) --- src/app/(tabs)/_layout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx index eb7592c6..13c69336 100644 --- a/src/app/(tabs)/_layout.tsx +++ b/src/app/(tabs)/_layout.tsx @@ -4,6 +4,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Icon from '../../../assets/icons'; import colors from '../../styles/colors'; +import globalStyles from '../../styles/globalStyles'; function HomeIcon({ color }: { color: string }) { return ( @@ -36,7 +37,7 @@ function TabNav() { Date: Tue, 23 Apr 2024 21:49:37 -0700 Subject: [PATCH 3/6] [saved] Implement the favorited stories button (#90) * Added favorite button and updated styling * fixing storyObjects bug * Add pubsub to favorites * Run prettier --------- Co-authored-by: Kyle Ramachandran <156966341+kylezryr@users.noreply.github.com> Co-authored-by: Kyle Ramachandran Co-authored-by: Aditya Pawar <34043950+adityapawar1@users.noreply.github.com> Co-authored-by: Aditya Pawar --- assets/icons.tsx | 1 - assets/save_story.png | Bin 1052 -> 0 bytes assets/saved_story.png | Bin 12911 -> 0 bytes package-lock.json | 22 +++++ package.json | 5 +- src/app/(tabs)/library/index.tsx | 21 ++++- src/app/(tabs)/story/index.tsx | 31 ++++--- src/app/(tabs)/story/styles.ts | 5 ++ .../FavoriteStoryButton.tsx | 76 ++++++++++++++++++ .../SaveStoryButton/SaveStoryButton.tsx | 36 ++++++--- src/queries/savedStories.tsx | 20 ++++- 11 files changed, 187 insertions(+), 30 deletions(-) delete mode 100644 assets/save_story.png delete mode 100644 assets/saved_story.png create mode 100644 src/components/FavoriteStoryButton/FavoriteStoryButton.tsx diff --git a/assets/icons.tsx b/assets/icons.tsx index 44728bb1..df93d8cc 100644 --- a/assets/icons.tsx +++ b/assets/icons.tsx @@ -110,7 +110,6 @@ const IconSvgs: Record = { `} /> ), - home_inactive: ( Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91JfH&r1ONa40RR91JOBUy0E^%0TmS$AhDk(0RA>e5n@@7uFciimr$e`7 zhCjm&P7v1VhMpkVWYbNP6Eryi^aM>$(8;zeN=^{ENH^V(6Qo@wfn;E^B&pr+b1h;E2p>P%^ zpbQq?-QDePZEbx)!+h{wlCB94d0Rp7Dd53i@ClWUJ+Q82S)K8Cd|s(k*x2~g@AvBv zl1CH3Rpd;qVzKz~->L8O;o+ePUuKrbW`fVi zqM-RzouJ&`-(P#ciMQ+o$~yrUE$teUld(|NMSdNuoR(_(yQfn;M+^dj^sVI7We(n$i%k7S&sd<5^duoYdS(=jKw9*6>g(U-yqNOC) z$t;{?>qU`$<1GA2r@nU*mXIMN=))V14f=4r*LT@(7=xLG;_HeuMsDMM!AIJIj+ z-`aK91W&7fuzLz$U`I&ND}KE^SwwMKU=uto54IuPy_rRDh0p@0&C}8Xm)bw*$iH1o zujaJQW@&S55sxI9h2znyIlX2UHpdq6NTS^USeB<1wpTUrpfqe!iaLA_ZvBkld~lhZ zpF%t`^;e5_9bT5F6}DG3@u235?{~1Uza|=*>cZb3SXT?YEKe(JugVe5m2@KfhX)Ho z@W>Wszd=xStsT;;;yUxe!J7-$vX1B$+n4|4^{FA9&C=_%S$Gv%UgtA13$IiA zt9W*1;ZQ8bmrI=+zT z9%pIy_a(shlL!-0X8zg|h$sEc;L`8BK;WetRm=0VB}580$ppr4fxLNn~&jVDeU zEM9H+KsFT*Zf$RGk1(8%JPkB`<2~{0FeYC?Y7t~69vrU$oKh3CHY3NAPD{&z$sM_P z-@%0@)71V7V0ptG@80a)V&HhGRB|UaHZVDHrOD-TdyttEaD%eK!k;1k#DG)4tbieX zh|K@q4~vtSMgTu5m&+Z$@H4>bCc$B4&fHZTbyaS1&QS@A_V9h}K0ZF4)oQg-P~N|w WSfpXwp?D_%00005M}G(m zG(mT@JH+>VIR4Znc`1SMqsgbeN)=4c|xLCJoX0j?MOq< z)JT2Gp1OqU;9TAF!w+H>lbWL_C-Lt2t~k};T91+Ud36exmo(-_)CIg|`VbnM@!|O0 zusHpt!R3;BSF#>c_o|ZfXAulKYqV{R{nx2dCxy$Luw}4*xB#UFF6s7V)K5cB=V2S^ zpK`@lj@2a1<2a8(WJ$wH@M@KSq&=|^-*!T0Q!YHwogSI@u3Y{;*X6*%2Ea<%r0A>H z6ADmSE9Sb~VT;J^skV9xFXkJz2P}X8Ha~ruj$jBQRDh-BH+?>PI<*dTNf+on~ zsf(TwO;3WV^YyApN*MJXnIvy|ZN+PSO@o((#eNWbOl#QuswP?-vfatOIgBGY;;ZIW z`_d!oXvyBG%fC{C;@8b~jqAgaqF7umUgjUW9uIN)v7MbiX~fWg(XU38A17Qm$6MA- z3_(Niq_vcjrJZm^R*oM|?C)1UXZUy65PWbY zjC1>F-6D;+8;4JYPj*8qfiq5ijFVmNI9Qjk z%PL?0WC@Er-)p7tW}89lSLaiglqXBL2h^t27!pIxIV&1TUAd&v-M2|Yx|wCmdQKWZ zJIJ+L9!*4SMldAG*>0VA`}W*+k@4`+5rIC5-pOZv(cWS7)9~piXQfc4X?r2&lHV8G zG`mXg(?3I@5!lAP>6Mj%i?i=zNSYaHgaWaE_JQ?V0MCLj8Ki8*x3&zC(}jCZwp=aE z3w`y*HZ#D9-6zhqA2)bsP@zG{z+{Z2GA5rSgL-9~88eq91szR*(hEyEW*|M*$FWE% zT->NQrPJl_6fDQ(3PZroMr@L5g)`BCJ}&m}}1S zIi}y|8SZijVh0>mBmTUn1x^}=5i+guIeuD+%ccqHp|Ba1jc1L%N*YN@@wC61oZ`o2 zLJ+2ilnsPtV8IN<3CC>S3`NvKlo^#6M{4QStC$^-u%{8apIKUPH+dySu3@v&DrXwk z7^4U>s}&o>V@fPiZgUwJl4xJ*P9PQU#fj0Ty&YO1#fLN7VbQ*5Eer$-b9*tKZia8m z8*HB6;?8&}p|kwz^YENVSW6!3@$_rEmn0?hY^NIkNH2sRhVJ2%BR(ht2FIULtG9BG z6o8x&GAlw7jEQnGJ*wPY@VU(U138Oec9!=ht|1Cg%Hdi@n^{0g0)^zGvnsDhJXmWM%99!6@@R;Ln z)_#@Tb4!lmJRQ6AKk~IP1*=@?)2!dm8!hP|5v!~~*SEL%@w7yXYks&?=97^aWjT6e z=69{=T|CRuMNWvAU0+YA=6tpq#_jUtX$?+5>#Wpgi_{xB;yNqmn@_LP_UYB1`P_Fb zq@<&A#WYRKggN`Hq;j-S`@IFdzj>cVReTp?-q%ektmJ&klQ+~~l3?s9)Njsn(@^H> z2#$Ox*1q(dt!#cYB4LE4wK3r%SI&a42e5I&jiEgko*)!U8{1-D@cab=zK2VOB{d!= zgbCgm>vnpKKxzTfItD zwh%gKsrt)G<9jKZbb+ z?x?)g{r4)~JVBT{(!VTA%MrKO@R9Q_C?jU4^9WzmdP4peL_=?oihq1=9e!2&BPB5q z$JHyq((RO%JOTsGknu_N2`q4y^Yp6!E%#BS_>fleH4Dxjr6Cbr9s+f8oPdI6Msnaj)lF-6uJDPBjcoZ@AI;msZuQ=LT_rC`#lf7ERs;k z7gvt?O-K2uxf+sj=S#@VJ1_9C1fX^QqdY`O|s7iSI=sDcRura8q8Dc>X*&nQwS+)O{ze71e$Ds39UnKc@c9z<8GHy#ka? zh&k8YV*#~qS~EA9efq5=Z_2WLOb9|z#Cb$MErcg3W+NS17r(uGqpoYfx2JSEv$sLBvx`EqIVGRVR}})G2b_nEvNS)#H<4)Olddys z3NRP69d8{~G3K2VE$MKv{ZmG1lfJ7>u=zXwen;WAdMx+&*1qYQDf(c%fiJ&D%*R{n zwX$8Se_2bdp(cz(qpS{G;4hxUTp6LuqX^!NlSvWAd`wJLVqJ^ zNXzkVMvggO(&Fu*dnUoPNhOwL_QdA#}EaqJ+$P?mZ1*RnZ z$vL%syao12iqbb*nfdTnw*#xIl^*B+Izd@tjaILGR|xdE6ujkpS_t1Mk@Ou9h|l-l z4E$ag6a*lt@(xe2;MRKsd!4G&b#}Rrv+}9ujm20D-{j*D;&Jb`#n&zm#rqsiD9j6`p2ch9E+<4LCgYYptro1RmEaDuyEu(B$BYrJa(Hp z7IQNdj{r|j)D^N2{4YS>7=LpVkY|Ly1RWi24DKCvYPkSm2! z@VF;ykfQrHh|&+=kUipv$Eg}nP-DUbiM4DoK8Ej%^?kOhxk>jh*VoM;7S`Jo36 zi0KZI9Ld8|6|UyP7x_?)0SEb@qRGFedD6pQ;@`IPa1#0yTAF`#@syix$U|3A0s!J& zs2D!<@&sg^#@hs`h{Z&0j3V%_6`uZb_%T}~Fn||yV?{m(Jo9Sdy{F(WfFZgOGJeMb zJQa=c;{`XMOdO*T$>YG9YeD}W^JeP5+BCQN*J!i0{vUlaCw+zeI@P{t` z3Xu|}d;{R{b3?7qx0emB-j=@xKmUvwix;3|!d$C=HfPRCY~_RHu(crT&FW46UJCzz zUkd*>_wB!UJp=#uS_*;jZePjI?aX1qkBFKispZW#W>Qt5e@z>OTR$#@SHvDhZUR!o+z zE{z*_M2e3}(;^wOifDS9j4qW=Yu7{znO%)Lj6;>S?&iH;*GrAcfPt-5)Pz1>-Uhdc zk^#0TzBo!{M_%4o%uO8pki_VLxohUHlQgILcIH7r3gsG-E&=9V)ea|VCb%NKpz|>= zB3`XGeZfwfyG7FM>)WNq9VvtyLYWHFx;RTcklZZsr;;TRjC03QZgM%Bk$fXZyEt&R z8AQjW`Qp6O4oSh!o3Lm5ko5a6PDV=75A z<>;3i&{33CuxJZUGL>CCoaMx2BRN`Btu zP+ph91nl<@JUnB8OZ+>qWv z(w^k}68(F-?-C-kw?8InzJ4%rw8;9#Yh8N@mdb7a+5k9%cGZd91pd3S zk-W{AnC`Z=g+|g89ZI%R#ME6_Y(8269p57BNCsK;`dC{wsip2c`yd>y*mWEQqq5QW zHB;oHGQWJt^~|Oc?RUOb08gW{v~3_VK60l?GXzb$zcbsS0!`Pt`J;RdjG9n~LkP0S zs3WvKhBZ2jaA^;kzK9CBq=@N&9|2SEej2bMe*u<$ zMdFcdhDyIR!Q%i14N1s&wX^ONoN=b!%iCF);FlE4nMIi{ugeZz%_7r|2)7lH&}XroN1^v8 z#u*5Ehhu=88m4u3!{)pVblM}ePGb5~lK;%(?4&t`DWhMD_H{jlmp>9~2CfPrO@H(% zP}$E%jNXhP7<(}(Cr(fjsqD>Y^*51>0j)S|&E(HE4N4e`fb*0^NjhwuiJBV1Lx`|s zlHjY^eP4=pr6_s&E}XH=N@M>Nl^DO5{K=XkO=ag=E6L9G{W2zo)i~B;F|B{SrH+fz zQG;qJ+i|m`;vK-{-#QI2DaP#Kz};1cak=Mm?|xLmoLp`1tw`CnhS37%2lPoB6N8F6 zT{mG`4F}eai}PNuq+C{xCj?8SH0vAQyR#J$3|))RdnG7>B1>;F9d=|bA?eEI0rGO& z23ng;-7JztF1W7X+e2lC-?7}C$ZVk!`@rh?z2?Sja`h?OWCvXCar#NAO_-ar_68Ot za~3%Ve3$E%F(!6Sy-$u4qYvvZQ`&Z5*1^AMGEj%MVAjb2bTaZ%%OJP7KK8KOfwA3X zF1!g(8iTnxTqfIUrR?^~1Y1%r=? ziN;<@$+p&fIFekijH&Yug5?Pah^a!o0B1Ve%){|y!2$A}??}@co%-4BRQ9p0K0R<` zBw93fF~(G7Y-FU>0$48scra2KE9<2tnnB*BT^Qm3G6;$QcSNo6;2uF|K=+6}$Snk} z0qjswNV78$8)!|*vNx5%1D|&%n8G6&H62lzo4cNjWm1W)pGtIKa1T+@*jrXam0Y)= z*G>^6w2!hLz&EC#;J~PxSjHQTw+_G!9iBkd5()(o$(Y(3l}XXQ)oh>yj93(c%6MjX zO#*f^luVWfvUz!JbqwuwNx{Dz%~PJWD0}sUsMP*-!EYL}~ITojOM#Y4wCv z&;gE+b^t1^jN>tBj>`>r(q1pr>PGwbbv%X5`j{g66 zpsx}>1g*Ks2cF%vACK$&yDl2y$%*6v@l=^M+>%1l%s%6!CI~AMyJHUYeyT*hM#JuR z;+5n_83iLdFFtkAjRlurY+exndn_H_0(Oe(*eu-vp8p-|Avsa-#eh~vYPRsd_S-s; z{t|zS0qzT~`(3mI=?9*mRJL2|0j8p~!>2t#R;6U$dg%UehGo5AgS1w6af#n@_wrZ} zXroxpJNCKd!9F}L_eA`yy+B1fU|U`gZ?7!MtD$EllBdQIjD`B%cS53f%JBJs&%}jS zU&}7Ca<-=52F6T!2P=tE%pkw3*hv7GK-V3G!h~A5hme^u@v*v%ts5&R_nhNeAZ0D_ zJ0alkT1gf5a%hlcLP6KlGYwoTBv7Ua@B;jcCKl| z+v~86M@bWa79f93YArB&2wNbgl9&#VK)WnyB`z$LHhq6kU8WomS}O;3IuKL+?i7Nd z`r3mChQ8T~vjI#^*B#1g1y@uL(32pn60vv%)WGMF$s{cB1?>K8=qcKb&{_wW>JGAe zIQu6WIpmn`3Fa#sd{s@7qUXxynL;83As-;VtK0wizZZ=V23-Zl?EWE3op@l3qW7Ai zK#Exd%LCFsk^LWqiaXDkD_k|D?;^Y65?S%;34p0B2uW}zRb52M5ny*V+qflr<|~+& zjz>g-0*noaqvA~pK>EN*;w?b(laMdU`z7j($I%Qo{jR(G&$`V2&WTT}s(yZ~*z>XA zlv)m74tQ)Pw`S+WnT_K-3LURe+pyHkYcBxil>{|N2@yJqC7>V@NX{5QcE)RlE?1&> zY&OW|9&u9V{{k65bclW;zhZPA)h$BkJGM0WH70t*ugx|Znh$}MW&e!K?WFvkP$Q`K z;KlCjO}zQxcuR`Hui;%Rm95Vp{DnZ|7yy~CXZ?ZuL6c8N28+_YFZpJD-xPOu_9lO$ z7sz4}m$Zumy)_>vcY!M5ow7d(Q|vb^w`HVv`Df*DmsPJuOLYCUZ7sy)5P1YMf!p_zAg!xjBWA=_j zuRpd^j6S_~uLPfXg_*(1@nS;*v-kr^7b%scY%tJ+)1P#(P^;o&HKtlzg2%Za41s`3 z%le?z=&D}ux3?dojRN-M_eb8~H)1>yny8L>Izvln*!50JTK%+FM2QjVAb=^?q|VO4$Z&ondZoeW$)sch=e@C7DBf2pP{{l0$(KP7w@wh8NXW(u-W*f4%0Z7;HAB9 zU082S+I7vcCI3tq%owk|5Q<>TDHTYsGuZrl{qx9)YR8tg2|4<)-S0rd0^bTVKpTWQ z=)ox2`u#w=N!+^yKK(Ib&d|Ryr(~MOo|~KV_AvQfCO;w)B+J_ipPmD`{O+x;Izj1I zleB$yOnT-W#CbIMj@&#=+hKj~x2xvz)M2s)^nXZa#lASlx6cC zD}Ho&$TY`pIldc`<#l%<*Qz#tmh8;@HpY~`LxGM$JHMe1M;=@?^6wtp_+?q6 zS6UBKg1ESOj<9QbGan*|r11~e>ljkKmimkRZbu`CPAm0l4QIO_N^!G3z#i6%L%m>? z;ZN2mmbQe{y^rSaF9DmZX!PcMGpoG#WDVnKSA|0L$BRpEmnx;_kMr$=X-xXQH#V== zJ5ALV7-HRpUX_4-v3$2!FW$b?_Ur9xMj$0s`w5%BY85QBFVW}=Y3Kr@%8513r81u* z=EBNn??n*zm=BP9Wje=r8rICB4~ z2bY&e*O+d75q1_hu~@ARX&0WA8rnd##d%06 z%Leppj)hV|g0LT=4pKD7s4*&dn#R?6-Q-zsG~Qe#R|``jyyv?Drx!w-=)1qW#ZZXD zCcsk2p`oFrag(`I(gzGqhi?1eRbr#gZ!6S8#5xD(K@G=4wd)d$Zb@{}MCqNcA{j%K z{dg`r);IAVty^|ffp$b$+mhGnyo8dH&{+`Ei_;?+_Y8)xyn#{7`x#_AaO&LSg-&p; zBkS0tTd5>EgG2w);zQ{FU5ReOi)xL5hny%vYWU?`%l3OrJ{cp~vAN)x2!Z`}`@S_;3+4Fq3{$@rwIi=%Q zORk-}BSzO?%za=|3dmZ*b!TT9=2FSu z98Cx^nceX6)tgyl0*n0EM(~U`=jbCBu)zl_QZrr&Q-{N--|D{1aa{Ub3}VjuDeYG@ zNO9?UlsPs1*p-Q_;Wu})?F`+omy;VX`SH0Dr>4#$?zOmz^LoJdQ7S29wrk6j>cLqV zwJ@6n=Q9s-CeJx*U+FsQ>@UadhungYzc5!j3_9%9y)wqD!&8KHsV~bMNt{<6J|!ha1xm9WwmiTc2OZisON_R<{#@Y%E^i#S zv++}x?Fztkp1<={h{i>M2ELF8fvtxY!3}De_w6%ekjxB@qv_ShjA(TU#l6* zHQt3P%LApH^PITq<)P)4%6_hwP%X83cjzwj=$s7+Od&C)B{uTUb zBbcDbouE%~w@UVkH3*!;QddK0cQhU#QMT0-0fj=Y>IAChI JufUi@{VzM@4g>%I diff --git a/package-lock.json b/package-lock.json index 4bd8250b..65702c95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,7 @@ }, "devDependencies": { "@babel/core": "^7.20.0", + "@iconify/react": "^4.1.1", "@types/react": "~18.2.14", "@types/react-native": "^0.72.3", "@types/react-native-htmlview": "^0.16.1", @@ -3773,6 +3774,27 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@iconify/react": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@iconify/react/-/react-4.1.1.tgz", + "integrity": "sha512-jed14EjvKjee8mc0eoscGxlg7mSQRkwQG3iX3cPBCO7UlOjz0DtlvTqxqEcHUJGh+z1VJ31Yhu5B9PxfO0zbdg==", + "dev": true, + "dependencies": { + "@iconify/types": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/cyberalien" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true + }, "node_modules/@jest/create-cache-key-function": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", diff --git a/package.json b/package.json index d058a0c8..bea0a195 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "expo": "~49.0.11", "expo-constants": "~14.4.2", "expo-font": "~11.4.0", + "expo-image": "~1.3.5", "expo-linking": "~5.0.2", "expo-router": "^2.0.0", "expo-status-bar": "~1.6.0", @@ -72,11 +73,11 @@ "react-native-vector-icons": "^10.0.2", "react-scroll-to-top": "^3.0.0", "use-debounce": "^10.0.0", - "validator": "^13.11.0", - "expo-image": "~1.3.5" + "validator": "^13.11.0" }, "devDependencies": { "@babel/core": "^7.20.0", + "@iconify/react": "^4.1.1", "@types/react": "~18.2.14", "@types/react-native": "^0.72.3", "@types/react-native-htmlview": "^0.16.1", diff --git a/src/app/(tabs)/library/index.tsx b/src/app/(tabs)/library/index.tsx index 2542da9e..3d50558a 100644 --- a/src/app/(tabs)/library/index.tsx +++ b/src/app/(tabs)/library/index.tsx @@ -13,7 +13,7 @@ import { fetchUserStoriesReadingList, } from '../../../queries/savedStories'; import { FlatList } from 'react-native-gesture-handler'; -import { usePubSub } from '../../../utils/PubSubContext'; +import { Channel, usePubSub } from '../../../utils/PubSubContext'; function LibraryScreen() { const { user } = useSession(); @@ -25,6 +25,7 @@ function LibraryScreen() { ); const { channels } = usePubSub(); let updateReadingListTimeout: NodeJS.Timeout | null = null; + let updateFavoritesListTimeout: NodeJS.Timeout | null = null; const favoritesPressed = () => { setFavoritesSelected(true); @@ -60,6 +61,20 @@ function LibraryScreen() { ); }; + useEffect(() => { + if (updateFavoritesListTimeout) { + clearTimeout(updateFavoritesListTimeout); + } + + updateFavoritesListTimeout = setTimeout( + () => + fetchUserStoriesFavorites(user?.id).then(favoriteStories => { + setFavoriteStories(favoriteStories); + }), + 4000, + ); + }, [channels[Channel.FAVORITES]]); + useEffect(() => { if (updateReadingListTimeout) { clearTimeout(updateReadingListTimeout); @@ -70,9 +85,9 @@ function LibraryScreen() { fetchUserStoriesReadingList(user?.id).then(readingList => { setReadingListStories(readingList); }), - 5000, + 4000, ); - }, [channels]); + }, [channels[Channel.SAVED_STORIES]]); useEffect(() => { (async () => { diff --git a/src/app/(tabs)/story/index.tsx b/src/app/(tabs)/story/index.tsx index 33ada971..281359d5 100644 --- a/src/app/(tabs)/story/index.tsx +++ b/src/app/(tabs)/story/index.tsx @@ -16,6 +16,8 @@ import { RenderHTML } from 'react-native-render-html'; import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; +import FavoriteStoryButton from '../../../components/FavoriteStoryButton/FavoriteStoryButton'; +import SaveStoryButton from '../../../components/SaveStoryButton/SaveStoryButton'; import ReactionPicker from '../../../components/ReactionPicker/ReactionPicker'; import BackButton from '../../../components/BackButton/BackButton'; import { fetchStory } from '../../../queries/stories'; @@ -117,20 +119,23 @@ function StoryScreen() { )} /> - - + + Share Story + + + { + isStoryInFavorites(storyId, user?.id).then(storyInReadingList => { + setStoryIsFavorited(storyInReadingList); + }); + }, [storyId]); + + useEffect(() => { + isStoryInFavorites(storyId, user?.id).then(storyInFavorites => { + setStoryIsFavorited(storyInFavorites); + publish(Channel.FAVORITES, storyId, storyInFavorites); + }); + }, [storyId]); + + const favoriteStory = async (favorited: boolean) => { + setStoryIsFavorited(favorited); + + if (favorited) { + publish(Channel.FAVORITES, storyId, true); + await addUserStoryToFavorites(user?.id, storyId); + } else { + publish(Channel.FAVORITES, storyId, false); + await deleteUserStoryToFavorites(user?.id, storyId); + } + }; + + const renderFavoritedIcon = () => { + return ( + + + + ); + }; + + const renderNotFavoritedIcon = () => { + return ( + + + + ); + }; + + return ( + favoriteStory(!storyIsFavorited)}> + {storyIsFavorited ? renderFavoritedIcon() : renderNotFavoritedIcon()} + + ); +} diff --git a/src/components/SaveStoryButton/SaveStoryButton.tsx b/src/components/SaveStoryButton/SaveStoryButton.tsx index 307d1d7e..dbf88956 100644 --- a/src/components/SaveStoryButton/SaveStoryButton.tsx +++ b/src/components/SaveStoryButton/SaveStoryButton.tsx @@ -1,4 +1,7 @@ import { useEffect, useState } from 'react'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import Svg, { Path } from 'react-native-svg'; + import { addUserStoryToReadingList, deleteUserStoryToReadingList, @@ -6,17 +9,12 @@ import { } from '../../queries/savedStories'; import { Channel, usePubSub } from '../../utils/PubSubContext'; import { useSession } from '../../utils/AuthContext'; -import { Image } from 'expo-image'; -import { TouchableOpacity } from 'react-native-gesture-handler'; type SaveStoryButtonProps = { storyId: number; defaultState?: boolean | null; }; -const saveStoryImage = require('../../../assets/save_story.png'); -const savedStoryImage = require('../../../assets/saved_story.png'); - export default function SaveStoryButton({ storyId, defaultState = null, @@ -54,13 +52,31 @@ export default function SaveStoryButton({ } }; + const renderSavedStoryImage = () => { + return ( + + + + ); + }; + + const renderSaveStoryImage = () => { + return ( + + + + ); + }; + return ( saveStory(!storyIsSaved)}> - {storyIsSaved ? ( - - ) : ( - - )} + {storyIsSaved ? renderSavedStoryImage() : renderSaveStoryImage()} ); } diff --git a/src/queries/savedStories.tsx b/src/queries/savedStories.tsx index 5ab847d7..13aeae64 100644 --- a/src/queries/savedStories.tsx +++ b/src/queries/savedStories.tsx @@ -48,7 +48,7 @@ async function addUserStory( ) { const { error } = await supabase .from('saved_stories') - .upsert([{ user_id: user_id, story_id: story_id, name: name }]) + .upsert([{ user_id, story_id, name }]) .select(); if (error) { @@ -128,3 +128,21 @@ export async function isStoryInReadingList( return data; } + +export async function isStoryInFavorites( + storyId: number, + userId: string | undefined, +): Promise { + const { data, error } = await supabase.rpc('is_story_saved_for_user', { + list_name: 'favorites', + story_db_id: storyId, + user_uuid: userId, + }); + + if (error) { + console.error(error); + return false; + } + + return data; +} From ea30f15b38a1e6597bf3f04f4fbe5bc95881fc29 Mon Sep 17 00:00:00 2001 From: emilysunaryo <99292788+emilysunaryo@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:32:49 -0700 Subject: [PATCH 4/6] [story] Update story page (#93) * progress on database functions to pull genre information * pushing changes before rebase * ran prettier * finished styling genre preview card component * styling complete for genre screen, need to implement tone, topic dropdown selection logic * semi finished genre screen, need to debug drop down logic * figuring out filter context * reaction styling * progress on story page styling * progress on story styling * finished styling stories * made reaction button sticky, still need correct icon * Fix quotes on except * Fix some styling * Make option bar sticky * Run prettier * Fix eslint errors * App is done --------- Co-authored-by: Aditya Pawar <34043950+adityapawar1@users.noreply.github.com> Co-authored-by: Aditya Pawar --- assets/icons.tsx | 3 + src/app/(tabs)/genre/index.tsx | 1 + src/app/(tabs)/story/index.tsx | 249 ++++++++---------- src/app/(tabs)/story/styles.ts | 59 ++++- src/components/AuthorImage/AuthorImage.tsx | 43 +++ src/components/AuthorImage/styles.ts | 34 +++ .../FavoriteStoryButton.tsx | 23 +- src/components/OptionBar/OptionBar.tsx | 50 ++++ src/components/OptionBar/styles.ts | 19 ++ .../ReactionPicker/ReactionPicker.tsx | 2 +- src/components/ReactionPicker/styles.ts | 4 +- .../SaveStoryButton/SaveStoryButton.tsx | 12 +- src/queries/stories.tsx | 20 +- 13 files changed, 335 insertions(+), 184 deletions(-) create mode 100644 src/components/AuthorImage/AuthorImage.tsx create mode 100644 src/components/AuthorImage/styles.ts create mode 100644 src/components/OptionBar/OptionBar.tsx create mode 100644 src/components/OptionBar/styles.ts diff --git a/assets/icons.tsx b/assets/icons.tsx index df93d8cc..254e1eb9 100644 --- a/assets/icons.tsx +++ b/assets/icons.tsx @@ -4,10 +4,12 @@ import { SvgXml } from 'react-native-svg'; export type IconType = | 'home_outline' + | 'share_outline' | 'document_outline' | 'search_outline' | 'close_modal_button' | 'red_x' + | 'share_outline' | 'green_check' | 'hide_password' | 'grey_dot' @@ -21,6 +23,7 @@ export type IconType = const IconSvgs: Record = { home_outline: , + share_outline: , search_outline: , document_outline: , settings_gear: , diff --git a/src/app/(tabs)/genre/index.tsx b/src/app/(tabs)/genre/index.tsx index 5bf00f55..b5d1df56 100644 --- a/src/app/(tabs)/genre/index.tsx +++ b/src/app/(tabs)/genre/index.tsx @@ -7,6 +7,7 @@ import { Text, 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'; diff --git a/src/app/(tabs)/story/index.tsx b/src/app/(tabs)/story/index.tsx index 281359d5..91c8b6db 100644 --- a/src/app/(tabs)/story/index.tsx +++ b/src/app/(tabs)/story/index.tsx @@ -1,12 +1,11 @@ -import { useLocalSearchParams, router } from 'expo-router'; +import { router, useLocalSearchParams } from 'expo-router'; +import * as cheerio from 'cheerio'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, FlatList, ScrollView, - Share, Text, - TouchableOpacity, View, useWindowDimensions, } from 'react-native'; @@ -16,14 +15,15 @@ import { RenderHTML } from 'react-native-render-html'; import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; -import FavoriteStoryButton from '../../../components/FavoriteStoryButton/FavoriteStoryButton'; -import SaveStoryButton from '../../../components/SaveStoryButton/SaveStoryButton'; +import Icon from '../../../../assets/icons'; +import AuthorImage from '../../../components/AuthorImage/AuthorImage'; import ReactionPicker from '../../../components/ReactionPicker/ReactionPicker'; -import BackButton from '../../../components/BackButton/BackButton'; import { fetchStory } from '../../../queries/stories'; import { Story } from '../../../queries/types'; -import colors from '../../../styles/colors'; import globalStyles, { fonts } from '../../../styles/globalStyles'; +import BackButton from '../../../components/BackButton/BackButton'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import OptionBar from '../../../components/OptionBar/OptionBar'; function StoryScreen() { const [isLoading, setLoading] = useState(true); @@ -49,144 +49,98 @@ function StoryScreen() { }); }, [storyId]); - const onShare = async () => { - try { - const result = await Share.share({ - message: `Check out this story from Girls Write Now!!!\n${story.link}/`, - }); - if (result.action === Share.sharedAction) { - if (result.activityType) { - // shared with activity type of result.activityType - } else { - // shared - } - } else if (result.action === Share.dismissedAction) { - // dismissed - } - } catch (error) { - console.log(error); - } - }; - return ( {isLoading ? ( ) : ( - - router.back()} /> - - {story?.title} - - { - router.push({ - pathname: '/author', - params: { author: story.author_id.toString() }, - }); - }} + + - - - - By {story.author_name} - - - - - - ( - - - {item} - - + router.back()} /> + + {story?.featured_media ? ( + + ) : ( + No image available )} + + + {story?.title} + + - - - - + + + index.toString()} // Add a key extractor for performance optimization + renderItem={({ item, index }) => ( + + {item} + + )} + /> - - - - - - - - - - Author's Process - - - - - { - router.push({ - pathname: '/author', - params: { author: story.author_id.toString() }, - }); - }} - > + + + + + + + + + Author's Process + + + - - - - - - + + + + + + )} ); diff --git a/src/app/(tabs)/story/styles.ts b/src/app/(tabs)/story/styles.ts index 4967a79a..202e6220 100644 --- a/src/app/(tabs)/story/styles.ts +++ b/src/app/(tabs)/story/styles.ts @@ -1,5 +1,6 @@ import { StyleSheet } from 'react-native'; +import globalStyles from '../../../styles/globalStyles'; import colors from '../../../styles/colors'; const styles = StyleSheet.create({ @@ -7,6 +8,11 @@ const styles = StyleSheet.create({ paddingLeft: 24, paddingRight: 24, }, + image: { + width: '100%', + height: 200, + marginBottom: 16, + }, authorImage: { backgroundColor: '#D9D9D9', width: 21, @@ -28,9 +34,11 @@ const styles = StyleSheet.create({ flexWrap: 'wrap', borderRadius: 10, marginBottom: 16, + marginTop: 15, }, genresBorder: { backgroundColor: '#D9D9D9', + padding: 10, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, @@ -39,40 +47,67 @@ const styles = StyleSheet.create({ marginRight: 8, }, genresText: { - backgroundColor: '#D9D9D9', + fontFamily: 'Manrope-Regular', + fontSize: 12, + color: 'white', }, shareButtonText: { - color: colors.white, + fontFamily: 'Manrope-Regular', + fontSize: 14, + color: 'white', + marginLeft: -5, + textDecorationLine: 'underline', }, excerpt: { + fontFamily: 'Manrope-Regular', + fontSize: 16, textAlign: 'left', - paddingVertical: 16, + color: 'black', }, story: { + fontFamily: 'Manrope-Regular', + fontSize: 16, + textAlign: 'left', + color: 'black', marginBottom: 16, }, authorProcess: { - marginBottom: 16, + fontFamily: 'Manrope-Bold', + fontSize: 20, + textAlign: 'left', + color: 'black', + marginBottom: 5, + marginTop: 10, }, process: { + fontFamily: 'Manrope-Regular', + fontSize: 16, + textAlign: 'left', + color: 'black', marginBottom: 16, }, - options: { - flex: 1, - flexDirection: 'row', - justifyContent: 'flex-end', - }, backToTopButtonText: { - fontFamily: 'Manrope-Regular', - fontSize: 12, - fontWeight: '800', + fontFamily: 'Manrope-Bold', + fontSize: 15, textAlign: 'left', color: 'black', + textDecorationLine: 'underline', }, bottomReactionContainer: { flex: 1, justifyContent: 'space-around', }, + button_style: { + width: 125, + marginBottom: 16, + borderRadius: 8, + height: 35, + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-around', + alignItems: 'center', + backgroundColor: '#EB563B', + }, }); export default styles; diff --git a/src/components/AuthorImage/AuthorImage.tsx b/src/components/AuthorImage/AuthorImage.tsx new file mode 100644 index 00000000..92b1dc88 --- /dev/null +++ b/src/components/AuthorImage/AuthorImage.tsx @@ -0,0 +1,43 @@ +import { router } from 'expo-router'; +import { Text, TouchableOpacity, View } from 'react-native'; +import { Image } from 'expo-image'; + +import styles from './styles'; +import globalStyles from '../../styles/globalStyles'; + +type AuthorImageProps = { + author_name: string; + author_id: string; + author_Uri: string; + // pressFunction: (event: GestureResponderEvent) => void; +}; + +function AuthorImage({ + author_name, + author_id, + author_Uri, // pressFunction, +}: AuthorImageProps) { + return ( + { + router.push({ + pathname: '/author', + params: { author: author_id }, + }); + }} + > + + Authors: + + + {author_name} + + + + ); +} + +export default AuthorImage; diff --git a/src/components/AuthorImage/styles.ts b/src/components/AuthorImage/styles.ts new file mode 100644 index 00000000..2309c278 --- /dev/null +++ b/src/components/AuthorImage/styles.ts @@ -0,0 +1,34 @@ +import { StyleSheet } from 'react-native'; + +import colors from '../../styles/colors'; +import globalStyles from '../../styles/globalStyles'; +export default StyleSheet.create({ + author: { + display: 'flex', + flexDirection: 'row', + gap: 10, + marginLeft: 12, + }, + authorText: { + fontFamily: 'Manrope-Regular', + fontSize: 15, + fontWeight: '400', + textAlign: 'left', + color: 'black', + }, + author_image: { + backgroundColor: '#D9D9D9', + width: 25, + height: 25, + borderRadius: 100 / 2, + }, + author_container: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + }, + author_text: { + fontSize: 18, + fontWeight: 'bold', + }, +}); diff --git a/src/components/FavoriteStoryButton/FavoriteStoryButton.tsx b/src/components/FavoriteStoryButton/FavoriteStoryButton.tsx index 46bf3f8d..ff8113cc 100644 --- a/src/components/FavoriteStoryButton/FavoriteStoryButton.tsx +++ b/src/components/FavoriteStoryButton/FavoriteStoryButton.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { TouchableOpacity } from 'react-native-gesture-handler'; import Svg, { Path } from 'react-native-svg'; @@ -18,7 +18,7 @@ export default function FavoriteStoryButton({ storyId, }: FavoriteStoryButtonProps) { const { user } = useSession(); - const { publish } = usePubSub(); + const { channels, publish } = usePubSub(); const [storyIsFavorited, setStoryIsFavorited] = useState(false); useEffect(() => { @@ -34,6 +34,15 @@ export default function FavoriteStoryButton({ }); }, [storyId]); + useEffect(() => { + const value = channels[Channel.FAVORITES][storyId]; + if (value == undefined) { + return; + } + + setStoryIsFavorited(value); + }, [channels[Channel.FAVORITES][storyId]]); + const favoriteStory = async (favorited: boolean) => { setStoryIsFavorited(favorited); @@ -46,7 +55,7 @@ export default function FavoriteStoryButton({ } }; - const renderFavoritedIcon = () => { + const renderFavoritedIcon = useMemo(() => { return ( ); - }; + }, []); - const renderNotFavoritedIcon = () => { + const renderNotFavoritedIcon = useMemo(() => { return ( ); - }; + }, []); return ( favoriteStory(!storyIsFavorited)}> - {storyIsFavorited ? renderFavoritedIcon() : renderNotFavoritedIcon()} + {storyIsFavorited ? renderFavoritedIcon : renderNotFavoritedIcon} ); } diff --git a/src/components/OptionBar/OptionBar.tsx b/src/components/OptionBar/OptionBar.tsx new file mode 100644 index 00000000..783dae66 --- /dev/null +++ b/src/components/OptionBar/OptionBar.tsx @@ -0,0 +1,50 @@ +import { Share, View } from 'react-native'; +import Icon from '../../../assets/icons'; +import ReactionPicker from '../ReactionPicker/ReactionPicker'; +import SaveStoryButton from '../SaveStoryButton/SaveStoryButton'; +import FavoriteStoryButton from '../FavoriteStoryButton/FavoriteStoryButton'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import styles from './styles'; +import { Story } from '../../queries/types'; + +type OptionBarProps = { + storyId: number; + story: Story; +}; + +function OptionBar({ storyId, story }: OptionBarProps) { + const onShare = async () => { + try { + const result = await Share.share({ + message: `Check out this story from Girls Write Now! \n${story.link}/`, + }); + if (result.action === Share.sharedAction) { + if (result.activityType) { + // shared with activity type of result.activityType + } else { + // shared + } + } else if (result.action === Share.dismissedAction) { + // dismissed + } + } catch (error) { + console.log(error); + } + }; + + return ( + + + + + + + + + + + + ); +} + +export default OptionBar; diff --git a/src/components/OptionBar/styles.ts b/src/components/OptionBar/styles.ts new file mode 100644 index 00000000..6dcb70fa --- /dev/null +++ b/src/components/OptionBar/styles.ts @@ -0,0 +1,19 @@ +import { StyleSheet } from 'react-native'; + +const styles = StyleSheet.create({ + options: { + backgroundColor: 'white', + paddingTop: 16, + flex: 1, + flexDirection: 'row', + justifyContent: 'space-between', + paddingBottom: 22, + }, + right: { + alignSelf: 'flex-end', + gap: 8, + flexDirection: 'row', + }, +}); + +export default styles; diff --git a/src/components/ReactionPicker/ReactionPicker.tsx b/src/components/ReactionPicker/ReactionPicker.tsx index 637a402e..71f1455d 100644 --- a/src/components/ReactionPicker/ReactionPicker.tsx +++ b/src/components/ReactionPicker/ReactionPicker.tsx @@ -68,7 +68,7 @@ const ReactionPicker = ({ storyId }: ReactionPickerProps) => { key={i} onPress={() => handleReactionPress(reaction)} > - + ))} diff --git a/src/components/ReactionPicker/styles.ts b/src/components/ReactionPicker/styles.ts index 5a5a7052..3d46e32d 100644 --- a/src/components/ReactionPicker/styles.ts +++ b/src/components/ReactionPicker/styles.ts @@ -12,15 +12,15 @@ const styles = StyleSheet.create({ borderRadius: 20, padding: 10, alignSelf: 'center', - marginBottom: 10, }, reactionsContainer: { + flex: 1, flexDirection: 'row', gap: 5, justifyContent: 'space-between', padding: 10, position: 'absolute', // Positioning the container above the toggle button - bottom: 50, + bottom: -2, backgroundColor: '#D9D9D9', borderRadius: 20, }, diff --git a/src/components/SaveStoryButton/SaveStoryButton.tsx b/src/components/SaveStoryButton/SaveStoryButton.tsx index dbf88956..3e22084e 100644 --- a/src/components/SaveStoryButton/SaveStoryButton.tsx +++ b/src/components/SaveStoryButton/SaveStoryButton.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { TouchableOpacity } from 'react-native-gesture-handler'; import Svg, { Path } from 'react-native-svg'; @@ -52,7 +52,7 @@ export default function SaveStoryButton({ } }; - const renderSavedStoryImage = () => { + const renderSavedStoryImage = useMemo(() => { return ( ); - }; + }, []); - const renderSaveStoryImage = () => { + const renderSaveStoryImage = useMemo(() => { return ( ); - }; + }, []); return ( saveStory(!storyIsSaved)}> - {storyIsSaved ? renderSavedStoryImage() : renderSaveStoryImage()} + {storyIsSaved ? renderSavedStoryImage : renderSaveStoryImage} ); } diff --git a/src/queries/stories.tsx b/src/queries/stories.tsx index a42b2a33..50574f8e 100644 --- a/src/queries/stories.tsx +++ b/src/queries/stories.tsx @@ -151,32 +151,32 @@ export async function fetchNewStories(): Promise { } } -export async function fetchStoryPreviewById( - storyId: number, +export async function fetchStoryPreviewByIds( + storyIds: number[], ): Promise { - const { data, error } = await supabase.rpc('curr_story_preview_by_id', { - input_story_id: storyId, + 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 ID: ${error}`, + `An error occured when trying to fetch story preview by IDs: ${error}`, ); } else { return data; } } -export async function fetchStoryPreviewByIds( - storyIds: number[], +export async function fetchStoryPreviewById( + storyId: number, ): Promise { - const { data, error } = await supabase.rpc('curr_story_preview_by_ids', { - input_ids: storyIds, + const { data, error } = await supabase.rpc('curr_story_preview_by_id', { + input_story_id: storyId, }); if (error) { console.log(error); throw new Error( - `An error occured when trying to fetch story preview by IDs: ${error}`, + `An error occured when trying to fetch story preview by ID: ${error}`, ); } else { return data; 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 5/6] [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 && ( */} {/* */} {/*