From 4014c5e3af562a9c8951259589f6dbcc3129cf57 Mon Sep 17 00:00:00 2001 From: li-madison Date: Sat, 2 May 2026 10:07:29 -0500 Subject: [PATCH 1/7] delete unused files --- backend/BACKEND_GUIDE.md | 390 ------------------------------- backend/HistoryPage.jsx | 68 ------ backend/NutritionAPI.js | 116 --------- backend/SearchHistoryService.js | 92 -------- backend/SearchPage.jsx | 105 --------- backend/firebase_auth_context.js | 79 ------- 6 files changed, 850 deletions(-) delete mode 100644 backend/BACKEND_GUIDE.md delete mode 100644 backend/HistoryPage.jsx delete mode 100644 backend/NutritionAPI.js delete mode 100644 backend/SearchHistoryService.js delete mode 100644 backend/SearchPage.jsx delete mode 100644 backend/firebase_auth_context.js diff --git a/backend/BACKEND_GUIDE.md b/backend/BACKEND_GUIDE.md deleted file mode 100644 index 17a2b35..0000000 --- a/backend/BACKEND_GUIDE.md +++ /dev/null @@ -1,390 +0,0 @@ -# Connecting a Backend (Firebase) - -## What is a Backend? - -The backend is the part of the app users don't see — but it's what makes everything real. - -### Key Responsibilities - -* **Data Storage** — Saves user data so it persists between sessions -* **Authentication** — Knows who you are -* **APIs** — Lets your app talk to external data sources -* **Security** — Controls who can access what - -### Analogy - -Think of a restaurant: - -* Frontend = dining area (what you see) -* Backend = kitchen (where everything actually happens) - ---- - -## What is Firebase? - -Firebase is a Backend-as-a-Service (BaaS) — a backend you don't have to build yourself. - -### What it gives you - -* **Firestore** — a real-time cloud database -* **Authentication** — handles login/register securely -* **Hosting** — deploy your app in one command -* No servers to manage - ---- - -## Setting Up Firebase - -### Step 1: Create a Project - -* Go to [Firebase Console](https://console.firebase.google.com) -* Click **"Add Project"** → give it a name → disable Analytics - -### Step 2: Register Your Web App - -* Click the **``** icon -* Copy the config object it gives you - -### Step 3: Set Up Firestore - -* Click **Firestore Database → Create Database** -* Choose **Test Mode** (for dev day) -* Click through the location prompt - -### Step 4: Enable Authentication - -* Click **Authentication → Get Started** -* Enable **Email/Password** - ---- - -## Firebase Configuration - -```javascript -// src/firebase/firebase.js -import { initializeApp } from "firebase/app"; -import { getAuth } from "firebase/auth"; -import { getFirestore } from "firebase/firestore"; - -const firebaseConfig = { - apiKey: process.env.REACT_APP_FIREBASE_API_KEY, - authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, - projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, - // ... rest of the keys from your .env -}; - -const app = initializeApp(firebaseConfig); - -export const auth = getAuth(app); -export const db = getFirestore(app); -``` - -### Why environment variables? - -* API keys are secrets — never put them directly in code -* `.env` file stays off GitHub (it's in `.gitignore`) -* `REACT_APP_` prefix is required by Create React App - ---- - ---- - -# User Authentication - -## What is Authentication? - -Authentication answers one question: **who is this person?** - -### What it lets users do - -* Register an account -* Log in -* Stay logged in (Firebase remembers them) -* Log out - ---- - -## The Core Functions - -Three lines. Firebase does everything else. - -```javascript -// src/firebase/auth.js -createUserWithEmailAndPassword(auth, email, password) // Register -signInWithEmailAndPassword(auth, email, password) // Login -signOut(auth) // Logout -``` - -Firebase handles: password hashing, tokens, session management, security. - ---- - -## Auth Context (The Big Idea) - -Without Auth Context, every component would need to ask Firebase "is anyone logged in?" separately. - -Instead, we ask **once** at the top of the app and share the answer everywhere. - -```javascript -// src/contexts/AuthContext.jsx -export function AuthProvider({ children }) { - const [currentUser, setCurrentUser] = useState(null); - - useEffect(() => { - // Firebase calls this any time the user signs in or out - const unsubscribe = onAuthStateChanged(auth, (user) => { - setCurrentUser(user); - }); - return unsubscribe; - }, []); - - return ( - - {children} - - ); -} -``` - -### How any component reads the user - -```javascript -const { currentUser } = useAuth(); -// currentUser.uid ← unique ID for this user -// currentUser.email ← their email -// currentUser ← null if not logged in -``` - ---- - -## Auth Flow - -1. User enters email + password on `/login` -2. `loginUser()` calls Firebase -3. Firebase verifies credentials and returns a user object -4. `onAuthStateChanged` fires → `currentUser` updates everywhere -5. Navbar shows email + "Logout" button - ---- - -## Key Takeaways - -* Firebase handles auth securely — don't try to build this yourself -* Auth Context shares user state across the whole app -* `currentUser` is `null` when no one is logged in — always check for this - ---- - ---- - -# Connecting an API - -## What is an API? - -An API is how two applications talk to each other. - -### Analogy - -* You = your app -* Restaurant Kitchen = external data server -* Waiter = API - -You tell the waiter what you want → the kitchen makes it → the waiter brings it back. - ---- - -## This App: MealDB API - -MealDB gives us: - -* Food details and ingredients by name -* Dietary category filters (Vegan, Vegetarian, Seafood) -* Ingredient images - -No API key needed for the free tier. - ---- - -## Fetching Data - -```javascript -const response = await fetch( - `https://www.themealdb.com/api/json/v1/1/search.php?s=${foodName}` -); -const apiData = await response.json(); -``` - ---- - -## Key Concepts - -### async / await - -Code that "waits" for data before continuing. -Without it, you'd try to display data before it arrives. - -```javascript -// Wrong — data isn't there yet -const data = fetch(url); // returns a Promise, not data - -// Right — wait for it -const response = await fetch(url); -const data = await response.json(); -``` - -### try / catch - -Handles errors safely — network issues, bad responses, typos. - -```javascript -try { - const response = await fetch(url); - const data = await response.json(); - // use data here -} catch (error) { - console.error("Something went wrong:", error); -} -``` - ---- - -## Example Flow (Food.jsx) - -1. User clicks a food card -2. `fetchFoodDetails()` runs -3. Fetch request sent to MealDB with the food name -4. Response comes back with ingredients + nutrition -5. `setFoodDetails(...)` updates React state -6. UI re-renders with real data - ---- - -## Key Takeaways - -* `async/await` lets you write async code that reads like normal code -* Always wrap API calls in `try/catch` -* Never expose API keys in code — use `.env` - ---- - ---- - -# CRUD Using Firestore - -## What is CRUD? - -Every real app needs to do four things to data: - -* **Create** → Save something new -* **Read** → Fetch it back -* **Update** → Change it -* **Delete** → Remove it - ---- - -## This App: Saving Favorites to Firestore - -Old approach: `localStorage` — data lives in the browser only. Refresh on another device? Gone. - -New approach: **Firestore** — data lives in the cloud. Works everywhere, forever. - ---- - -## Firestore Data Structure - -``` -favorites/ ← collection - {userId}/ ← one document per user (keyed by their UID) - items: [ ← array of favorited foods - { id, name, image, price, category }, - ... - ] -``` - -One document per user. UID is the document ID. - ---- - -## Read — Load Favorites - -```javascript -// Runs when the user logs in -const snap = await getDoc(doc(db, "favorites", currentUser.uid)); -if (snap.exists()) { - setFavorites(snap.data().items || []); -} -``` - ---- - -## Create / Update — Save Favorites - -```javascript -// When the user clicks ❤️ — overwrite the whole items array -await setDoc(doc(db, "favorites", currentUser.uid), { - items: updatedFavorites, -}); -``` - -We read the current list, add or remove the item, then write the whole thing back. - ---- - -## The "It's Alive" Moment - -1. Log in → click ❤️ on a food card -2. Open **Firebase Console → Firestore → Data** -3. Find `favorites → your UID → items` -4. **It's there.** In the cloud. In real time. -5. Refresh the page → still there -6. Open another browser → still there - -That's the difference between localStorage and a real backend. - ---- - -## What Happens If You're Not Logged In? - -```javascript -if (!currentUser) { - addToast("🔒 Log in to save favorites!"); - return; -} -``` - -No user → no UID → can't write to Firestore. -The app gracefully tells the user instead of crashing. - ---- - -## Firestore Security Rules - -Controls who can read/write what. Without rules, anyone can access any document. - -``` -match /favorites/{userId} { - allow read, write: if request.auth != null - && request.auth.uid == userId; -} -``` - -Translation: **"You can only touch your own document."** - ---- - -## Key Takeaways - -* CRUD is the backbone of every real app -* Firestore replaces localStorage with cloud persistence -* Auth + Firestore together = data that belongs to a specific person -* Security rules are what make this safe - ---- - -## What Students Should Be Able to Answer - -* Why do we use environment variables instead of pasting keys directly? -* What does Firebase Auth give you that you couldn't build in an afternoon? -* Why do we wrap API calls in `try/catch`? -* What does Firestore actually look like — what's stored and where? -* How does every component know who's logged in without asking Firebase each time? diff --git a/backend/HistoryPage.jsx b/backend/HistoryPage.jsx deleted file mode 100644 index 211a9b9..0000000 --- a/backend/HistoryPage.jsx +++ /dev/null @@ -1,68 +0,0 @@ -// pages/HistoryPage.jsx -// ───────────────────────────────────────────── -// Shows the logged-in user's past food searches. -// Pulls from Firestore on mount. -// This proves the Firestore READ is working. -// ───────────────────────────────────────────── - -import { useEffect, useState } from "react"; -import { useAuth } from "../contexts/AuthContext"; -import { getSearchHistory, clearSearchHistory } from "../utils/SearchHistoryService"; - -export default function HistoryPage() { - const { currentUser } = useAuth(); - const [history, setHistory] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [clearing, setClearing] = useState(false); - - useEffect(() => { - async function load() { - const { data, error: fetchError } = await getSearchHistory(currentUser.uid); - setLoading(false); - if (fetchError) { setError(fetchError); return; } - setHistory(data); - } - load(); - }, [currentUser]); - - async function handleClear() { - setClearing(true); - const { error: clearError } = await clearSearchHistory(currentUser.uid); - setClearing(false); - if (clearError) { setError(clearError); return; } - setHistory([]); - } - - if (loading) return

