Merge branch 'mannBranch'

This commit is contained in:
Mann Patel
2025-03-30 00:25:41 -06:00
21 changed files with 1819 additions and 262 deletions

View File

@@ -24,6 +24,6 @@
### Database
1. To Create the DB use the command bellow
```Bash
python3 ./SQL_code/init-db.py
python3 ./mysql-code/init-db.py
```
- MySql Version 9.2.0

View File

@@ -1,3 +0,0 @@
import subprocess
if (subprocess.run("mysql -u root mysql < SQL_code/Schema.sql", shell=True, check=True)):
print("successfully created the Marketplace databse")

View File

@@ -7,7 +7,7 @@ exports.addToFavorite = async (req, res) => {
// Use parameterized query to prevent SQL injection
const [result] = await db.execute(
"INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)",
[userID, productsID]
[userID, productsID],
);
res.json({
@@ -20,18 +20,32 @@ exports.addToFavorite = async (req, res) => {
}
};
//Get all products
// Get all products along with their image URLs
exports.getAllProducts = async (req, res) => {
try {
const [data, fields] = await db.execute("SELECT * FROM Product");
const [data, fields] = await db.execute(`
SELECT
P.ProductID,
P.Name AS ProductName,
P.Price,
P.Date AS DateUploaded,
U.Name AS SellerName,
I.URL AS ProductImage,
C.Name AS Category
FROM Product P
JOIN Image_URL I ON p.ProductID = i.ProductID
JOIN User U ON P.UserID = U.UserID
JOIN Category C ON P.CategoryID = C.CategoryID;
`);
console.log(data);
res.json({
success: true,
message: "Product added to favorites successfully",
message: "Products fetched successfully",
data,
});
} catch (error) {
console.error("Error finding user:", error);
console.error("Error finding products:", error);
return res.status(500).json({
found: false,
error: "Database error occurred",
@@ -39,6 +53,61 @@ exports.getAllProducts = async (req, res) => {
}
};
exports.getProductById = async (req, res) => {
const { id } = req.params;
console.log("Received Product ID:", id);
try {
const [data] = await db.execute(
`
SELECT p.*, i.URL AS image_url
FROM Product p
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
WHERE p.ProductID = ?
`,
[id],
);
// Log raw data for debugging
console.log("Raw Database Result:", data);
if (data.length === 0) {
console.log("No product found with ID:", id);
return res.status(404).json({
success: false,
message: "Product not found",
});
}
// Collect all image URLs
const images = data
.map((row) => row.image_url)
.filter((url) => url !== null);
// Create product object with all details from first row and collected images
const product = {
...data[0], // Base product details
images: images, // Collected image URLs
};
// Log processed product for debugging
console.log("Processed Product:", product);
res.json({
success: true,
message: "Product fetched successfully",
data: product,
});
} catch (error) {
console.error("Full Error Details:", error);
return res.status(500).json({
success: false,
message: "Database error occurred",
error: error.message,
});
}
};
// db_con.query(
// "SELECT ProductID FROM product WHERE ProductID = ?",
// [productID],

View File

@@ -0,0 +1,164 @@
const db = require("../utils/database");
exports.searchProductsByName = async (req, res) => {
const { name } = req.query;
if (name.length === 0) {
console.log("Searching for products with no name", name);
}
console.log("Searching for products with name:", name);
try {
// Modify SQL to return all products when no search term is provided
const sql = `
SELECT p.*, i.URL as image
FROM Product p
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
${name ? "WHERE p.Name LIKE ?" : ""}
ORDER BY p.ProductID
`;
const params = name ? [`%${name}%`] : [];
console.log("Executing SQL:", sql);
console.log("With parameters:", params);
const [data] = await db.execute(sql, params);
console.log("Raw Database Result:", data);
if (data.length === 0) {
console.log("No products found matching:", name);
return res.status(404).json({
success: false,
message: "No products found matching your search",
});
}
// Group products by ProductID to handle multiple images per product
const productsMap = new Map();
data.forEach((row) => {
if (!productsMap.has(row.ProductID)) {
const product = {
ProductID: row.ProductID,
Name: row.Name,
Description: row.Description,
Price: row.Price,
images: row.image,
};
productsMap.set(row.ProductID, product);
} else if (row.image_url) {
productsMap.get(row.ProductID).images.push(row.image_url);
}
});
const products = Array.from(productsMap.values());
console.log("Processed Products:", products);
res.json({
success: true,
message: "Products fetched successfully",
data: products,
count: products.length,
});
} catch (error) {
console.error("Database Error:", error);
return res.status(500).json({
success: false,
message: "Database error occurred",
error: error.message || "Unknown database error",
});
}
};
// exports.searchProductsByName = async (req, res) => {
// const { name } = req.query;
// // Add better validation and error handling
// if (!name || typeof name !== "string") {
// return res.status(400).json({
// success: false,
// message: "Valid search term is required",
// });
// }
// console.log("Searching for products with name:", name);
// try {
// // Log the SQL query and parameters for debugging
// const sql = `
// SELECT p.*, i.URL AS image_url
// FROM Product p
// LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
// WHERE p.Name LIKE ?
// `;
// const params = [`%${name}%`];
// console.log("Executing SQL:", sql);
// console.log("With parameters:", params);
// const [data] = await db.execute(sql, params);
// // Log raw data for debugging
// console.log("Raw Database Result:", data);
// if (data.length === 0) {
// console.log("No products found matching:", name);
// return res.status(404).json({
// success: false,
// message: "No products found matching your search",
// });
// }
// // Group products by ProductID to handle multiple images per product
// const productsMap = new Map();
// data.forEach((row) => {
// if (!productsMap.has(row.ProductID)) {
// // Create a clean object without circular references
// const product = {
// ProductID: row.ProductID,
// Name: row.Name,
// Description: row.Description,
// Price: row.Price,
// // Add any other product fields you need
// images: row.image_url ? [row.image_url] : [],
// };
// productsMap.set(row.ProductID, product);
// } else if (row.image_url) {
// // Add additional image to existing product
// productsMap.get(row.ProductID).images.push(row.image_url);
// }
// });
// // Convert map to array of products
// const products = Array.from(productsMap.values());
// // Log processed products for debugging
// console.log("Processed Products:", products);
// res.json({
// success: true,
// message: "Products fetched successfully",
// data: products,
// count: products.length,
// });
// } catch (error) {
// // Enhanced error logging
// console.error("Database Error Details:", {
// message: error.message,
// code: error.code,
// errno: error.errno,
// sqlState: error.sqlState,
// sqlMessage: error.sqlMessage,
// sql: error.sql,
// });
// return res.status(500).json({
// success: false,
// message: "Database error occurred",
// error: error.message || "Unknown database error",
// });
// }
// };

View File

@@ -5,6 +5,7 @@ const db = require("./utils/database");
const userRouter = require("./routes/user");
const productRouter = require("./routes/product");
const searchRouter = require("./routes/search");
const { generateEmailTransporter } = require("./utils/mail");
const {
cleanupExpiredCodes,
@@ -34,6 +35,7 @@ checkDatabaseConnection(db);
//Routes
app.use("/api/user", userRouter); //prefix with /api/user
app.use("/api/product", productRouter); //prefix with /api/product
app.use("/api/search_products", searchRouter); //prefix with /api/product
// Set up a scheduler to run cleanup every hour
setInterval(cleanupExpiredCodes, 60 * 60 * 1000);

View File

@@ -1,43 +0,0 @@
package main
import (
"fmt"
"math"
)
func cosine(x, y []int) float64 {
var dotProduct, normX, normY float64
for i := 1; i < len(x); i++ {
dotProduct += float64(x[i] * y[i])
normX += float64(x[i] * x[i])
normY += float64(y[i] * y[i])
}
return dotProduct / (math.Sqrt(normX) * math.Sqrt(normY))
}
func main() {
history := [][]int{
{1, 0, 1, 0},
{0, 1, 0, 1},
{1, 0, 1, 1},
}
productCategory := [][]int{
{1, 1, 0, 0},
{0, 0, 0, 1},
{0, 0, 1, 0},
{0, 0, 1, 1},
{0, 1, 0, 0},
{0, 1, 1, 1},
{1, 1, 1, 0},
{0, 0, 0, 1},
{1, 1, 1, 1},
}
// Calculate similarity between first search and each product
for i, product := range productCategory {
sim := cosine(history[0], product)
fmt.Printf("Similarity with product %d: %f\n", i, sim)
}
}

View File

@@ -1,10 +1,20 @@
// routes/product.js
const express = require("express");
const { addToFavorite, getAllProducts } = require("../controllers/product");
const {
addToFavorite,
getAllProducts,
getProductById,
} = require("../controllers/product");
const router = express.Router();
router.post("/add_fav_product", addToFavorite);
// Add detailed logging middleware
router.use((req, res, next) => {
console.log(`Incoming ${req.method} request to ${req.path}`);
next();
});
router.post("/add_fav_product", addToFavorite);
router.get("/get_product", getAllProducts);
router.get("/:id", getProductById); // Simplified route
module.exports = router;

14
backend/routes/search.js Normal file
View File

@@ -0,0 +1,14 @@
// routes/product.js
const express = require("express");
const { searchProductsByName } = require("../controllers/search");
const router = express.Router();
// Add detailed logging middleware
router.use((req, res, next) => {
console.log(`Incoming ${req.method} request to ${req.path}`);
next();
});
router.get("/search", searchProductsByName);
module.exports = router;

View File

@@ -4,8 +4,7 @@ const mysql = require("mysql2");
const pool = mysql.createPool({
host: "localhost",
user: "root",
database: "marketplace",
password: "12345678",
database: "Marketplace",
});
//Export a promise for promise-based query

View File

@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@tailwindcss/vite": "^4.0.9",
"axios": "^1.8.4",
"lucide-react": "^0.477.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -1770,6 +1771,12 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -1824,6 +1831,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1898,7 +1916,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -1993,6 +2010,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2153,6 +2182,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -2182,7 +2220,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -2283,7 +2320,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -2293,7 +2329,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -2331,7 +2366,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -2344,7 +2378,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -2732,6 +2765,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -2748,6 +2801,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -2780,7 +2848,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -2831,7 +2898,6 @@
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -2856,7 +2922,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -2931,7 +2996,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3002,7 +3066,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3015,7 +3078,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -3031,7 +3093,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -3895,12 +3956,32 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -4251,6 +4332,12 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.0.9",
"axios": "^1.8.4",
"lucide-react": "^0.477.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",

View File

@@ -13,6 +13,8 @@ import Transactions from "./pages/Transactions";
import Favorites from "./pages/Favorites";
import ProductDetail from "./pages/ProductDetail";
import ItemForm from "./pages/MyListings";
import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage
import axios from "axios";
function App() {
// Authentication state - initialize from localStorage if available
@@ -30,6 +32,8 @@ function App() {
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [userId, setUserId] = useState(null);
// New verification states
const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying'
const [tempUserData, setTempUserData] = useState(null);
@@ -70,7 +74,7 @@ function App() {
email: userData.email,
// Add any other required fields
}),
}
},
);
if (!response.ok) {
@@ -119,7 +123,7 @@ function App() {
email: tempUserData.email,
code: code,
}),
}
},
);
if (!response.ok) {
@@ -163,7 +167,7 @@ function App() {
"Content-Type": "application/json",
},
body: JSON.stringify(userData),
}
},
);
if (!response.ok) {
@@ -189,6 +193,9 @@ function App() {
sessionStorage.setItem("isAuthenticated", "true");
sessionStorage.setItem("user", JSON.stringify(newUser));
// After successful signup, send session data to server
sendSessionDataToServer(); // Call it after signup
// Reset verification steps
setVerificationStep("initial");
setTempUserData(null);
@@ -222,11 +229,12 @@ function App() {
setError("Email and password are required");
setIsLoading(false);
return;
} else if (!formValues.email.endsWith("@ucalgary.ca")) {
setError("Please use your UCalgary email address (@ucalgary.ca)");
setIsLoading(false);
return;
}
// else if (!formValues.email.endsWith("@ucalgary.ca")) {
// setError("Please use your UCalgary email address (@ucalgary.ca)");
// setIsLoading(false);
// return;
// }
try {
if (isSignUp) {
// Handle Sign Up with verification
@@ -265,7 +273,7 @@ function App() {
email: formValues.email,
password: formValues.password,
}),
}
},
);
if (!response.ok) {
@@ -291,6 +299,9 @@ function App() {
sessionStorage.setItem("isAuthenticated", "true");
sessionStorage.setItem("user", JSON.stringify(userObj));
// After successful signup, send session data to server
sendSessionDataToServer(); // Call it after signup
sessionStorage.getItem("user");
console.log("Login successful for:", userData.email);
@@ -355,6 +366,48 @@ function App() {
setError("");
};
const sendSessionDataToServer = async () => {
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");
return;
}
// Prepare the data to send
const requestData = {
userId: user.ID, // or user.ID depending on your user structure
email: user.email,
isAuthenticated,
};
console.log("Sending user data to the server:", requestData);
// Send data to Python server (replace with your actual server URL)
const response = await fetch("http://localhost:5000/api/user/session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestData),
});
// Check the response
if (response.ok) {
const result = await response.json();
console.log("Server response:", result);
} else {
console.error("Failed to send session data to the server");
}
} catch (error) {
console.error("Error sending session data:", error);
}
};
// Login component
const LoginComponent = () => (
<div className="flex h-screen bg-white">
@@ -509,8 +562,8 @@ function App() {
{isLoading
? "Please wait..."
: isSignUp
? "Create Account"
: "Sign In"}
? "Create Account"
: "Sign In"}
</button>
</div>
</form>
@@ -633,6 +686,16 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/search"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<SearchPage />
</div>
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={

View File

@@ -1,10 +1,11 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import UserDropdown from "./UserDropdown";
import { Search, Heart } from "lucide-react";
const Navbar = ({ onLogout, userName }) => {
const [searchQuery, setSearchQuery] = useState("");
const navigate = useNavigate();
const handleSearchChange = (e) => {
setSearchQuery(e.target.value);
@@ -12,8 +13,14 @@ const Navbar = ({ onLogout, userName }) => {
const handleSearchSubmit = (e) => {
e.preventDefault();
console.log("Searching for:", searchQuery);
// TODO: Implement search functionality
// if (!searchQuery.trim()) return;
// Navigate to search page with query
navigate({
pathname: "/search",
search: `?name=${encodeURIComponent(searchQuery)}`,
});
};
return (
@@ -41,13 +48,19 @@ const Navbar = ({ onLogout, userName }) => {
<input
type="text"
placeholder="Search for books, electronics, furniture..."
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 rounded-md focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
value={searchQuery}
onChange={handleSearchChange}
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="h-5 w-5 text-gray-400" />
</div>
<button
type="submit"
className="absolute inset-y-0 right-0 flex items-center px-3 text-gray-500 hover:text-green-500"
>
Search
</button>
</div>
</form>
</div>
@@ -61,6 +74,7 @@ const Navbar = ({ onLogout, userName }) => {
>
<Heart className="h-6 w-6" />
</Link>
{/* User Profile */}
<UserDropdown onLogout={onLogout} userName={userName} />
</div>

View File

@@ -11,7 +11,7 @@ const Home = () => {
const fetchProducts = async () => {
try {
const response = await fetch(
"http://localhost:3030/api/product/get_product"
"http://localhost:3030/api/product/get_product",
);
if (!response.ok) throw new Error("Failed to fetch products");
@@ -21,15 +21,15 @@ const Home = () => {
setListings(
data.data.map((product) => ({
id: product.ProductID,
title: product.Name,
title: product.ProductName, // Use the alias from SQL
price: product.Price,
category: product.CategoryID,
image: product.ImageURL,
category: product.Category, // Ensure this gets the category name
image: product.ProductImage, // Use the alias for image URL
condition: "New", // Modify based on actual data
seller: "Unknown", // Modify if seller info is available
datePosted: "Just now",
isFavorite: false,
}))
seller: product.SellerName, // Fetch seller name properly
datePosted: product.DateUploaded, // Use the actual date
isFavorite: false, // Default state
})),
);
} else {
throw new Error(data.message || "Error fetching products");
@@ -39,7 +39,6 @@ const Home = () => {
setError(error.message);
}
};
fetchProducts();
}, []);
@@ -50,8 +49,8 @@ const Home = () => {
prevListings.map((listing) =>
listing.id === id
? { ...listing, isFavorite: !listing.isFavorite }
: listing
)
: listing,
),
);
};
@@ -112,65 +111,186 @@ const Home = () => {
</div> */}
{/* Recent Listings */}
<div>
<div className="relative py-4">
<h2 className="text-xl font-semibold text-gray-800 mb-4">
Recent Listings
Recommendation
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
{listings.map((listing) => (
<Link
key={listing.id}
to={`/product/${listing.id}`}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow"
>
<div className="relative">
<img
src={listing.image}
alt={listing.title}
className="w-full h-48 object-cover"
/>
<button
onClick={(e) => toggleFavorite(listing.id, e)}
className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm"
>
<Heart
className={`h-5 w-5 ${
listing.isFavorite
? "text-red-500 fill-red-500"
: "text-gray-400"
}`}
/>
</button>
</div>
<div className="p-4">
<div className="flex justify-between items-start mb-2">
<div className="relative">
{/* Left Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("RecomContainer")
.scrollBy({ left: -400, behavior: "smooth" })
}
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"
>
</button>
{/* Scrollable Listings Container */}
<div
id="RecomContainer"
className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0"
>
{listings.map((listing) => (
<Link
key={listing.id}
to={`/product/${listing.id}`}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative"
>
<div className="relative">
<img
src={listing.image}
alt={listing.title}
className="w-full h-48 object-cover"
/>
<button
onClick={(e) => toggleFavorite(listing.id, e)}
className="absolute top-2 right-2 p-2 bg-white rounded-full shadow-sm"
>
<Heart
className={`h-6 w-6 ${
listing.isFavorite
? "text-red-500 fill-red-500"
: "text-gray-400"
}`}
/>
</button>
</div>
<div className="p-4">
<h3 className="text-lg font-medium text-gray-800 leading-tight">
{listing.title}
</h3>
<span className="font-semibold text-green-600">
<span className="font-semibold text-green-600 block mt-1">
${listing.price}
</span>
<div className="flex items-center text-sm text-gray-500 mt-2">
<Tag className="h-4 w-4 mr-1" />
<span>{listing.category}</span>
<span className="mx-2"></span>
<span>{listing.condition}</span>
</div>
<div className="flex justify-between items-center pt-2 border-t border-gray-100 mt-3">
<span className="text-xs text-gray-500">
{listing.datePosted}
</span>
<span className="text-sm font-medium text-gray-700">
{listing.seller}
</span>
</div>
</div>
</Link>
))}
</div>
{/* Right Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("RecomContainer")
.scrollBy({ left: 400, behavior: "smooth" })
}
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"
>
</button>
</div>
</div>
{/* Recent Listings */}
<div className="relative py-4">
<h2 className="text-xl font-semibold text-gray-800 mb-4">
Recent Listings
</h2>
<div className="relative">
{/* Left Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("listingsContainer")
.scrollBy({ left: -400, behavior: "smooth" })
}
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"
>
</button>
{/* Scrollable Listings Container */}
<div
id="listingsContainer"
className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0"
>
{listings.map((listing) => (
<Link
key={listing.id}
to={`/product/${listing.id}`}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative"
>
<div className="relative">
<img
src={listing.image}
alt={listing.title}
className="w-full h-48 object-cover"
/>
<button
onClick={(e) => toggleFavorite(listing.id, e)}
className="absolute top-2 right-2 p-2 bg-white rounded-full shadow-sm"
>
<Heart
className={`h-6 w-6 ${
listing.isFavorite
? "text-red-500 fill-red-500"
: "text-gray-400"
}`}
/>
</button>
</div>
<div className="flex items-center text-sm text-gray-500 mb-3">
<Tag className="h-4 w-4 mr-1" />
<span>{listing.category}</span>
<span className="mx-2"></span>
<span>{listing.condition}</span>
</div>
<div className="p-4">
<h3 className="text-lg font-medium text-gray-800 leading-tight">
{listing.title}
</h3>
<span className="font-semibold text-green-600 block mt-1">
${listing.price}
</span>
<div className="flex justify-between items-center pt-2 border-t border-gray-100">
<span className="text-xs text-gray-500">
{listing.datePosted}
</span>
<span className="text-sm font-medium text-gray-700">
{listing.seller}
</span>
<div className="flex items-center text-sm text-gray-500 mt-2">
<Tag className="h-4 w-4 mr-1" />
<span>{listing.category}</span>
<span className="mx-2"></span>
<span>{listing.condition}</span>
</div>
<div className="flex justify-between items-center pt-2 border-t border-gray-100 mt-3">
<span className="text-xs text-gray-500">
{listing.datePosted}
</span>
<span className="text-sm font-medium text-gray-700">
{listing.seller}
</span>
</div>
</div>
</div>
</Link>
))}
</Link>
))}
</div>
{/* Right Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("listingsContainer")
.scrollBy({ left: 400, behavior: "smooth" })
}
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"
>
</button>
</div>
</div>
</div>

View File

@@ -1,129 +1,166 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useParams, Link } from "react-router-dom";
import {
Heart,
ArrowLeft,
Tag,
User,
Calendar,
Share,
Flag,
} from "lucide-react";
import { Heart, ArrowLeft, Tag, User, Calendar } from "lucide-react";
const ProductDetail = () => {
const { id } = useParams();
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [isFavorite, setIsFavorite] = useState(false);
const [showContactForm, setShowContactForm] = useState(false);
const [message, setMessage] = useState("");
const [currentImage, setCurrentImage] = useState(0);
// Sample data for demonstration
const product = [
{
id: 0,
title: "Dell XPS 13 Laptop - 2023 Model",
price: 850,
shortDescription:
"Dell XPS 13 laptop in excellent condition. Intel Core i7, 16GB RAM, 512GB SSD. Includes charger and original box.",
description:
"Selling my Dell XPS 13 laptop. Only 6 months old and in excellent condition. Intel Core i7 processor, 16GB RAM, 512GB SSD. Battery life is still excellent (around 10 hours of regular use). Comes with original charger and box. Selling because I'm upgrading to a MacBook for design work.\n\nSpecs:\n- Intel Core i7 11th Gen\n- 16GB RAM\n- 512GB NVMe SSD\n- 13.4\" FHD+ Display (1920x1200)\n- Windows 11 Pro\n- Backlit Keyboard\n- Thunderbolt 4 ports",
condition: "Like New",
category:
"Electronics, Electronics, Electronics, Electronics , Electronics , Electronics, Electronicss",
datePosted: "2023-03-02",
images: [
"/image1.avif",
"/image2.avif",
"/image3.avif",
"/image3.avif",
"/image3.avif",
],
seller: {
name: "Michael T.",
rating: 4.8,
memberSince: "January 2022",
avatar: "/Profile.jpg",
},
},
];
// Fetch product details
useEffect(() => {
const fetchProduct = async () => {
try {
setLoading(true);
const response = await fetch(`http://localhost:3030/api/product/${id}`);
console.log(product[id]);
if (!response.ok) {
throw new Error("Failed to fetch product");
}
const toggleFavorite = () => {
setIsFavorite(!isFavorite);
const result = await response.json();
console.log(result);
if (result.success) {
setProduct(result.data);
setError(null);
} else {
throw new Error(result.message || "Error fetching product");
}
} catch (error) {
console.error("Error fetching product:", error);
setError(error.message);
setProduct(null);
} finally {
setLoading(false);
}
};
fetchProduct();
}, [id]);
// Handle favorite toggle
const toggleFavorite = async () => {
try {
const response = await fetch(
"http://localhost:3030/api/product/add_to_favorite",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: 1, // Replace with actual user ID
productsID: id,
}),
},
);
const result = await response.json();
if (result.success) {
setIsFavorite(!isFavorite);
}
} catch (error) {
console.error("Error toggling favorite:", error);
}
};
// Handle message submission
const handleSendMessage = (e) => {
e.preventDefault();
// TODO: this would send the message to the seller
// TODO: Implement actual message sending logic
console.log("Message sent:", message);
setMessage("");
setShowContactForm(false);
// Show confirmation or success message
alert("Message sent to seller!");
};
// Function to split description into paragraphs
const formatDescription = (text) => {
return text.split("\n\n").map((paragraph, index) => (
<p key={index} className="mb-4">
{paragraph.split("\n").map((line, i) => (
<span key={i}>
{line}
{i < paragraph.split("\n").length - 1 && <br />}
</span>
))}
</p>
));
};
// image navigation
// Image navigation
const nextImage = () => {
setCurrentImage((prev) =>
prev === product.images.length - 1 ? 0 : prev + 1,
);
if (product && product.images) {
setCurrentImage((prev) =>
prev === product.images.length - 1 ? 0 : prev + 1,
);
}
};
const prevImage = () => {
setCurrentImage((prev) =>
prev === 0 ? product.images.length - 1 : prev - 1,
);
if (product && product.images) {
setCurrentImage((prev) =>
prev === 0 ? product.images.length - 1 : prev - 1,
);
}
};
const selectImage = (index) => {
setCurrentImage(index);
};
// Render loading state
if (loading) {
return (
<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>
);
}
// Render error state
if (error) {
return (
<div className="flex justify-center items-center h-screen">
<div className="text-center">
<h2 className="text-2xl text-red-500 mb-4">Error Loading Product</h2>
<p className="text-gray-600">{error}</p>
<Link
to="/"
className="mt-4 inline-block bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
>
Back to Listings
</Link>
</div>
</div>
);
}
// Render product details
return (
<div className="max-w-6xl mx-auto px-4 py-8">
{/* Breadcrumb & Back Link */}
<div className="mb-6">
<Link
to="/"
to="/search"
className="flex items-center text-green-600 hover:text-green-700"
>
<ArrowLeft className="h-4 w-4 mr-1" />
<span>Back to listings</span>
<span>Back</span>
</Link>
</div>
<div className="flex flex-col md:flex-row gap-8">
{/* Left Column - Images */}
<div className="md:w-3/5">
{/* Main Image */}
<div className="bg-white border border-gray-200 mb-4 relative">
<img
src={product[id].images[currentImage]}
alt={product[id].title}
className="w-full h-auto object-contain cursor-pointer"
onClick={nextImage}
/>
{product.images && product.images.length > 0 ? (
<img
src={product.images[currentImage]}
alt={product.Name}
className="w-full h-auto object-contain cursor-pointer"
onClick={nextImage}
/>
) : (
<div className="w-full h-96 flex items-center justify-center bg-gray-200 text-gray-500">
No Image Available
</div>
)}
</div>
{/* Thumbnail Images */}
{product[id].images.length > 1 && (
{product.images && product.images.length > 1 && (
<div className="flex gap-2 overflow-x-auto pb-2">
{product[id].images.map((image, index) => (
{product.images.map((image, index) => (
<div
key={index}
className={`bg-white border ${currentImage === index ? "border-green-500" : "border-gray-200"} min-w-[100px] cursor-pointer`}
@@ -131,7 +168,7 @@ const ProductDetail = () => {
>
<img
src={image}
alt={`${product[id].title} - view ${index + 1}`}
alt={`${product.Name} - view ${index + 1}`}
className="w-full h-auto object-cover"
/>
</div>
@@ -140,13 +177,11 @@ const ProductDetail = () => {
)}
</div>
{/* Right Column - Details */}
<div className="md:w-2/5">
{/* Product Info Card */}
<div className="bg-white border border-gray-200 p-6 mb-6">
<div className="flex justify-between items-start mb-4">
<h1 className="text-2xl font-bold text-gray-800">
{product[id].title}
{product.Name}
</h1>
<button
onClick={toggleFavorite}
@@ -159,30 +194,27 @@ const ProductDetail = () => {
</div>
<div className="text-2xl font-bold text-green-600 mb-4">
${product[id].price}
${product.Price}
</div>
<div className="flex flex-wrap gap-x-4 gap-y-2 mb-6 text-sm">
<div className="flex items-center text-gray-600">
<Tag className="h-4 w-4 mr-1" />
<span>{product[id].category}</span>
<span>{product.Category}</span>
</div>
<div className="flex items-center text-gray-600">
<span className="font-medium">Condition:</span>
<span className="ml-1">{product[id].condition}</span>
<span className="ml-1">{product.condition}</span>
</div>
<div className="flex items-center text-gray-600">
<Calendar className="h-4 w-4 mr-1" />
<span>Posted on {product[id].datePosted}</span>
<span>Posted on {product.Date}</span>
</div>
</div>
{/* Short Description */}
<div className="bg-gray-50 p-4 mb-6 border border-gray-200">
<p className="text-gray-700">{product[id].shortDescription}</p>
<p className="text-gray-700">{product.Description}</p>
</div>
{/* Contact Button */}
<button
onClick={() => setShowContactForm(!showContactForm)}
className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3"
@@ -190,7 +222,6 @@ const ProductDetail = () => {
Contact Seller
</button>
{/* TODO:Contact Form */}
{showContactForm && (
<div className="border border-gray-200 p-4 mb-4">
<h3 className="font-medium text-gray-800 mb-2">
@@ -204,8 +235,8 @@ const ProductDetail = () => {
<input
type="email"
id="email"
value={product[id].User.email}
onChange={(e) => setEmail(e.target.value)}
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
required
/>
@@ -217,8 +248,6 @@ const ProductDetail = () => {
<input
type="tel"
id="phone"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
required
/>
@@ -233,8 +262,8 @@ const ProductDetail = () => {
<input
type="text"
id="contactMessage"
value={contactMessage}
onChange={(e) => setContactMessage(e.target.value)}
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Hi, is this item still available?"
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
/>
@@ -249,35 +278,27 @@ const ProductDetail = () => {
</div>
)}
{/* Seller Info */}
<div className="pt-4 border-t border-gray-200">
<div className="flex items-center mb-3">
<div className="mr-3">
{product[id].seller.avatar ? (
<img
src={product[id].seller.avatar}
alt="Seller"
className="h-12 w-12 rounded-full"
/>
) : (
<div className="h-12 w-12 rounded-full bg-gray-200 flex items-center justify-center">
<User className="h-6 w-6 text-gray-600" />
</div>
)}
<div className="h-12 w-12 rounded-full bg-gray-200 flex items-center justify-center">
<User className="h-6 w-6 text-gray-600" />
</div>
</div>
<div>
<h3 className="font-medium text-gray-800">
{product[id].seller.name}
{product.UserID || "Unknown Seller"}
</h3>
<p className="text-sm text-gray-500">
Member since {product[id].seller.memberSince}
Member since{" "}
{product.seller ? product.seller.memberSince : "N/A"}
</p>
</div>
</div>
<div className="text-sm text-gray-600">
<div>
<span className="font-medium">Rating:</span>{" "}
{product[id].seller.rating}/5
{product.seller ? `${product.seller.rating}/5` : "N/A"}
</div>
</div>
</div>
@@ -285,15 +306,12 @@ const ProductDetail = () => {
</div>
</div>
{/* Description Section */}
<div className="mt-8">
{/* <div className="mt-8">
<h2 className="text-xl font-bold text-gray-800 mb-4">Description</h2>
<div className="bg-white border border-gray-200 p-6">
<div className="text-gray-700">
{formatDescription(product[id].description)}
</div>
<div className="text-gray-700">{product.Description}</div>
</div>
</div>
</div> */}
</div>
);
};

View File

@@ -0,0 +1,203 @@
import React, { useState, useEffect } from "react";
import { Filter, Grid, Heart, Tag, X } from "lucide-react";
import { useLocation, Link } from "react-router-dom";
import axios from "axios";
const SearchPage = () => {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const nameParam = queryParams.get("name") || "";
const initialSearchQuery = location.state?.query || nameParam || "";
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
const [isFilterOpen, setIsFilterOpen] = useState(false);
useEffect(() => {
fetchProducts(initialSearchQuery);
}, [initialSearchQuery]);
const fetchProducts = async (query) => {
setLoading(true);
setError(null);
try {
const response = await axios.get(
`http://localhost:3030/api/search_products/search`,
{
params: { name: query },
},
);
if (response.data.success) {
const transformedProducts = response.data.data.map((product) => ({
id: product.ProductID,
title: product.Name,
description: product.Description || "",
price: product.Price || 0,
category: product.Category || "Uncategorized",
condition: product.Condition || "Used",
image: product.images,
seller: product.SellerName || "Unknown Seller",
isFavorite: false,
}));
setProducts(transformedProducts);
setFilteredProducts(transformedProducts);
} else {
setError(response.data.message || "Failed to fetch products");
setProducts([]);
setFilteredProducts([]);
}
} catch (err) {
console.error("Error fetching products:", err);
setError(err.response?.data?.message || "Error connecting to the server");
setProducts([]);
setFilteredProducts([]);
} finally {
setLoading(false);
}
};
const toggleFavorite = (id, e) => {
e.preventDefault();
setProducts((prev) =>
prev.map((product) =>
product.id === id
? { ...product, isFavorite: !product.isFavorite }
: product,
),
);
};
const filterProducts = () => {
let result = products;
result = result.filter(
(product) =>
product.price >= priceRange.min && product.price <= priceRange.max,
);
setFilteredProducts(result);
};
const applyFilters = () => {
filterProducts();
setIsFilterOpen(false);
};
const resetFilters = () => {
setPriceRange({ min: 0, max: 1000 });
setFilteredProducts(products);
};
return (
<div className="container mx-auto px-4 py-8">
<div className="flex flex-col md:flex-row gap-6">
<div
className={`
fixed inset-0 z-50 bg-white transform transition-transform duration-300
${isFilterOpen ? "translate-x-0" : "translate-x-full"}
md:translate-x-0 md:relative md:block md:w-72
overflow-y-auto shadow-lg rounded-lg
`}
>
<div className="md:hidden flex justify-between items-center p-4 border-b">
<h3 className="text-lg font-semibold">Filters</h3>
<button onClick={() => setIsFilterOpen(false)}>
<X className="text-gray-600" />
</button>
</div>
<div className="p-4 space-y-4">
<div className="bg-gray-50 rounded-lg p-3">
<h3 className="font-semibold text-gray-700 mb-3">Price Range</h3>
<div className="space-y-2">
<div className="flex space-x-2">
<input
type="number"
placeholder="Min"
value={priceRange.min}
onChange={(e) =>
setPriceRange((prev) => ({
...prev,
min: Number(e.target.value),
}))
}
className="w-full p-2 border rounded text-gray-700"
/>
<input
type="number"
placeholder="Max"
value={priceRange.max}
onChange={(e) =>
setPriceRange((prev) => ({
...prev,
max: Number(e.target.value),
}))
}
className="w-full p-2 border rounded text-gray-700"
/>
</div>
</div>
</div>
<div className="flex space-x-2">
<button
onClick={applyFilters}
className="w-full bg-green-500 text-white p-3 rounded-lg hover:bg-green-600 transition-colors"
>
Apply Filters
</button>
<button
onClick={resetFilters}
className="w-full bg-gray-200 text-gray-700 p-3 rounded-lg hover:bg-gray-300 transition-colors"
>
Reset
</button>
</div>
</div>
</div>
<div className="flex-1 mt-4 md:mt-0">
<h2 className="text-2xl font-bold text-gray-800">
{filteredProducts.length} Results
{searchQuery && (
<span className="text-lg font-normal text-gray-600">
{" "}
for "{searchQuery}"
</span>
)}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
{filteredProducts.map((listing) => (
<Link
key={listing.id}
to={`/product/${listing.id}`}
className="bg-white border border-gray-200 rounded-lg hover:shadow-md transition-shadow block"
>
<img
src={listing.image}
alt={listing.title}
className="w-full h-48 object-cover rounded-t-lg"
/>
<div className="p-4">
<h3 className="text-lg font-medium text-gray-800">
{listing.title}
</h3>
<p className="text-green-600 font-semibold">
${Number(listing.price).toFixed(2)}
</p>
</div>
</Link>
))}
</div>
</div>
</div>
</div>
);
};
export default SearchPage;

761
mysql-code/example-data.sql Normal file
View File

@@ -0,0 +1,761 @@
-- Inserting sample data into the Marketplace database
-- Clear existing data (if needed)
SET
FOREIGN_KEY_CHECKS = 0;
TRUNCATE TABLE Product_Category;
TRUNCATE TABLE Favorites;
TRUNCATE TABLE History;
TRUNCATE TABLE Recommendation;
TRUNCATE TABLE Transaction;
TRUNCATE TABLE Review;
TRUNCATE TABLE Image_URL;
TRUNCATE TABLE Product;
TRUNCATE TABLE Category;
TRUNCATE TABLE UserRole;
TRUNCATE TABLE User;
TRUNCATE TABLE AuthVerification;
SET
FOREIGN_KEY_CHECKS = 1;
-- Insert Users
INSERT INTO
User (
UserID,
Name,
Email,
UCID,
Password,
Phone,
Address
)
VALUES
(
1,
'John Doe',
'john.doe@example.com',
'U123456',
'hashedpassword1',
'555-123-4567',
'123 Main St, Calgary, AB'
),
(
2,
'Jane Smith',
'jane.smith@example.com',
'U234567',
'hashedpassword2',
'555-234-5678',
'456 Oak Ave, Calgary, AB'
),
(
3,
'Michael Brown',
'michael.b@example.com',
'U345678',
'hashedpassword3',
'555-345-6789',
'789 Pine Rd, Calgary, AB'
),
(
4,
'Sarah Wilson',
'sarah.w@example.com',
'U456789',
'hashedpassword4',
'555-456-7890',
'101 Elm Blvd, Calgary, AB'
),
(
5,
'David Taylor',
'david.t@example.com',
'U567890',
'hashedpassword5',
'555-567-8901',
'202 Maple Dr, Calgary, AB'
);
-- Insert User Roles
INSERT INTO
UserRole (UserID, Client, Admin)
VALUES
(1, TRUE, TRUE),
(2, TRUE, FALSE),
(3, TRUE, FALSE),
(4, TRUE, FALSE),
(5, TRUE, FALSE);
-- Insert Categories
INSERT INTO
Category (CategoryID, Name)
VALUES
(1, 'Textbooks'),
(2, 'Electronics'),
(3, 'Furniture'),
(4, 'Clothing'),
(5, 'Sports Equipment'),
(6, 'Musical Instruments'),
(7, 'Art Supplies'),
(8, 'Kitchen Appliances'),
(9, 'Gaming'),
(10, 'Bicycles'),
(11, 'Computer Accessories'),
(12, 'Stationery'),
(13, 'Fitness Equipment'),
(14, 'Winter Sports'),
(15, 'Lab Equipment'),
(16, 'Camping Gear'),
(17, 'School Supplies'),
(18, 'Office Furniture'),
(19, 'Books (Non-textbook)'),
(20, 'Math & Science Resources'),
(21, 'Engineering Tools'),
(22, 'Backpacks & Bags'),
(23, 'Audio Equipment'),
(24, 'Dorm Essentials'),
(25, 'Smartphones & Tablets'),
(26, 'Winter Clothing'),
(27, 'Photography Equipment'),
(28, 'Event Tickets'),
(29, 'Software Licenses'),
(30, 'Transportation (Car Pool)');
-- Insert Products
INSERT INTO
Product (
ProductID,
Name,
Price,
StockQuantity,
UserID,
Description,
CategoryID,
Date
)
VALUES
(
1,
'Calculus Textbook 8th Edition',
79.99,
5,
1,
'Like new calculus textbook, minor highlighting',
1,
'2024-10-15 10:00:00'
),
(
2,
'HP Laptop',
699.99,
1,
2,
'2023 HP Pavilion, 16GB RAM, 512GB SSD',
2,
'2024-10-10 14:30:00'
),
(
3,
'Dorm Desk',
120.00,
1,
3,
'Sturdy desk perfect for studying, minor scratches',
3,
'2024-10-12 09:15:00'
),
(
4,
'University Hoodie',
35.00,
3,
1,
'Size L, university logo, worn twice',
4,
'2024-10-14 16:45:00'
),
(
5,
'Basketball',
25.50,
1,
4,
'Slightly used indoor basketball',
5,
'2024-10-11 11:20:00'
),
(
6,
'Acoustic Guitar',
175.00,
1,
2,
'Beginner acoustic guitar with case',
6,
'2024-10-09 13:10:00'
),
(
7,
'Physics Textbook',
65.00,
2,
5,
'University Physics 14th Edition, good condition',
1,
'2024-10-08 10:30:00'
),
(
8,
'Mini Fridge',
85.00,
1,
3,
'Small dorm fridge, works perfectly',
8,
'2024-10-13 15:00:00'
),
(
9,
'PlayStation 5 Controller',
55.00,
1,
4,
'Extra controller, barely used',
9,
'2024-10-07 17:20:00'
),
(
10,
'Mountain Bike',
350.00,
1,
5,
'Trek mountain bike, great condition, new tires',
10,
'2024-10-06 14:00:00'
),
(
11,
'Wireless Mouse',
22.99,
3,
1,
'Logitech wireless mouse with battery',
11,
'2024-10-05 09:30:00'
),
(
12,
'Chemistry Lab Coat',
30.00,
2,
2,
'Size M, required for chem labs',
15,
'2024-10-04 13:45:00'
),
(
13,
'Graphing Calculator',
75.00,
1,
3,
'TI-84 Plus, perfect working condition',
12,
'2024-10-03 11:15:00'
),
(
14,
'Yoga Mat',
20.00,
1,
4,
'Thick yoga mat, barely used',
13,
'2024-10-02 16:00:00'
),
(
15,
'Winter Jacket',
120.00,
1,
5,
'Columbia winter jacket, size XL, very warm',
26,
'2024-10-01 10:20:00'
),
(
16,
'Computer Science Textbook',
70.00,
1,
1,
'Introduction to Algorithms, like new',
1,
'2024-09-30 14:30:00'
),
(
17,
'Desk Lamp',
15.00,
2,
2,
'LED desk lamp with adjustable brightness',
24,
'2024-09-29 12:00:00'
),
(
18,
'Scientific Calculator',
25.00,
1,
3,
'Casio scientific calculator',
12,
'2024-09-28 11:30:00'
),
(
19,
'Bluetooth Speaker',
45.00,
1,
4,
'JBL Bluetooth speaker, great sound',
23,
'2024-09-27 15:45:00'
),
(
20,
'Backpack',
40.00,
1,
5,
'North Face backpack, lots of pockets',
22,
'2024-09-26 09:15:00'
);
-- Insert Image URLs
INSERT INTO
Image_URL (URL, ProductID)
VALUES
('/image1.avif', 1),
('/image1.avif', 2),
('/image1.avif', 3),
('/image1.avif', 4),
('/image1.avif', 5),
('/image1.avif', 6),
('/image1.avif', 7),
('/image1.avif', 8),
('/image1.avif', 9),
('/image1.avif', 10);
-- Insert Product-Category relationships (products with multiple categories)
INSERT INTO
Product_Category (ProductID, CategoryID)
VALUES
(1, 1),
(1, 17),
(1, 20), -- Calculus book: Textbooks, School Supplies, Math Resources
(2, 2),
(2, 11),
(2, 25), -- Laptop: Electronics, Computer Accessories, Smartphones & Tablets
(3, 3),
(3, 18),
(3, 24), -- Desk: Furniture, Office Furniture, Dorm Essentials
(4, 4),
(4, 26), -- Hoodie: Clothing, Winter Clothing
(5, 5),
(5, 13), -- Basketball: Sports Equipment, Fitness Equipment
(6, 6),
(6, 23), -- Guitar: Musical Instruments, Audio Equipment
(7, 1),
(7, 15),
(7, 20), -- Physics book: Textbooks, Lab Equipment, Math & Science Resources
(8, 8),
(8, 24), -- Mini Fridge: Kitchen Appliances, Dorm Essentials
(9, 9),
(9, 2), -- PS5 Controller: Gaming, Electronics
(10, 10),
(10, 5),
(10, 13), -- Mountain Bike: Bicycles, Sports Equipment, Fitness Equipment
(11, 11),
(11, 2), -- Mouse: Computer Accessories, Electronics
(12, 15),
(12, 17), -- Lab Coat: Lab Equipment, School Supplies
(13, 12),
(13, 17),
(13, 20), -- Calculator: Stationery, School Supplies, Math & Science Resources
(14, 13),
(14, 5), -- Yoga Mat: Fitness Equipment, Sports Equipment
(15, 26),
(15, 4),
(15, 14), -- Winter Jacket: Winter Clothing, Clothing, Winter Sports
(16, 1),
(16, 17),
(16, 19), -- CS Book: Textbooks, School Supplies, Books (Non-textbook)
(17, 24),
(17, 2), -- Desk Lamp: Dorm Essentials, Electronics
(18, 12),
(18, 17),
(18, 20), -- Scientific Calculator: Stationery, School Supplies, Math & Science
(19, 23),
(19, 2),
(19, 24), -- Bluetooth Speaker: Audio Equipment, Electronics, Dorm Essentials
(20, 22),
(20, 17),
(20, 24);
-- Backpack: Backpacks & Bags, School Supplies, Dorm Essentials
-- Insert History records
--
INSERT INTO
History (HistoryID, UserID, ProductID, Date)
VALUES
(1, 1, 1, '2024-10-15 11:30:00'),
(2, 1, 2, '2024-10-14 13:45:00'),
(3, 1, 5, '2024-10-13 09:20:00'),
(4, 1, 4, '2024-10-12 16:10:00');
--
INSERT INTO
History (HistoryID, UserID, ProductID, Date)
VALUES
(1, 2, 1, '2024-10-15 11:30:00'), -- User 2 viewed Calculus Textbook
(2, 3, 2, '2024-10-14 13:45:00'), -- User 3 viewed HP Laptop
(3, 4, 3, '2024-10-13 09:20:00'), -- User 4 viewed Dorm Desk
(4, 5, 4, '2024-10-12 16:10:00'), -- User 5 viewed University Hoodie
(5, 1, 5, '2024-10-11 14:30:00'), -- User 1 viewed Basketball
(6, 2, 6, '2024-10-10 10:15:00'), -- User 2 viewed Acoustic Guitar
(7, 3, 7, '2024-10-09 15:40:00'), -- User 3 viewed Physics Textbook
(8, 4, 8, '2024-10-08 11:25:00'), -- User 4 viewed Mini Fridge
(9, 5, 9, '2024-10-07 17:50:00'), -- User 5 viewed PS5 Controller
(10, 1, 10, '2024-10-06 14:15:00');
-- User 1 viewed Mountain Bike
-- Insert Reviews
INSERT INTO
Review (
ReviewID,
UserID,
ProductID,
Comment,
Rating,
Date
)
VALUES
(
1,
2,
1,
'Great condition, exactly as described!',
5,
'2024-10-16 09:30:00'
),
(
2,
3,
2,
'Works well, but had a small scratch not mentioned in the listing.',
4,
'2024-10-15 14:20:00'
),
(
3,
4,
6,
'Perfect for beginners, sounds great!',
5,
'2024-10-14 11:10:00'
),
(
4,
5,
8,
'Keeps my drinks cold, but a bit noisy at night.',
3,
'2024-10-13 16:45:00'
),
(
5,
1,
10,
'Excellent bike, well maintained!',
5,
'2024-10-12 13:25:00'
);
-- Insert Favorites
INSERT INTO
Favorites (UserID, ProductID)
VALUES
(1, 2), -- User 1 likes HP Laptop
(1, 7), -- User 1 likes Physics Textbook
(2, 3), -- User 2 likes Dorm Desk
(2, 10), -- User 2 likes Mountain Bike
(3, 6), -- User 3 likes Acoustic Guitar
(4, 5), -- User 4 likes Basketball
(5, 8);
-- User 5 likes Mini Fridge
-- Insert Transactions
INSERT INTO
Transaction (
TransactionID,
UserID,
ProductID,
Date,
PaymentStatus
)
VALUES
(1, 2, 1, '2024-10-16 10:30:00', 'Completed'),
(2, 3, 6, '2024-10-15 15:45:00', 'Completed'),
(3, 4, 8, '2024-10-14 12:20:00', 'Pending'),
(4, 5, 10, '2024-10-13 17:10:00', 'Completed'),
(5, 1, 4, '2024-10-12 14:30:00', 'Completed');
-- Insert Recommendations
INSERT INTO
Recommendation (RecommendationID_PK, UserID, RecommendedProductID)
VALUES
(1, 1, 7), -- Recommend Physics Textbook to User 1
(2, 1, 13), -- Recommend Graphing Calculator to User 1
(3, 2, 3), -- Recommend Dorm Desk to User 2
(4, 2, 17), -- Recommend Desk Lamp to User 2
(5, 3, 16), -- Recommend CS Textbook to User 3
(6, 4, 14), -- Recommend Yoga Mat to User 4
(7, 5, 15);
INSERT INTO
Recommendation (RecommendationID_PK, UserID, RecommendedProductID)
VALUES
(12, 1, 19),
(13, 1, 9),
(14, 1, 11),
(15, 1, 16),
-- Insert Authentication records
INSERT INTO
AuthVerification (Email, VerificationCode, Authenticated, Date)
VALUES
(
'john.doe@example.com',
'123456',
TRUE,
'2024-10-01 09:00:00'
),
(
'jane.smith@example.com',
'234567',
TRUE,
'2024-10-02 10:15:00'
),
(
'michael.b@example.com',
'345678',
TRUE,
'2024-10-03 11:30:00'
),
(
'sarah.w@example.com',
'456789',
TRUE,
'2024-10-04 12:45:00'
),
(
'david.t@example.com',
'567890',
TRUE,
'2024-10-05 14:00:00'
);
INSERT INTO
Product (
ProductID,
Name,
Description,
Price,
StockQuantity,
CategoryID
)
VALUES
(
101,
'Smart Coffee Maker',
'Wi-Fi enabled coffee machine with scheduling feature',
129.99,
50,
11
),
(
102,
'Ergonomic Office Chair',
'Adjustable mesh chair with lumbar support',
199.99,
35,
12
),
(
103,
'Wireless Mechanical Keyboard',
'RGB-backlit wireless keyboard with mechanical switches',
89.99,
60,
13
),
(
104,
'Portable Solar Charger',
'Foldable solar power bank with USB-C support',
59.99,
40,
14
),
(
105,
'Noise-Canceling Headphones',
'Over-ear Bluetooth headphones with ANC',
179.99,
25,
15
),
(
106,
'Smart Water Bottle',
'Tracks water intake and glows as a hydration reminder',
39.99,
75,
11
),
(
107,
'Compact Air Purifier',
'HEPA filter air purifier for small rooms',
149.99,
30,
16
),
(
108,
'Smart LED Desk Lamp',
'Adjustable LED lamp with voice control',
69.99,
45,
12
),
(
109,
'4K Streaming Device',
'HDMI streaming stick with voice remote',
49.99,
80,
17
),
(
110,
'Smart Plant Monitor',
'Bluetooth-enabled sensor for plant health tracking',
34.99,
55,
18
),
(
111,
'Wireless Charging Pad',
'Fast-charging pad for Qi-compatible devices',
29.99,
90,
13
),
(
112,
'Mini Projector',
'Portable projector with built-in speakers',
129.99,
20,
14
),
(
113,
'Foldable Bluetooth Keyboard',
'Ultra-thin keyboard for travel use',
39.99,
70,
19
),
(
114,
'Smart Alarm Clock',
'AI-powered alarm clock with sunrise simulation',
79.99,
40,
15
),
(
115,
'Touchscreen Toaster',
'Customizable toaster with a digital display',
99.99,
30,
11
),
(
116,
'Cordless Vacuum Cleaner',
'Lightweight handheld vacuum with strong suction',
159.99,
25,
16
),
(
117,
'Smart Bike Lock',
'Fingerprint and app-controlled bike security lock',
89.99,
35,
20
),
(
118,
'Bluetooth Sleep Headband',
'Comfortable sleep headband with built-in speakers',
49.99,
60,
18
),
(
119,
'Retro Game Console',
'Plug-and-play console with 500+ classic games',
79.99,
50,
17
),
(
120,
'Automatic Pet Feeder',
'App-controlled food dispenser for pets',
99.99,
40,
20
);
SELECT
p.*,
i.URL AS image_url
FROM
Product p
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
WHERE
p.ProductID = 1

4
mysql-code/init-db.py Normal file
View File

@@ -0,0 +1,4 @@
import subprocess
if (subprocess.run("mysql -u root mysql < mysql-code/Schema.sql", shell=True, check=True)):
print("successfully created the Marketplace databse")

View File

@@ -0,0 +1,48 @@
const net = require("net");
// Function to get recommendations from the Python server
function getRecommendations(userId) {
const client = new net.Socket();
// Connect to the server on localhost at port 9999
client.connect(9999, "localhost", function () {
console.log(`Connected to server, sending user_id: ${userId}`);
// Send the user_id in JSON format
const message = JSON.stringify({ user_id: userId });
client.write(message);
});
// Listen for data from the server
client.on("data", function (data) {
const recommendations = JSON.parse(data.toString());
console.log(
`Recommendations for User ${userId}:`,
recommendations.recommendations,
);
// Close the connection after receiving the response
client.destroy();
});
// Handle connection errors
client.on("error", function (error) {
console.error("Connection error:", error.message);
});
// Handle connection close
client.on("close", function () {
console.log(`Connection to server closed for User ${userId}`);
});
}
// Function to simulate multiple users requesting recommendations
function simulateClients() {
for (let i = 1; i <= 5; i++) {
setTimeout(() => {
getRecommendations(i); // Simulate clients with IDs 1 to 5
}, i * 1000); // Stagger requests every second
}
}
simulateClients();

View File

@@ -0,0 +1,26 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
app = Flask(__name__)
CORS(app) # Enable CORS for all routes
@app.route('/api/user/session', methods=['POST'])
def handle_session_data():
try:
data = request.get_json()
user_id = data.get('userId')
email = data.get('email')
is_authenticated = data.get('isAuthenticated')
if not user_id or not email or is_authenticated is None:
return jsonify({'error': 'Invalid data'}), 400
print(f"Received session data: User ID: {user_id}, Email: {email}, Authenticated: {is_authenticated}")
return jsonify({'message': 'Session data received successfully'})
except Exception as e:
print(f"Error: {e}")
return jsonify({'error': 'Server error'}), 500
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)