diff --git a/package-lock.json b/package-lock.json index aac3b82a..ccd4c392 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", @@ -3641,6 +3644,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", @@ -12127,6 +12177,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 3e8c0f7b..ca31a24f 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 92edca0a..a5b82774 100644 --- a/src/app/(tabs)/genre/index.tsx +++ b/src/app/(tabs)/genre/index.tsx @@ -14,15 +14,18 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; import BackButton from '../../../components/BackButton/BackButton'; +import FilterModal, { + CATEGORIES as FilterCategories, +} from '../../../components/FilterModal/FilterModal'; import GenreStoryPreviewCard from '../../../components/GenreStoryPreviewCard/GenreStoryPreviewCard'; import { fetchGenreStoryPreviews, fetchGenres } from '../../../queries/genres'; import { fetchStoryPreviewById } from '../../../queries/stories'; import { StoryPreview, GenreStories, Genre } from '../../../queries/types'; import globalStyles from '../../../styles/globalStyles'; - -//TODO figure out the logic for the tone and topic dropdowns, especially when we're dealing with multiselect on both parts +import { TagFilter, useFilter } from '../../../utils/FilterContext'; function GenreScreen() { + const { filters } = useFilter(); const [genreStoryInfo, setGenreStoryInfo] = useState(); const [genreStoryIds, setGenreStoryIds] = useState([]); const [subgenres, setSubgenres] = useState([]); @@ -148,6 +151,21 @@ function GenreScreen() { getGenre(); }, [genreName]); + const flattenenedFilters = Array.from(filters) + .map(([id, parent]) => [...parent.children, parent as TagFilter]) + .flat(); + const activeFilterNames = flattenenedFilters.filter(({ active }) => active); + + const activeGenreNames = activeFilterNames + .filter(({ category }) => category == FilterCategories.GENRE) + .map(({ name }) => name); + const activeToneNames = activeFilterNames + .filter(({ category }) => category == FilterCategories.TONE) + .map(({ name }) => name); + const activeTopicNames = activeFilterNames + .filter(({ category }) => category == FilterCategories.TOPIC) + .map(({ name }) => name); + useEffect(() => { const showAllStoryPreviews = async () => { if (genreStoryIds.length > 0) { diff --git a/src/app/(tabs)/home/index.tsx b/src/app/(tabs)/home/index.tsx index f9067788..7be7bd9f 100644 --- a/src/app/(tabs)/home/index.tsx +++ b/src/app/(tabs)/home/index.tsx @@ -7,6 +7,7 @@ import styles from './styles'; import Icon from '../../../../assets/icons'; import ContentCard from '../../../components/ContentCard/ContentCard'; import PreviewCard from '../../../components/PreviewCard/PreviewCard'; +import ReactionPicker from '../../../components/ReactionPicker/ReactionPicker'; import RecentSearchCard from '../../../components/RecentSearchCard/RecentSearchCard'; import { fetchUsername } from '../../../queries/profiles'; import { @@ -113,6 +114,7 @@ function HomeScreen() { Recommended For You + Back To Top + + + )} diff --git a/src/app/(tabs)/story/styles.ts b/src/app/(tabs)/story/styles.ts index cbe432a8..67b6ce58 100644 --- a/src/app/(tabs)/story/styles.ts +++ b/src/app/(tabs)/story/styles.ts @@ -114,6 +114,10 @@ const styles = StyleSheet.create({ textAlign: 'left', color: 'black', }, + bottomReactionContainer: { + flex: 1, + justifyContent: 'space-around', + }, }); export default styles; 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/ReactionPicker/ReactionPicker.tsx b/src/components/ReactionPicker/ReactionPicker.tsx new file mode 100644 index 00000000..36d1fa7d --- /dev/null +++ b/src/components/ReactionPicker/ReactionPicker.tsx @@ -0,0 +1,47 @@ +import { faFaceSmile } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import React, { useState } from 'react'; +import { View, TouchableOpacity, StyleSheet, Text } from 'react-native'; +import { Icon } from 'react-native-elements'; + +import styles from './styles'; +import globalStyles from '../../styles/globalStyles'; + +const ReactionPicker = () => { + const [showReactions, setShowReactions] = useState(false); + + const toggleReactions = () => setShowReactions(!showReactions); + + // Dummy onPress functions + const onSmilePress = () => console.log('Smile pressed'); + const onHeartPress = () => console.log('Heart pressed'); + const onThumbsUpPress = () => console.log('Thumbs up pressed'); + const onLaughPress = () => console.log('Laugh pressed'); + + return ( + + + + + {showReactions && ( + + + ❤️ + + + 🙂 + + + 👍 + + + 😂 + + + )} + + + ); +}; + +export default ReactionPicker; diff --git a/src/components/ReactionPicker/styles.ts b/src/components/ReactionPicker/styles.ts new file mode 100644 index 00000000..246806cf --- /dev/null +++ b/src/components/ReactionPicker/styles.ts @@ -0,0 +1,28 @@ +import { StyleSheet } from 'react-native'; + +import colors from '../../styles/colors'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'flex-end', + }, + reactionView: { + backgroundColor: '#D9D9D9', + borderRadius: 20, + padding: 10, + alignSelf: 'center', + marginBottom: 10, + }, + reactionsContainer: { + flexDirection: 'row', + 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/utils/FilterContext.tsx b/src/utils/FilterContext.tsx index eb14f0ef..6b81de5d 100644 --- a/src/utils/FilterContext.tsx +++ b/src/utils/FilterContext.tsx @@ -156,7 +156,6 @@ export function FilterContextProvider({ } as TagFilter; }); }; - useEffect(() => { getTags().then(tags => dispatch({ type: 'SET_TAGS', tags: tags ?? [] })); }, []);