Loading history...

; - if (error) return

{error}

; - - return ( -
-

Your Search History

- - {history.length === 0 ? ( -

No searches yet. Go search for a food!

- ) : ( - <> - - - {history.map((item, i) => ( -
- "{item.query}" - - {new Date(item.searchedAt).toLocaleString()} - -

Top result: {item.topResult.description}

- {item.topResult.nutrients?.Calories && ( -

Calories: {item.topResult.nutrients.Calories.value} kcal

- )} -
- ))} - - )} -
- ); -} diff --git a/backend/NutritionAPI.js b/backend/NutritionAPI.js deleted file mode 100644 index 51c5624..0000000 --- a/backend/NutritionAPI.js +++ /dev/null @@ -1,116 +0,0 @@ -// utils/NutritionAPI.js -// ───────────────────────────────────────────── -// All calls to the USDA FoodData Central API. -// Docs: https://fdc.nal.usda.gov/api-guide.html -// -// Always returns { data, error } — never throws. -// The frontend just checks: if (error) show error, else show data. -// ───────────────────────────────────────────── - -const BASE = "https://api.nal.usda.gov/fdc/v1"; -const API_KEY = import.meta.env.VITE_USDA_API_KEY; - -// ───────────────────────────────────────────── -// SEARCH — find foods by name -// query: string e.g. "apple", "chicken breast" -// returns list of food items with basic info -// ───────────────────────────────────────────── -export async function searchFoods(query) { - try { - const params = new URLSearchParams({ - query, - api_key: API_KEY, - dataType: "Foundation,SR Legacy", - pageSize: 10, - }); - const res = await fetch(`${BASE}/foods/search?${params}`); - if (!res.ok) throw Object.assign(new Error(), { status: res.status }); - const json = await res.json(); - - const foods = json.foods || []; - - // Clean up the response — only return what the frontend needs - const cleaned = foods.map((food) => ({ - fdcId: food.fdcId, - description: food.description, - brandOwner: food.brandOwner || null, - dataType: food.dataType, - nutrients: extractNutrients(food.foodNutrients), - })); - - return { data: cleaned, error: null }; - } catch (err) { - const msg = err.response?.status === 403 - ? "Invalid API key. Check your VITE_USDA_API_KEY in .env" - : err.response?.status === 429 - ? "Too many requests. Slow down and try again." - : "Failed to fetch foods. Check your internet connection."; - return { data: [], error: msg }; - } -} - -// ───────────────────────────────────────────── -// GET DETAIL — full nutrient breakdown for one food -// fdcId: number, e.g. 173944 -// ───────────────────────────────────────────── -export async function getFoodDetail(fdcId) { - try { - const res = await fetch(`${BASE}/food/${fdcId}?api_key=${API_KEY}`); - if (!res.ok) throw new Error(); - const food = await res.json(); - - return { - data: { - fdcId: food.fdcId, - description: food.description, - dataType: food.dataType, - nutrients: extractNutrients(food.foodNutrients), - }, - error: null, - }; - } catch (err) { - return { data: null, error: "Could not load food details." }; - } -} - -// ───────────────────────────────────────────── -// HELPER — pull the nutrients we care about -// from the raw USDA foodNutrients array. -// Each nutrient has a nutrientId — these IDs -// are stable across all USDA foods. -// ───────────────────────────────────────────── -function extractNutrients(rawNutrients = []) { - // USDA nutrient ID → human-readable label - // Full list: https://fdc.nal.usda.gov/food-details/747447/nutrients - const NUTRIENT_MAP = { - 1008: { label: "Calories", unit: "kcal" }, - 1003: { label: "Protein", unit: "g" }, - 1004: { label: "Total Fat", unit: "g" }, - 1005: { label: "Carbohydrates", unit: "g" }, - 1079: { label: "Fiber", unit: "g" }, - 2000: { label: "Total Sugars", unit: "g" }, - 1258: { label: "Saturated Fat", unit: "g" }, - 1253: { label: "Cholesterol", unit: "mg" }, - 1093: { label: "Sodium", unit: "mg" }, - 1087: { label: "Calcium", unit: "mg" }, - 1089: { label: "Iron", unit: "mg" }, - 1092: { label: "Potassium", unit: "mg" }, - 1106: { label: "Vitamin A", unit: "µg" }, - 1162: { label: "Vitamin C", unit: "mg" }, - 1114: { label: "Vitamin D", unit: "µg" }, - }; - - const result = {}; - - for (const n of rawNutrients) { - const id = n.nutrientId ?? n.nutrient?.id; - if (NUTRIENT_MAP[id]) { - result[NUTRIENT_MAP[id].label] = { - value: Math.round((n.value ?? n.amount ?? 0) * 10) / 10, // 1 decimal - unit: NUTRIENT_MAP[id].unit, - }; - } - } - - return result; -} diff --git a/backend/SearchHistoryService.js b/backend/SearchHistoryService.js deleted file mode 100644 index 3e55db2..0000000 --- a/backend/SearchHistoryService.js +++ /dev/null @@ -1,92 +0,0 @@ -// utils/SearchHistoryService.js -// ───────────────────────────────────────────── -// Saves and retrieves a user's food search history in Firestore. -// -// Firestore structure: -// searchHistory/ ← collection -// {uid}/ ← one document per user (keyed by their UID) -// searches: [ ← array of search records -// { -// query: "banana", -// searchedAt: Timestamp, -// topResult: { fdcId, description, nutrients } -// }, -// ... -// ] -// ───────────────────────────────────────────── - -import { - doc, - getDoc, - setDoc, - updateDoc, - arrayUnion, -} from "firebase/firestore"; -import { db } from "../firebase/firebase"; - -const histRef = (uid) => doc(db, "searchHistory", uid); - -// ───────────────────────────────────────────── -// READ — get all past searches for a user -// ───────────────────────────────────────────── -export async function getSearchHistory(uid) { - try { - const snap = await getDoc(histRef(uid)); - if (!snap.exists()) return { data: [], error: null }; - - // Return newest first - const searches = snap.data().searches || []; - return { data: [...searches].reverse(), error: null }; - } catch (err) { - return { data: [], error: "Could not load search history." }; - } -} - -// ───────────────────────────────────────────── -// WRITE — save a search result to history -// query: the string the user searched -// topResult: the first food item returned (fdcId, description, nutrients) -// ───────────────────────────────────────────── -export async function saveSearchToHistory(uid, query, topResult) { - const record = { - query, - searchedAt: new Date().toISOString(), // ISO string — easier to display than Firestore Timestamp - topResult: { - fdcId: topResult.fdcId, - description: topResult.description, - nutrients: topResult.nutrients, - }, - }; - - try { - const snap = await getDoc(histRef(uid)); - - if (!snap.exists()) { - // First search — create the document - await setDoc(histRef(uid), { searches: [record] }); - } else { - // Append to existing array - // Note: arrayUnion won't work here because objects with timestamps - // won't match — we use updateDoc + arrayUnion with the full record - await updateDoc(histRef(uid), { - searches: arrayUnion(record), - }); - } - - return { error: null }; - } catch (err) { - return { error: "Could not save search." }; - } -} - -// ───────────────────────────────────────────── -// DELETE — clear all search history for a user -// ───────────────────────────────────────────── -export async function clearSearchHistory(uid) { - try { - await setDoc(histRef(uid), { searches: [] }); - return { error: null }; - } catch (err) { - return { error: "Could not clear history." }; - } -} diff --git a/backend/SearchPage.jsx b/backend/SearchPage.jsx deleted file mode 100644 index a4ecabb..0000000 --- a/backend/SearchPage.jsx +++ /dev/null @@ -1,105 +0,0 @@ -// pages/SearchPage.jsx -// ───────────────────────────────────────────── -// This is the page that proves the backend works. -// It wires together: -// 1. USDA API call (NutritionAPI.js) -// 2. Firestore save (SearchHistoryService.js) -// 3. Auth state (useAuth) -// -// The frontend team can style this however they want — -// the logic here is what matters. -// ───────────────────────────────────────────── - -import { useState } from "react"; -import { searchFoods } from "../utils/NutritionAPI"; -import { saveSearchToHistory } from "../utils/SearchHistoryService"; -import { useAuth } from "../contexts/AuthContext"; - -export default function SearchPage() { - const { currentUser } = useAuth(); - const [query, setQuery] = useState(""); - const [results, setResults] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [saved, setSaved] = useState(false); // did we save to Firestore? - - async function handleSearch(e) { - e.preventDefault(); - if (!query.trim()) return; - - setLoading(true); - setError(null); - setSaved(false); - - // ── Step 1: Hit USDA API ────────────────── - const { data, error: apiError } = await searchFoods(query); - - setLoading(false); - - if (apiError) { - setError(apiError); - return; - } - - if (data.length === 0) { - setError(`No foods found for "${query}". Try a different term.`); - return; - } - - setResults(data); - - // ── Step 2: Save to Firestore (only if logged in) ── - if (currentUser) { - const { error: saveError } = await saveSearchToHistory( - currentUser.uid, - query, - data[0] // save the top result - ); - if (!saveError) setSaved(true); - } - } - - return ( -
-

