Skip to content

Commit

Permalink
feat: added post detail screen
Browse files Browse the repository at this point in the history
- add htmlToNative  component
- refactored stores and screens
  • Loading branch information
burhanyilmaz committed Jan 16, 2024
1 parent 11c91ca commit 3cb3e61
Show file tree
Hide file tree
Showing 15 changed files with 263 additions and 18 deletions.
Binary file modified bun.lockb
Binary file not shown.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"build:android:local": "eas build --platform android --local --profile preview"
},
"dependencies": {
"@native-html/iframe-plugin": "^2.6.1",
"@react-navigation/bottom-tabs": "^6.5.11",
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
Expand All @@ -30,9 +31,11 @@
"nativewind": "^2.0.11",
"react": "18.2.0",
"react-native": "0.72.6",
"react-native-render-html": "^6.3.4",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
"react-native-svg": "^14.1.0",
"react-native-webview": "^13.6.4",
"wretch": "^2.8.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Image, StyleSheet } from 'react-native';
const BlogTabHeaderBackgroundImage = () => (
<Image
resizeMode="cover"
className="bg-zinc-50"
className="bg-zinc-100"
source={{ uri: blogHeaderBg }}
style={StyleSheet.absoluteFill}
/>
Expand Down
93 changes: 93 additions & 0 deletions src/components/HtmlToNativeViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import IframeRenderer, { iframeModel } from '@native-html/iframe-plugin';
import colors from '@theme/colors';
import { width } from '@utils/helpers';
import { observer } from 'mobx-react-lite';
import { StyleSheet } from 'react-native';
import RenderHtml from 'react-native-render-html';
import { WebView } from 'react-native-webview';

type Props = {
html?: string;
customContentWidth?: number;
};

const customHTMLElementModels = {
iframe: iframeModel,
};

const renderers = {
iframe: IframeRenderer,
};

const HtmlToNativeViewer = ({ html, customContentWidth }: Props) => {
if (!html) {
return null;
}

return (
<RenderHtml
WebView={WebView}
source={{ html }}
tagsStyles={styles}
renderers={renderers}
renderersProps={{
iframe: {
webViewProps: {},
scalesPageToFit: true,
},
img: {
enableExperimentalPercentWidth: true,
},
}}
contentWidth={customContentWidth || width - 40}
customHTMLElementModels={customHTMLElementModels}
/>
);
};

const styles = StyleSheet.create({
a: {},
li: {
marginVertical: 4,
color: colors.zinc[700],
},
h1: {
marginVertical: 6,
color: colors.zinc[700],
},
h2: {
marginVertical: 6,
color: colors.zinc[700],
},
h3: {
marginVertical: 6,
color: colors.zinc[700],
},
h4: {
marginVertical: 6,
color: colors.zinc[700],
},
h5: {
marginVertical: 6,
color: colors.zinc[700],
},
h6: {
marginVertical: 6,
color: colors.zinc[700],
},
img: {
resizeMode: 'contain',
alignSelf: 'flex-start',
},
p: {
marginVertical: 4,
fontSize: 16,
width: width - 40,
color: colors.zinc[700],
},
b: {
color: colors.zinc[700],
},
});

export default observer(HtmlToNativeViewer);
43 changes: 43 additions & 0 deletions src/components/PostDetail/PostDetailHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import colors from '@theme/colors';
import { getHitSlop, width } from '@utils/helpers';
import { ArrowLeft, Bookmark } from 'lucide-react-native';
import { Image, Pressable, StyleSheet, View } from 'react-native';

type Props = {
topMargin: number;
isSavedPost: boolean;
headerImage?: string;
onPressSave: () => void;
onPressBack: () => void;
};

const PostDetailHeader = ({
topMargin,
headerImage,
onPressSave,
onPressBack,
isSavedPost,
}: Props) => (
<View className="w-full absolute overflow-hidden">
<View
style={{ marginTop: topMargin + 16 }}
className="absolute z-40 px-5 flex-row justify-between items-center content-between w-full">
<Pressable onPress={onPressBack} hitSlop={getHitSlop({ value: 40 })}>
<ArrowLeft className="text-zinc-50" />
</Pressable>
<Pressable onPress={onPressSave} hitSlop={getHitSlop({ value: 40 })}>
<Bookmark className="text-zinc-50" fill={isSavedPost ? colors.zinc[50] : 'transparent'} />
</Pressable>
</View>
<View style={styles.header} className="absolute bg-zinc-600 z-20 w-full opacity-50" />
<Image resizeMode="cover" style={styles.header} source={{ uri: headerImage }} />
</View>
);

const styles = StyleSheet.create({
header: {
height: width * 0.6,
},
});

export default PostDetailHeader;
6 changes: 4 additions & 2 deletions src/containers/Post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { PostType } from '@store/PostStore';
import savedStore from '@store/SavedStore';
import { observer } from 'mobx-react-lite';

const Post = ({ post }: { post: PostType }) => (
type Props = { post: PostType; onPressPost: () => void };

const Post = ({ post, onPressPost }: Props) => (
<PostCard
image={post.image}
title={post.title}
category={post.category}
onPressPost={() => null}
onPressPost={onPressPost}
isSaved={savedStore.isSavedPost(post.id)}
onPressSave={() => savedStore.addPost(post)}
/>
Expand Down
12 changes: 10 additions & 2 deletions src/navigators/BlogNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import ExploreScreen from '@screens/Blog/ExploreScreen';
import BlogHomeScreen from '@screens/Blog/HomeScreen';
import PostDetailScreen from '@screens/Blog/PostDetailScreen';
import SavedScreen from '@screens/Blog/SavedScreen';
import { PostType } from '@store/PostStore';

export type BlogTabNavigatorParamList = {
Home: undefined;
Expand All @@ -17,7 +19,9 @@ export type BlogTabNavigatorParamList = {

export type BlogStackNavigatorParamList = {
BlogTab: undefined;
PostDetails: undefined;
PostDetail: {
post: PostType;
};
};

const Tab = createBottomTabNavigator<BlogTabNavigatorParamList>();
Expand All @@ -42,7 +46,11 @@ const BlogTab = () => (
const BlogNavigator = () => (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="BlogTab" component={BlogTab} />
<Stack.Screen name="PostDetails" component={BlogTab} />
<Stack.Screen
name="PostDetail"
component={PostDetailScreen}
options={{ animation: 'slide_from_right' }}
/>
</Stack.Navigator>
);

Expand Down
39 changes: 31 additions & 8 deletions src/screens/Blog/ExploreScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,51 @@
import Input from '@components/core/Input';
import Post from '@containers/Post';
import useDebounce from '@hooks/useDebounce';
import { BlogStackNavigatorParamList } from '@navigators/BlogNavigator';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { PostType } from '@store/PostStore';
import searchStore from '@store/SearchStore';
import colors from '@theme/colors';
import { text } from '@theme/text';
import { BookText, Search } from 'lucide-react-native';
import { observer } from 'mobx-react-lite';
import { useEffect } from 'react';
import { ActivityIndicator, FlatList, SafeAreaView, Text, View } from 'react-native';
import {
ActivityIndicator,
FlatList,
ListRenderItem,
SafeAreaView,
Text,
View,
} from 'react-native';

const ExploreScreen = () => {
const value = useDebounce(searchStore.term, 300);
const { navigate } = useNavigation<NavigationProp<BlogStackNavigatorParamList>>();

useEffect(() => {
if (value.length > 2) {
searchStore.searchPost();
}
}, [value]);

const ListEmpty = () => (
<View className="items-center mt-4">
<BookText className="text-zinc-500 mb-2" size={28} />
<Text className={text({ type: 'subtitle', class: 'text-zinc-600' })}>There is no posts.</Text>
</View>
const ListEmpty = () => {
if (!searchStore.loading && searchStore.term) {
return (
<View className="items-center mt-4">
<BookText className="text-zinc-500 mb-2" size={28} />
<Text className={text({ type: 'subtitle', class: 'text-zinc-600' })}>
There is no posts.
</Text>
</View>
);
}

return null;
};

const RenderItem: ListRenderItem<PostType> = ({ item: post }) => (
<Post post={post} onPressPost={() => navigate('PostDetail', { post })} />
);

return (
Expand All @@ -47,9 +70,9 @@ const ExploreScreen = () => {
removeClippedSubviews
initialNumToRender={6}
maxToRenderPerBatch={6}
renderItem={RenderItem}
ListEmptyComponent={ListEmpty}
data={searchStore.listResult || []}
renderItem={({ item }) => <Post post={item} />}
ListEmptyComponent={!searchStore.loading && searchStore.term ? ListEmpty : null}
/>
</SafeAreaView>
);
Expand Down
7 changes: 6 additions & 1 deletion src/screens/Blog/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import Post from '@containers/Post';
import { BlogStackNavigatorParamList } from '@navigators/BlogNavigator';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import postStore, { PostType } from '@store/PostStore';
import colors from '@theme/colors';
import { observer } from 'mobx-react-lite';
import { useEffect } from 'react';
import { ActivityIndicator, FlatList, ListRenderItem, View } from 'react-native';

const BlogHomeScreen = () => {
const { navigate } = useNavigation<NavigationProp<BlogStackNavigatorParamList>>();
const posts = Array.from(postStore.posts.values());

useEffect(
Expand All @@ -15,7 +18,9 @@ const BlogHomeScreen = () => {
[],
);

const RenderItem: ListRenderItem<PostType> = ({ item }) => <Post post={item} />;
const RenderItem: ListRenderItem<PostType> = ({ item: post }) => (
<Post post={post} onPressPost={() => navigate('PostDetail', { post })} />
);

return (
<View className="bg-white flex-1">
Expand Down
47 changes: 47 additions & 0 deletions src/screens/Blog/PostDetailScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import HtmlToNativeViewer from '@components/HtmlToNativeViewer';
import PostDetailHeader from '@components/PostDetail/PostDetailHeader';
import { BlogStackNavigatorParamList } from '@navigators/BlogNavigator';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import savedStore from '@store/SavedStore';
import { text } from '@theme/text';
import { width } from '@utils/helpers';
import { observer } from 'mobx-react-lite';
import { SafeAreaView, ScrollView, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const HEADER_HEIGHT = width * 0.6;
const CONTENT_MARGIN_TOP = HEADER_HEIGHT * 0.5;

const PostDetailScreen = () => {
const {
params: { post },
} = useRoute<RouteProp<BlogStackNavigatorParamList, 'PostDetail'>>();
const { goBack } = useNavigation();
const { top } = useSafeAreaInsets();
const onPressSave = () => post && savedStore.addPost(post);

return (
<SafeAreaView className="bg-zinc-50 flex-1">
<PostDetailHeader
topMargin={top}
onPressBack={goBack}
onPressSave={onPressSave}
headerImage={post?.headerImage}
isSavedPost={savedStore.isSavedPost(post.id)}
/>
<ScrollView className="rounded-md bg-zinc-50" style={{ marginTop: CONTENT_MARGIN_TOP + top }}>
<View className="px-5 pb-8">
<Text className={text({ type: 'subhead', class: 'text-zinc-500 mt-4 mb-1' })}>
{post?.category}
</Text>
<Text numberOfLines={3} className={text({ type: 'title4', class: 'text-zinc-900 mb-2' })}>
{post?.title}
</Text>
<HtmlToNativeViewer html={post?.content} />
</View>
</ScrollView>
</SafeAreaView>
);
};

export default observer(PostDetailScreen);
Loading

0 comments on commit 3cb3e61

Please sign in to comment.