diff --git a/__pycache__/services.cpython-311.pyc b/__pycache__/services.cpython-311.pyc index 2d5f1cf0..5a393094 100644 Binary files a/__pycache__/services.cpython-311.pyc and b/__pycache__/services.cpython-311.pyc differ diff --git a/app.py b/app.py index 751c2c16..b2aa7abe 100644 --- a/app.py +++ b/app.py @@ -9,12 +9,63 @@ from datetime import timedelta import os from dotenv import load_dotenv +from flask_cors import CORS # Import CORS load_dotenv() app = Flask(__name__) +# Enable CORS with specific settings +CORS(app, resources={r"/*": {"origins": "*", "allow_headers": ["Content-Type", "Accept"], "methods": ["GET", "POST", "OPTIONS"]}}) + +# Handle OPTIONS requests explicitly +@app.route('/', defaults={'path': ''}, methods=['OPTIONS']) +@app.route('/', methods=['OPTIONS']) +def handle_options(path): + response = make_response() + response.headers.add('Access-Control-Allow-Origin', '*') + response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Accept') + response.headers.add('Access-Control-Allow-Methods', 'GET,POST,OPTIONS') + return response CORS(app) +# Fetch or create user by ID +@app.route('/users/fetch/', methods=['POST']) +def fetch_user(user_id): + try: + # Check if user exists + user_doc = db.collection('users').document(user_id).get() + + if user_doc.exists: + # User exists, return user data + user_data = user_doc.to_dict() + return jsonify({"success": True, "data": user_data, "existing": True}) + else: + # User doesn't exist, create new user + data = request.get_json() or {} + + # Check required fields for new user + if 'userEmail' not in data or 'userName' not in data: + return jsonify({"success": False, "error": "Missing required fields for new user: userEmail and userName"}), 400 + + # Create user document in Firestore + user_ref = db.collection('users').document(user_id) + + # Set user data + user_data = { + "userId": user_id, + "userEmail": data['userEmail'], + "userName": data['userName'], + "numRecipe": 0, + "streak": 0 + # Add other default fields as needed + } + + user_ref.set(user_data) + + return jsonify({"success": True, "data": user_data, "existing": False}), 201 + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + @app.route('/extract-text', methods=['POST']) def extract_text_endpoint(): if 'file' not in request.files: @@ -113,6 +164,20 @@ def create_recipe(): if not data or 'dishName' not in data or 'ingredients' not in data or 'recipeSteps' not in data or 'userId' not in data: return jsonify({"success": False, "error": "Missing required fields: dishName, ingredients, recipeSteps, and userId"}), 400 + # Validate route field if provided + route = data.get('route', 'original') # Default to 'original' if not provided + valid_routes = ['original', 'local', 'sustainable'] + if route not in valid_routes: + return jsonify({"success": False, "error": f"Invalid route value. Must be one of: {', '.join(valid_routes)}"}), 400 + + user_id = data['userId'] + # Check if user exists + user_ref = db.collection('users').document(user_id) + user_doc = user_ref.get() + + if not user_doc.exists: + return jsonify({"success": False, "error": f"User with ID {user_id} does not exist. Please create the user first."}), 404 + # Create recipe document in Firestore recipe_ref = db.collection('recipes').document() @@ -122,6 +187,7 @@ def create_recipe(): "ingredients": data['ingredients'], "recipeSteps": data['recipeSteps'], "userId": data['userId'], + "route": route, # Add the route field } # Add timestamp in Firestore but don't include in response @@ -130,13 +196,9 @@ def create_recipe(): recipe_ref.set(firestore_data) # Update user's recipe count - user_ref = db.collection('users').document(data['userId']) - user_doc = user_ref.get() - - if user_doc.exists: - user_data = user_doc.to_dict() - current_count = user_data.get('numRecipe', 0) - user_ref.update({"numRecipe": current_count + 1}) + user_data = user_doc.to_dict() + current_count = user_data.get('numRecipe', 0) + user_ref.update({"numRecipe": current_count + 1}) return jsonify({"success": True, "data": recipe_data, "id": recipe_ref.id}), 201 except Exception as e: @@ -147,10 +209,14 @@ def create_recipe(): def get_recipes_by_user(user_id): try: # Query recipes collection where userId matches - recipes_query = db.collection('recipes').where('userId', '==', user_id).stream() + recipes_query = db.collection('recipes').where('userId', '==', user_id) + + + # Execute query + recipes_results = recipes_query.stream() recipes = [] - for doc in recipes_query: + for doc in recipes_results: recipe_data = doc.to_dict() recipe_data['id'] = doc.id recipes.append(recipe_data) @@ -159,5 +225,67 @@ def get_recipes_by_user(user_id): except Exception as e: return jsonify({"success": False, "error": str(e)}), 500 +# Get a user's most common recipe route +@app.route('/users//most-common-route', methods=['GET']) +def get_most_common_route(user_id): + try: + # Check if user exists + user_doc = db.collection('users').document(user_id).get() + + if not user_doc.exists: + return jsonify({"success": False, "error": f"User with ID {user_id} does not exist"}), 404 + + # Query recipes collection where userId matches + recipes_query = db.collection('recipes').where('userId', '==', user_id).stream() + + # Count occurrences of each route + route_counts = { + 'original': 0, + 'local': 0, + 'sustainable': 0 + } + + total_recipes = 0 + for doc in recipes_query: + recipe_data = doc.to_dict() + route = recipe_data.get('route', 'original') + if route in route_counts: + route_counts[route] += 1 + total_recipes += 1 + + if total_recipes == 0: + # No recipes found, default to 'original' + most_common_route = 'original' + most_common_count = 0 + else: + # Find the route with the highest count + most_common_route = 'original' # Default in case of tie + most_common_count = 0 + + for route, count in route_counts.items(): + if count > most_common_count: + most_common_route = route + most_common_count = count + + # Calculate percentage if there are recipes + percentage = (most_common_count / total_recipes * 100) if total_recipes > 0 else 0 + + return jsonify({ + "success": True, + "data": { + "userId": user_id, + "mostCommonRoute": most_common_route, + "count": most_common_count, + "totalRecipes": total_recipes, + "percentage": round(percentage, 2), + "routeCounts": route_counts + } + }) + + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + + + if __name__ == "__main__": app.run(debug=True) \ No newline at end of file diff --git a/my-app/app/(tabs)/index.tsx b/my-app/app/(tabs)/index.tsx index 0139069e..924f8145 100644 --- a/my-app/app/(tabs)/index.tsx +++ b/my-app/app/(tabs)/index.tsx @@ -1,9 +1,14 @@ import { View, Text, StyleSheet, Image, TouchableOpacity, ScrollView } from 'react-native'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Ionicons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; import { Alert } from 'react-native'; +<<<<<<< HEAD +import { useAuth } from '../../context/AuthContext'; +import { router } from 'expo-router'; +======= import { useRouter, useLocalSearchParams } from 'expo-router'; +>>>>>>> 7579ee1b39ce557d39554c11e483639cc686c2ed // Define a type for user data type UserProfile = { @@ -13,24 +18,140 @@ type UserProfile = { zipcode: string; favoriteRoute: string; recipesCount: number; - bio?: string; - achievements?: string[]; +}; + +// Define a type for the most common route response +type MostCommonRouteResponse = { + success: boolean; + data: { + userId: string; + mostCommonRoute: string; + count: number; + totalRecipes: number; + percentage: number; + routeCounts: { + original: number; + local: number; + sustainable: number; + } + } +} + +// Function to get a human-readable route name +const getRouteDisplayName = (route: string): string => { + switch (route) { + case 'original': return 'The OG'; + case 'local': return 'Local'; + case 'sustainable': return 'Green Warrior'; + default: return route; + } }; export default function ProfileScreen() { + // Get auth context for logout + const { logout, userInfo } = useAuth(); + const [loading, setLoading] = useState(false); + const [routeInfo, setRouteInfo] = useState(null); + + // Log user info for debugging + useEffect(() => { + if (userInfo) { + console.log('Profile Screen - Auth0 User Info:', userInfo); + // Fetch user's favorite route and recipe count + fetchUserRouteInfo(); + } + }, [userInfo]); + + // Fetch user's favorite route info from the API + const fetchUserRouteInfo = async () => { + if (!userInfo?.sub) return; + + setLoading(true); + try { + // Try different API URLs in order of preference + const API_URLS = [ + 'http://127.0.0.1:5000', + 'http://10.0.2.2:5000' + ]; + + let response = null; + + // Try each URL until one works + for (const baseUrl of API_URLS) { + const url = `${baseUrl}/users/${userInfo.sub}/most-common-route`; + console.log(`Fetching route info from: ${url}`); + + try { + const res = await fetch(url); + if (res.ok) { + response = res; + console.log(`Successfully connected to: ${baseUrl}`); + break; // Exit the loop if successful + } + } catch (error) { + console.log(`Failed to connect to ${baseUrl}: ${error}`); + // Continue to try the next URL + } + } + + if (!response) { + console.error('Failed to fetch user route info'); + return; + } + + const data = await response.json() as MostCommonRouteResponse; + console.log('User route info:', data); + + if (data.success) { + setRouteInfo(data.data); + + // Update profile with new data + setProfile(prev => ({ + ...prev, + favoriteRoute: getRouteDisplayName(data.data.mostCommonRoute), + recipesCount: data.data.totalRecipes + })); + } + } catch (error) { + console.error('Error fetching user route info:', error); + } finally { + setLoading(false); + } + }; + + // Handle logout + const handleLogout = async () => { + try { + await logout(); + // Navigate to the root index page + router.replace('/'); + } catch (error) { + console.error('Logout error:', error); + Alert.alert('Error', 'Failed to log out. Please try again.'); + } + }; + // User profile mock data const [profile, setProfile] = useState({ - name: "Kim", + name: userInfo?.name || "User", role: "Food Explorer", location: "Lincoln, NE", zipcode: "60201", - favoriteRoute: "FavRoute", - recipesCount: 27, - bio: "Passionate about local food and supporting farmers in my community. I love creating seasonal recipes and exploring new farmers markets.", - achievements: ["Market Maven", "Recipe Creator", "Local Food Champion"] + favoriteRoute: "Loading...", + recipesCount: 0 }); - const [profilePhoto, setProfilePhoto] = useState(null); + // Update profile when userInfo changes + useEffect(() => { + if (userInfo) { + setProfile(prev => ({ + ...prev, + name: userInfo.name || userInfo.email || "User" + })); + } + }, [userInfo]); + + const [profilePhoto, setProfilePhoto] = useState(null); const pickProfile = async () => { try { @@ -40,19 +161,19 @@ export default function ProfileScreen() { Alert.alert('Permission Required', 'Sorry, we need media library permissions.'); return; } - + const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [1, 1], quality: 1, }); - + if (!result.canceled && result.assets[0].uri) { - setProfilePhoto(result.assets[0].uri); + setProfilePhoto(result.assets[0].uri); } } catch (error) { - console.error('Error picking profile image:', error); + console.error('Error picking profile image:', error); } }; @@ -70,25 +191,47 @@ export default function ProfileScreen() { + source={require('../../assets/images/replatelogo1.png')} // Make sure the logo path is correct + style={styles.logo} + /> RePlate + + {/* Logout button in top right */} + + + Logout + - + {profilePhoto ? ( + + ) : userInfo?.picture ? ( + + ) : ( + + + {profile.name.charAt(0).toUpperCase()} + + + )} Hi {profile.name}! You are a {profile.role}! - {profile.favoriteRoute} is your most taken route + + {routeInfo ? ( + + {routeInfo.totalRecipes > 0 + ? `Your preferred route is ${profile.favoriteRoute} (${routeInfo.percentage}%)` + : 'You haven\'t created any recipes yet'} + + ) : ( + {profile.favoriteRoute} + )} @@ -96,9 +239,15 @@ export default function ProfileScreen() { {profile.zipcode} - # recipes made: + # recipes saved: {profile.recipesCount} + {userInfo?.email && ( + + Email: + {userInfo.email} + + )} @@ -121,14 +270,18 @@ export default function ProfileScreen() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#84a24d', + backgroundColor: '#84a24d', padding: 20, }, header: { flexDirection: 'row', // Align logo and title horizontally alignItems: 'center', // Vertically center them marginBottom: 20, // Add some spacing below the header +<<<<<<< HEAD + justifyContent: 'space-between', // Space elements evenly +======= marginTop: 10, +>>>>>>> 7579ee1b39ce557d39554c11e483639cc686c2ed }, logo: { width: 50, // Adjust width as per your logo size @@ -150,7 +303,7 @@ const styles = StyleSheet.create({ width: 150, height: 150, borderRadius: 75, - backgroundColor: '#e6e0d9', + backgroundColor: '#e6e0d9', justifyContent: 'center', alignItems: 'center', marginBottom: 20, @@ -221,7 +374,7 @@ const styles = StyleSheet.create({ justifyContent: 'center', gap: 15, marginVertical: 10, - marginBottom: 100, + marginBottom: 100, }, // actionButton: { // flexDirection: 'row', @@ -246,4 +399,33 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: '500', }, + logoutButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#e0e0e0', + paddingVertical: 6, + paddingHorizontal: 10, + borderRadius: 15, + gap: 4, + marginLeft: 'auto', // Push to the right + }, + logoutButtonText: { + color: '#666', + fontSize: 12, + fontWeight: '500', + fontFamily: 'Nunito', + }, + defaultProfileImage: { + width: '100%', + height: '100%', + borderRadius: 75, + backgroundColor: '#e6e0d9', + justifyContent: 'center', + alignItems: 'center', + }, + defaultProfileText: { + fontSize: 60, + fontWeight: 'bold', + color: '#84a24d', + }, }); diff --git a/my-app/app/index.tsx b/my-app/app/index.tsx index d38e0b43..bfb55075 100644 --- a/my-app/app/index.tsx +++ b/my-app/app/index.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { View, Text, TouchableOpacity, StyleSheet, Image, ActivityIndicator } from 'react-native'; import { router } from 'expo-router'; import { useState, useEffect, useRef } from 'react'; @@ -33,10 +34,17 @@ console.log(redirectUri); console.log('=========================================='); export default function LoginPage() { - const { isAuthenticated, login, isLoading } = useAuth(); + const { isAuthenticated, login, logout, isLoading, userInfo } = useAuth(); const [localLoading, setLocalLoading] = useState(false); const navigatedRef = useRef(false); + // Debug the backend user + // useEffect(() => { + // if (backendUser) { + // console.log('Backend user loaded:', backendUser); + // } + // }, [backendUser]); + // If already authenticated, go to tabs - simple version useEffect(() => { if (isAuthenticated && !isLoading && !navigatedRef.current) { @@ -46,6 +54,19 @@ export default function LoginPage() { } }, [isAuthenticated, isLoading]); + // Handle logout + const handleLogout = async () => { + try { + setLocalLoading(true); + await logout(); + console.log('User logged out'); + } catch (error) { + console.error('Logout error', error); + } finally { + setLocalLoading(false); + } + }; + // Handle login with Auth0 const handleLogin = async () => { try { @@ -102,24 +123,37 @@ export default function LoginPage() { // Login UI return ( - - Welcome to ClickBite! {/* You can add your app logo here */} {/* */} - - Log In / Sign Up - - - - - Sign in to access your account and start using the app. - - + {isAuthenticated ? ( + // Show logout button if authenticated + <> + + Welcome back, {userInfo?.name || 'User'}! + + + Log Out + + + ) : ( + // Show login button if not authenticated + <> + + Log In / Sign Up + + + + + Sign in to access your account and start using the app. + + + + )} ); } @@ -152,6 +186,13 @@ const styles = StyleSheet.create({ marginBottom: -10, fontFamily: 'Baloo', }, + welcomeText: { + fontSize: 22, + color: '#ffffff', + marginBottom: 20, + fontFamily: 'Nunito', + textAlign: 'center', + }, loginButton: { backgroundColor: '#e6e0d9', paddingVertical: 15, @@ -160,11 +201,25 @@ const styles = StyleSheet.create({ width: '80%', alignItems: 'center', }, + logoutButton: { + backgroundColor: '#d9534f', + paddingVertical: 15, + paddingHorizontal: 40, + borderRadius: 25, + width: '80%', + alignItems: 'center', + }, loginButtonText: { fontSize: 18, fontWeight: 'bold', fontFamily: 'NunitoBold', }, + buttonText: { + fontSize: 18, + fontWeight: 'bold', + color: '#ffffff', + fontFamily: 'NunitoBold', + }, infoContainer: { width: '80%', marginTop: 30, diff --git a/my-app/context/AuthContext.tsx b/my-app/context/AuthContext.tsx index 1b25a50b..ad3133d7 100644 --- a/my-app/context/AuthContext.tsx +++ b/my-app/context/AuthContext.tsx @@ -8,6 +8,7 @@ type UserInfo = { email?: string; picture?: string; sub?: string; // Auth0 user ID + nickname?: string; [key: string]: any; // Allow for additional properties }; @@ -41,12 +42,18 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children useEffect(() => { const checkAuthStatus = async () => { try { + console.log('Checking authentication status...'); const token = await AsyncStorage.getItem('auth0Token'); const user = await AsyncStorage.getItem('auth0User'); if (token && user) { + console.log('Found saved authentication'); setIsAuthenticated(true); - setUserInfo(JSON.parse(user)); + const parsedUser = JSON.parse(user); + setUserInfo(parsedUser); + console.log('User info loaded:', parsedUser); + } else { + console.log('No saved authentication found'); } } catch (error) { console.error('Error checking auth status', error); @@ -58,21 +65,117 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children checkAuthStatus(); }, []); + // Fetch user information from the backend + const getUserInfo = async (user: UserInfo) => { + try { + // Try different API URLs in order of preference + const API_URLS = [ + 'http://127.0.0.1:5000', + 'http://10.0.2.2:5000' + ]; + + let response = null; + let lastError = null; + let successfulUrl = ''; + + // Try each URL until one works + for (const baseUrl of API_URLS) { + const url = `${baseUrl}/users/fetch/${user.sub}`; + console.log(`Attempting to connect to: ${url}`); + + try { + response = await fetch(url, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + userEmail: user.email, + userName: user.name || user.nickname || user.email + }) + }); + + if (response.ok) { + console.log(`Successfully connected to: ${baseUrl}`); + successfulUrl = baseUrl; + break; // Exit the loop if successful + } + } catch (error: any) { + lastError = error; + console.log(`Failed to connect to ${baseUrl}: ${error.message || 'Unknown error'}`); + // Continue to try the next URL + } + } + + if (!response || !response.ok) { + console.error('All connection attempts failed:', lastError); + throw new Error('Failed to connect to any backend endpoint'); + } + + const data = await response.json(); + console.log(`User data response from ${successfulUrl}:`, data); + + if (data.success) { + // Merge Auth0 and backend user data + const mergedUserData = { + ...user, + ...data.data + }; + + console.log('User successfully fetched or created:', data.existing ? 'Existing user' : 'New user'); + return mergedUserData; + } else { + throw new Error(data.error || 'Unknown error from server'); + } + } catch (error) { + console.error('Error fetching user info:', error); + + // Create a local fallback user when backend is unreachable + console.log('Using Auth0 user data as fallback'); + return { + ...user, + userId: user.sub, + userEmail: user.email, + userName: user.name || user.nickname || user.email, + numRecipe: 0, + streak: 0 + }; + } + }; + // Login function const login = async (accessToken: string, user: UserInfo) => { try { + console.log('Logging in user:', user); + console.log('user name:', user.name || user.nickname); + console.log('user email:', user.email); await AsyncStorage.setItem('auth0Token', accessToken); await AsyncStorage.setItem('auth0User', JSON.stringify(user)); + setIsAuthenticated(true); - setUserInfo(user); + setUserInfo(user); // Set initial userInfo from Auth0 + + try { + // Try to get backend user data + const mergedUserData = await getUserInfo(user); + setUserInfo(mergedUserData); + console.log('Final user data after merging:', mergedUserData); + } catch (backendError) { + console.error('Error getting backend user data:', backendError); + // We still set the user as authenticated with Auth0 data + } + + console.log('Login successful'); } catch (error) { console.error('Error storing auth data', error); } }; - // Logout function - simplified version + // Logout function const logout = async () => { try { + console.log('Logging out user'); // Clear AsyncStorage await AsyncStorage.removeItem('auth0Token'); await AsyncStorage.removeItem('auth0User'); @@ -83,16 +186,23 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // Navigate to login page router.replace('/'); + console.log('Logout successful'); } catch (error) { console.error('Error during logout', error); } }; return ( - + {children} ); }; -export default AuthContext; \ No newline at end of file +export default AuthContext; \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index eaa717a3..4d814483 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ flask firebase_admin gunicorn -python-dotenv \ No newline at end of file +python-dotenv +Flask-CORS \ No newline at end of file