Nutrition Search

-

Powered by USDA FoodData Central

- - {/* Search form */} -
- setQuery(e.target.value)} - placeholder='Try "banana", "chicken breast", "oats"' - /> - -
- - {/* Status messages */} - {error &&

{error}

} - {saved &&

Search saved to your history!

} - - {/* Results */} - {results.map((food) => ( -
-

{food.description}

- {food.brandOwner &&

{food.brandOwner}

} - - {/* Nutrition table — hand this structure to the frontend team */} - - - {Object.entries(food.nutrients).map(([name, { value, unit }]) => ( - - - - - ))} - -
{name}{value} {unit}
-
- ))} -
- ); -} diff --git a/backend/firebase_auth_context.js b/backend/firebase_auth_context.js deleted file mode 100644 index ce4c266..0000000 --- a/backend/firebase_auth_context.js +++ /dev/null @@ -1,79 +0,0 @@ -// ══════════════════════════════════════════════ -// firebase/firebase.js -// ══════════════════════════════════════════════ -// Paste this into src/firebase/firebase.js - -import { initializeApp } from "firebase/app"; -import { getAuth } from "firebase/auth"; -import { getFirestore } from "firebase/firestore"; - -const firebaseConfig = { - apiKey: import.meta.env.VITE_FIREBASE_API_KEY, - authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, - projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, - storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, - messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID, - appId: import.meta.env.VITE_FIREBASE_APP_ID, -}; - -const app = initializeApp(firebaseConfig); -export const auth = getAuth(app); -export const db = getFirestore(app); - - -// ══════════════════════════════════════════════ -// firebase/auth.js -// ══════════════════════════════════════════════ -// Paste this into src/firebase/auth.js - -import { - createUserWithEmailAndPassword, - signInWithEmailAndPassword, - signOut, -} from "firebase/auth"; -import { auth } from "./firebase"; - -export const registerUser = (email, password) => - createUserWithEmailAndPassword(auth, email, password); - -export const loginUser = (email, password) => - signInWithEmailAndPassword(auth, email, password); - -export const logoutUser = () => signOut(auth); - - -// ══════════════════════════════════════════════ -// contexts/AuthContext.jsx -// ══════════════════════════════════════════════ -// Paste this into src/contexts/AuthContext.jsx - -import { createContext, useContext, useEffect, useState } from "react"; -import { onAuthStateChanged } from "firebase/auth"; -import { auth } from "../firebase/firebase"; - -const AuthContext = createContext(null); - -export function AuthProvider({ children }) { - const [currentUser, setCurrentUser] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, (user) => { - setCurrentUser(user); - setLoading(false); - }); - return unsubscribe; - }, []); - - if (loading) return

