+ {/* 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.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 */}

- {/* 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.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) => (
-
-
-

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.price}
-
-
-
-
- {history.category}
-
-
-
-
- {history.datePosted}
-
-
- {history.seller}
-
-
-
-
- ))}
-
-
- {/* Right Button - Overlaid on products */}
-
-
-
- {/* Footer - Added here */}
+ {/* Footer */}