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}
+
+ );
+}