Loading...

; - - return ( - - {children} - - ); -} - -export function useAuth() { - return useContext(AuthContext); -} From b49249f8ee574c0eace708504d3dc82fe672701b Mon Sep 17 00:00:00 2001 From: li-madison Date: Sat, 2 May 2026 10:14:21 -0500 Subject: [PATCH 2/7] mereged final and fixed errors --- public/index.html | 43 +++++++++++++++++++++++++++++++ src/App.js | 2 +- src/components/Food.jsx | 2 +- src/components/Navbar.jsx | 2 +- src/components/ToastContainer.jsx | 2 +- 5 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 public/index.html diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..aa069f2 --- /dev/null +++ b/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/src/App.js b/src/App.js index f804464..70625a2 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,7 @@ import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { AuthProvider } from './contexts/AuthContext'; -import { AppProvider } from './context/AppContext'; +import { AppProvider } from './contexts/AppContext'; import Navbar from './components/Navbar'; import Hero from './components/Hero'; diff --git a/src/components/Food.jsx b/src/components/Food.jsx index e884c06..d0c70e9 100644 --- a/src/components/Food.jsx +++ b/src/components/Food.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useContext } from 'react'; -import { AppContext } from '../context/AppContext'; +import { AppContext } from '../contexts/AppContext'; import { AiFillHeart, AiOutlineHeart } from 'react-icons/ai'; // Price tier assigned per TheMealDB category diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index fd5858c..db4868b 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,6 +1,6 @@ import React, { useState, useContext } from 'react'; import { useNavigate } from 'react-router-dom'; -import { AppContext } from '../context/AppContext'; +import { AppContext } from '../contexts/AppContext'; import { useAuth } from '../contexts/AuthContext'; import { logoutUser } from '../firebase/auth'; import { AiOutlineMenu, AiOutlineSearch, AiOutlineClose, AiFillTag } from 'react-icons/ai'; diff --git a/src/components/ToastContainer.jsx b/src/components/ToastContainer.jsx index 5d5d92d..5fb762b 100644 --- a/src/components/ToastContainer.jsx +++ b/src/components/ToastContainer.jsx @@ -1,5 +1,5 @@ import React, { useContext } from 'react'; -import { AppContext } from '../context/AppContext'; +import { AppContext } from '../contexts/AppContext'; const ToastContainer = () => { const { toasts } = useContext(AppContext); From 3bd9dc37332e474e15626c95eda6cce2043d0e3a Mon Sep 17 00:00:00 2001 From: Sky1219 Date: Sat, 2 May 2026 21:38:38 -0500 Subject: [PATCH 3/7] Update --- src/App.js | 4 +- src/components/Food.jsx | 528 +++------------------- src/components/Navbar.jsx | 183 +------- src/components/ToastContainer.jsx | 2 +- src/{contexts => context}/AppContext.js | 0 src/{contexts => context}/AuthContext.jsx | 0 6 files changed, 79 insertions(+), 638 deletions(-) rename src/{contexts => context}/AppContext.js (100%) rename src/{contexts => context}/AuthContext.jsx (100%) diff --git a/src/App.js b/src/App.js index 70625a2..56ee5a7 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,7 @@ import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; -import { AuthProvider } from './contexts/AuthContext'; -import { AppProvider } from './contexts/AppContext'; +import { AuthProvider } from './context/AuthContext'; +import { AppProvider } from './context/AppContext'; import Navbar from './components/Navbar'; import Hero from './components/Hero'; diff --git a/src/components/Food.jsx b/src/components/Food.jsx index d0c70e9..10fba1f 100644 --- a/src/components/Food.jsx +++ b/src/components/Food.jsx @@ -1,503 +1,93 @@ import React, { useState, useEffect, useContext } from 'react'; -import { AppContext } from '../contexts/AppContext'; +import { AppContext } from '../context/AppContext'; import { AiFillHeart, AiOutlineHeart } from 'react-icons/ai'; -// Price tier assigned per TheMealDB category -const CATEGORY_PRICES = { - Chicken: '$$', - Beef: '$$$', - Pasta: '$$', - Seafood: '$$$$', -}; - +// Backend pricing constants +const CATEGORY_PRICES = { Chicken: '$$', Beef: '$$$', Pasta: '$$', Seafood: '$$$$' }; const INITIAL_CATEGORIES = Object.keys(CATEGORY_PRICES); const Food = () => { - const [allFoods, setAllFoods] = useState([]); const [foods, setFoods] = useState([]); - const [foodDetails, setFoodDetails] = useState(null); - const [expandedCardId, setExpandedCardId] = useState(null); - const [showIngredientsToggle, setShowIngredientsToggle] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [ingredientSearchQuery, setIngredientSearchQuery] = useState(''); const [isSearching, setIsSearching] = useState(false); - const [visibleCount, setVisibleCount] = useState(8); - - const { favorites, toggleFavorite, addToCart } = useContext(AppContext); - - // Fetch initial menu from TheMealDB on mount - useEffect(() => { - const fetchInitialFoods = async () => { - setIsSearching(true); - try { - const results = await Promise.all( - INITIAL_CATEGORIES.map((cat) => - fetch(`https://www.themealdb.com/api/json/v1/1/filter.php?c=${cat}`) - .then((r) => r.json()) - .then((d) => - (d.meals || []).slice(0, 4).map((meal) => ({ - id: meal.idMeal, - name: meal.strMeal, - category: cat.toLowerCase(), - image: meal.strMealThumb, - price: CATEGORY_PRICES[cat], - })) - ) - ) - ); - const combined = results.flat(); - setAllFoods(combined); - setFoods(combined); - } catch (error) { - console.error('Error fetching initial foods:', error); - } - setIsSearching(false); - }; - - fetchInitialFoods(); - }, []); //end API + const { favorites, toggleFavorite } = useContext(AppContext); - // Ingredient search debounce - useEffect(() => { - if (!ingredientSearchQuery) return; - - const timeoutId = setTimeout(() => { - executeSearch(ingredientSearchQuery); - }, 400); - - return () => clearTimeout(timeoutId); - }, [ingredientSearchQuery]); - - // Filter by category (local, against pre-loaded allFoods) - const filterType = (category) => { - setFoods(allFoods.filter((item) => item.category === category)); - }; - - // Filter by price (local, against pre-loaded allFoods) - const filterPrice = (price) => { - setFoods(allFoods.filter((item) => item.price === price)); - }; - - // Ingredient Search (TheMealDB API) - const executeSearch = async (query) => { - setIsSearching(true); - try { - const response = await fetch( - `https://www.themealdb.com/api/json/v1/1/filter.php?i=${query}` - ); - const apiData = await response.json(); - - if (apiData.meals) { - const mappedFoods = apiData.meals.map((meal) => ({ - id: meal.idMeal, - name: meal.strMeal, - category: 'searched', - image: meal.strMealThumb, - price: '$$', - })); - setFoods(mappedFoods); - } else { - setFoods([]); - } - } catch (error) { - console.error('Error fetching ingredient search:', error); - } - setIsSearching(false); - }; - - // Dietary / category filter (TheMealDB API) - const searchByCategory = async (categoryStr) => { + // Fetch initial menu from TheMealDB + const fetchInitialFoods = async () => { setIsSearching(true); try { - const response = await fetch( - `https://www.themealdb.com/api/json/v1/1/filter.php?c=${categoryStr}` + const results = await Promise.all( + INITIAL_CATEGORIES.map((cat) => + fetch(`https://www.themealdb.com/api/json/v1/1/filter.php?c=${cat}`) + .then((r) => r.json()) + .then((d) => + (d.meals || []).slice(0, 4).map((meal) => ({ + id: meal.idMeal, + name: meal.strMeal, + image: meal.strMealThumb, + price: CATEGORY_PRICES[cat], + })) + ) + ) ); - const apiData = await response.json(); - - if (apiData.meals) { - const mappedFoods = apiData.meals.map((meal) => ({ - id: meal.idMeal, - name: meal.strMeal, - category: categoryStr.toLowerCase(), - image: meal.strMealThumb, - price: '$$$', - })); - setFoods(mappedFoods); - } else { - setFoods([]); - } + setFoods(results.flat()); } catch (error) { - console.error('Error fetching category:', error); + console.error('Error fetching initial foods:', error); } setIsSearching(false); }; - // Fetch meal details (ingredients, nutrition) from TheMealDB - const fetchFoodDetails = async (food) => { - setIsLoading(true); - setFoodDetails(null); - - try { - const searchTerm = food.name.split(' ').pop(); - const response = await fetch( - `https://www.themealdb.com/api/json/v1/1/search.php?s=${searchTerm}` - ); - const apiData = await response.json(); - - let ingredients = []; - if (apiData.meals && apiData.meals.length > 0) { - const meal = apiData.meals[0]; - for (let i = 1; i <= 20; i++) { - if (meal[`strIngredient${i}`] && meal[`strIngredient${i}`].trim() !== '') { - ingredients.push({ - name: meal[`strIngredient${i}`], - measure: meal[`strMeasure${i}`], - }); - } - } - } - - const COMMON_ALLERGENS = [ - 'cheese', 'milk', 'butter', 'cream', 'nut', 'peanut', - 'almond', 'shrimp', 'crab', 'soy', 'wheat', 'egg', - ]; - - ingredients = ingredients.map((ing) => ({ - ...ing, - isAllergen: COMMON_ALLERGENS.some((allergen) => - ing.name.toLowerCase().includes(allergen) - ), - })); - - const detectedAllergens = ingredients - .filter((ing) => ing.isAllergen) - .map((ing) => ing.name); - - const mockNutrition = { - calories: Math.floor(Math.random() * 500) + 300, - protein: Math.floor(Math.random() * 30) + 10, - fat: Math.floor(Math.random() * 40) + 10, - carbs: Math.floor(Math.random() * 60) + 20, - }; - - setFoodDetails({ ingredients, nutrition: mockNutrition, detectedAllergens }); - } catch (error) { - console.error('Error fetching data:', error); - } - setIsLoading(false); - }; - - const handleCardClick = (item) => { - if (expandedCardId === item.id) { - setExpandedCardId(null); - setShowIngredientsToggle(false); - } else { - setExpandedCardId(item.id); - setShowIngredientsToggle(false); - fetchFoodDetails(item); - } - }; + useEffect(() => { + fetchInitialFoods(); + }, []); return (

