Merge branch 'mannBranch'

This commit is contained in:
Mann Patel
2025-04-18 20:25:13 -06:00
22 changed files with 974 additions and 688 deletions

View File

@@ -25,6 +25,12 @@
1. python3 server.py #Start The Server 1. python3 server.py #Start The Server
``` ```
--- ---
### Recommendation system
1. Install the dependencies
```Bash
pip install mysql.connector
```
### Database ### Database
1. MySql Version 9.2.0 1. MySql Version 9.2.0

View File

@@ -1,5 +1,37 @@
const db = require("../utils/database"); const db = require("../utils/database");
exports.addProduct = async (req, res) => {
const { userID, name, price, qty, description, category, images } = req.body;
try {
const [result] = await db.execute(
`INSERT INTO Product (Name, Price, StockQuantity, UserID, Description, CategoryID) VALUES (?, ?, ?, ?, ?, ?)`,
[name, price, qty, userID, description, category],
);
const productID = result.insertId;
if (images && images.length > 0) {
const imageInsertPromises = images.map((imagePath) =>
db.execute(`INSERT INTO Image_URL (URL, ProductID) VALUES (?, ?)`, [
imagePath,
productID,
]),
);
await Promise.all(imageInsertPromises); //perallel
}
res.json({
success: true,
message: "Product and images added successfully",
});
} catch (error) {
console.error("Error adding product or images:", error);
console.log(error);
return res.json({ error: "Could not add product or images" });
}
};
exports.addFavorite = async (req, res) => { exports.addFavorite = async (req, res) => {
const { userID, productID } = req.body; const { userID, productID } = req.body;
console.log(userID); console.log(userID);

View File

@@ -134,6 +134,62 @@ exports.completeSignUp = async (req, res) => {
} }
}; };
exports.doLogin = async (req, res) => {
const { email, password } = req.body;
// Input validation
if (!email || !password) {
return res.status(400).json({
found: false,
error: "Email and password are required",
});
}
try {
// Query to find user with matching email
const query = "SELECT * FROM User WHERE email = ?";
const [data, fields] = await db.execute(query, [email]);
// Check if user was found
if (data && data.length > 0) {
const user = data[0];
// Verify password match
if (user.Password === password) {
// Consider using bcrypt for secure password comparison
// Return user data without password
return res.json({
found: true,
userID: user.UserID,
name: user.Name,
email: user.Email,
UCID: user.UCID,
phone: user.Phone,
address: user.Address,
});
} else {
// Password doesn't match
return res.json({
found: false,
error: "Invalid email or password",
});
}
} else {
// User not found
return res.json({
found: false,
error: "Invalid email or password",
});
}
} catch (error) {
console.error("Error logging in:", error);
return res.status(500).json({
found: false,
error: "Database error occurred",
});
}
};
exports.getAllUser = async (req, res) => { exports.getAllUser = async (req, res) => {
try { try {
const [users, fields] = await db.execute("SELECT * FROM User;"); const [users, fields] = await db.execute("SELECT * FROM User;");
@@ -174,6 +230,7 @@ exports.findUserByEmail = async (req, res) => {
UCID: user.UCID, UCID: user.UCID,
phone: user.Phone, phone: user.Phone,
address: user.Address, address: user.Address,
password: user.Password,
// Include any other fields your user might have // Include any other fields your user might have
// Make sure the field names match exactly with your database column names // Make sure the field names match exactly with your database column names
}); });
@@ -201,7 +258,7 @@ exports.updateUser = async (req, res) => {
const phone = req.body?.phone; const phone = req.body?.phone;
const UCID = req.body?.UCID; const UCID = req.body?.UCID;
const address = req.body?.address; const address = req.body?.address;
const password = req.body?.password;
if (!userId) { if (!userId) {
return res.status(400).json({ error: "User ID is required" }); return res.status(400).json({ error: "User ID is required" });
} }
@@ -213,7 +270,7 @@ exports.updateUser = async (req, res) => {
if (phone) updateData.phone = phone; if (phone) updateData.phone = phone;
if (UCID) updateData.UCID = UCID; if (UCID) updateData.UCID = UCID;
if (address) updateData.address = address; if (address) updateData.address = address;
if (password) updateData.password = password;
if (Object.keys(updateData).length === 0) { if (Object.keys(updateData).length === 0) {
return res.status(400).json({ error: "No valid fields to update" }); return res.status(400).json({ error: "No valid fields to update" });
} }

View File

@@ -6,6 +6,7 @@ const {
removeFavorite, removeFavorite,
getAllProducts, getAllProducts,
getProductById, getProductById,
addProduct,
} = require("../controllers/product"); } = require("../controllers/product");
const router = express.Router(); const router = express.Router();
@@ -19,6 +20,7 @@ router.post("/addFavorite", addFavorite);
router.post("/getFavorites", getFavorites); router.post("/getFavorites", getFavorites);
router.post("/delFavorite", removeFavorite); router.post("/delFavorite", removeFavorite);
router.post("/addProduct", addProduct);
router.get("/getProduct", getAllProducts); router.get("/getProduct", getAllProducts);
router.get("/:id", getProductById); // Simplified route router.get("/:id", getProductById); // Simplified route

View File

@@ -4,6 +4,6 @@ const { getReviews, submitReview } = require("../controllers/review");
const router = express.Router(); const router = express.Router();
router.get("/:id", getReviews); router.get("/:id", getReviews);
router.post("/add", submitReview); router.post("/addReview", submitReview);
module.exports = router; module.exports = router;

View File

@@ -7,6 +7,7 @@ const {
findUserByEmail, findUserByEmail,
updateUser, updateUser,
deleteUser, deleteUser,
doLogin,
} = require("../controllers/user"); } = require("../controllers/user");
const router = express.Router(); const router = express.Router();
@@ -26,6 +27,9 @@ router.get("/fetch_all_users", getAllUser);
//Fetch One user Data with all fields: //Fetch One user Data with all fields:
router.post("/find_user", findUserByEmail); router.post("/find_user", findUserByEmail);
//Fetch One user Data with all fields:
router.post("/do_login", doLogin);
//Update A uses Data: //Update A uses Data:
router.post("/update", updateUser); router.post("/update", updateUser);

View File

@@ -1,11 +1,9 @@
const mysql = require("mysql2"); const mysql = require("mysql2");
//Create a pool of connections to allow multiple query happen at the same time
const pool = mysql.createPool({ const pool = mysql.createPool({
host: "localhost", host: "localhost",
user: "root", user: "root",
database: "Marketplace", database: "Marketplace",
}); });
//Export a promise for promise-based query
module.exports = pool.promise(); module.exports = pool.promise();

View File

@@ -52,6 +52,10 @@ function App() {
return () => window.removeEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize);
}, []); }, []);
useEffect(() => {
sendSessionDataToServer();
}, []);
// Send verification code // Send verification code
const sendVerificationCode = async (userData) => { const sendVerificationCode = async (userData) => {
try { try {
@@ -245,7 +249,7 @@ function App() {
UCID: formValues.ucid, UCID: formValues.ucid,
phone: formValues.phone, phone: formValues.phone,
password: formValues.password, // This will be needed for the final signup password: formValues.password, // This will be needed for the final signup
address: "NOT_GIVEN", address: formValues.address, // Add this line
client: 1, client: 1,
admin: 0, admin: 0,
}; };
@@ -261,7 +265,7 @@ function App() {
// Make API call to localhost:3030/find_user // Make API call to localhost:3030/find_user
const response = await fetch( const response = await fetch(
"http://localhost:3030/api/user/find_user", "http://localhost:3030/api/user/do_login",
{ {
method: "POST", method: "POST",
headers: { headers: {
@@ -296,7 +300,7 @@ function App() {
// Save to localStorage to persist across refreshes // Save to localStorage to persist across refreshes
sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("isAuthenticated", "true");
sessionStorage.setItem("user", JSON.stringify(userObj)); sessionStorage.setItem("user", JSON.stringify(userObj));
sendSessionDataToServer(); // Call it after signup
sessionStorage.getItem("user"); sessionStorage.getItem("user");
console.log("Login successful for:", userData.email); console.log("Login successful for:", userData.email);
@@ -365,8 +369,8 @@ function App() {
try { try {
// Retrieve data from sessionStorage // Retrieve data from sessionStorage
const user = JSON.parse(sessionStorage.getItem("user")); const user = JSON.parse(sessionStorage.getItem("user"));
const isAuthenticated = // const isAuthenticated =
sessionStorage.getItem("isAuthenticated") === "true"; // sessionStorage.getItem("isAuthenticated") === "true";
if (!user || !isAuthenticated) { if (!user || !isAuthenticated) {
console.log("User is not authenticated"); console.log("User is not authenticated");
@@ -527,6 +531,25 @@ function App() {
</div> </div>
)} )}
{isSignUp && (
<div>
<label
htmlFor="address"
className="block mb-1 text-sm font-medium text-gray-800"
>
Address
</label>
<input
type="text"
id="address"
name="address"
placeholder="Your address"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
required={isSignUp}
/>
</div>
)}
<div> <div>
<label <label
htmlFor="password" htmlFor="password"

View File

@@ -11,7 +11,7 @@ const FloatingAlert = ({ message, onClose, duration = 3000 }) => {
}, [onClose, duration]); }, [onClose, duration]);
return ( return (
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-green-500 text-white px-4 py-2 rounded-xl shadow-lg z-50 text-center"> <div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-emerald-500 text-white px-4 py-2 rounded-xl shadow-lg z-50 text-center">
{message} {message}
</div> </div>
); );

View File

@@ -35,7 +35,7 @@ const Navbar = ({ onLogout, userName }) => {
alt="Campus Plug" alt="Campus Plug"
className="h-8 px-2" className="h-8 px-2"
/> />
<span className="hidden md:block [color:#009966] font-bold text-xl"> <span className="hidden md:block text-emerald-600 font-bold text-xl">
Campus Plug Campus Plug
</span> </span>
</Link> </Link>
@@ -48,7 +48,7 @@ const Navbar = ({ onLogout, userName }) => {
<input <input
type="text" type="text"
placeholder="Search for anything..." placeholder="Search for anything..."
className="w-full p-2 pl-10 pr-4 border border-gray-300 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500" className="w-full p-2 pl-10 pr-4 border border-gray-300 focus:outline-none focus:border-[#ed7f30]-500 focus:ring-1 focus:ring-[#ed7f30]-500"
value={searchQuery} value={searchQuery}
onChange={handleSearchChange} onChange={handleSearchChange}
/> />
@@ -57,7 +57,7 @@ const Navbar = ({ onLogout, userName }) => {
</div> </div>
<button <button
type="submit" type="submit"
className="absolute inset-y-0 right-0 flex items-center px-3 text-gray-500 hover:text-green-500" className="absolute inset-y-0 right-0 flex items-center px-3 text-gray-500 hover:text-[#ed7f30]-500"
> >
Search Search
</button> </button>
@@ -70,7 +70,7 @@ const Navbar = ({ onLogout, userName }) => {
{/* Favorites Button */} {/* Favorites Button */}
<Link <Link
to="/favorites" to="/favorites"
className="p-2 text-gray-600 hover:text-green-600" className="p-2 text-gray-600 hover:text-[#ed7f30]-600"
> >
<Heart className="h-6 w-6" /> <Heart className="h-6 w-6" />
</Link> </Link>

View File

@@ -1,4 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { X, ChevronLeft, Plus, Trash2, Check } from "lucide-react";
const ProductForm = ({ const ProductForm = ({
editingProduct, editingProduct,
@@ -7,6 +8,7 @@ const ProductForm = ({
onCancel, onCancel,
}) => { }) => {
const [selectedCategory, setSelectedCategory] = useState(""); const [selectedCategory, setSelectedCategory] = useState("");
const storedUser = JSON.parse(sessionStorage.getItem("user"));
const categories = [ const categories = [
"Electronics", "Electronics",
@@ -27,6 +29,92 @@ const ProductForm = ({
"Other", "Other",
]; ];
// Map category names to their respective IDs
const categoryMapping = {
Electronics: 1,
Clothing: 2,
"Home & Garden": 3,
"Toys & Games": 4,
Books: 5,
"Sports & Outdoors": 6,
Automotive: 7,
"Beauty & Personal Care": 8,
"Health & Wellness": 9,
Jewelry: 10,
"Art & Collectibles": 11,
"Food & Beverages": 12,
"Office Supplies": 13,
"Pet Supplies": 14,
"Music & Instruments": 15,
Other: 16,
};
const handleSave = async () => {
// Check if the user has selected at least one category
if (!(editingProduct.categories || []).length) {
alert("Please select at least one category");
return;
}
try {
// First, upload images if there are any
const imagePaths = [];
// If we have files to upload, we'd handle the image upload here
// This is a placeholder for where you'd implement image uploads
// For now, we'll simulate the API expecting paths:
if (editingProduct.images && editingProduct.images.length > 0) {
// Simulating image paths for demo purposes
// In a real implementation, you would upload these files first
// and then use the returned paths
editingProduct.images.forEach((file, index) => {
const simulatedPath = `/public/uploads/${file.name}`;
imagePaths.push(simulatedPath);
});
}
// Get the category ID from the first selected category
const categoryName = (editingProduct.categories || [])[0];
const categoryID = categoryMapping[categoryName] || 3; // Default to 3 if not found
// Prepare payload according to API expectations
const payload = {
name: editingProduct.name || "",
price: parseFloat(editingProduct.price) || 0,
qty: 1, // Hardcoded as per your requirement
userID: storedUser.ID,
description: editingProduct.description || "",
category: categoryID,
images: imagePaths,
};
console.log("Sending payload:", payload);
const response = await fetch(
"http://localhost:3030/api/product/addProduct",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
},
);
if (!response.ok) {
const errorData = await response.text();
throw new Error(`Failed to add product: ${errorData}`);
}
const data = await response.json();
console.log("Product added:", data);
if (onSave) onSave(data);
} catch (error) {
console.error("Error saving product:", error);
alert(`Error saving product: ${error.message}`);
}
};
const addCategory = () => { const addCategory = () => {
if ( if (
selectedCategory && selectedCategory &&
@@ -49,64 +137,93 @@ const ProductForm = ({
})); }));
}; };
const toggleSoldStatus = () => {
setEditingProduct((prev) => ({
...prev,
isSold: !prev.isSold,
}));
};
return ( return (
<div className="bg-white border-2 border-gray-200 rounded-md p-6 shadow-lg"> <div className="bg-white border border-gray-200 shadow-md p-6">
{/* Back Button */} {/* Back Button */}
<button <button
onClick={onCancel} onClick={onCancel}
className="mb-4 text-sm text-emerald-600 hover:text-emerald-800 flex items-center font-medium" className="mb-4 text-emerald-600 hover:text-emerald-800 flex items-center gap-1"
> >
Back to Listings <ChevronLeft size={16} />
<span>Back to Listings</span>
</button> </button>
<h3 className="text-xl font-bold text-gray-800 mb-6 border-b-2 border-gray-100 pb-3"> <h3 className="text-xl font-bold text-gray-800 mb-6 border-b border-gray-200 pb-3">
{editingProduct?.id ? "Edit Your Product" : "List a New Product"} {editingProduct?.id ? "Edit Your Product" : "List a New Product"}
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Product Name */} {/* Product Name */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-1">
Product Name Product Name
</label> </label>
<input <input
type="text" type="text"
value={editingProduct.name} value={editingProduct.name || ""}
onChange={(e) => onChange={(e) =>
setEditingProduct({ ...editingProduct, name: e.target.value }) setEditingProduct({ ...editingProduct, name: e.target.value })
} }
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500" className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
/> />
</div> </div>
{/* Price */} {/* Price */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-1">
Price ($) Price ($)
</label> </label>
<input <input
type="number" type="number"
value={editingProduct.price} value={editingProduct.price || ""}
onChange={(e) => onChange={(e) =>
setEditingProduct({ setEditingProduct({
...editingProduct, ...editingProduct,
price: e.target.value, price: e.target.value,
}) })
} }
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500" className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
/> />
</div> </div>
{/* Categories - Dropdown with Add button */} {/* Sold Status */}
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2"> <div className="flex items-center mt-2">
<input
type="checkbox"
id="soldStatus"
checked={editingProduct.isSold || false}
onChange={toggleSoldStatus}
className="w-4 h-4 text-emerald-600 rounded focus:ring-emerald-500"
/>
<label htmlFor="soldStatus" className="ml-2 text-sm text-gray-700">
Mark as {editingProduct.isSold ? "Available" : "Sold"}
</label>
{editingProduct.isSold && (
<span className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
Sold
</span>
)}
</div>
</div>
{/* Categories */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Categories Categories
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
<select <select
value={selectedCategory} value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)} onChange={(e) => setSelectedCategory(e.target.value)}
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500" className="flex-1 px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
> >
<option value="" disabled> <option value="" disabled>
Select a category Select a category
@@ -125,61 +242,42 @@ const ProductForm = ({
type="button" type="button"
onClick={addCategory} onClick={addCategory}
disabled={!selectedCategory} disabled={!selectedCategory}
className="px-4 py-2 bg-emerald-600 text-white rounded-md hover:bg-emerald-700 disabled:bg-gray-300 disabled:cursor-not-allowed" className="px-3 py-2 bg-emerald-600 text-white hover:bg-emerald-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center gap-1"
> >
Add <Plus size={16} />
<span>Add</span>
</button> </button>
</div> </div>
{/* Selected Categories */} {/* Selected Categories */}
{(editingProduct.categories || []).length > 0 ? ( {(editingProduct.categories || []).length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2"> <div className="mt-2 flex flex-wrap gap-2">
{(editingProduct.categories || []).map((category) => ( {(editingProduct.categories || []).map((category) => (
<span <span
key={category} key={category}
className="inline-flex items-center px-3 py-1 rounded-md text-sm font-medium bg-emerald-100 text-emerald-800" className="inline-flex items-center px-2 py-1 bg-emerald-100 text-emerald-800"
> >
{category} {category}
<button <button
type="button" type="button"
onClick={() => removeCategory(category)} onClick={() => removeCategory(category)}
className="ml-2 text-emerald-600 hover:text-emerald-800" className="ml-1 text-emerald-600 hover:text-emerald-800"
> >
× <X size={14} />
</button> </button>
</span> </span>
))} ))}
</div> </div>
) : ( ) : (
<p className="text-xs text-gray-500 mt-2"> <p className="text-xs text-gray-500 mt-1">
Please select at least one category Please select at least one category
</p> </p>
)} )}
</div> </div>
{/* Status - Updated to Unsold/Sold */} {/* Description */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Status
</label>
<select
value={editingProduct.status}
onChange={(e) =>
setEditingProduct({
...editingProduct,
status: e.target.value,
})
}
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
>
<option value="Unsold">Unsold</option>
<option value="Sold">Sold</option>
</select>
</div>
{/* Description - New Field */}
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-1">
Description Description
</label> </label>
<textarea <textarea
@@ -191,19 +289,17 @@ const ProductForm = ({
}) })
} }
rows="4" rows="4"
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500" className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
placeholder="Describe your product in detail..." placeholder="Describe your product in detail..."
></textarea> ></textarea>
</div> </div>
{/* Simplified Image Upload */} {/* Image Upload */}
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-1">
Product Images{" "} Product Images <span className="text-gray-500">(Max 5)</span>
<span className="text-gray-500 text-sm">(Max 5)</span>
</label> </label>
{/* Simple file input */}
<input <input
type="file" type="file"
accept="image/*" accept="image/*"
@@ -212,7 +308,7 @@ const ProductForm = ({
const files = Array.from(e.target.files).slice(0, 5); const files = Array.from(e.target.files).slice(0, 5);
setEditingProduct((prev) => ({ setEditingProduct((prev) => ({
...prev, ...prev,
images: [...prev.images, ...files].slice(0, 5), images: [...(prev.images || []), ...files].slice(0, 5),
})); }));
}} }}
className="hidden" className="hidden"
@@ -220,30 +316,41 @@ const ProductForm = ({
/> />
<label <label
htmlFor="image-upload" htmlFor="image-upload"
className="block w-full p-3 border-2 border-dashed border-emerald-200 bg-emerald-50 rounded-md text-center cursor-pointer hover:bg-emerald-100 transition-colors" className="block w-full p-3 border border-gray-300 bg-gray-50 text-center cursor-pointer hover:bg-gray-100"
> >
<span className="text-emerald-700 font-medium"> <span className="text-emerald-600 font-medium">
Click to upload images Click to upload images
</span> </span>
</label> </label>
{/* Image previews */} {/* Image previews */}
{editingProduct.images.length > 0 && ( {(editingProduct.images || []).length > 0 && (
<div className="mt-4"> <div className="mt-3">
<p className="text-sm text-gray-600 mb-2"> <div className="flex justify-between items-center mb-2">
<p className="text-sm text-gray-600">
{editingProduct.images.length}{" "} {editingProduct.images.length}{" "}
{editingProduct.images.length === 1 ? "image" : "images"}{" "} {editingProduct.images.length === 1 ? "image" : "images"}{" "}
selected selected
</p> </p>
<div className="flex flex-wrap gap-3"> <button
onClick={() =>
setEditingProduct((prev) => ({ ...prev, images: [] }))
}
className="text-sm text-red-600 hover:text-red-800 flex items-center gap-1"
>
<Trash2 size={14} />
<span>Clear all</span>
</button>
</div>
<div className="flex flex-wrap gap-2">
{editingProduct.images.map((img, idx) => ( {editingProduct.images.map((img, idx) => (
<div <div
key={idx} key={idx}
className="relative w-20 h-20 border-2 border-gray-200 rounded-md overflow-hidden" className="relative w-20 h-20 border border-gray-200 overflow-hidden"
> >
<img <img
src={URL.createObjectURL(img)} src={URL.createObjectURL(img)}
alt={`Product image ${idx + 1}`} alt={`Product ${idx + 1}`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
<button <button
@@ -256,22 +363,11 @@ const ProductForm = ({
})); }));
}} }}
className="absolute top-0 right-0 bg-white bg-opacity-80 w-6 h-6 flex items-center justify-center text-gray-700 hover:text-red-600" className="absolute top-0 right-0 bg-white bg-opacity-80 w-6 h-6 flex items-center justify-center text-gray-700 hover:text-red-600"
title="Remove image"
> >
&times; <X size={14} />
</button> </button>
</div> </div>
))} ))}
{editingProduct.images.length > 0 && (
<button
onClick={() =>
setEditingProduct((prev) => ({ ...prev, images: [] }))
}
className="text-sm text-red-600 hover:text-red-800 mt-2"
>
Clear all
</button>
)}
</div> </div>
</div> </div>
)} )}
@@ -279,21 +375,35 @@ const ProductForm = ({
</div> </div>
{/* Actions */} {/* Actions */}
<div className="mt-8 flex justify-end gap-4 border-t-2 border-gray-100 pt-4"> <div className="mt-6 flex justify-between border-t border-gray-200 pt-4">
<button
onClick={toggleSoldStatus}
className={`flex items-center gap-1 px-4 py-2 rounded-md transition-colors ${
editingProduct.isSold
? "bg-green-100 text-green-700 hover:bg-green-200"
: "bg-red-100 text-red-700 hover:bg-red-200"
}`}
>
<Check size={16} />
<span>Mark as {editingProduct.isSold ? "Available" : "Sold"}</span>
</button>
<div className="flex gap-3">
<button <button
onClick={onCancel} onClick={onCancel}
className="bg-gray-100 text-gray-700 px-6 py-2 rounded-md hover:bg-gray-200 font-medium" className="bg-gray-100 text-gray-700 px-4 py-2 hover:bg-gray-200 rounded-md"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={onSave} onClick={handleSave}
className="bg-emerald-600 text-white px-8 py-2 rounded-md hover:bg-emerald-700 font-medium" className="bg-emerald-600 text-white px-6 py-2 hover:bg-emerald-700 rounded-md"
> >
{editingProduct.id ? "Update Product" : "Add Product"} {editingProduct.id ? "Update Product" : "Add Product"}
</button> </button>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from "react";
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from "react-router-dom";
import { User, Settings, ShoppingBag, DollarSign, LogOut } from 'lucide-react'; import { User, Settings, ShoppingBag, DollarSign, LogOut } from "lucide-react";
const UserDropdown = ({ onLogout, userName }) => { const UserDropdown = ({ onLogout, userName }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -8,7 +8,7 @@ const UserDropdown = ({ onLogout, userName }) => {
const navigate = useNavigate(); const navigate = useNavigate();
// Use passed userName or fallback to default // Use passed userName or fallback to default
const displayName = userName || 'User'; const displayName = userName || "User";
const toggleDropdown = () => { const toggleDropdown = () => {
setIsOpen(!isOpen); setIsOpen(!isOpen);
@@ -22,9 +22,9 @@ const UserDropdown = ({ onLogout, userName }) => {
} }
}; };
document.addEventListener('mousedown', handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener("mousedown", handleClickOutside);
}; };
}, []); }, []);
@@ -39,7 +39,7 @@ const UserDropdown = ({ onLogout, userName }) => {
} }
// Navigate to login page (this may be redundant as App.jsx should handle redirection) // Navigate to login page (this may be redundant as App.jsx should handle redirection)
navigate('/login'); navigate("/login");
}; };
return ( return (
@@ -48,8 +48,8 @@ const UserDropdown = ({ onLogout, userName }) => {
className="flex items-center focus:outline-none" className="flex items-center focus:outline-none"
onClick={toggleDropdown} onClick={toggleDropdown}
> >
<div className="h-8 w-8 rounded-full bg-green-100 flex items-center justify-center"> <div className="h-8 w-8 rounded-full bg-emerald-100 flex items-center justify-center">
<User className="h-5 w-5 text-green-600" /> <User className="h-5 w-5 text-emerald-600" />
</div> </div>
</button> </button>

View File

@@ -1,34 +1,24 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Heart, Tag, Trash2 } from "lucide-react"; import { Heart, Trash2 } from "lucide-react";
const Favorites = () => { const Favorites = () => {
const [favorites, setFavorites] = useState([]); const [favorites, setFavorites] = useState([]);
const [showFilters, setShowFilters] = useState(false);
const [sortBy, setSortBy] = useState("dateAdded"); const [sortBy, setSortBy] = useState("dateAdded");
const [filterCategory, setFilterCategory] = useState("All");
const storedUser = JSON.parse(sessionStorage.getItem("user")); const storedUser = JSON.parse(sessionStorage.getItem("user"));
const mapCategory = (id) => {
const categories = {
1: "Electronics",
2: "Textbooks",
3: "Furniture",
4: "Clothing",
5: "Kitchen",
6: "Other",
};
return categories[id] || "Other";
};
function reloadPage() { function reloadPage() {
var doctTimestamp = new Date(performance.timing.domLoading).getTime(); const docTimestamp = new Date(performance.timing.domLoading).getTime();
var now = Date.now(); const now = Date.now();
if (now > doctTimestamp) { if (now > docTimestamp) {
location.reload(); location.reload();
} }
} }
const mapCategory = (id) => {
return id || "Other";
};
const removeFromFavorites = async (itemID) => { const removeFromFavorites = async (itemID) => {
const response = await fetch( const response = await fetch(
"http://localhost:3030/api/product/delFavorite", "http://localhost:3030/api/product/delFavorite",
@@ -43,14 +33,13 @@ const Favorites = () => {
}), }),
}, },
); );
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
reloadPage(); reloadPage();
} }
if (!response.ok) throw new Error("Failed to fetch products"); if (!response.ok) throw new Error("Failed to remove from favorites");
console.log(response);
console.log(`Add Product -> History: ${itemID}`);
}; };
useEffect(() => { useEffect(() => {
@@ -68,7 +57,6 @@ const Favorites = () => {
); );
const data = await response.json(); const data = await response.json();
const favoritesData = data.favorites; const favoritesData = data.favorites;
if (!Array.isArray(favoritesData)) { if (!Array.isArray(favoritesData)) {
@@ -78,10 +66,11 @@ const Favorites = () => {
const transformed = favoritesData.map((item) => ({ const transformed = favoritesData.map((item) => ({
id: item.ProductID, id: item.ProductID,
title: item.Name, name: item.Name,
price: parseFloat(item.Price), price: parseFloat(item.Price),
category: mapCategory(item.CategoryID), categories: [mapCategory(item.Category)],
image: item.image_url || "/default-image.jpg", image: item.image_url || "/default-image.jpg",
description: item.Description || "",
seller: item.SellerName, seller: item.SellerName,
datePosted: formatDatePosted(item.Date), datePosted: formatDatePosted(item.Date),
dateAdded: item.Date || new Date().toISOString(), dateAdded: item.Date || new Date().toISOString(),
@@ -112,19 +101,13 @@ const Favorites = () => {
return 0; return 0;
}); });
const filteredFavorites =
filterCategory === "All"
? sortedFavorites
: sortedFavorites.filter((item) => item.category === filterCategory);
return ( return (
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">My Favorites</h1> <h1 className="text-2xl font-bold text-gray-800">My Favorites</h1>
</div> </div>
{/* Favorites List */} {sortedFavorites.length === 0 ? (
{filteredFavorites.length === 0 ? (
<div className="bg-white border border-gray-200 p-8 text-center"> <div className="bg-white border border-gray-200 p-8 text-center">
<Heart className="h-12 w-12 text-gray-300 mx-auto mb-4" /> <Heart className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-medium text-gray-700 mb-2"> <h3 className="text-xl font-medium text-gray-700 mb-2">
@@ -136,56 +119,72 @@ const Favorites = () => {
</p> </p>
<Link <Link
to="/" to="/"
className="inline-block bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4" className="inline-block bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4"
> >
Browse Listings Browse Listings
</Link> </Link>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredFavorites.map((item) => ( {sortedFavorites.map((product) => (
<div <div
key={item.id} key={product.id}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow relative" className="border-2 border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
> >
<button <Link to={`/product/${product.id}`}>
onClick={() => removeFromFavorites(item.id)} <div className="h-48 bg-gray-200 flex items-center justify-center">
className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm text-red-500 hover:bg-red-50" {product.image ? (
title="Remove from favorites"
>
<Trash2 className="h-5 w-5" />
</button>
<Link to={`/product/${item.id}`}>
<img <img
src={item.image} src={product.image}
alt={item.title} alt={product.name}
className="w-full h-48 object-cover" className="w-full h-full object-cover"
/> />
) : (
<div className="text-gray-400">No image</div>
)}
</div>
<div className="p-4"> <div className="p-4">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start">
<h3 className="text-lg font-medium text-gray-800 leading-tight"> <h3 className="text-lg font-semibold text-gray-800">
{item.title} {product.name}
</h3> </h3>
<span className="font-semibold text-green-600"> <button
${item.price} onClick={(e) => {
</span> e.stopPropagation();
e.preventDefault();
removeFromFavorites(product.id);
}}
className="text-red-500 hover:text-red-600"
>
<Trash2 size={24} />
</button>
</div> </div>
<div className="flex items-center text-sm text-gray-500 mb-3"> <p className="text-emerald-600 font-bold mt-1">
<Tag className="h-4 w-4 mr-1" /> ${product.price.toFixed(2)}
<span>{item.category}</span> </p>
</div>
<div className="flex justify-between items-center pt-2 border-t border-gray-100"> {product.categories.length > 0 && (
<span className="text-xs text-gray-500"> <div className="mt-2 flex flex-wrap gap-1">
Listed {item.datePosted} {product.categories.map((category) => (
</span> <span
<span className="text-sm font-medium text-gray-700"> key={category}
{item.seller} className="text-xs bg-gray-100 text-gray-600 px-2 py-1"
>
{category}
</span> </span>
))}
</div> </div>
)}
<p className="text-gray-500 text-sm mt-2 line-clamp-2">
{product.description}
</p>
<p className="text-gray-400 text-xs mt-2">
Posted {product.datePosted}
</p>
</div> </div>
</Link> </Link>
</div> </div>
@@ -193,14 +192,66 @@ const Favorites = () => {
</div> </div>
)} )}
{/* Show count if there are favorites */} {sortedFavorites.length > 0 && (
{filteredFavorites.length > 0 && (
<div className="mt-6 text-sm text-gray-500"> <div className="mt-6 text-sm text-gray-500">
Showing {filteredFavorites.length}{" "} Showing {sortedFavorites.length}{" "}
{filteredFavorites.length === 1 ? "item" : "items"} {sortedFavorites.length === 1 ? "item" : "items"}
{filterCategory !== "All" && ` in ${filterCategory}`}
</div> </div>
)} )}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<h3 className="text-lg font-semibold mb-2">Campus Marketplace</h3>
<p className="text-gray-400 text-sm">
Your trusted university trading platform
</p>
</div>
<div className="flex space-x-6">
<div>
<h4 className="font-medium mb-2">Quick Links</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">
<Link to="/" className="hover:text-white transition">
Home
</Link>
</li>
<li className="mb-1">
<Link to="/selling" className="hover:text-white transition">
Sell an Item
</Link>
</li>
<li className="mb-1">
<Link
to="/favorites"
className="hover:text-white transition"
>
My Favorites
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Contact</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">support@campusmarket.com</li>
<li className="mb-1">University of Calgary</li>
</ul>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-6 pt-6 text-center text-sm text-gray-400">
<p>
© {new Date().getFullYear()} Campus Marketplace. All rights
reserved.
</p>
</div>
</div>
</footer>
</div> </div>
); );
}; };

View File

@@ -1,6 +1,12 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { Tag } from "lucide-react"; import {
Tag,
ChevronLeft,
ChevronRight,
Bookmark,
BookmarkCheck,
} from "lucide-react";
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
@@ -15,7 +21,6 @@ const Home = () => {
//After user data storing the session. //After user data storing the session.
const storedUser = JSON.parse(sessionStorage.getItem("user")); const storedUser = JSON.parse(sessionStorage.getItem("user"));
const toggleFavorite = async (id) => { const toggleFavorite = async (id) => {
const response = await fetch( const response = await fetch(
"http://localhost:3030/api/product/addFavorite", "http://localhost:3030/api/product/addFavorite",
@@ -187,7 +192,8 @@ const Home = () => {
}; };
return ( return (
<div> <div className="flex flex-col min-h-screen">
<div className="flex-grow">
{/* Hero Section with School Background */} {/* Hero Section with School Background */}
<div className="relative py-12 px-4 mb-8 shadow-sm"> <div className="relative py-12 px-4 mb-8 shadow-sm">
{/* Background Image - Positioned at bottom */} {/* Background Image - Positioned at bottom */}
@@ -206,12 +212,12 @@ const Home = () => {
Buy and Sell on Campus Buy and Sell on Campus
</h1> </h1>
<p className="text-white mb-6"> <p className="text-white mb-6">
The marketplace exclusively for university students. Find everything The marketplace exclusively for university students. Find
you need or sell what you don't. everything you need or sell what you don't.
</p> </p>
<button <button
onClick={handleSelling} onClick={handleSelling}
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-green-400 transition-colors" className="bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-emerald-400 transition-colors"
> >
Post an Item Post an Item
</button> </button>
@@ -240,13 +246,13 @@ const Home = () => {
} }
className="absolute left-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12" className="absolute left-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
> >
<ChevronLeft size={24} />{" "}
</button> </button>
{/* Scrollable Listings Container */} {/* Scrollable Listings Container */}
<div <div
id="RecomContainer" id="RecomContainer"
className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0" className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0 rounded"
> >
{recommended.map((recommended) => ( {recommended.map((recommended) => (
<Link <Link
@@ -267,9 +273,9 @@ const Home = () => {
e.preventDefault(); e.preventDefault();
toggleFavorite(recommended.id); toggleFavorite(recommended.id);
}} }}
className="absolute top-2 right-2 p-2 bg-white rounded-full shadow-sm hover:bg-gray-100 transition" className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm"
> >
<span className="text-xl font-bold text-gray-600">+</span> <Bookmark className="text-white w-5 h-5" />
</button> </button>
</div> </div>
@@ -277,7 +283,7 @@ const Home = () => {
<h3 className="text-lg font-medium text-gray-800 leading-tight"> <h3 className="text-lg font-medium text-gray-800 leading-tight">
{recommended.title} {recommended.title}
</h3> </h3>
<span className="font-semibold text-green-600 block mt-1"> <span className="font-semibold text-emerald-600 block mt-1">
${recommended.price} ${recommended.price}
</span> </span>
@@ -308,7 +314,7 @@ const Home = () => {
} }
className="absolute right-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12" className="absolute right-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
> >
<ChevronRight size={24} />{" "}
</button> </button>
</div> </div>
</div> </div>
@@ -335,7 +341,7 @@ const Home = () => {
} }
className="absolute left-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12" className="absolute left-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
> >
<ChevronLeft size={24} />{" "}
</button> </button>
{/* Scrollable Listings Container */} {/* Scrollable Listings Container */}
@@ -362,9 +368,9 @@ const Home = () => {
e.preventDefault(); e.preventDefault();
toggleFavorite(listing.id); toggleFavorite(listing.id);
}} }}
className="absolute top-2 right-2 p-2 bg-white rounded-full shadow-sm hover:bg-gray-100 transition" className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm"
> >
<span className="text-xl font-bold text-gray-600">+</span> <Bookmark className="text-white w-5 h-5" />
</button> </button>
</div> </div>
@@ -372,7 +378,7 @@ const Home = () => {
<h3 className="text-lg font-medium text-gray-800 leading-tight"> <h3 className="text-lg font-medium text-gray-800 leading-tight">
{listing.title} {listing.title}
</h3> </h3>
<span className="font-semibold text-green-600 block mt-1"> <span className="font-semibold text-emerald-600 block mt-1">
${listing.price} ${listing.price}
</span> </span>
@@ -403,12 +409,12 @@ const Home = () => {
} }
className="absolute right-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12" className="absolute right-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
> >
<ChevronRight size={24} />{" "}
</button> </button>
</div> </div>
</div> </div>
{/* Recent Listings */} {/* History Section */}
{showAlert && ( {showAlert && (
<FloatingAlert <FloatingAlert
message="Product added to favorites!" message="Product added to favorites!"
@@ -428,7 +434,7 @@ const Home = () => {
} }
className="absolute left-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12" className="absolute left-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
> >
<ChevronLeft size={24} />{" "}
</button> </button>
{/* Scrollable Listings Container */} {/* Scrollable Listings Container */}
@@ -454,9 +460,9 @@ const Home = () => {
e.preventDefault(); e.preventDefault();
toggleFavorite(history.id); toggleFavorite(history.id);
}} }}
className="absolute top-2 right-2 p-2 bg-white rounded-full shadow-sm hover:bg-gray-100 transition" className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm"
> >
<span className="text-xl font-bold text-gray-600">+</span> <Bookmark className="text-white w-5 h-5" />
</button> </button>
</div> </div>
@@ -464,7 +470,7 @@ const Home = () => {
<h3 className="text-lg font-medium text-gray-800 leading-tight"> <h3 className="text-lg font-medium text-gray-800 leading-tight">
{history.title} {history.title}
</h3> </h3>
<span className="font-semibold text-green-600 block mt-1"> <span className="font-semibold text-emerald-600 block mt-1">
${history.price} ${history.price}
</span> </span>
@@ -495,11 +501,67 @@ const Home = () => {
} }
className="absolute right-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12" className="absolute right-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
> >
<ChevronRight size={24} />{" "}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{/* Footer - Added here */}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<h3 className="text-lg font-semibold mb-2">Campus Marketplace</h3>
<p className="text-gray-400 text-sm">
Your trusted university trading platform
</p>
</div>
<div className="flex space-x-6">
<div>
<h4 className="font-medium mb-2">Quick Links</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">
<Link to="/" className="hover:text-white transition">
Home
</Link>
</li>
<li className="mb-1">
<Link to="/selling" className="hover:text-white transition">
Sell an Item
</Link>
</li>
<li className="mb-1">
<Link
to="/favorites"
className="hover:text-white transition"
>
My Favorites
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Contact</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">support@campusmarket.com</li>
<li className="mb-1">University of Calgary</li>
</ul>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-6 pt-6 text-center text-sm text-gray-400">
<p>
© {new Date().getFullYear()} Campus Marketplace. All rights
reserved.
</p>
</div>
</div>
</footer>
</div>
); );
}; };

View File

@@ -9,10 +9,10 @@ import {
Star, Star,
Phone, Phone,
Mail, Mail,
Bookmark,
} from "lucide-react"; } from "lucide-react";
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
const ProductDetail = () => { const ProductDetail = () => {
const { id } = useParams(); const { id } = useParams();
const [product, setProduct] = useState(null); const [product, setProduct] = useState(null);
@@ -34,7 +34,6 @@ const ProductDetail = () => {
const [showAlert, setShowAlert] = useState(false); const [showAlert, setShowAlert] = useState(false);
const storedUser = JSON.parse(sessionStorage.getItem("user")); const storedUser = JSON.parse(sessionStorage.getItem("user"));
const toggleFavorite = async (id) => { const toggleFavorite = async (id) => {
const response = await fetch( const response = await fetch(
"http://localhost:3030/api/product/addFavorite", "http://localhost:3030/api/product/addFavorite",
@@ -56,7 +55,6 @@ const ProductDetail = () => {
console.log(`Add Product -> History: ${id}`); console.log(`Add Product -> History: ${id}`);
}; };
const [reviewForm, setReviewForm] = useState({ const [reviewForm, setReviewForm] = useState({
rating: 3, rating: 3,
comment: "", comment: "",
@@ -94,11 +92,14 @@ const ProductDetail = () => {
userId: storedUser.ID, userId: storedUser.ID,
}; };
const response = await fetch(`http://localhost:3030/api/review/addReview`, { const response = await fetch(
`http://localhost:3030/api/review/addReview`,
{
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(reviewData), body: JSON.stringify(reviewData),
}); },
);
const result = await response.json(); const result = await response.json();
@@ -208,7 +209,6 @@ const ProductDetail = () => {
fetchReviews(); fetchReviews();
}, [id]); }, [id]);
// Image navigation // Image navigation
const nextImage = () => { const nextImage = () => {
if (product?.images?.length > 0) { if (product?.images?.length > 0) {
@@ -248,7 +248,7 @@ const ProductDetail = () => {
if (loading.product) { if (loading.product) {
return ( return (
<div className="flex justify-center items-center h-screen"> <div className="flex justify-center items-center h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-green-500"></div> <div className="animate-spin h-32 w-32 border-t-2 border-green-500"></div>
</div> </div>
); );
} }
@@ -262,7 +262,7 @@ const ProductDetail = () => {
<p className="text-gray-600">{error.product}</p> <p className="text-gray-600">{error.product}</p>
<Link <Link
to="/" to="/"
className="mt-4 inline-block bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600" className="mt-4 inline-block bg-green-500 text-white px-4 py-2 hover:bg-green-600"
> >
Back to Listings Back to Listings
</Link> </Link>
@@ -279,7 +279,7 @@ const ProductDetail = () => {
<h2 className="text-2xl text-red-500 mb-4">Product Not Found</h2> <h2 className="text-2xl text-red-500 mb-4">Product Not Found</h2>
<Link <Link
to="/" to="/"
className="mt-4 inline-block bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600" className="mt-4 inline-block bg-green-500 text-white px-4 py-2 hover:bg-green-600"
> >
Back to Listings Back to Listings
</Link> </Link>
@@ -290,7 +290,6 @@ const ProductDetail = () => {
// Render product details // Render product details
return ( return (
<div className="max-w-6xl mx-auto px-4 py-8"> <div className="max-w-6xl mx-auto px-4 py-8">
<div className="mb-6"> <div className="mb-6">
<Link <Link
@@ -320,8 +319,7 @@ const ProductDetail = () => {
onClick={nextImage} onClick={nextImage}
onError={(e) => { onError={(e) => {
e.target.onerror = null; e.target.onerror = null;
e.target.src = e.target.src = "https://via.placeholder.com/400x300";
"https://via.placeholder.com/400x300?text=Image+Not+Available";
}} }}
/> />
{product.images.length > 1 && ( {product.images.length > 1 && (
@@ -331,11 +329,11 @@ const ProductDetail = () => {
e.stopPropagation(); e.stopPropagation();
prevImage(); prevImage();
}} }}
className="bg-white/70 p-1 rounded-full" className="bg-white/70 p-1"
> >
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
</button> </button>
<div className="text-sm bg-white/70 px-2 py-1 rounded"> <div className="text-sm bg-white/70 px-2 py-1 ">
{currentImage + 1}/{product.images.length} {currentImage + 1}/{product.images.length}
</div> </div>
</div> </div>
@@ -353,7 +351,7 @@ const ProductDetail = () => {
{product.images.map((image, index) => ( {product.images.map((image, index) => (
<div <div
key={index} key={index}
className={`bg-white border ${currentImage === index ? "border-green-500" : "border-gray-200"} min-w-[100px] cursor-pointer`} className={`bg-white border ${currentImage === index ? "border-green-500 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`}
onClick={() => selectImage(index)} onClick={() => selectImage(index)}
> >
<img <img
@@ -378,15 +376,14 @@ const ProductDetail = () => {
{product.Name || "Unnamed Product"} {product.Name || "Unnamed Product"}
</h1> </h1>
<button <button
onClick={() => toggleFavorite(product.ProductID)} onClick={(e) => {
className="p-2 hover:bg-gray-100 rounded-full" e.stopPropagation();
aria-label={ e.preventDefault();
isFavorite ? "Remove from favorites" : "Add to favorites" toggleFavorite(product.ProductID);
} }}
className="top-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm"
> >
<Heart <Bookmark className="text-white w-5 h-5" />
className={`h-6 w-6 ${isFavorite ? "text-red-500 fill-red-500" : "text-gray-400"}`}
/>
</button> </button>
</div> </div>
@@ -455,7 +452,7 @@ const ProductDetail = () => {
{/* Seller Info */} {/* Seller Info */}
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">
<div className="mr-3"> <div className="mr-3">
<div className="h-12 w-12 rounded-full bg-gray-200 flex items-center justify-center"> <div className="h-12 w-12 bg-gray-200 flex items-center justify-center">
<User className="h-6 w-6 text-gray-600" /> <User className="h-6 w-6 text-gray-600" />
</div> </div>
</div> </div>
@@ -480,7 +477,7 @@ const ProductDetail = () => {
<div className="bg-white border border-gray-200 p-6"> <div className="bg-white border border-gray-200 p-6">
{loading.reviews ? ( {loading.reviews ? (
<div className="flex justify-center py-8"> <div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-green-500"></div> <div className="animate-spin h-8 w-8 border-t-2 border-green-500"></div>
</div> </div>
) : error.reviews ? ( ) : error.reviews ? (
<div className="text-red-500 mb-4"> <div className="text-red-500 mb-4">
@@ -527,7 +524,7 @@ const ProductDetail = () => {
<div className="mt-4"> <div className="mt-4">
<button <button
onClick={() => setShowReviewForm(true)} onClick={() => setShowReviewForm(true)}
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4 rounded" className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
> >
Write a Review Write a Review
</button> </button>
@@ -536,7 +533,7 @@ const ProductDetail = () => {
{/* Review Popup Form */} {/* Review Popup Form */}
{showReviewForm && ( {showReviewForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6"> <div className="bg-white shadow-xl max-w-md w-full p-6">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-gray-800"> <h3 className="text-xl font-bold text-gray-800">
Write a Review Write a Review
@@ -585,7 +582,7 @@ const ProductDetail = () => {
id="comment" id="comment"
value={reviewForm.comment} value={reviewForm.comment}
onChange={handleReviewInputChange} onChange={handleReviewInputChange}
className="w-full p-3 border border-gray-300 rounded focus:outline-none focus:border-green-500" className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
rows="4" rows="4"
required required
></textarea> ></textarea>
@@ -595,13 +592,13 @@ const ProductDetail = () => {
<button <button
type="button" type="button"
onClick={() => setShowReviewForm(false)} onClick={() => setShowReviewForm(false)}
className="px-4 py-2 border border-gray-300 rounded text-gray-700 hover:bg-gray-100" className="px-4 py-2 border border-gray-300 text-gray-700 hover:bg-gray-100"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600" className="px-4 py-2 bg-green-500 text-white hover:bg-green-600"
disabled={loading.submitting} disabled={loading.submitting}
> >
{loading.submitting ? "Submitting..." : "Submit Review"} {loading.submitting ? "Submitting..." : "Submit Review"}

View File

@@ -17,9 +17,6 @@ const SearchPage = () => {
const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 }); const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
const [isFilterOpen, setIsFilterOpen] = useState(false); const [isFilterOpen, setIsFilterOpen] = useState(false);
useEffect(() => { useEffect(() => {
fetchProducts(initialSearchQuery); fetchProducts(initialSearchQuery);
}, [initialSearchQuery]); }, [initialSearchQuery]);
@@ -149,7 +146,7 @@ const SearchPage = () => {
<div className="flex space-x-2"> <div className="flex space-x-2">
<button <button
onClick={applyFilters} onClick={applyFilters}
className="w-full bg-green-500 text-white p-3 hover:bg-green-600 transition-colors" className="w-full bg-emerald-500 text-white p-3 hover:bg-emerald-600 transition-colors"
> >
Apply Filters Apply Filters
</button> </button>
@@ -190,7 +187,7 @@ const SearchPage = () => {
<h3 className="text-lg font-medium text-gray-800"> <h3 className="text-lg font-medium text-gray-800">
{listing.title} {listing.title}
</h3> </h3>
<p className="text-green-600 font-semibold"> <p className="text-emerald-600 font-semibold">
${Number(listing.price).toFixed(2)} ${Number(listing.price).toFixed(2)}
</p> </p>
</div> </div>
@@ -199,6 +196,60 @@ const SearchPage = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Footer - Added here */}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<h3 className="text-lg font-semibold mb-2">Campus Marketplace</h3>
<p className="text-gray-400 text-sm">
Your trusted university trading platform
</p>
</div>
<div className="flex space-x-6">
<div>
<h4 className="font-medium mb-2">Quick Links</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">
<Link to="/" className="hover:text-white transition">
Home
</Link>
</li>
<li className="mb-1">
<Link to="/selling" className="hover:text-white transition">
Sell an Item
</Link>
</li>
<li className="mb-1">
<Link
to="/favorites"
className="hover:text-white transition"
>
My Favorites
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Contact</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">support@campusmarket.com</li>
<li className="mb-1">University of Calgary</li>
</ul>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-6 pt-6 text-center text-sm text-gray-400">
<p>
© {new Date().getFullYear()} Campus Marketplace. All rights
reserved.
</p>
</div>
</div>
</footer>
</div> </div>
); );
}; };

View File

@@ -10,9 +10,7 @@ const Settings = () => {
phone: "", phone: "",
UCID: "", UCID: "",
address: "", address: "",
currentPassword: "", password: "",
newPassword: "",
confirmPassword: "",
}); });
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@@ -62,10 +60,7 @@ const Settings = () => {
UCID: data.UCID || storedUser.UCID || "", UCID: data.UCID || storedUser.UCID || "",
phone: data.phone || storedUser.phone || "", phone: data.phone || storedUser.phone || "",
address: data.address || storedUser.address || "", address: data.address || storedUser.address || "",
// Reset password fields password: data.password,
currentPassword: "",
newPassword: "",
confirmPassword: "",
})); }));
} else { } else {
throw new Error(data.error || "Failed to retrieve user data"); throw new Error(data.error || "Failed to retrieve user data");
@@ -146,41 +141,6 @@ const Settings = () => {
} }
}; };
const handlePasswordUpdate = async (e) => {
e.preventDefault();
try {
// Validate passwords match
if (userData.newPassword !== userData.confirmPassword) {
alert("New passwords do not match!");
return;
}
// TODO: Implement the actual password update API call
console.log("Password updated");
// Update password in localStorage
const storedUser = JSON.parse(localStorage.getItem("user"));
const updatedUser = {
...storedUser,
password: userData.newPassword,
};
localStorage.setItem("user", JSON.stringify(updatedUser));
// Reset password fields
setUserData((prevData) => ({
...prevData,
currentPassword: "",
newPassword: "",
confirmPassword: "",
}));
alert("Password updated successfully!");
} catch (error) {
console.error("Error updating password:", error);
alert("Failed to update password: " + error.message);
}
};
const handleDeleteAccount = async () => { const handleDeleteAccount = async () => {
if ( if (
window.confirm( window.confirm(
@@ -230,7 +190,7 @@ const Settings = () => {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex justify-center items-center h-64"> <div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-emerald-500"></div>
</div> </div>
); );
} }
@@ -274,7 +234,7 @@ const Settings = () => {
id="name" id="name"
value={userData.name} value={userData.name}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
required required
/> />
</div> </div>
@@ -291,7 +251,7 @@ const Settings = () => {
id="email" id="email"
value={userData.email} value={userData.email}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
required required
readOnly // Email is often used as identifier and not changeable readOnly // Email is often used as identifier and not changeable
/> />
@@ -309,7 +269,7 @@ const Settings = () => {
id="phone" id="phone"
value={userData.phone} value={userData.phone}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
/> />
</div> </div>
@@ -325,7 +285,7 @@ const Settings = () => {
id="UCID" id="UCID"
value={userData.UCID} value={userData.UCID}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
required required
/> />
</div> </div>
@@ -342,14 +302,29 @@ const Settings = () => {
id="address" id="address"
value={userData.address} value={userData.address}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
/>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 mb-1"
>
password
</label>
<input
type="text"
id="password"
value={userData.password}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
/> />
</div> </div>
</div> </div>
<button <button
type="submit" type="submit"
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4" className="bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4"
> >
Update Profile Update Profile
</button> </button>
@@ -357,84 +332,10 @@ const Settings = () => {
</div> </div>
</div> </div>
{/* Security Section */}
<div className="bg-white border border-gray-200 mb-6">
<div className="border-b border-gray-200 p-4">
<div className="flex items-center">
<Lock className="h-5 w-5 text-gray-500 mr-2" />
<h2 className="text-lg font-medium text-gray-800">Password</h2>
</div>
</div>
<div className="p-4">
<form onSubmit={handlePasswordUpdate}>
<div className="space-y-4 mb-4">
<div>
<label
htmlFor="currentPassword"
className="block text-sm font-medium text-gray-700 mb-1"
>
Current Password
</label>
<input
type="password"
id="currentPassword"
value={userData.currentPassword}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
required
/>
</div>
<div>
<label
htmlFor="newPassword"
className="block text-sm font-medium text-gray-700 mb-1"
>
New Password
</label>
<input
type="password"
id="newPassword"
value={userData.newPassword}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
required
/>
</div>
<div>
<label
htmlFor="confirmPassword"
className="block text-sm font-medium text-gray-700 mb-1"
>
Confirm New Password
</label>
<input
type="password"
id="confirmPassword"
value={userData.confirmPassword}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
required
/>
</div>
</div>
<button
type="submit"
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
>
Change Password
</button>
</form>
</div>
</div>
{/* Privacy Section */} {/* Privacy Section */}
{showAlert && ( {showAlert && (
<FloatingAlert <FloatingAlert
message="Removed Your History! 😉" message="We Removed Your History! 😉"
onClose={() => setShowAlert(false)} onClose={() => setShowAlert(false)}
/> />
)} )}
@@ -448,7 +349,7 @@ const Settings = () => {
<div className="p-4"> <div className="p-4">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center pb-4 border-b border-gray-100"> <div className="flex justify-between items-center">
<div className="flex items-start"> <div className="flex items-start">
<History className="h-5 w-5 text-gray-500 mr-2 mt-0.5" /> <History className="h-5 w-5 text-gray-500 mr-2 mt-0.5" />
<div> <div>

View File

@@ -452,5 +452,4 @@ VALUES
1, 1,
'This is a great fake product! Totally recommend it.', 'This is a great fake product! Totally recommend it.',
5, 5,
NOW ()
); );

View File

@@ -30,7 +30,7 @@ CREATE TABLE Category (
-- Product Entity -- Product Entity
CREATE TABLE Product ( CREATE TABLE Product (
ProductID INT PRIMARY KEY, ProductID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL, Name VARCHAR(255) NOT NULL,
Price DECIMAL(10, 2) NOT NULL, Price DECIMAL(10, 2) NOT NULL,
StockQuantity INT, StockQuantity INT,

Binary file not shown.

View File

@@ -1,7 +1,4 @@
# pip install mysql.connector # pip install mysql.connector
#
import mysql.connector import mysql.connector
from sklearn.metrics.pairwise import cosine_similarity from sklearn.metrics.pairwise import cosine_similarity
import numpy as np import numpy as np

View File

@@ -1,7 +1,3 @@
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
from flask_cors import CORS from flask_cors import CORS
from app import get_recommendations from app import get_recommendations