Merge branch 'mannBranch'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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],
|
||||
|
||||
164
backend/controllers/search.js
Normal file
164
backend/controllers/search.js
Normal 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",
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
14
backend/routes/search.js
Normal 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;
|
||||
@@ -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
|
||||
|
||||
115
frontend/package-lock.json
generated
115
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
203
frontend/src/pages/SearchPage.jsx
Normal file
203
frontend/src/pages/SearchPage.jsx
Normal 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
761
mysql-code/example-data.sql
Normal 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
4
mysql-code/init-db.py
Normal 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")
|
||||
48
recommondation-engine/client.js
Normal file
48
recommondation-engine/client.js
Normal 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();
|
||||
26
recommondation-engine/server.py
Normal file
26
recommondation-engine/server.py
Normal 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)
|
||||
Reference in New Issue
Block a user