Top Rated Menu Items

+
+ {foods.map((item, index) => { + // Check if item is already favorited + const isFav = favorites.some(f => f.id === item.id || f.name === item.name); -
-
-

Filter Type

-
- - - - - -
-
+ return ( +
-
-

Filter Price

-
- - - - -
-
- - {/* Dietary Filters (live API) */} -
-

Dietary (API)

-
- - - -
-
-
- - {/* Ingredient Search Bar */} -
-
- setIngredientSearchQuery(e.target.value)} - /> -
-
- - {isSearching && ( -
- {[1, 2, 3, 4].map((n) => ( -
- ))} -
- )} - - {foods.length === 0 && !isSearching && ( -

- No foods found matching those criteria. -

- )} - - {!isSearching && ( -
- {foods.slice(0, visibleCount).map((item, index) => { - const isFav = favorites.some( - (f) => f.id === item.id || f.name === item.name - ); - return ( + {/* Favorite Button */}
-
{ - e.stopPropagation(); - toggleFavorite(item); - }} - > - {isFav ? ( - - ) : ( - - )} -
- -
{ - e.stopPropagation(); - addToCart(item); - }} - > - + Cart -
- -
handleCardClick(item)}> - {item.name} -
-

{item.name}

-

- - {item.price} - -

-
-
- - {expandedCardId === item.id && ( -
- {isLoading ? ( -
-
-
-
- ) : foodDetails ? ( -
-
-

- Est. Nutrition -

-
-
-

{foodDetails.nutrition.calories}

-

Cal

-
-
-

{foodDetails.nutrition.protein}g

-

Pro

-
-
-

{foodDetails.nutrition.fat}g

-

Fat

-
-
-

{foodDetails.nutrition.carbs}g

-

Carbs

-
-
-
- - - {showIngredientsToggle && ( -
- {foodDetails.detectedAllergens && - foodDetails.detectedAllergens.length > 0 && ( -
-

- ⚠️ Allergen Warning -

-

- Contains: {foodDetails.detectedAllergens.join(', ')} -

-
- )} - -

- Ingredients List -

- {foodDetails.ingredients.length > 0 ? ( -
- {foodDetails.ingredients.map((ing, idx) => ( -
- {ing.name} { - e.target.style.display = 'none'; - }} - /> - - {ing.name}{' '} - | {ing.measure} - -
- ))} -
- ) : ( -

- No ingredients found. -

- )} -
- )} -
- ) : ( -

- Failed to load data. -

- )} -
- )} +
- ); - })} -
- )} - {/* Pagination */} - {visibleCount < foods.length && !isSearching && ( -
- -
- )} + {item.name} +
+

{item.name}

+

+ + {item.price} + +

