Merge pull request #2 from Sosokker/home_page

resolve conflict
This commit is contained in:
Tantikon P. 2025-05-08 22:31:54 +07:00 committed by GitHub
commit e5b6b50376
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 467 additions and 14 deletions

View File

@ -33,7 +33,8 @@
"resizeMode": "contain", "resizeMode": "contain",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
} }
] ],
"expo-secure-store"
], ],
"experiments": { "experiments": {
"typedRoutes": true "typedRoutes": true

View File

@ -1,7 +1,15 @@
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { Tabs } from "expo-router"; import { Tabs, Redirect } from "expo-router";
import { useAuth } from ".././context/auth-context";
export default function TabLayout() { export default function TabLayout() {
const { isAuthenticated, isLoading } = useAuth();
// If not authenticated and not loading, redirect to welcome
if (!isLoading && !isAuthenticated) {
return <Redirect href="/welcome" />;
}
return ( return (
<Tabs <Tabs
screenOptions={{ screenOptions={{
@ -28,7 +36,7 @@ export default function TabLayout() {
}} }}
> >
<Tabs.Screen <Tabs.Screen
name="index" name="home"
options={{ options={{
title: "Home", title: "Home",
tabBarIcon: ({ color }) => ( tabBarIcon: ({ color }) => (

View File

@ -52,7 +52,7 @@ const foodHighlights = [
]; ];
const navigateToFoodDetail = (foodId: string) => { const navigateToFoodDetail = (foodId: string) => {
router.push({ pathname: "/food/[id]", params: { id: foodId } }); router.push({ pathname: "/recipe-detail", params: { id: foodId } });
}; };
export default function HomeScreen() { export default function HomeScreen() {
@ -217,14 +217,11 @@ export default function HomeScreen() {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
{/* Highlights Section */}
{/* Food Highlights Section */} <View className="px-6 mb-6">
<View className="mx-4 mb-6"> <View className="flex-row items-center mb-4">
<View className="flex-row items-center mb-3"> <Text className="text-2xl font-bold mr-2">Highlights</Text>
<Text className="text-lg font-bold text-[#333] mr-2"> <FontAwesome name="star" size={20} color="#ffd60a" />
Food Highlights
</Text>
<IconSymbol name="star.fill" size={16} color="#FFCC00" />
</View> </View>
<View className="w-full"> <View className="w-full">
{foodHighlights.map((food) => ( {foodHighlights.map((food) => (

View File

@ -10,7 +10,7 @@ export default function NotFoundScreen() {
<Stack.Screen options={{ title: 'Oops!' }} /> <Stack.Screen options={{ title: 'Oops!' }} />
<ThemedView style={styles.container}> <ThemedView style={styles.container}>
<ThemedText type="title">This screen does not exist.</ThemedText> <ThemedText type="title">This screen does not exist.</ThemedText>
<Link href="/" style={styles.link}> <Link href="/welcome" style={styles.link}>
<ThemedText type="link">Go to home screen!</ThemedText> <ThemedText type="link">Go to home screen!</ThemedText>
</Link> </Link>
</ThemedView> </ThemedView>

View File

@ -1,10 +1,12 @@
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { GestureHandlerRootView } from "react-native-gesture-handler"; import { GestureHandlerRootView } from "react-native-gesture-handler";
import { AuthProvider } from "./context/auth-context";
import "../global.css"; import "../global.css";
export default function RootLayout() { export default function RootLayout() {
return ( return (
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
<AuthProvider>
<Stack screenOptions={{ headerShown: false }}> <Stack screenOptions={{ headerShown: false }}>
<Stack.Screen <Stack.Screen
name="(tabs)" name="(tabs)"
@ -20,6 +22,7 @@ export default function RootLayout() {
}} }}
/> />
</Stack> </Stack>
</AuthProvider>
</GestureHandlerRootView> </GestureHandlerRootView>
); );
} }

View File

@ -0,0 +1,111 @@
import React, { createContext, useState, useContext, useEffect } from 'react';
import * as SecureStore from 'expo-secure-store';
import { router } from 'expo-router';
type AuthContextType = {
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
signup: (name: string, email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
};
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [authState, setAuthState] = useState({
isAuthenticated: false,
isLoading: true
});
// Use a single useEffect to check authentication status only once on mount
useEffect(() => {
// Check if user is logged in on app start
async function loadToken() {
try {
const token = await SecureStore.getItemAsync('userToken');
// Update state only once with both values
setAuthState({
isAuthenticated: !!token,
isLoading: false
});
} catch (error) {
console.log('Error loading token:', error);
setAuthState({
isAuthenticated: false,
isLoading: false
});
}
}
loadToken();
}, []); // Empty dependency array ensures this runs only once
const login = async (email: string, password: string) => {
try {
// In a real app, you would validate credentials with a backend
await SecureStore.setItemAsync('userToken', 'dummy-auth-token');
setAuthState({
...authState,
isAuthenticated: true
});
// Redirect to home tab specifically
router.replace('../(tabs)/home');
} catch (error) {
console.error('Login error:', error);
throw error;
}
};
const signup = async (name: string, email: string, password: string) => {
try {
// In a real app, you would register the user with a backend
await SecureStore.setItemAsync('userToken', 'dummy-auth-token');
setAuthState({
...authState,
isAuthenticated: true
});
// Redirect to home tab specifically
router.replace('./(tabs)/home');
} catch (error) {
console.error('Signup error:', error);
throw error;
}
};
const logout = async () => {
try {
await SecureStore.deleteItemAsync('userToken');
setAuthState({
...authState,
isAuthenticated: false
});
router.replace('/');
} catch (error) {
console.error('Logout error:', error);
throw error;
}
};
return (
<AuthContext.Provider
value={{
isAuthenticated: authState.isAuthenticated,
isLoading: authState.isLoading,
login,
signup,
logout
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

24
app/index.tsx Normal file
View File

@ -0,0 +1,24 @@
import React from 'react';
import { View, ActivityIndicator } from 'react-native';
import { Redirect } from 'expo-router';
import { useAuth } from './context/auth-context';
export default function Index() {
const { isAuthenticated, isLoading } = useAuth();
// Show loading indicator while checking auth status
if (isLoading) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: 'white' }}>
<ActivityIndicator size="large" color="#ffd60a" />
</View>
);
}
// Redirect based on authentication status
if (isAuthenticated) {
return <Redirect href="/welcome" />;
} else {
return <Redirect href="/welcome" />;
}
}

106
app/login.tsx Normal file
View File

@ -0,0 +1,106 @@
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, SafeAreaView, StatusBar, Alert } from 'react-native';
import { router } from 'expo-router';
import { Feather } from '@expo/vector-icons';
import { useAuth } from './context/auth-context';
export default function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { login } = useAuth();
const handleLogin = async () => {
if (!email || !password) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
try {
setIsLoading(true);
await login(email, password);
} catch (error) {
Alert.alert('Error', 'Failed to login. Please try again.');
} finally {
setIsLoading(false);
}
};
return (
<SafeAreaView className="flex-1 bg-white">
<StatusBar barStyle="dark-content" />
<View className="px-6 py-10 flex-1">
{/* Back Button */}
<TouchableOpacity
className="bg-[#ffd60a] p-3 rounded-lg w-12 mb-8"
onPress={() => router.back()}
>
<Feather name="arrow-left" size={24} color="#bb0718" />
</TouchableOpacity>
{/* Header */}
<Text className="text-3xl font-bold mb-8">Login to your account</Text>
{/* Form */}
<View className="space-y-6">
<View>
<Text className="text-gray-700 mb-2 font-medium">Email</Text>
<TextInput
className="bg-gray-100 py-4 px-4 rounded-xl"
placeholder="Enter your email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
</View>
<View>
<Text className="text-gray-700 mb-2 font-medium">Password</Text>
<View className="flex-row items-center bg-gray-100 rounded-xl">
<TextInput
className="flex-1 py-4 px-4"
placeholder="Enter your password"
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoCapitalize="none"
/>
<TouchableOpacity
className="pr-4"
onPress={() => setShowPassword(!showPassword)}
>
<Feather name={showPassword ? "eye-off" : "eye"} size={20} color="gray" />
</TouchableOpacity>
</View>
</View>
<TouchableOpacity>
<Text className="text-right text-[#bb0718] font-medium">Forgot Password?</Text>
</TouchableOpacity>
<TouchableOpacity
className="bg-[#ffd60a] py-4 rounded-xl mt-6"
onPress={handleLogin}
disabled={isLoading}
>
<Text className="text-center font-bold text-lg">
{isLoading ? 'Logging in...' : 'Login'}
</Text>
</TouchableOpacity>
</View>
{/* Sign Up Link */}
<View className="flex-row justify-center mt-8">
<Text className="text-gray-600">Don't have an account? </Text>
<TouchableOpacity onPress={() => router.push('/signup')}>
<Text className="text-[#bb0718] font-medium">Sign Up</Text>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
}

141
app/signup.tsx Normal file
View File

@ -0,0 +1,141 @@
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, SafeAreaView, StatusBar, Alert, ScrollView } from 'react-native';
import { router } from 'expo-router';
import { Feather } from '@expo/vector-icons';
import { useAuth } from './context/auth-context';
export default function SignupScreen() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { signup } = useAuth();
const handleSignup = async () => {
if (!name || !email || !password || !confirmPassword) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
if (password !== confirmPassword) {
Alert.alert('Error', 'Passwords do not match');
return;
}
try {
setIsLoading(true);
await signup(name, email, password);
} catch (error) {
Alert.alert('Error', 'Failed to sign up. Please try again.');
} finally {
setIsLoading(false);
}
};
return (
<SafeAreaView className="flex-1 bg-white">
<StatusBar barStyle="dark-content" />
<ScrollView className="flex-1">
<View className="px-6 py-10">
{/* Back Button */}
<TouchableOpacity
className="bg-[#ffd60a] p-3 rounded-lg w-12 mb-8"
onPress={() => router.back()}
>
<Feather name="arrow-left" size={24} color="#bb0718" />
</TouchableOpacity>
{/* Header */}
<Text className="text-3xl font-bold mb-8">Create an account</Text>
{/* Form */}
<View className="space-y-6">
<View>
<Text className="text-gray-700 mb-2 font-medium">Full Name</Text>
<TextInput
className="bg-gray-100 py-4 px-4 rounded-xl"
placeholder="Enter your full name"
value={name}
onChangeText={setName}
/>
</View>
<View>
<Text className="text-gray-700 mb-2 font-medium">Email</Text>
<TextInput
className="bg-gray-100 py-4 px-4 rounded-xl"
placeholder="Enter your email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
</View>
<View>
<Text className="text-gray-700 mb-2 font-medium">Password</Text>
<View className="flex-row items-center bg-gray-100 rounded-xl">
<TextInput
className="flex-1 py-4 px-4"
placeholder="Enter your password"
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoCapitalize="none"
/>
<TouchableOpacity
className="pr-4"
onPress={() => setShowPassword(!showPassword)}
>
<Feather name={showPassword ? "eye-off" : "eye"} size={20} color="gray" />
</TouchableOpacity>
</View>
</View>
<View>
<Text className="text-gray-700 mb-2 font-medium">Confirm Password</Text>
<View className="flex-row items-center bg-gray-100 rounded-xl">
<TextInput
className="flex-1 py-4 px-4"
placeholder="Confirm your password"
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry={!showPassword}
autoCapitalize="none"
/>
<TouchableOpacity
className="pr-4"
onPress={() => setShowPassword(!showPassword)}
>
<Feather name={showPassword ? "eye-off" : "eye"} size={20} color="gray" />
</TouchableOpacity>
</View>
</View>
<TouchableOpacity
className="bg-[#ffd60a] py-4 rounded-xl mt-6"
onPress={handleSignup}
disabled={isLoading}
>
<Text className="text-center font-bold text-lg">
{isLoading ? 'Signing up...' : 'Sign Up'}
</Text>
</TouchableOpacity>
</View>
{/* Login Link */}
<View className="flex-row justify-center mt-8 mb-4">
<Text className="text-gray-600">Already have an account? </Text>
<TouchableOpacity onPress={() => router.push('/login')}>
<Text className="text-[#bb0718] font-medium">Login</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}

51
app/welcome.tsx Normal file
View File

@ -0,0 +1,51 @@
import React from 'react';
import { View, Text, Image, TouchableOpacity, SafeAreaView, StatusBar } from 'react-native';
import { router } from 'expo-router';
import { Feather } from '@expo/vector-icons';
export default function WelcomeScreen() {
return (
<SafeAreaView className="flex-1 bg-white">
<StatusBar barStyle="dark-content" />
<View className="flex-1 justify-between px-6 py-10">
{/* Logo and Welcome Text */}
<View className="items-center mt-10">
<View className="w-32 h-32 items-center justify-center bg-[#ffd60a] rounded-full mb-8">
<Feather name="book-open" size={60} color="#bb0718" />
</View>
<Text className="text-4xl font-bold text-center">Welcome to ChefHai</Text>
<Text className="text-gray-600 text-center mt-4 text-lg">
Discover, cook and share delicious recipes with food lovers around the world
</Text>
</View>
{/* Food Image */}
<View className="items-center my-8">
<Image
source={{ uri: "/placeholder.svg?height=300&width=300&query=colorful food dishes arrangement" }}
className="w-72 h-72 rounded-full"
/>
</View>
{/* Buttons */}
<View className="space-y-4 mb-6">
<TouchableOpacity
className="bg-[#ffd60a] py-4 rounded-xl mb-4"
onPress={() => router.push('/login')}
>
<Text className="text-center font-bold text-lg">Login</Text>
</TouchableOpacity>
<TouchableOpacity
className="bg-white border border-[#ffd60a] py-4 rounded-xl"
onPress={() => router.push('/signup')}
>
<Text className="text-center font-bold text-lg text-[#bb0718]">Sign Up</Text>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
}

10
package-lock.json generated
View File

@ -22,6 +22,7 @@
"expo-image-picker": "~16.1.4", "expo-image-picker": "~16.1.4",
"expo-linking": "~7.1.4", "expo-linking": "~7.1.4",
"expo-router": "~5.0.6", "expo-router": "~5.0.6",
"expo-secure-store": "~14.2.3",
"expo-splash-screen": "~0.30.8", "expo-splash-screen": "~0.30.8",
"expo-status-bar": "~2.2.3", "expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.4", "expo-symbols": "~0.4.4",
@ -6439,6 +6440,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/expo-secure-store": {
"version": "14.2.3",
"resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-14.2.3.tgz",
"integrity": "sha512-hYBbaAD70asKTFd/eZBKVu+9RTo9OSTMMLqXtzDF8ndUGjpc6tmRCoZtrMHlUo7qLtwL5jm+vpYVBWI8hxh/1Q==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-splash-screen": { "node_modules/expo-splash-screen": {
"version": "0.30.8", "version": "0.30.8",
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.30.8.tgz", "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.30.8.tgz",

View File

@ -40,7 +40,8 @@
"react-native-screens": "~4.10.0", "react-native-screens": "~4.10.0",
"react-native-web": "~0.20.0", "react-native-web": "~0.20.0",
"react-native-webview": "13.13.5", "react-native-webview": "13.13.5",
"tailwindcss": "^3.4.17" "tailwindcss": "^3.4.17",
"expo-secure-store": "~14.2.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",