Skip to content

Commit

Permalink
feat: completed home tab
Browse files Browse the repository at this point in the history
- created blog navigator
- created post store
- created splash and home screen
  • Loading branch information
burhanyilmaz committed Jan 13, 2024
1 parent 894d1ba commit b015665
Show file tree
Hide file tree
Showing 26 changed files with 583 additions and 21 deletions.
9 changes: 7 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module.exports = {
'@typescript-eslint/no-shadow': ['error'],
'no-shadow': 'off',
'no-undef': 'off',
indent: ['error', 2],
indent: ['error', 2, { SwitchCase: 1 }],
quotes: ['error', 'single'],
semi: ['error', 'always'],
'no-console': ['error'],
Expand All @@ -19,6 +19,11 @@ module.exports = {
'eslint-disable-next-line': 'off',
'prettier/prettier': 'error',
'import/newline-after-import': ['error'],
'padding-line-between-statements': ['error', { blankLine: 'always', prev: '*', next: 'export' }],
'padding-line-between-statements': [
'error',
{ blankLine: 'always', prev: '*', next: 'export' },
],
'react/jsx-indent': ['error', 2],
'react/jsx-indent-props': ['error', 2],
},
};
Binary file added assets/images/blogHeaderBg.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/welcomeScreenBg.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ module.exports = function (api) {
{
root: ['./src'],
alias: {
'@types': './src/types',
'@utils': './src/utils',
'@store': './src/store',
'@hooks': './src/hooks',
'@theme': './src/theme',
'@screens': './src/screens',
'@services': './src/services',
'@containers': './src/containers',
'@components': './src/components',
'@navigators': './src/navigators',
},
Expand Down
Binary file modified bun.lockb
Binary file not shown.
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,24 @@
"build:android:local": "eas build --platform android --local --profile preview"
},
"dependencies": {
"@react-navigation/bottom-tabs": "^6.5.11",
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
"class-variance-authority": "^0.7.0",
"expo": "~49.0.15",
"expo-status-bar": "~1.6.0",
"expo-updates": "~0.18.19",
"lucide-react-native": "^0.308.0",
"mobx": "^6.12.0",
"mobx-react-lite": "^4.0.5",
"mobx-state-tree": "^5.4.0",
"nativewind": "^2.0.11",
"react": "18.2.0",
"react-native": "0.72.6",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0"
"react-native-screens": "~3.22.0",
"react-native-svg": "^14.1.0",
"wretch": "^2.8.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
Expand Down
13 changes: 13 additions & 0 deletions src/components/BlogTabHeader/BlogTabHeaderBackgroundImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { blogHeaderBg } from '@components/Images';
import { Image, StyleSheet } from 'react-native';

const BlogTabHeaderBackgroundImage = () => (
<Image
resizeMode="cover"
className="bg-zinc-50"
source={{ uri: blogHeaderBg }}
style={StyleSheet.absoluteFill}
/>
);

export default BlogTabHeaderBackgroundImage;
15 changes: 15 additions & 0 deletions src/components/BlogTabHeader/BlogTabHeaderGoBack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { StackActions, useNavigation } from '@react-navigation/native';
import { ArrowLeft } from 'lucide-react-native';
import { Pressable } from 'react-native';

const BlogTabHeaderGoBack = () => {
const { dispatch } = useNavigation();

return (
<Pressable className="ml-5" onPress={() => dispatch(StackActions.popToTop)}>
<ArrowLeft className="text-zinc-800" />
</Pressable>
);
};

export default BlogTabHeaderGoBack;
10 changes: 10 additions & 0 deletions src/components/BlogTabHeader/BlogTabHeaderTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { text } from '@theme/text';
import { Text } from 'react-native';

type Props = { title: string };

const BlogTabHeaderTitle = ({ title }: Props) => (
<Text className={text({ type: 'title4', class: 'text-zinc-800 self-center' })}>{title}</Text>
);

export default BlogTabHeaderTitle;
59 changes: 59 additions & 0 deletions src/components/BottomTab/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import { text } from '@theme/text';
import { getHitSlop } from '@utils/helpers';
import { Bookmark, Compass, LayoutGrid } from 'lucide-react-native';
import { memo } from 'react';
import { Platform, Pressable, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const GetTabIcon = (screenName: 'Home' | 'Explore' | 'Saved', isActive: boolean) => {
const className = isActive ? 'text-zinc-900 mb-1' : 'text-zinc-400 mb-1';

switch (screenName) {
case 'Home':
return <LayoutGrid className={className} />;
case 'Explore':
return <Compass className={className} />;
case 'Saved':
return <Bookmark className={className} />;
default:
return null;
}
};

const BottomNavigation = ({ navigation, state }: BottomTabBarProps) => {
const { bottom } = useSafeAreaInsets();

const onPressNavigate = (screen: string) => () => navigation.navigate(screen);

return (
<View className="border-t-b1 border-t-zinc-100 bg-zinc-50">
<View
className="pt-2 px-5 flex-row justify-between"
style={{
paddingBottom: Platform.select({
ios: bottom,
android: bottom + 16,
}),
}}>
{state?.routeNames.map((route, index) => {
const isActive = state.index === index;
const title = route.split('Flow')?.[0]?.trim() as 'Home' | 'Explore' | 'Saved';

return (
<Pressable
className="items-center"
key={`bottomTab-${route}`}
onPress={onPressNavigate(route)}
hitSlop={getHitSlop({ value: 15 })}>
{GetTabIcon(title, isActive)}
<Text className={text({ tabTitleIsActive: isActive })}>{title}</Text>
</Pressable>
);
})}
</View>
</View>
);
};

export default memo(BottomNavigation);
4 changes: 4 additions & 0 deletions src/components/Images.ts

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions src/components/Logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Text, View } from 'react-native';

type Props = {
parts: [string, string];
};
const Logo = ({ parts }: Props) => (
<View className="flex-row">
<Text className="text-zinc-700 text-[28px] font-Bold">{parts[0]}</Text>
<View className="bg-zinc-900 rounded-md">
<Text className="text-white text-[28px] font-Bold mx-2">{parts[1]}</Text>
</View>
</View>
);

export default Logo;
56 changes: 56 additions & 0 deletions src/components/Post/PostCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { text } from '@theme/text';
import { width } from '@utils/helpers';
import { Bookmark } from 'lucide-react-native';
import {
ActivityIndicator,
Image,
Pressable,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';

const CARD_IMAGE_WIDTH = (width - 40) * 0.3;
const CARD_IMAGE_HEIGHT = (width - 40) * 0.35;

type Props = {
image: string;
title: string;
category: string;
onPressPost: () => void;
onPressSave: () => void;
};

const PostCard = ({ image, title, category, onPressPost, onPressSave }: Props) => (
<Pressable
onPress={onPressPost}
className="pb-4 bg-white border-b-b1 border-zinc-100 mb-4 flex-row">
<View style={styles.cardImage} className="items-center justify-center bg-zinc-50 rounded-md">
{image ? (
<Image source={{ uri: image }} className="mb-2 rounded-md" style={styles.cardImage} />
) : (
<ActivityIndicator />
)}
</View>
<View className="flex-1 ml-4">
<View className="mb-2 h-5">
<Text className={text({ type: 'subhead', class: 'text-zinc-500' })}>{category}</Text>
</View>
<Text className={text({ type: 'title4', class: 'text-zinc-900' })} numberOfLines={3}>
{title}
</Text>
<View className="absolute bottom-0 left-0 w-full self-center justify-center items-end">
<TouchableOpacity onPress={onPressSave}>
<Bookmark className="text-zinc-300" />
</TouchableOpacity>
</View>
</View>
</Pressable>
);

const styles = StyleSheet.create({
cardImage: { width: CARD_IMAGE_WIDTH, height: CARD_IMAGE_HEIGHT },
});

export default PostCard;
14 changes: 14 additions & 0 deletions src/containers/Post.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import PostCard from '@components/Post/PostCard';
import { PostType } from '@store/PostStore';
import { observer } from 'mobx-react-lite';

const Post = ({ post }: { post: PostType }) => (
<PostCard
image={post.image}
title={post.title}
onPressPost={() => null}
category={post.category}
/>
);

export default observer(Post);
48 changes: 48 additions & 0 deletions src/navigators/BlogNavigator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import BlogTabHeaderBackgroundImage from '@components/BlogTabHeader/BlogTabHeaderBackgroundImage';
import BlogTabHeaderGoBack from '@components/BlogTabHeader/BlogTabHeaderGoBack';
import BlogTabHeaderTitle from '@components/BlogTabHeader/BlogTabHeaderTitle';
import BottomTab from '@components/BottomTab';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import BlogHomeScreen from '@screens/Blog/HomeScreen';
import TestScreen from '@screens/Test';

export type BlogTabNavigatorParamList = {
Home: undefined;
Saved: undefined;
Explore: undefined;
BlogSplash: undefined;
};

export type BlogStackNavigatorParamList = {
BlogTab: undefined;
PostDetails: undefined;
};

const Tab = createBottomTabNavigator<BlogTabNavigatorParamList>();

const Stack = createNativeStackNavigator<BlogStackNavigatorParamList>();

const BlogTab = () => (
<Tab.Navigator
tabBar={props => <BottomTab {...props} />}
screenOptions={{
lazy: true,
headerLeft: BlogTabHeaderGoBack,
headerBackground: BlogTabHeaderBackgroundImage,
headerTitle: props => <BlogTabHeaderTitle title={props.children} />,
}}>
<Tab.Screen name="Home" component={BlogHomeScreen} />
<Tab.Screen name="Explore" component={TestScreen} />
<Tab.Screen name="Saved" component={TestScreen} />
</Tab.Navigator>
);

const BlogNavigator = () => (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="BlogTab" component={BlogTab} />
<Stack.Screen name="PostDetails" component={BlogTab} />
</Stack.Navigator>
);

export default BlogNavigator;
15 changes: 15 additions & 0 deletions src/navigators/MainNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import BlogSplashScreen from '@screens/Blog/SplashScreen';
import WelcomeScreen from '@screens/Welcome';

import BlogNavigator from './BlogNavigator';

export type MainNavigatorParamList = {
Welcome: undefined;
BlogSplash: undefined;
Blog: undefined;
};

const Stack = createNativeStackNavigator<MainNavigatorParamList>();
Expand All @@ -12,6 +17,16 @@ const MainNavigator = () => (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Welcome" component={WelcomeScreen} />
<Stack.Screen
name="BlogSplash"
component={BlogSplashScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Blog"
component={BlogNavigator}
options={{ animation: 'slide_from_right' }}
/>
</Stack.Navigator>
</NavigationContainer>
);
Expand Down
39 changes: 39 additions & 0 deletions src/screens/Blog/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Post from '@containers/Post';
import postStore, { PostType } from '@store/PostStore';
import { observer } from 'mobx-react-lite';
import { useEffect } from 'react';
import { ActivityIndicator, FlatList, ListRenderItem, View } from 'react-native';

const BlogHomeScreen = () => {
const posts = Array.from(postStore.posts.values());

useEffect(
() => () => {
postStore.clearAll();
},
[],
);

const RenderItem: ListRenderItem<PostType> = ({ item }) => <Post post={item} />;

return (
<View className="bg-white flex-1">
{postStore.loading && posts.length > 0 && (
<ActivityIndicator size="large" className="absolute z-50 right-0 left-0 bottom-4" />
)}
<FlatList
data={posts}
windowSize={6}
className="px-5 pt-4"
removeClippedSubviews
initialNumToRender={6}
maxToRenderPerBatch={6}
renderItem={RenderItem}
onEndReached={postStore.increasePage}
ListEmptyComponent={<ActivityIndicator />}
/>
</View>
);
};

export default observer(BlogHomeScreen);
23 changes: 23 additions & 0 deletions src/screens/Blog/SplashScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Logo from '@components/Logo';
import { MainNavigatorParamList } from '@navigators/MainNavigator';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { useEffect } from 'react';
import { View } from 'react-native';

const BlogSplashScreen = () => {
const { navigate } = useNavigation<NavigationProp<MainNavigatorParamList>>();

useEffect(() => {
setTimeout(() => {
navigate('Blog');
}, 10);
}, []);

return (
<View className="items-center justify-center flex-1 bg-zinc-50">
<Logo parts={['Your ', 'Logo']} />
</View>
);
};

export default BlogSplashScreen;
Loading

0 comments on commit b015665

Please sign in to comment.