+
+
+ ); + })} +
); }; -export default Food; +export default Food; \ No newline at end of file diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index db4868b..e457c27 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,19 +1,16 @@ -import React, { useState, useContext } from 'react'; +import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { AppContext } from '../contexts/AppContext'; -import { useAuth } from '../contexts/AuthContext'; +import { useAuth } from '../context/AuthContext'; import { logoutUser } from '../firebase/auth'; + import { AiOutlineMenu, AiOutlineSearch, AiOutlineClose, AiFillTag } from 'react-icons/ai'; import { BsFillCartFill, BsFillSaveFill } from 'react-icons/bs'; -import { TbTruckDelivery } from 'react-icons/tb' -import { FaUserFriends, FaWallet } from 'react-icons/fa' -import { MdFavorite, MdHelp } from 'react-icons/md' +import { TbTruckDelivery } from 'react-icons/tb'; +import { FaUserFriends, FaWallet } from 'react-icons/fa'; +import { MdFavorite, MdHelp } from 'react-icons/md'; const Navbar = () => { const [nav, setNav] = useState(false); - const [favMenuOpen, setFavMenuOpen] = useState(false); - const [cartMenuOpen, setCartMenuOpen] = useState(false); - const { favorites, cart, removeFromCart, addToast } = useContext(AppContext); const { currentUser } = useAuth(); const navigate = useNavigate(); @@ -22,36 +19,6 @@ const Navbar = () => { navigate('/login'); } - // Form Validation States - const [checkoutEmail, setCheckoutEmail] = useState(''); - const [checkoutCard, setCheckoutCard] = useState(''); - const [emailError, setEmailError] = useState(''); - const [cardError, setCardError] = useState(''); - - const handleCheckout = (e) => { - e.preventDefault(); - setEmailError(''); - setCardError(''); - let isValid = true; - - if (!checkoutEmail.includes('@') || !checkoutEmail.includes('.')) { - setEmailError('Please enter a valid email address.'); - isValid = false; - } - - if (checkoutCard.length < 16) { - setCardError('Credit card must be 16 digits.'); - isValid = false; - } - - if (isValid) { - addToast('🎉 Mock Order Placed Successfully!'); - setCartMenuOpen(false); - setCheckoutEmail(''); - setCheckoutCard(''); - } - }; - return (
{/* Left side */} @@ -71,61 +38,19 @@ const Navbar = () => { {/* Search Input */}
- +
- {/* Auth + Cart + Favorites buttons */} -
- - - {/* Favorites Dropdown*/} - {favMenuOpen && ( -
-

Your Favorites ❤️

- {favorites.length === 0 ? ( -

No favorites yet.
Click the heart on a food card to save it!

- ) : ( - favorites.map((fav, i) => ( -
- {fav.name} -
-

{fav.name}

-

{fav.price || '$$'}

-
-
- )) - )} -
- )} - - {/* Login / Logout */} {currentUser ? (
- {currentUser.email} + + {currentUser.email} +
- {/* Mobile Menu */} - {/* Overlay */} + {/* Mobile Menu Overlay */} {nav ?
: ''} - {/* Side drawer menu */} -
- setNav(!nav)} - size={30} - className='absolute right-4 top-4 cursor-pointer' - /> +
+ setNav(!nav)} size={30} className='absolute right-4 top-4 cursor-pointer' />

Best Eats

@@ -172,76 +91,8 @@ const Navbar = () => {
- - {/*]Cart Checkout*/} - {cartMenuOpen &&
setCartMenuOpen(false)} className='bg-black/80 fixed w-full h-screen z-50 top-0 left-0 transition-opacity'>
} - -
- setCartMenuOpen(false)} - size={30} - className='absolute right-4 top-4 cursor-pointer hover:text-red-500 transition-colors' - /> -

Your Order

- - {cart.length === 0 ? ( -

Your cart is empty.

- ) : ( -
- {/* Cart Items List */} - {cart.map((item, index) => ( -
-
- {item.name} -

{item.name}

-
-
-

{item.price || '$$'}

- -
-
- ))} - -
-

Total:

-

$$$

-
- - {/* Mock Checkout Form*/} -

Checkout Details

-
-
- setCheckoutEmail(e.target.value)} - /> - {emailError &&

{emailError}

} -
- -
- setCheckoutCard(e.target.value.replace(/\D/g, ''))} // Only allow numbers - maxLength="16" - /> - {cardError &&

{cardError}

} -
- - -
-
- )} -
); }; -export default Navbar; +export default Navbar; \ No newline at end of file diff --git a/src/components/ToastContainer.jsx b/src/components/ToastContainer.jsx index 5fb762b..5d5d92d 100644 --- a/src/components/ToastContainer.jsx +++ b/src/components/ToastContainer.jsx @@ -1,5 +1,5 @@ import React, { useContext } from 'react'; -import { AppContext } from '../contexts/AppContext'; +import { AppContext } from '../context/AppContext'; const ToastContainer = () => { const { toasts } = useContext(AppContext); diff --git a/src/contexts/AppContext.js b/src/context/AppContext.js similarity index 100% rename from src/contexts/AppContext.js rename to src/context/AppContext.js diff --git a/src/contexts/AuthContext.jsx b/src/context/AuthContext.jsx similarity index 100% rename from src/contexts/AuthContext.jsx rename to src/context/AuthContext.jsx From 37ee4832b9a3975ca9dba9ebe6a717dbbde8df85 Mon Sep 17 00:00:00 2001 From: SafeBigfoot65 Date: Sun, 3 May 2026 17:44:18 -0500 Subject: [PATCH 4/7] Project Done Co-authored-by: Copilot --- package-lock.json | 56 +++++-------------- package.json | 4 +- src/App.js | 16 +++++- src/components/Category.jsx | 13 ++++- src/components/Food.jsx | 42 +++++++++++++-- src/components/HeadlineCards.jsx | 41 +++++++++++++- src/components/Hero.jsx | 15 +++++- src/components/Navbar.jsx | 90 ++++++++++++++++++++++++++++++- src/components/ToastContainer.jsx | 11 +++- src/context/AppContext.js | 32 ++++++++++- yarn.lock | 26 ++++----- 11 files changed, 276 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index e4e2d38..f2855d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,10 @@ "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.2.1", - "firebase": "^12.12.0", + "firebase": "^12.12.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^4.4.0", + "react-icons": "^4.12.0", "react-router-dom": "^7.14.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.0" @@ -64,7 +64,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.9.tgz", "integrity": "sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", @@ -965,7 +964,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -1568,7 +1566,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.6.tgz", "integrity": "sha512-Mz7xMPxoy9kPS/JScj6fJs03TZ/fZ1dJPlMjRAgTaxaS0fUBk8FV/A2rRgfPsVCZqALNwMexD+0Uaf5zlcKPpw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-module-imports": "^7.18.6", @@ -2343,9 +2340,9 @@ } }, "node_modules/@firebase/ai": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.11.0.tgz", - "integrity": "sha512-+oqOne/h5J51LezazR+VyzKe3AK455W29JXnb4jOeVvQhC7FymledN5+XE+w5vEcMhRQ6n1f62fdGs4A44X32A==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.11.1.tgz", + "integrity": "sha512-WGTF81W3WBKJY+c7xqTzO15OGAkCAs8cpADqflAI0skhTZjIkhF0qyf55rq4Ctt6jKygkv99rPfMrjAHTgXaVQ==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.3", @@ -2405,7 +2402,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.11.tgz", "integrity": "sha512-yxADFW35LYkP8oSGobGsYIrI42I+GPCvKTNHx4meT9Yq3C950IVz1eANoBk822I9tbKv1wyv9P4Bv1G5TpucFw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/component": "0.7.2", "@firebase/logger": "0.5.0", @@ -2472,7 +2468,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.11.tgz", "integrity": "sha512-KaACDjXkK5VLpI01vEs592R7/8s5DjFdIXfKoR385ly1SmK3Tu+jMHCIB4MsiY5jsez6v7VlEX/3rJ90dVkHyA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/app": "0.14.11", "@firebase/component": "0.7.2", @@ -2489,7 +2484,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.4.tgz", "integrity": "sha512-crX9TA5SVYZwLPG7/R16IsH8FLlgkPXjJUVhsVpHVDSqJiq3D/NuFTM5ctxGTExXAOeIn//69tQw47CPerM8MQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/logger": "0.5.0" } @@ -2949,7 +2943,6 @@ "integrity": "sha512-AmWf3cHAOMbrCPG4xdPKQaj5iHnyYfyLKZxwz+Xf55bqKbpAmcYifB4jQinT2W9XhDRHISOoPyBOariJpCG6FA==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -4541,7 +4534,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.1.19", @@ -4944,7 +4938,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.7.tgz", "integrity": "sha512-l4L6Do+tfeM2OK0GJsU7TUcM/1oN/N25xHm3Jb4z3OiDU4Lj8dIuxX9LpVMS9riSXQs42D1ieX7b85/r16H9Fw==", "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.30.7", "@typescript-eslint/type-utils": "5.30.7", @@ -4997,7 +4990,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.7.tgz", "integrity": "sha512-Rg5xwznHWWSy7v2o0cdho6n+xLhK2gntImp0rJroVVFkcYFYQ8C8UJTSuTw/3CnExBmPjycjmUJkxVmjXsld6A==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.30.7", "@typescript-eslint/types": "5.30.7", @@ -5349,7 +5341,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5469,7 +5460,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6278,7 +6268,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001366", "electron-to-chromium": "^1.4.188", @@ -6985,7 +6974,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -7944,7 +7932,6 @@ "integrity": "sha512-d4ixhz5SKCa1D6SCPrivP7yYVi7nyD6A4vs6HIAul9ujBzcEmZVM3/0NN/yu5nKhmO1wjp5xQ46iRfmDGlOviA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", - "peer": true, "dependencies": { "@eslint/eslintrc": "^1.3.0", "@humanwhocodes/config-array": "^0.9.2", @@ -8445,7 +8432,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -9240,12 +9226,12 @@ } }, "node_modules/firebase": { - "version": "12.12.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.12.0.tgz", - "integrity": "sha512-5Ap+pN5iEJUvBlQEZEmLuUm7Gvu6I5xv1jZ5SiSNyw4jrwlHo+4tmZv3OPPoKfN9eo1kBwyyBvi+pWHIPXwfYw==", + "version": "12.12.1", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.12.1.tgz", + "integrity": "sha512-ee7xA+bTJLfjB9BP/8FQr3EkxmpAAGc1lNc5QkWgTDpUw24HYXFPm7FEWRdLtGnygxIdYpFmepSc5VjkI6NHhw==", "license": "Apache-2.0", "dependencies": { - "@firebase/ai": "2.11.0", + "@firebase/ai": "2.11.1", "@firebase/analytics": "0.10.21", "@firebase/analytics-compat": "0.2.27", "@firebase/app": "0.14.11", @@ -10851,7 +10837,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -13698,7 +13683,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -14503,7 +14487,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", @@ -15624,7 +15607,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -16008,7 +15990,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -16164,7 +16145,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -16180,9 +16160,9 @@ "license": "MIT" }, "node_modules/react-icons": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.4.0.tgz", - "integrity": "sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", "license": "MIT", "peerDependencies": { "react": "*" @@ -16199,7 +16179,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -16682,7 +16661,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.0.tgz", "integrity": "sha512-vL8xjY4yOQEw79DvyXLijhnhh+R/O9zpF/LEgkCebZFtb6ELeN9H3/2T0r8+mp+fFTBHZ5qGpOpW2ela2zRt3g==", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -18038,7 +18016,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -18380,7 +18357,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.73.0.tgz", "integrity": "sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -18451,7 +18427,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -18505,7 +18480,6 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz", "integrity": "sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw==", "license": "MIT", - "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -18561,7 +18535,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -18903,7 +18876,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", diff --git a/package.json b/package.json index 591cbbe..0ca0fb1 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,10 @@ "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.2.1", - "firebase": "^12.12.0", + "firebase": "^12.12.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^4.4.0", + "react-icons": "^4.12.0", "react-router-dom": "^7.14.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.0" diff --git a/src/App.js b/src/App.js index 2033c26..f598e43 100644 --- a/src/App.js +++ b/src/App.js @@ -1,15 +1,27 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { AuthProvider } from './context/AuthContext'; import { AppProvider } from './context/AppContext'; +import ToastContainer from './components/ToastContainer'; import LoginPage from './pages/LoginPage'; import RegisterPage from './pages/RegisterPage'; +import Navbar from './components/Navbar'; + +import Hero from './components/Hero'; +import HeadlineCards from './components/HeadlineCards'; +import Food from './components/Food'; +import Category from './components/Category'; // Main page layout — all the existing components together function MainLayout() { return ( -
-

Welcome to HackUTD's Devday!

+
+ + + + + +
); } diff --git a/src/components/Category.jsx b/src/components/Category.jsx index e3ada3a..d4a57a2 100644 --- a/src/components/Category.jsx +++ b/src/components/Category.jsx @@ -18,7 +18,18 @@ const Category = () => { }, []); return ( -
+
+

+ Eat What Makes You Happy +

+
+ {categories.map((item, index) => ( +
+

{item.name}

+ {item.name} +
+ ))} +
); }; diff --git a/src/components/Food.jsx b/src/components/Food.jsx index d7bc4cc..2580e7c 100644 --- a/src/components/Food.jsx +++ b/src/components/Food.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useContext } from 'react'; import { AppContext } from '../context/AppContext'; import { AiFillHeart, AiOutlineHeart } from 'react-icons/ai'; +import {data} from '../data/data.js'; // Backend pricing constants const CATEGORY_PRICES = { Chicken: '$$', Beef: '$$$', Pasta: '$$', Seafood: '$$$$' }; @@ -13,6 +14,7 @@ const Food = () => { const { favorites, toggleFavorite } = useContext(AppContext); // Fetch initial menu from TheMealDB + useEffect(() => { const fetchInitialFoods = async () => { setIsSearching(true); try { @@ -23,7 +25,7 @@ const Food = () => { .then((d) => (d.meals || []).slice(0, 4).map((meal) => ({ id: meal.idMeal, - name: meal.strMeal, + namefet: meal.strMeal, image: meal.strMealThumb, price: CATEGORY_PRICES[cat], })) @@ -31,16 +33,50 @@ const Food = () => { ) ); setFoods(results.flat()); + console.log('Initial foods fetched:', results.flat()); } catch (error) { console.error('Error fetching initial foods:', error); } setIsSearching(false); }; + fetchInitialFoods(); +}, []); return ( -
- +
+

