diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5524aac..e87b52d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,7 +12,7 @@ import Selling from "./pages/Selling"; import Transactions from "./pages/Transactions"; import Favorites from "./pages/Favorites"; import ProductDetail from "./pages/ProductDetail"; -import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage +import SearchPage from "./pages/SearchPage"; function App() { // Authentication state - initialize from localStorage if available @@ -30,6 +30,11 @@ function App() { const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(false); + // Product recommendation states + const [isGeneratingRecommendations, setIsGeneratingRecommendations] = + useState(false); + const [recommendations, setRecommendations] = useState([]); + // New verification states const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying' const [tempUserData, setTempUserData] = useState(null); @@ -51,8 +56,69 @@ function App() { }, []); useEffect(() => { - sendSessionDataToServer(); - }, []); + if (isAuthenticated && user) { + sendSessionDataToServer(); + } + }, [isAuthenticated, user]); + + // Generate product recommendations when user logs in + useEffect(() => { + if (isAuthenticated && user) { + generateProductRecommendations(); + } + }, [isAuthenticated, user]); + + // Generate product recommendations + const generateProductRecommendations = async () => { + try { + setIsGeneratingRecommendations(true); + + // Add a short delay to simulate calculation time + await new Promise((resolve) => setTimeout(resolve, 500)); + + console.log("Generating product recommendations for user:", user.ID); + + // Make API call to get recommendations + const response = await fetch( + "http://localhost:3030/api/recommendations/generate", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + userId: user.ID, + }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to generate recommendations"); + } + + const result = await response.json(); + + if (result.success) { + console.log( + "Recommendations generated successfully:", + result.recommendations, + ); + setRecommendations(result.recommendations); + + // Store recommendations in session storage for access across the app + sessionStorage.setItem( + "userRecommendations", + JSON.stringify(result.recommendations), + ); + } else { + console.error("Error generating recommendations:", result.message); + } + } catch (err) { + console.error("Error generating product recommendations:", err); + } finally { + setIsGeneratingRecommendations(false); + } + }; // Send verification code const sendVerificationCode = async (userData) => { @@ -180,6 +246,7 @@ function App() { if (result.success) { // Create user object from API response const newUser = { + ID: result.userID || result.ID, name: result.name || userData.name, email: result.email || userData.email, UCID: result.UCID || userData.ucid, @@ -194,13 +261,17 @@ function App() { sessionStorage.setItem("user", JSON.stringify(newUser)); // After successful signup, send session data to server - sendSessionDataToServer(); // Call it after signup + sendSessionDataToServer(); // Reset verification steps setVerificationStep("initial"); setTempUserData(null); console.log("Signup completed successfully"); + + // Generate recommendations for the new user + generateProductRecommendations(); + return true; } else { setError(result.message || "Failed to complete signup"); @@ -299,9 +370,11 @@ function App() { sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("user", JSON.stringify(userObj)); - sessionStorage.getItem("user"); - console.log("Login successful for:", userData.email); + + // Start generating recommendations with a slight delay + // This will happen in the useEffect, but we set a loading state to show to the user + setIsGeneratingRecommendations(true); } else { // Show error message for invalid credentials setError("Invalid email or password"); @@ -335,11 +408,12 @@ function App() { setUser(null); setVerificationStep("initial"); setTempUserData(null); + setRecommendations([]); // Clear localStorage - // sessionStorage.removeItem("user"); sessionStorage.removeItem("isAuthenticated"); + sessionStorage.removeItem("userRecommendations"); console.log("User logged out"); }; @@ -367,8 +441,6 @@ function App() { try { // Retrieve data from sessionStorage const user = JSON.parse(sessionStorage.getItem("user")); - // const isAuthenticated = - // sessionStorage.getItem("isAuthenticated") === "true"; if (!user || !isAuthenticated) { console.log("User is not authenticated"); @@ -403,6 +475,13 @@ function App() { } }; + // Loading overlay component + const LoadingOverlay = () => ( +
+
+
+ ); + // Login component const LoginComponent = () => (
@@ -671,6 +750,9 @@ function App() { return (
+ {/* Show loading overlay when generating recommendations */} + {isGeneratingRecommendations && } + {/* Only show navbar when authenticated */} {isAuthenticated && ( @@ -687,7 +769,7 @@ function App() { element={
- +
} diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 9acdd69..594148a 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,12 +1,6 @@ -import { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; -import { - Tag, - ChevronLeft, - ChevronRight, - Bookmark, - BookmarkCheck, -} from "lucide-react"; +import { useState, useEffect, useRef } from "react"; +import { Tag, ChevronLeft, ChevronRight, Bookmark, Loader } from "lucide-react"; import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed @@ -14,38 +8,50 @@ const Home = () => { const navigate = useNavigate(); const [listings, setListings] = useState([]); const [recommended, setRecommended] = useState([]); - const [history, sethistory] = useState([]); + const [history, setHistory] = useState([]); const [error, setError] = useState(null); const [showAlert, setShowAlert] = useState(false); + const [isLoading, setIsLoading] = useState({ + recommendations: true, + listings: true, + history: true, + }); + const recommendationsFetched = useRef(false); + const historyFetched = useRef(false); //After user data storing the session. const storedUser = JSON.parse(sessionStorage.getItem("user")); const toggleFavorite = async (id) => { - const response = await fetch( - "http://localhost:3030/api/product/addFavorite", - { - method: "POST", - headers: { - "Content-Type": "application/json", + try { + const response = await fetch( + "http://localhost:3030/api/product/addFavorite", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + userID: storedUser.ID, + productID: id, + }), }, - body: JSON.stringify({ - userID: storedUser.ID, - productID: id, - }), - }, - ); - const data = await response.json(); - if (data.success) { - setShowAlert(true); + ); + const data = await response.json(); + if (data.success) { + setShowAlert(true); + // Close alert after 3 seconds + setTimeout(() => setShowAlert(false), 3000); + } + console.log(`Add Product -> Favorites: ${id}`); + } catch (error) { + console.error("Error adding favorite:", error); } - console.log(`Add Product -> History: ${id}`); }; const addHistory = async (id) => { - const response = await fetch( - "http://localhost:3030/api/history/addHistory", - { + try { + await fetch("http://localhost:3030/api/history/addHistory", { method: "POST", headers: { "Content-Type": "application/json", @@ -54,22 +60,23 @@ const Home = () => { userID: storedUser.ID, productID: id, }), - }, - ); + }); + } catch (error) { + console.error("Error adding to history:", error); + } }; - function reloadPage() { - var doctTimestamp = new Date(performance.timing.domLoading).getTime(); - var now = Date.now(); - var tenSec = 10 * 1000; - if (now > doctTimestamp + tenSec) { - location.reload(); - } - } - + // Fetch recommended products useEffect(() => { - const fetchrecomProducts = async () => { + const fetchRecommendedProducts = async () => { + // Skip if already fetched or no user data + if (recommendationsFetched.current || !storedUser || !storedUser.ID) + return; + + setIsLoading((prev) => ({ ...prev, recommendations: true })); try { + recommendationsFetched.current = true; // Mark as fetched before the API call + const response = await fetch( "http://localhost:3030/api/engine/recommended", { @@ -82,36 +89,42 @@ const Home = () => { }), }, ); - if (!response.ok) throw new Error("Failed to fetch products"); + if (!response.ok) throw new Error("Failed to fetch recommendations"); const data = await response.json(); if (data.success) { setRecommended( data.data.map((product) => ({ id: product.ProductID, - title: product.ProductName, // Use the alias from SQL + title: product.ProductName, price: product.Price, - category: product.Category, // Ensure this gets the category name - image: product.ProductImage, // Use the alias for image URL - seller: product.SellerName, // Fetch seller name properly - datePosted: product.DateUploaded, // Use the actual date - isFavorite: false, // Default state + category: product.Category, + image: product.ProductImage, + seller: product.SellerName, + datePosted: product.DateUploaded, + isFavorite: false, })), ); - reloadPage(); } else { - throw new Error(data.message || "Error fetching products"); + throw new Error(data.message || "Error fetching recommendations"); } } catch (error) { - console.error("Error fetching products:", error); + console.error("Error fetching recommendations:", error); setError(error.message); + // Reset the flag if there's an error so it can try again + recommendationsFetched.current = false; + } finally { + setIsLoading((prev) => ({ ...prev, recommendations: false })); } }; - fetchrecomProducts(); - }, []); + fetchRecommendedProducts(); + }, [storedUser]); // Keep dependency + + // Fetch all products useEffect(() => { const fetchProducts = async () => { + setIsLoading((prev) => ({ ...prev, listings: true })); try { const response = await fetch( "http://localhost:3030/api/product/getProduct", @@ -119,18 +132,17 @@ const Home = () => { if (!response.ok) throw new Error("Failed to fetch products"); const data = await response.json(); - if (data.success) { setListings( data.data.map((product) => ({ id: product.ProductID, - title: product.ProductName, // Use the alias from SQL + title: product.ProductName, price: product.Price, - category: product.Category, // Ensure this gets the category name - image: product.ProductImage, // Use the alias for image URL - seller: product.SellerName, // Fetch seller name properly - datePosted: product.DateUploaded, // Use the actual date - isFavorite: false, // Default state + category: product.Category, + image: product.ProductImage, + seller: product.SellerName, + datePosted: product.DateUploaded, + isFavorite: false, })), ); } else { @@ -139,15 +151,24 @@ const Home = () => { } catch (error) { console.error("Error fetching products:", error); setError(error.message); + } finally { + setIsLoading((prev) => ({ ...prev, listings: false })); } }; + fetchProducts(); }, []); + // Fetch user history useEffect(() => { - const fetchrecomProducts = async () => { - // Get the user's data from localStorage + const fetchUserHistory = async () => { + // Skip if already fetched or no user data + if (historyFetched.current || !storedUser || !storedUser.ID) return; + + setIsLoading((prev) => ({ ...prev, history: true })); try { + historyFetched.current = true; // Mark as fetched before the API call + const response = await fetch( "http://localhost:3030/api/history/getHistory", { @@ -160,52 +181,168 @@ const Home = () => { }), }, ); - if (!response.ok) throw new Error("Failed to fetch products"); + if (!response.ok) throw new Error("Failed to fetch history"); const data = await response.json(); if (data.success) { - sethistory( + setHistory( data.data.map((product) => ({ id: product.ProductID, - title: product.ProductName, // Use the alias from SQL + title: product.ProductName, price: product.Price, - category: product.Category, // Ensure this gets the category name - image: product.ProductImage, // Use the alias for image URL - seller: product.SellerName, // Fetch seller name properly - datePosted: product.DateUploaded, // Use the actual date + category: product.Category, + image: product.ProductImage, + seller: product.SellerName, + datePosted: product.DateUploaded, })), ); } else { - throw new Error(data.message || "Error fetching products"); + throw new Error(data.message || "Error fetching history"); } } catch (error) { - console.error("Error fetching products:", error); + console.error("Error fetching history:", error); setError(error.message); + // Reset the flag if there's an error so it can try again + historyFetched.current = false; + } finally { + setIsLoading((prev) => ({ ...prev, history: false })); } }; - fetchrecomProducts(); - }, []); + + fetchUserHistory(); + }, [storedUser]); // Keep dependency const handleSelling = () => { navigate("/selling"); }; + // Loading indicator component + const LoadingSection = () => ( +
+ +
+ ); + + // Product card component to reduce duplication + const ProductCard = ({ product, addToHistory = false }) => ( + addHistory(product.id) : undefined} + className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative" + > +
+ {product.title} + +
+ +
+

+ {product.title} +

+ + ${product.price} + + +
+ + {product.category} +
+ +
+ {product.datePosted} + + {product.seller} + +
+
+ + ); + + // Scrollable product list component to reduce duplication + const ScrollableProductList = ({ + containerId, + products, + children, + isLoading, + addToHistory = false, + }) => ( +
+ {children} + +
+ + +
+ {isLoading ? ( + + ) : products.length > 0 ? ( + products.map((product) => ( + + )) + ) : ( +
+ No products available +
+ )} +
+ + +
+
+ ); + return (
{/* Hero Section with School Background */}
- {/* Background Image - Positioned at bottom */}
University of Calgary - {/* Dark overlay for better text readability */}
- {/* Content */}

Buy and Sell on Campus @@ -223,290 +360,53 @@ const Home = () => {

- {/* Recent Listings */} + {/* Floating Alert */} {showAlert && ( setShowAlert(false)} /> )} -
+ + {/* Recommendations Section */} +

- Recommendation + Recommended For You

+
-
- {/* Left Button - Overlaid on products */} - - - {/* Scrollable Listings Container */} -
- {recommended.map((recommended) => ( - addHistory(recommended.id)} - className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative" - > -
- {recommended.title} - -
- -
-

- {recommended.title} -

- - ${recommended.price} - - -
- - {recommended.category} -
- -
- - {recommended.datePosted} - - - {recommended.seller} - -
-
- - ))} -
- - {/* Right Button - Overlaid on products */} - -
-
- - {/* Recent Listings */} - {showAlert && ( - setShowAlert(false)} - /> - )} -
+ {/* Recent Listings Section */} +

Recent Listings

- -
- {/* Left Button - Overlaid on products */} - - - {/* Scrollable Listings Container */} -
- {listings.map((listing) => ( - -
- {listing.title} addHistory(listing.id)} - className="w-full h-48 object-cover" - /> - -
- -
-

- {listing.title} -

- - ${listing.price} - - -
- - {listing.category} -
- -
- - {listing.datePosted} - - - {listing.seller} - -
-
- - ))} -
- - {/* Right Button - Overlaid on products */} - -
-
+ {/* History Section */} - {showAlert && ( - setShowAlert(false)} - /> + {(history.length > 0 || isLoading.history) && ( + +

+ Your Browsing History +

+
)} -
-

History

- -
- {/* Left Button - Overlaid on products */} - - - {/* Scrollable Listings Container */} -
- {history.map((history) => ( - -
- {history.title} - -
- -
-

- {history.title} -

- - ${history.price} - - -
- - {history.category} -
- -
- - {history.datePosted} - - - {history.seller} - -
-
- - ))} -
- - {/* Right Button - Overlaid on products */} - -
-
- {/* Footer - Added here */} + {/* Footer */}