diff --git a/app.json b/app.json index 2fd62b4..93fa4bc 100644 --- a/app.json +++ b/app.json @@ -33,7 +33,8 @@ "resizeMode": "contain", "backgroundColor": "#ffffff" } - ] + ], + "expo-secure-store" ], "experiments": { "typedRoutes": true diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index e6b977c..084ecca 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,7 +1,15 @@ 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() { + const { isAuthenticated, isLoading } = useAuth(); + + // If not authenticated and not loading, redirect to welcome + if (!isLoading && !isAuthenticated) { + return ; + } + return ( ( diff --git a/app/(tabs)/index.tsx b/app/(tabs)/home.tsx similarity index 95% rename from app/(tabs)/index.tsx rename to app/(tabs)/home.tsx index 072d868..f587fb9 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/home.tsx @@ -52,7 +52,7 @@ const foodHighlights = [ ]; const navigateToFoodDetail = (foodId: string) => { - router.push({ pathname: "/food/[id]", params: { id: foodId } }); + router.push({ pathname: "/recipe-detail", params: { id: foodId } }); }; export default function HomeScreen() { @@ -217,14 +217,11 @@ export default function HomeScreen() { - - {/* Food Highlights Section */} - - - - Food Highlights - - + {/* Highlights Section */} + + + Highlights + {foodHighlights.map((food) => ( diff --git a/app/+not-found.tsx b/app/+not-found.tsx index 215b0ed..f4b2a33 100644 --- a/app/+not-found.tsx +++ b/app/+not-found.tsx @@ -10,7 +10,7 @@ export default function NotFoundScreen() { This screen does not exist. - + Go to home screen! diff --git a/app/_layout.tsx b/app/_layout.tsx index 79068cd..a979f52 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,10 +1,12 @@ import { Stack } from "expo-router"; import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { AuthProvider } from "./context/auth-context"; import "../global.css"; export default function RootLayout() { return ( + + ); } diff --git a/app/context/auth-context.tsx b/app/context/auth-context.tsx new file mode 100644 index 0000000..4173068 --- /dev/null +++ b/app/context/auth-context.tsx @@ -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; + signup: (name: string, email: string, password: string) => Promise; + logout: () => Promise; +}; + +const AuthContext = createContext(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 ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} \ No newline at end of file diff --git a/app/index.tsx b/app/index.tsx new file mode 100644 index 0000000..7769c7d --- /dev/null +++ b/app/index.tsx @@ -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 ( + + + + ); + } + + // Redirect based on authentication status + if (isAuthenticated) { + return ; + } else { + return ; + } +} \ No newline at end of file diff --git a/app/login.tsx b/app/login.tsx new file mode 100644 index 0000000..d87cae0 --- /dev/null +++ b/app/login.tsx @@ -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 ( + + + + + {/* Back Button */} + router.back()} + > + + + + {/* Header */} + Login to your account + + {/* Form */} + + + Email + + + + + Password + + + setShowPassword(!showPassword)} + > + + + + + + + Forgot Password? + + + + + {isLoading ? 'Logging in...' : 'Login'} + + + + + {/* Sign Up Link */} + + Don't have an account? + router.push('/signup')}> + Sign Up + + + + + ); +} \ No newline at end of file diff --git a/app/signup.tsx b/app/signup.tsx new file mode 100644 index 0000000..e860e13 --- /dev/null +++ b/app/signup.tsx @@ -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 ( + + + + + + {/* Back Button */} + router.back()} + > + + + + {/* Header */} + Create an account + + {/* Form */} + + + Full Name + + + + + Email + + + + + Password + + + setShowPassword(!showPassword)} + > + + + + + + + Confirm Password + + + setShowPassword(!showPassword)} + > + + + + + + + + {isLoading ? 'Signing up...' : 'Sign Up'} + + + + + {/* Login Link */} + + Already have an account? + router.push('/login')}> + Login + + + + + + ); +} \ No newline at end of file diff --git a/app/welcome.tsx b/app/welcome.tsx new file mode 100644 index 0000000..716e445 --- /dev/null +++ b/app/welcome.tsx @@ -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 ( + + + + + {/* Logo and Welcome Text */} + + + + + + Welcome to ChefHai + + Discover, cook and share delicious recipes with food lovers around the world + + + + {/* Food Image */} + + + + + {/* Buttons */} + + router.push('/login')} + > + Login + + + router.push('/signup')} + > + Sign Up + + + + + ); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a73cf83..726325b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "expo-image-picker": "~16.1.4", "expo-linking": "~7.1.4", "expo-router": "~5.0.6", + "expo-secure-store": "~14.2.3", "expo-splash-screen": "~0.30.8", "expo-status-bar": "~2.2.3", "expo-symbols": "~0.4.4", @@ -6439,6 +6440,15 @@ "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": { "version": "0.30.8", "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.30.8.tgz", diff --git a/package.json b/package.json index bd73d78..72dc3d1 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "react-native-screens": "~4.10.0", "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", - "tailwindcss": "^3.4.17" + "tailwindcss": "^3.4.17", + "expo-secure-store": "~14.2.3" }, "devDependencies": { "@babel/core": "^7.25.2",