+ Top Rated Menu Items +

+
+ {foods.map((item, index) => { + const isFav = favorites.some(f => f.id === item.id || f.name === item.namefet); + return ( +
+
+ +
+ < img src = {item.image} alt = {item.name} className='w-full h-[200px] object-cover rounded-t-lg'/> +
+

{item.namefet}

+

+ {item.price} +

+
+
+ );})} +
+ ); }; diff --git a/src/components/HeadlineCards.jsx b/src/components/HeadlineCards.jsx index 8e8717b..f195d6f 100644 --- a/src/components/HeadlineCards.jsx +++ b/src/components/HeadlineCards.jsx @@ -2,7 +2,46 @@ import React from 'react'; const HeadlineCards = () => { return ( -
+
+ {/* First Card */} +
+ {/* Overlay */} +
+

Sun's Out, BOGO's Out

+

Through 8/26

+ +
+ / +
+ {/* Second Card */} +
+
+ {/* Overlay */} +

Spicy Chicken Sandwhich

+

Through 9/25

+ +
+ / +
+ {/* Third Card */} +
+
+ {/* Overlay */} +

Beef Patty

+

Through 4/9

+ +
+ / +
); }; diff --git a/src/components/Hero.jsx b/src/components/Hero.jsx index 4523189..907f287 100644 --- a/src/components/Hero.jsx +++ b/src/components/Hero.jsx @@ -2,7 +2,20 @@ import React from 'react'; const Hero = () => { return ( -
+
+
+
+

The Best +

+

Foods Delivered + +

+
+ / +
); }; diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index d82094f..5080e9f 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,7 +1,12 @@ -import React, { useState } from 'react'; +import React, { useState, useContext } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import { logoutUser } from '../firebase/auth'; +import { AiOutlineMenu, AiOutlineSearch, AiOutlineClose, AiFillTag } from 'react-icons/ai'; +import { BsFillCartFill, BsFillSaveFill } from 'react-icons/bs'; +import {TbTruckDelivery} from 'react-icons/tb'; +import {FaUserFriends, FaWallet} from 'react-icons/fa'; +import {MdFavorite, MdHelp} from 'react-icons/md'; const Navbar = () => { const [nav, setNav] = useState(false); @@ -16,9 +21,90 @@ const Navbar = () => { } return ( -
+
+
+
setNav(!nav)} className = 'cursor-pointer'> + +
+

+ Best Eats +

+
+

Delivery

+

Pickup

+
+
+ +
+ + +
+ +
+ +
+ + {currentUser ? ( +
+ + {currentUser.email} + + + +
+ ) : ( + + ) + } + + {/* Mobile Menu */} + {/* Overlay */} + {nav ?
+ {/* Side drawer menu */} + +
+ setNav(!nav)} size={30} className='absolute right-4 top-4 cursor-pointer'/> +

+ Best Eats +

+ + +
+
: ''} + +
+ ); }; diff --git a/src/components/ToastContainer.jsx b/src/components/ToastContainer.jsx index 36a15b5..6339a61 100644 --- a/src/components/ToastContainer.jsx +++ b/src/components/ToastContainer.jsx @@ -1,8 +1,15 @@ -import React from 'react'; +import React, {useContext} from 'react'; +import { AppContext } from '../context/AppContext'; const ToastContainer = () => { + const { toasts } = useContext(AppContext); return ( -
+
+ {toasts.map((toast) => ( +
+ {toast.message} +
+ ))}
); }; diff --git a/src/context/AppContext.js b/src/context/AppContext.js index 223c27d..a2f4188 100644 --- a/src/context/AppContext.js +++ b/src/context/AppContext.js @@ -39,12 +39,42 @@ export const AppProvider = ({ children }) => { }, [currentUser]); //toggleFavorite + const toggleFavorite = (food) => { + if (!currentUser){ + addToast('🔒 Log in to save favorites!'); + return; + } + + setFavorites((prev) => { + const exists = prev.some((f) => f.id === food.id || f.name === food.name); + let updated; + if (exists) { + updated = prev.filter((f) => f.id !== food.id && f.name !== food.name); + } else { + updated = [...prev, food]; + } + saveFavoritesToFirestore(updated); + return updated; + }); + }; // Toasts + const [toasts , setToasts] = useState([]); + const addToast = (message) => { + const id = Date.now(); + setToasts((prev) => [...prev, { id, message }]); + setTimeout(() => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }, 3000); + + } return ( {children} diff --git a/yarn.lock b/yarn.lock index b7455c2..8d35fe3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1184,10 +1184,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@firebase/ai@2.11.0": - version "2.11.0" - resolved "https://registry.npmjs.org/@firebase/ai/-/ai-2.11.0.tgz" - integrity sha512-+oqOne/h5J51LezazR+VyzKe3AK455W29JXnb4jOeVvQhC7FymledN5+XE+w5vEcMhRQ6n1f62fdGs4A44X32A== +"@firebase/ai@2.11.1": + version "2.11.1" + resolved "https://registry.npmjs.org/@firebase/ai/-/ai-2.11.1.tgz" + integrity sha512-WGTF81W3WBKJY+c7xqTzO15OGAkCAs8cpADqflAI0skhTZjIkhF0qyf55rq4Ctt6jKygkv99rPfMrjAHTgXaVQ== dependencies: "@firebase/app-check-interop-types" "0.3.3" "@firebase/component" "0.7.2" @@ -4940,12 +4940,12 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -firebase@^12.12.0: - version "12.12.0" - resolved "https://registry.npmjs.org/firebase/-/firebase-12.12.0.tgz" - integrity sha512-5Ap+pN5iEJUvBlQEZEmLuUm7Gvu6I5xv1jZ5SiSNyw4jrwlHo+4tmZv3OPPoKfN9eo1kBwyyBvi+pWHIPXwfYw== +firebase@^12.12.1: + version "12.12.1" + resolved "https://registry.npmjs.org/firebase/-/firebase-12.12.1.tgz" + integrity sha512-ee7xA+bTJLfjB9BP/8FQr3EkxmpAAGc1lNc5QkWgTDpUw24HYXFPm7FEWRdLtGnygxIdYpFmepSc5VjkI6NHhw== dependencies: - "@firebase/ai" "2.11.0" + "@firebase/ai" "2.11.1" "@firebase/analytics" "0.10.21" "@firebase/analytics-compat" "0.2.27" "@firebase/app" "0.14.11" @@ -7935,10 +7935,10 @@ react-error-overlay@^6.0.11: resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== -react-icons@^4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/react-icons/-/react-icons-4.4.0.tgz" - integrity sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg== +react-icons@^4.12.0: + version "4.12.0" + resolved "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz" + integrity sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw== react-is@^16.13.1: version "16.13.1" From 17909aa5db57e861cd529c8d447b1b1c71a12766 Mon Sep 17 00:00:00 2001 From: SafeBigfoot65 Date: Sun, 3 May 2026 17:58:05 -0500 Subject: [PATCH 5/7] Changes to Food.jsx --- src/components/Food.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Food.jsx b/src/components/Food.jsx index 2580e7c..f5dcf44 100644 --- a/src/components/Food.jsx +++ b/src/components/Food.jsx @@ -9,8 +9,6 @@ const INITIAL_CATEGORIES = Object.keys(CATEGORY_PRICES); const Food = () => { const [foods, setFoods] = useState([]); - const [ingredientSearchQuery, setIngredientSearchQuery] = useState(''); - const [isSearching, setIsSearching] = useState(false); const { favorites, toggleFavorite } = useContext(AppContext); // Fetch initial menu from TheMealDB From 075e293a4f7119fd3549610dcaf4bfdfb6537f9a Mon Sep 17 00:00:00 2001 From: SafeBigfoot65 Date: Sun, 3 May 2026 18:00:44 -0500 Subject: [PATCH 6/7] More changes to Food,jsx --- src/components/Food.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Food.jsx b/src/components/Food.jsx index f5dcf44..9626ea6 100644 --- a/src/components/Food.jsx +++ b/src/components/Food.jsx @@ -14,7 +14,6 @@ const Food = () => { // Fetch initial menu from TheMealDB useEffect(() => { const fetchInitialFoods = async () => { - setIsSearching(true); try { const results = await Promise.all( INITIAL_CATEGORIES.map((cat) => @@ -35,7 +34,6 @@ const Food = () => { } catch (error) { console.error('Error fetching initial foods:', error); } - setIsSearching(false); }; fetchInitialFoods(); }, []); From ac888019fee997ea6914e296048ec389b42a3f10 Mon Sep 17 00:00:00 2001 From: SafeBigfoot65 Date: Sun, 3 May 2026 18:13:17 -0500 Subject: [PATCH 7/7] . --- src/components/Food.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Food.jsx b/src/components/Food.jsx index 9626ea6..543fe2a 100644 --- a/src/components/Food.jsx +++ b/src/components/Food.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useContext } from 'react'; import { AppContext } from '../context/AppContext'; import { AiFillHeart, AiOutlineHeart } from 'react-icons/ai'; -import {data} from '../data/data.js'; +// import {data} from '../data/data.js'; // Backend pricing constants const CATEGORY_PRICES = { Chicken: '$$', Beef: '$$$', Pasta: '$$', Seafood: '$$$$' };