Merge branch 'mannBranch'

This commit is contained in:
Mann Patel
2025-04-14 12:09:19 -06:00
65 changed files with 2272 additions and 1668 deletions

View File

@@ -1,29 +1,34 @@
### Some ground rules ### Some ground rules
1. Add both node_modules from Slient and Server to your `gitignore` file 1. Add both node_modules from Slient and Server to your ```gitignore``` file
2. Make a brach with the following naming conventionp, refix it with your name `name-some branch name`. 2. Make a brach with the following naming conventionp, refix it with your name ```Your-Name Branch-Name```.
---
### `frontend` ### Frontend
- Use React Js and vite as the node manger 1. `cd frontend` into the dir and then type command
```Bash ```Bash
#Install the needed lib with the command bellow
npm install npm install
``` #Start The Server
2. **Start The Server**, `cd frontend` into the dir and then type command
```Bash
npm run dev npm run dev
``` ```
### `backend` ---
1. Install the needed lib with the command bellow
### Backend
1. `cd backend` into the dir and then type command
```Bash ```Bash
#Install the needed lib with the command bellow
npm install npm install
``` #Start The Server
2. **Start The Server**, `cd backend` into the dir and then type command
```Bash
npm run dev npm run dev
``` ```
---
### Database ### Database
1. To Create the DB use the command bellow 1. MySql Version 9.2.0
2. To Create the DataBase use the command bellow:
```Bash ```Bash
python3 ./mysql-code/init-db.py 1. mysql -u root
2. use Marketplace;
3. \. PathToYour/Schema.sql
3. \. PathToYour/Init-Data.sql
``` ```
- MySql Version 9.2.0

View File

@@ -0,0 +1,90 @@
const db = require("../utils/database");
exports.HistoryByUserId = async (req, res) => {
const { id } = req.body;
try {
const [data] = await db.execute(
`
WITH RankedImages AS (
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,
ROW_NUMBER() OVER (PARTITION BY P.ProductID ORDER BY I.URL) AS RowNum
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
JOIN History H ON H.ProductID = P.ProductID
WHERE H.UserID = ?
)
SELECT
ProductID,
ProductName,
Price,
DateUploaded,
SellerName,
ProductImage,
Category
FROM RankedImages
WHERE RowNum = 1;
`,
[id],
);
res.json({
success: true,
message: "Products fetched successfully",
data,
});
} catch (error) {
console.error("Error finding products:", error);
return res.status(500).json({
found: false,
error: "Database error occurred",
});
}
};
exports.AddHistory = async (req, res) => {
const { userID, productID } = req.body;
console.log(userID);
try {
// Use parameterized query to prevent SQL injection
const [result] = await db.execute(
`INSERT INTO History (UserID, ProductID) VALUES (?, ?)`,
[userID, productID],
);
res.json({
success: true,
message: "Product added to history successfully",
});
} catch (error) {
console.error("Error adding favorite product:", error);
return res.json({ error: "Could not add favorite product" });
}
};
exports.DelHistory = async (req, res) => {
const { userID, productID } = req.body;
console.log(userID);
try {
// Use parameterized query to prevent SQL injection
const [result] = await db.execute(`DELETE FROM History WHERE UserID=?`, [
userID,
]);
res.json({
success: true,
message: "Product deleted from History successfully",
});
} catch (error) {
console.error("Error adding favorite product:", error);
return res.json({ error: "Could not add favorite product" });
}
};

View File

@@ -1,13 +1,13 @@
const db = require("../utils/database"); const db = require("../utils/database");
exports.addToFavorite = async (req, res) => { exports.addFavorite = async (req, res) => {
const { userID, productsID } = req.body; const { userID, productID } = req.body;
console.log(userID);
try { try {
// Use parameterized query to prevent SQL injection // Use parameterized query to prevent SQL injection
const [result] = await db.execute( const [result] = await db.execute(
"INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)", `INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`,
[userID, productsID], [userID, productID],
); );
res.json({ res.json({
@@ -20,25 +20,86 @@ exports.addToFavorite = async (req, res) => {
} }
}; };
exports.removeFavorite = async (req, res) => {
const { userID, productID } = req.body;
console.log(userID);
try {
// Use parameterized query to prevent SQL injection
const [result] = await db.execute(
`DELETE FROM Favorites WHERE UserID = ? AND ProductID = ?`,
[userID, productID],
);
res.json({
success: true,
message: "Product removed from favorites successfully",
});
} catch (error) {
console.error("Error removing favorite product:", error);
return res.json({ error: "Could not remove favorite product" });
}
};
exports.getFavorites = async (req, res) => {
const { userID } = req.body;
try {
const [favorites] = await db.execute(
`
SELECT
p.*,
u.Name AS SellerName,
i.URL AS image_url
FROM Favorites f
JOIN Product p ON f.ProductID = p.ProductID
JOIN User u ON p.UserID = u.UserID
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
WHERE f.UserID = ?
`,
[userID],
);
res.json({
success: true,
favorites: favorites,
});
} catch (error) {
console.error("Error retrieving favorites:", error);
res.status(500).json({ error: "Could not retrieve favorite products" });
}
};
// Get all products along with their image URLs // Get all products along with their image URLs
exports.getAllProducts = async (req, res) => { exports.getAllProducts = async (req, res) => {
try { try {
const [data, fields] = await db.execute(` const [data, fields] = await db.execute(`
WITH RankedImages AS (
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,
ROW_NUMBER() OVER (PARTITION BY P.ProductID ORDER BY I.URL) AS RowNum
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
)
SELECT SELECT
P.ProductID, ProductID,
P.Name AS ProductName, ProductName,
P.Price, Price,
P.Date AS DateUploaded, DateUploaded,
U.Name AS SellerName, SellerName,
I.URL AS ProductImage, ProductImage,
C.Name AS Category Category
FROM Product P FROM RankedImages
JOIN Image_URL I ON p.ProductID = i.ProductID WHERE RowNum = 1;
JOIN User U ON P.UserID = U.UserID
JOIN Category C ON P.CategoryID = C.CategoryID;
`); `);
console.log(data);
res.json({ res.json({
success: true, success: true,
message: "Products fetched successfully", message: "Products fetched successfully",
@@ -60,9 +121,10 @@ exports.getProductById = async (req, res) => {
try { try {
const [data] = await db.execute( const [data] = await db.execute(
` `
SELECT p.*, i.URL AS image_url SELECT p.*,U.Name AS SellerName,U.Email as SellerEmail,U.Phone as SellerPhone, i.URL AS image_url
FROM Product p FROM Product p
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
JOIN User U ON p.UserID = U.UserID
WHERE p.ProductID = ? WHERE p.ProductID = ?
`, `,
[id], [id],

View File

@@ -0,0 +1,53 @@
const db = require("../utils/database");
// TODO: Get the recommondaed product given the userID
exports.RecommondationByUserId = async (req, res) => {
const { id } = req.body;
try {
const [data, fields] = await db.execute(
`
WITH RankedImages AS (
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,
ROW_NUMBER() OVER (PARTITION BY P.ProductID ORDER BY I.URL) AS RowNum
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
JOIN Recommendation R ON P.ProductID = R.RecommendedProductID
WHERE R.UserID = ?
)
SELECT
ProductID,
ProductName,
Price,
DateUploaded,
SellerName,
ProductImage,
Category
FROM RankedImages
WHERE RowNum = 1;
`,
[id],
);
console.log(data);
res.json({
success: true,
message: "Products fetched successfully",
data,
});
} catch (error) {
console.error("Error finding products:", error);
return res.status(500).json({
found: false,
error: "Database error occurred",
});
}
};

View File

@@ -0,0 +1,302 @@
const db = require("../utils/database");
/**
* Get reviews for a specific product
* Returns both reviews for the product and reviews by the product owner for other products
*/
exports.getReviews = async (req, res) => {
const { id } = req.params;
console.log("Received Product ID:", id);
try {
// First query: Get reviews for this specific product
const [productReviews] = await db.execute(
`SELECT
R.ReviewID,
R.UserID,
R.ProductID,
R.Comment,
R.Rating,
R.Date AS ReviewDate,
U.Name AS ReviewerName,
P.Name AS ProductName,
'product' AS ReviewType
FROM Review R
JOIN User U ON R.UserID = U.UserID
JOIN Product P ON R.ProductID = P.ProductID
WHERE R.ProductID = ?`,
[id],
);
// // Second query: Get reviews written by the product owner for other products
// const [sellerReviews] = await db.execute(
// `SELECT
// R.ReviewID,
// R.UserID,
// R.ProductID,
// R.Comment,
// R.Rating,
// R.Date AS ReviewDate,
// U.Name AS ReviewerName,
// P.Name AS ProductName,
// 'seller' AS ReviewType
// FROM Review R
// JOIN User U ON R.UserID = U.UserID
// JOIN Product P ON R.ProductID = P.ProductID
// WHERE R.UserID = (
// SELECT UserID
// FROM Product
// WHERE ProductID = ?
// )
// AND R.ProductID != ?`,
// [id, id],
// );
// Combine the results
const combinedReviews = [...productReviews];
// Log data for debugging
console.log("Combined Reviews:", combinedReviews);
res.json({
success: true,
message: "Reviews fetched successfully",
data: combinedReviews,
});
} catch (error) {
console.error("Full Error Details:", error);
return res.status(500).json({
success: false,
message: "Database error occurred",
error: error.message,
});
}
};
/**
* Submit a new review for a product
*/
exports.submitReview = async (req, res) => {
const { productId, userId, rating, comment } = req.body;
// Validate required fields
if (!productId || !userId || !rating || !comment) {
return res.status(400).json({
success: false,
message: "Missing required fields",
});
}
// Validate rating is between 1 and 5
if (rating < 1 || rating > 5) {
return res.status(400).json({
success: false,
message: "Rating must be between 1 and 5",
});
}
try {
// Check if user has already reviewed this product
const [existingReview] = await db.execute(
`SELECT ReviewID FROM Review WHERE ProductID = ? AND UserID = ?`,
[productId, userId],
);
if (existingReview.length > 0) {
return res.status(400).json({
success: false,
message: "You have already reviewed this product",
});
}
// Check if user is trying to review their own product
const [productOwner] = await db.execute(
`SELECT UserID FROM Product WHERE ProductID = ?`,
[productId],
);
if (productOwner.length > 0 && productOwner[0].UserID === userId) {
return res.status(400).json({
success: false,
message: "You cannot review your own product",
});
}
// Insert the review into the database
const [result] = await db.execute(
`INSERT INTO Review (
ProductID,
UserID,
Rating,
Comment,
Date
) VALUES (?, ?, ?, ?, NOW())`,
[productId, userId, rating, comment],
);
// Get the inserted review id
const reviewId = result.insertId;
// Fetch the newly created review to return to client
const [newReview] = await db.execute(
`SELECT
R.ReviewID,
R.ProductID,
R.UserID,
R.Rating,
R.Comment,
R.Date AS ReviewDate,
U.Name AS ReviewerName,
P.Name AS ProductName
FROM Review R
JOIN User U ON R.UserID = U.UserID
JOIN Product P ON R.ProductID = P.ProductID
WHERE R.ReviewID = ?`,
[reviewId],
);
res.status(201).json({
success: true, // Fixed from false to true
message: "Review submitted successfully",
data: newReview[0],
});
} catch (error) {
console.error("Error submitting review:", error);
return res.status(500).json({
success: false,
message: "Database error occurred",
error: error.message,
});
}
};
// /**
// * Update an existing review
// */
// exports.updateReview = async (req, res) => {
// const { reviewId } = req.params;
// const { rating, comment } = req.body;
// const userId = req.body.userId; // Assuming you have middleware that validates the user
// // Validate required fields
// if (!reviewId || !rating || !comment) {
// return res.status(400).json({
// success: false,
// message: "Missing required fields",
// });
// }
// // Validate rating is between 1 and 5
// if (rating < 1 || rating > 5) {
// return res.status(400).json({
// success: false,
// message: "Rating must be between 1 and 5",
// });
// }
// try {
// // Check if review exists and belongs to the user
// const [existingReview] = await db.execute(
// `SELECT ReviewID, UserID FROM Review WHERE ReviewID = ?`,
// [reviewId],
// );
// if (existingReview.length === 0) {
// return res.status(404).json({
// success: false,
// message: "Review not found",
// });
// }
// if (existingReview[0].UserID !== userId) {
// return res.status(403).json({
// success: false,
// message: "You can only update your own reviews",
// });
// }
// // Update the review
// await db.execute(
// `UPDATE Review
// SET Rating = ?, Comment = ?, Date = NOW()
// WHERE ReviewID = ?`,
// [rating, comment, reviewId],
// );
// // Fetch the updated review
// const [updatedReview] = await db.execute(
// `SELECT
// R.ReviewID,
// R.ProductID,
// R.UserID,
// R.Rating,
// R.Comment,
// R.Date AS ReviewDate,
// U.Name AS ReviewerName,
// P.Name AS ProductName
// FROM Review R
// JOIN User U ON R.UserID = U.UserID
// JOIN Product P ON R.ProductID = P.ProductID
// WHERE R.ReviewID = ?`,
// [reviewId],
// );
// res.json({
// success: true,
// message: "Review updated successfully",
// data: updatedReview[0],
// });
// } catch (error) {
// console.error("Error updating review:", error);
// return res.status(500).json({
// success: false,
// message: "Database error occurred",
// error: error.message,
// });
// }
// };
// /**
// * Delete a review
// */
// exports.deleteReview = async (req, res) => {
// const { reviewId } = req.params;
// const userId = req.body.userId; // Assuming you have middleware that validates the user
// try {
// // Check if review exists and belongs to the user
// const [existingReview] = await db.execute(
// `SELECT ReviewID, UserID FROM Review WHERE ReviewID = ?`,
// [reviewId],
// );
// if (existingReview.length === 0) {
// return res.status(404).json({
// success: false,
// message: "Review not found",
// });
// }
// if (existingReview[0].UserID !== userId) {
// return res.status(403).json({
// success: false,
// message: "You can only delete your own reviews",
// });
// }
// // Delete the review
// await db.execute(`DELETE FROM Review WHERE ReviewID = ?`, [reviewId]);
// res.json({
// success: true,
// message: "Review deleted successfully",
// });
// } catch (error) {
// console.error("Error deleting review:", error);
// return res.status(500).json({
// success: false,
// message: "Database error occurred",
// error: error.message,
// });
// }
// };

View File

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

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

@@ -0,0 +1,14 @@
// routes/product.js
const express = require("express");
const {
HistoryByUserId,
DelHistory,
AddHistory,
} = require("../controllers/history");
const router = express.Router();
router.post("/getHistory", HistoryByUserId);
router.post("/delHistory", DelHistory);
router.post("/addHistory", AddHistory);
module.exports = router;

View File

@@ -1,7 +1,9 @@
// routes/product.js // routes/product.js
const express = require("express"); const express = require("express");
const { const {
addToFavorite, addFavorite,
getFavorites,
removeFavorite,
getAllProducts, getAllProducts,
getProductById, getProductById,
} = require("../controllers/product"); } = require("../controllers/product");
@@ -13,8 +15,11 @@ router.use((req, res, next) => {
next(); next();
}); });
router.post("/add_fav_product", addToFavorite); router.post("/addFavorite", addFavorite);
router.get("/get_product", getAllProducts); router.post("/getFavorites", getFavorites);
router.post("/delFavorite", removeFavorite);
router.get("/getProduct", getAllProducts);
router.get("/:id", getProductById); // Simplified route router.get("/:id", getProductById); // Simplified route
module.exports = router; module.exports = router;

View File

@@ -0,0 +1,8 @@
// routes/product.js
const express = require("express");
const { RecommondationByUserId } = require("../controllers/recommendation");
const router = express.Router();
router.post("/recommended", RecommondationByUserId);
module.exports = router;

9
backend/routes/review.js Normal file
View File

@@ -0,0 +1,9 @@
// routes/product.js
const express = require("express");
const { getReviews, submitReview } = require("../controllers/review");
const router = express.Router();
router.get("/:id", getReviews);
router.post("/add", submitReview);
module.exports = router;

View File

@@ -9,6 +9,6 @@ router.use((req, res, next) => {
next(); next();
}); });
router.get("/search", searchProductsByName); router.get("/getProduct", searchProductsByName);
module.exports = router; module.exports = router;

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -12,9 +12,7 @@ import Selling from "./pages/Selling";
import Transactions from "./pages/Transactions"; import Transactions from "./pages/Transactions";
import Favorites from "./pages/Favorites"; import Favorites from "./pages/Favorites";
import ProductDetail from "./pages/ProductDetail"; import ProductDetail from "./pages/ProductDetail";
import ItemForm from "./pages/MyListings";
import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage
import axios from "axios";
function App() { function App() {
// Authentication state - initialize from localStorage if available // Authentication state - initialize from localStorage if available
@@ -298,10 +296,7 @@ function App() {
// Save to localStorage to persist across refreshes // Save to localStorage to persist across refreshes
sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("isAuthenticated", "true");
sessionStorage.setItem("user", JSON.stringify(userObj)); sessionStorage.setItem("user", JSON.stringify(userObj));
// After successful signup, send session data to server
sendSessionDataToServer(); // Call it after signup sendSessionDataToServer(); // Call it after signup
sessionStorage.getItem("user"); sessionStorage.getItem("user");
console.log("Login successful for:", userData.email); console.log("Login successful for:", userData.email);
@@ -388,7 +383,7 @@ function App() {
console.log("Sending user data to the server:", requestData); console.log("Sending user data to the server:", requestData);
// Send data to Python server (replace with your actual server URL) // Send data to Python server (replace with your actual server URL)
const response = await fetch("http://localhost:5000/api/user/session", { const response = await fetch("http://0.0.0.0:5000/api/user/session", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -716,27 +711,6 @@ function App() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
{/* Add new selling routes */}
<Route
path="/selling/create"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<ItemForm />
</div>
</ProtectedRoute>
}
/>
<Route
path="/selling/edit/:id"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<ItemForm />
</div>
</ProtectedRoute>
}
/>
<Route <Route
path="/transactions" path="/transactions"
element={ element={

View File

@@ -0,0 +1,19 @@
// components/FloatingAlert.jsx
import { useEffect } from "react";
const FloatingAlert = ({ message, onClose, duration = 3000 }) => {
useEffect(() => {
const timer = setTimeout(() => {
onClose();
}, duration);
return () => clearTimeout(timer);
}, [onClose, duration]);
return (
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-green-500 text-white px-4 py-2 rounded-xl shadow-lg z-50 text-center">
{message}
</div>
);
};
export default FloatingAlert;

View File

@@ -35,7 +35,7 @@ const Navbar = ({ onLogout, userName }) => {
alt="Campus Plug" alt="Campus Plug"
className="h-8 px-2" className="h-8 px-2"
/> />
<span className="hidden md:block text-green-600 font-bold text-xl"> <span className="hidden md:block [color:#009966] font-bold text-xl">
Campus Plug Campus Plug
</span> </span>
</Link> </Link>

View File

@@ -0,0 +1,300 @@
import React, { useState } from "react";
const ProductForm = ({
editingProduct,
setEditingProduct,
onSave,
onCancel,
}) => {
const [selectedCategory, setSelectedCategory] = useState("");
const categories = [
"Electronics",
"Clothing",
"Home & Garden",
"Toys & Games",
"Books",
"Sports & Outdoors",
"Automotive",
"Beauty & Personal Care",
"Health & Wellness",
"Jewelry",
"Art & Collectibles",
"Food & Beverages",
"Office Supplies",
"Pet Supplies",
"Music & Instruments",
"Other",
];
const addCategory = () => {
if (
selectedCategory &&
!(editingProduct.categories || []).includes(selectedCategory)
) {
setEditingProduct((prev) => ({
...prev,
categories: [...(prev.categories || []), selectedCategory],
}));
setSelectedCategory("");
}
};
const removeCategory = (categoryToRemove) => {
setEditingProduct((prev) => ({
...prev,
categories: (prev.categories || []).filter(
(cat) => cat !== categoryToRemove,
),
}));
};
return (
<div className="bg-white border-2 border-gray-200 rounded-md p-6 shadow-lg">
{/* Back Button */}
<button
onClick={onCancel}
className="mb-4 text-sm text-emerald-600 hover:text-emerald-800 flex items-center font-medium"
>
Back to Listings
</button>
<h3 className="text-xl font-bold text-gray-800 mb-6 border-b-2 border-gray-100 pb-3">
{editingProduct?.id ? "Edit Your Product" : "List a New Product"}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Product Name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Product Name
</label>
<input
type="text"
value={editingProduct.name}
onChange={(e) =>
setEditingProduct({ ...editingProduct, name: e.target.value })
}
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
/>
</div>
{/* Price */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Price ($)
</label>
<input
type="number"
value={editingProduct.price}
onChange={(e) =>
setEditingProduct({
...editingProduct,
price: e.target.value,
})
}
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
/>
</div>
{/* Categories - Dropdown with Add button */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Categories
</label>
<div className="flex gap-2">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
>
<option value="" disabled>
Select a category
</option>
{categories
.filter(
(cat) => !(editingProduct.categories || []).includes(cat),
)
.map((category) => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
<button
type="button"
onClick={addCategory}
disabled={!selectedCategory}
className="px-4 py-2 bg-emerald-600 text-white rounded-md hover:bg-emerald-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Add
</button>
</div>
{/* Selected Categories */}
{(editingProduct.categories || []).length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{(editingProduct.categories || []).map((category) => (
<span
key={category}
className="inline-flex items-center px-3 py-1 rounded-md text-sm font-medium bg-emerald-100 text-emerald-800"
>
{category}
<button
type="button"
onClick={() => removeCategory(category)}
className="ml-2 text-emerald-600 hover:text-emerald-800"
>
×
</button>
</span>
))}
</div>
) : (
<p className="text-xs text-gray-500 mt-2">
Please select at least one category
</p>
)}
</div>
{/* Status - Updated to Unsold/Sold */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Status
</label>
<select
value={editingProduct.status}
onChange={(e) =>
setEditingProduct({
...editingProduct,
status: e.target.value,
})
}
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
>
<option value="Unsold">Unsold</option>
<option value="Sold">Sold</option>
</select>
</div>
{/* Description - New Field */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Description
</label>
<textarea
value={editingProduct.description || ""}
onChange={(e) =>
setEditingProduct({
...editingProduct,
description: e.target.value,
})
}
rows="4"
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
placeholder="Describe your product in detail..."
></textarea>
</div>
{/* Simplified Image Upload */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Product Images{" "}
<span className="text-gray-500 text-sm">(Max 5)</span>
</label>
{/* Simple file input */}
<input
type="file"
accept="image/*"
multiple
onChange={(e) => {
const files = Array.from(e.target.files).slice(0, 5);
setEditingProduct((prev) => ({
...prev,
images: [...prev.images, ...files].slice(0, 5),
}));
}}
className="hidden"
id="image-upload"
/>
<label
htmlFor="image-upload"
className="block w-full p-3 border-2 border-dashed border-emerald-200 bg-emerald-50 rounded-md text-center cursor-pointer hover:bg-emerald-100 transition-colors"
>
<span className="text-emerald-700 font-medium">
Click to upload images
</span>
</label>
{/* Image previews */}
{editingProduct.images.length > 0 && (
<div className="mt-4">
<p className="text-sm text-gray-600 mb-2">
{editingProduct.images.length}{" "}
{editingProduct.images.length === 1 ? "image" : "images"}{" "}
selected
</p>
<div className="flex flex-wrap gap-3">
{editingProduct.images.map((img, idx) => (
<div
key={idx}
className="relative w-20 h-20 border-2 border-gray-200 rounded-md overflow-hidden"
>
<img
src={URL.createObjectURL(img)}
alt={`Product image ${idx + 1}`}
className="w-full h-full object-cover"
/>
<button
onClick={() => {
const updated = [...editingProduct.images];
updated.splice(idx, 1);
setEditingProduct((prev) => ({
...prev,
images: updated,
}));
}}
className="absolute top-0 right-0 bg-white bg-opacity-80 w-6 h-6 flex items-center justify-center text-gray-700 hover:text-red-600"
title="Remove image"
>
&times;
</button>
</div>
))}
{editingProduct.images.length > 0 && (
<button
onClick={() =>
setEditingProduct((prev) => ({ ...prev, images: [] }))
}
className="text-sm text-red-600 hover:text-red-800 mt-2"
>
Clear all
</button>
)}
</div>
</div>
)}
</div>
</div>
{/* Actions */}
<div className="mt-8 flex justify-end gap-4 border-t-2 border-gray-100 pt-4">
<button
onClick={onCancel}
className="bg-gray-100 text-gray-700 px-6 py-2 rounded-md hover:bg-gray-200 font-medium"
>
Cancel
</button>
<button
onClick={onSave}
className="bg-emerald-600 text-white px-8 py-2 rounded-md hover:bg-emerald-700 font-medium"
>
{editingProduct.id ? "Update Product" : "Add Product"}
</button>
</div>
</div>
);
};
export default ProductForm;

View File

@@ -1,112 +1,141 @@
import { useState } from 'react'; import { useState, useEffect } from "react";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
import { Heart, Tag, Trash2, Filter, ChevronDown } from 'lucide-react'; import { Heart, Tag, Trash2 } from "lucide-react";
const Favorites = () => { const Favorites = () => {
const [favorites, setFavorites] = useState([ const [favorites, setFavorites] = useState([]);
{
id: 0,
title: 'Dell XPS 16 Laptop',
price: 850,
category: 'Electronics',
image: '/image1.avif',
condition: 'Like New',
seller: 'Michael T.',
datePosted: '5d ago',
dateAdded: '2023-03-08',
},
]);
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false);
const [sortBy, setSortBy] = useState('dateAdded'); const [sortBy, setSortBy] = useState("dateAdded");
const [filterCategory, setFilterCategory] = useState('All'); const [filterCategory, setFilterCategory] = useState("All");
const storedUser = JSON.parse(sessionStorage.getItem("user"));
// Function to remove item from favorites const mapCategory = (id) => {
const removeFromFavorites = (id) => { const categories = {
setFavorites(favorites.filter(item => item.id !== id)); 1: "Electronics",
2: "Textbooks",
3: "Furniture",
4: "Clothing",
5: "Kitchen",
6: "Other",
};
return categories[id] || "Other";
}; };
// Available categories for filtering function reloadPage() {
const categories = ['All', 'Electronics', 'Textbooks', 'Furniture', 'Kitchen', 'Other']; var doctTimestamp = new Date(performance.timing.domLoading).getTime();
var now = Date.now();
// Sort favorites based on selected sort option if (now > doctTimestamp) {
const sortedFavorites = [...favorites].sort((a, b) => { location.reload();
if (sortBy === 'dateAdded') {
return new Date(b.dateAdded) - new Date(a.dateAdded);
} else if (sortBy === 'priceHigh') {
return b.price - a.price;
} else if (sortBy === 'priceLow') {
return a.price - b.price;
} }
}
const removeFromFavorites = async (itemID) => {
const response = await fetch(
"http://localhost:3030/api/product/delFavorite",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: storedUser.ID,
productID: itemID,
}),
},
);
const data = await response.json();
if (data.success) {
reloadPage();
}
if (!response.ok) throw new Error("Failed to fetch products");
console.log(response);
console.log(`Add Product -> History: ${itemID}`);
};
useEffect(() => {
const fetchFavorites = async () => {
try {
const response = await fetch(
"http://localhost:3030/api/product/getFavorites",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userID: storedUser.ID }),
},
);
const data = await response.json();
const favoritesData = data.favorites;
if (!Array.isArray(favoritesData)) {
console.error("Expected an array but got:", favoritesData);
return;
}
const transformed = favoritesData.map((item) => ({
id: item.ProductID,
title: item.Name,
price: parseFloat(item.Price),
category: mapCategory(item.CategoryID),
image: item.image_url || "/default-image.jpg",
seller: item.SellerName,
datePosted: formatDatePosted(item.Date),
dateAdded: item.Date || new Date().toISOString(),
}));
setFavorites(transformed);
} catch (error) {
console.error("Failed to fetch favorites:", error);
}
};
fetchFavorites();
}, []);
const formatDatePosted = (dateString) => {
const postedDate = new Date(dateString);
const today = new Date();
const diffInMs = today - postedDate;
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
return `${diffInDays}d ago`;
};
const sortedFavorites = [...favorites].sort((a, b) => {
if (sortBy === "dateAdded")
return new Date(b.dateAdded) - new Date(a.dateAdded);
if (sortBy === "priceHigh") return b.price - a.price;
if (sortBy === "priceLow") return a.price - b.price;
return 0; return 0;
}); });
// Filter favorites based on selected category const filteredFavorites =
const filteredFavorites = filterCategory === 'All' filterCategory === "All"
? sortedFavorites ? sortedFavorites
: sortedFavorites.filter(item => item.category === filterCategory); : sortedFavorites.filter((item) => item.category === filterCategory);
return ( return (
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">My Favorites</h1> <h1 className="text-2xl font-bold text-gray-800">My Favorites</h1>
<button
className="flex items-center text-gray-600 hover:text-gray-800"
onClick={() => setShowFilters(!showFilters)}
>
<Filter className="h-5 w-5 mr-1" />
<span>Filter & Sort</span>
<ChevronDown className={`h-4 w-4 ml-1 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
</button>
</div> </div>
{/* Filters and Sorting */}
{showFilters && (
<div className="bg-white border border-gray-200 p-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Sort by
</label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
>
<option value="dateAdded">Recently Added</option>
<option value="priceHigh">Price (High to Low)</option>
<option value="priceLow">Price (Low to High)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Category
</label>
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
>
{categories.map((category) => (
<option key={category} value={category}>{category}</option>
))}
</select>
</div>
</div>
</div>
)}
{/* Favorites List */} {/* Favorites List */}
{filteredFavorites.length === 0 ? ( {filteredFavorites.length === 0 ? (
<div className="bg-white border border-gray-200 p-8 text-center"> <div className="bg-white border border-gray-200 p-8 text-center">
<Heart className="h-12 w-12 text-gray-300 mx-auto mb-4" /> <Heart className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-medium text-gray-700 mb-2">No favorites yet</h3> <h3 className="text-xl font-medium text-gray-700 mb-2">
No favorites yet
</h3>
<p className="text-gray-500 mb-4"> <p className="text-gray-500 mb-4">
Items you save will appear here. Start browsing to add items to your favorites. Items you save will appear here. Start browsing to add items to your
favorites.
</p> </p>
<Link <Link
to="/" to="/"
className="inline-block bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4" className="inline-block bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
> >
Browse Listings Browse Listings
@@ -115,7 +144,10 @@ const Favorites = () => {
) : ( ) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredFavorites.map((item) => ( {filteredFavorites.map((item) => (
<div key={item.id} className="bg-white border border-gray-200 hover:shadow-md transition-shadow relative"> <div
key={item.id}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow relative"
>
<button <button
onClick={() => removeFromFavorites(item.id)} onClick={() => removeFromFavorites(item.id)}
className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm text-red-500 hover:bg-red-50" className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm text-red-500 hover:bg-red-50"
@@ -123,28 +155,36 @@ const Favorites = () => {
> >
<Trash2 className="h-5 w-5" /> <Trash2 className="h-5 w-5" />
</button> </button>
<Link to={`/product/${item.id}`}> <Link to={`/product/${item.id}`}>
<img src={item.image} alt={item.title} className="w-full h-48 object-cover" /> <img
src={item.image}
alt={item.title}
className="w-full h-48 object-cover"
/>
<div className="p-4"> <div className="p-4">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<h3 className="text-lg font-medium text-gray-800 leading-tight"> <h3 className="text-lg font-medium text-gray-800 leading-tight">
{item.title} {item.title}
</h3> </h3>
<span className="font-semibold text-green-600">${item.price}</span> <span className="font-semibold text-green-600">
${item.price}
</span>
</div> </div>
<div className="flex items-center text-sm text-gray-500 mb-3"> <div className="flex items-center text-sm text-gray-500 mb-3">
<Tag className="h-4 w-4 mr-1" /> <Tag className="h-4 w-4 mr-1" />
<span>{item.category}</span> <span>{item.category}</span>
<span className="mx-2"></span>
<span>{item.condition}</span>
</div> </div>
<div className="flex justify-between items-center pt-2 border-t border-gray-100"> <div className="flex justify-between items-center pt-2 border-t border-gray-100">
<span className="text-xs text-gray-500">Listed {item.datePosted}</span> <span className="text-xs text-gray-500">
<span className="text-sm font-medium text-gray-700">{item.seller}</span> Listed {item.datePosted}
</span>
<span className="text-sm font-medium text-gray-700">
{item.seller}
</span>
</div> </div>
</div> </div>
</Link> </Link>
@@ -156,12 +196,13 @@ const Favorites = () => {
{/* Show count if there are favorites */} {/* Show count if there are favorites */}
{filteredFavorites.length > 0 && ( {filteredFavorites.length > 0 && (
<div className="mt-6 text-sm text-gray-500"> <div className="mt-6 text-sm text-gray-500">
Showing {filteredFavorites.length} {filteredFavorites.length === 1 ? 'item' : 'items'} Showing {filteredFavorites.length}{" "}
{filterCategory !== 'All' && ` in ${filterCategory}`} {filteredFavorites.length === 1 ? "item" : "items"}
{filterCategory !== "All" && ` in ${filterCategory}`}
</div> </div>
)} )}
</div> </div>
); );
}; };
export default Favorites; export default Favorites;

View File

@@ -1,17 +1,118 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from "lucide-react"; import { Tag } from "lucide-react";
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
const Home = () => { const Home = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [listings, setListings] = useState([]); const [listings, setListings] = useState([]);
const [recommended, setRecommended] = useState([]);
const [history, sethistory] = useState([]);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const storedUser = JSON.parse(sessionStorage.getItem("user"));
const [showAlert, setShowAlert] = useState(false);
const toggleFavorite = async (id) => {
const response = await fetch(
"http://localhost:3030/api/product/addFavorite",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: storedUser.ID,
productID: id,
}),
},
);
const data = await response.json();
if (data.success) {
setShowAlert(true);
}
console.log(response);
console.log(`Add Product -> History: ${id}`);
};
const addHistory = async (id) => {
const response = await fetch(
"http://localhost:3030/api/history/addHistory",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: storedUser.ID,
productID: id,
}),
},
);
};
function reloadPage() {
var doctTimestamp = new Date(performance.timing.domLoading).getTime();
var now = Date.now();
var tenSec = 10 * 1000;
if (now > doctTimestamp + tenSec) {
location.reload();
}
}
reloadPage();
useEffect(() => {
const fetchrecomProducts = async () => {
// Get the user's data from localStorage
console.log(storedUser);
try {
const response = await fetch(
"http://localhost:3030/api/engine/recommended",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: storedUser.ID,
}),
},
);
if (!response.ok) throw new Error("Failed to fetch products");
const data = await response.json();
console.log(data);
if (data.success) {
setRecommended(
data.data.map((product) => ({
id: product.ProductID,
title: product.ProductName, // Use the alias from SQL
price: product.Price,
category: product.Category, // Ensure this gets the category name
image: product.ProductImage, // Use the alias for image URL
seller: product.SellerName, // Fetch seller name properly
datePosted: product.DateUploaded, // Use the actual date
isFavorite: false, // Default state
})),
);
} else {
throw new Error(data.message || "Error fetching products");
}
} catch (error) {
console.error("Error fetching products:", error);
setError(error.message);
}
};
fetchrecomProducts();
//reloadPage();
}, []);
reloadPage();
useEffect(() => { useEffect(() => {
const fetchProducts = async () => { const fetchProducts = async () => {
try { try {
const response = await fetch( const response = await fetch(
"http://localhost:3030/api/product/get_product", "http://localhost:3030/api/product/getProduct",
); );
if (!response.ok) throw new Error("Failed to fetch products"); if (!response.ok) throw new Error("Failed to fetch products");
@@ -25,7 +126,6 @@ const Home = () => {
price: product.Price, price: product.Price,
category: product.Category, // Ensure this gets the category name category: product.Category, // Ensure this gets the category name
image: product.ProductImage, // Use the alias for image URL image: product.ProductImage, // Use the alias for image URL
condition: "New", // Modify based on actual data
seller: product.SellerName, // Fetch seller name properly seller: product.SellerName, // Fetch seller name properly
datePosted: product.DateUploaded, // Use the actual date datePosted: product.DateUploaded, // Use the actual date
isFavorite: false, // Default state isFavorite: false, // Default state
@@ -42,17 +142,49 @@ const Home = () => {
fetchProducts(); fetchProducts();
}, []); }, []);
// Toggle favorite status useEffect(() => {
const toggleFavorite = (id, e) => { const fetchrecomProducts = async () => {
e.preventDefault(); // Prevent navigation when clicking the heart icon // Get the user's data from localStorage
setListings((prevListings) => console.log(storedUser);
prevListings.map((listing) => try {
listing.id === id const response = await fetch(
? { ...listing, isFavorite: !listing.isFavorite } "http://localhost:3030/api/history/getHistory",
: listing, {
), method: "POST",
); headers: {
}; "Content-Type": "application/json",
},
body: JSON.stringify({
id: storedUser.ID,
}),
},
);
if (!response.ok) throw new Error("Failed to fetch products");
const data = await response.json();
console.log(data);
if (data.success) {
sethistory(
data.data.map((product) => ({
id: product.ProductID,
title: product.ProductName, // Use the alias from SQL
price: product.Price,
category: product.Category, // Ensure this gets the category name
image: product.ProductImage, // Use the alias for image URL
seller: product.SellerName, // Fetch seller name properly
datePosted: product.DateUploaded, // Use the actual date
})),
);
} else {
throw new Error(data.message || "Error fetching products");
}
} catch (error) {
console.error("Error fetching products:", error);
setError(error.message);
}
};
fetchrecomProducts();
}, []);
const handleSelling = () => { const handleSelling = () => {
navigate("/selling"); navigate("/selling");
@@ -90,27 +222,13 @@ const Home = () => {
</div> </div>
</div> </div>
{/* Categories */}
{/* <div className="mb-8">
<h2 className="text-xl font-semibold text-gray-800 mb-4">Categories</h2>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{categories.map((category) => (
<button
key={category.id}
className="flex flex-col items-center justify-center p-4 bg-white border border-gray-200 hover:border-green-500 hover:shadow-sm"
>
<div className="flex items-center justify-center w-12 h-12 bg-green-50 text-green-600 rounded-full mb-2">
{category.icon}
</div>
<span className="text-sm font-medium text-gray-700">
{category.name}
</span>
</button>
))}
</div>
</div> */}
{/* Recent Listings */} {/* Recent Listings */}
{showAlert && (
<FloatingAlert
message="Product added to favorites!"
onClose={() => setShowAlert(false)}
/>
)}
<div className="relative py-4"> <div className="relative py-4">
<h2 className="text-xl font-semibold text-gray-800 mb-4"> <h2 className="text-xl font-semibold text-gray-800 mb-4">
Recommendation Recommendation
@@ -134,53 +252,50 @@ const Home = () => {
id="RecomContainer" id="RecomContainer"
className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0" className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0"
> >
{listings.map((listing) => ( {recommended.map((recommended) => (
<Link <Link
key={listing.id} key={recommended.id}
to={`/product/${listing.id}`} to={`/product/${recommended.id}`}
onClick={() => addHistory(recommended.id)}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative" className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative"
> >
<div className="relative"> <div className="relative">
<img <img
src={listing.image} src={recommended.image}
alt={listing.title} alt={recommended.title}
className="w-full h-48 object-cover" className="w-full h-48 object-cover"
/> />
<button <button
onClick={(e) => toggleFavorite(listing.id, e)} onClick={(e) => {
className="absolute top-2 right-2 p-2 bg-white rounded-full shadow-sm" e.stopPropagation();
e.preventDefault();
toggleFavorite(recommended.id);
}}
className="absolute top-2 right-2 p-2 bg-white rounded-full shadow-sm hover:bg-gray-100 transition"
> >
<Heart <span className="text-xl font-bold text-gray-600">+</span>
className={`h-6 w-6 ${
listing.isFavorite
? "text-red-500 fill-red-500"
: "text-gray-400"
}`}
/>
</button> </button>
</div> </div>
<div className="p-4"> <div className="p-4">
<h3 className="text-lg font-medium text-gray-800 leading-tight"> <h3 className="text-lg font-medium text-gray-800 leading-tight">
{listing.title} {recommended.title}
</h3> </h3>
<span className="font-semibold text-green-600 block mt-1"> <span className="font-semibold text-green-600 block mt-1">
${listing.price} ${recommended.price}
</span> </span>
<div className="flex items-center text-sm text-gray-500 mt-2"> <div className="flex items-center text-sm text-gray-500 mt-2">
<Tag className="h-4 w-4 mr-1" /> <Tag className="h-4 w-4 mr-1" />
<span>{listing.category}</span> <span>{recommended.category}</span>
<span className="mx-2"></span>
<span>{listing.condition}</span>
</div> </div>
<div className="flex justify-between items-center pt-2 border-t border-gray-100 mt-3"> <div className="flex justify-between items-center pt-2 border-t border-gray-100 mt-3">
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500">
{listing.datePosted} {recommended.datePosted}
</span> </span>
<span className="text-sm font-medium text-gray-700"> <span className="text-sm font-medium text-gray-700">
{listing.seller} {recommended.seller}
</span> </span>
</div> </div>
</div> </div>
@@ -203,6 +318,12 @@ const Home = () => {
</div> </div>
{/* Recent Listings */} {/* Recent Listings */}
{showAlert && (
<FloatingAlert
message="Product added to favorites!"
onClose={() => setShowAlert(false)}
/>
)}
<div className="relative py-4"> <div className="relative py-4">
<h2 className="text-xl font-semibold text-gray-800 mb-4"> <h2 className="text-xl font-semibold text-gray-800 mb-4">
Recent Listings Recent Listings
@@ -236,19 +357,18 @@ const Home = () => {
<img <img
src={listing.image} src={listing.image}
alt={listing.title} alt={listing.title}
onClick={() => addHistory(listing.id)}
className="w-full h-48 object-cover" className="w-full h-48 object-cover"
/> />
<button <button
onClick={(e) => toggleFavorite(listing.id, e)} onClick={(e) => {
className="absolute top-2 right-2 p-2 bg-white rounded-full shadow-sm" e.stopPropagation();
e.preventDefault();
toggleFavorite(listing.id);
}}
className="absolute top-2 right-2 p-2 bg-white rounded-full shadow-sm hover:bg-gray-100 transition"
> >
<Heart <span className="text-xl font-bold text-gray-600">+</span>
className={`h-6 w-6 ${
listing.isFavorite
? "text-red-500 fill-red-500"
: "text-gray-400"
}`}
/>
</button> </button>
</div> </div>
@@ -263,8 +383,6 @@ const Home = () => {
<div className="flex items-center text-sm text-gray-500 mt-2"> <div className="flex items-center text-sm text-gray-500 mt-2">
<Tag className="h-4 w-4 mr-1" /> <Tag className="h-4 w-4 mr-1" />
<span>{listing.category}</span> <span>{listing.category}</span>
<span className="mx-2"></span>
<span>{listing.condition}</span>
</div> </div>
<div className="flex justify-between items-center pt-2 border-t border-gray-100 mt-3"> <div className="flex justify-between items-center pt-2 border-t border-gray-100 mt-3">
@@ -293,6 +411,98 @@ const Home = () => {
</button> </button>
</div> </div>
</div> </div>
{/* Recent Listings */}
{showAlert && (
<FloatingAlert
message="Product added to favorites!"
onClose={() => setShowAlert(false)}
/>
)}
<div className="relative py-4">
<h2 className="text-xl font-semibold text-gray-800 mb-4">History</h2>
<div className="relative">
{/* Left Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("HistoryContainer")
.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="HistoryContainer"
className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0"
>
{history.map((history) => (
<Link
key={history.id}
to={`/product/${history.id}`}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative"
>
<div className="relative">
<img
src={history.image}
alt={history.title}
className="w-full h-48 object-cover"
/>
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
toggleFavorite(history.id);
}}
className="absolute top-2 right-2 p-2 bg-white rounded-full shadow-sm hover:bg-gray-100 transition"
>
<span className="text-xl font-bold text-gray-600">+</span>
</button>
</div>
<div className="p-4">
<h3 className="text-lg font-medium text-gray-800 leading-tight">
{history.title}
</h3>
<span className="font-semibold text-green-600 block mt-1">
${history.price}
</span>
<div className="flex items-center text-sm text-gray-500 mt-2">
<Tag className="h-4 w-4 mr-1" />
<span>{history.category}</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">
{history.datePosted}
</span>
<span className="text-sm font-medium text-gray-700">
{history.seller}
</span>
</div>
</div>
</Link>
))}
</div>
{/* Right Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("HistoryContainer")
.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> </div>
); );
}; };

View File

@@ -1,550 +0,0 @@
import { useState, useEffect } from "react";
import { Link, useParams, useNavigate } from "react-router-dom";
import { ArrowLeft, Plus, X, Save, Trash } from "lucide-react";
const ItemForm = () => {
const { id } = useParams(); // If id exists, we are editing, otherwise creating
const navigate = useNavigate();
const isEditing = !!id;
const [formData, setFormData] = useState({
title: "",
price: "",
category: "",
condition: "",
shortDescription: "",
description: "",
images: [],
status: "active",
});
const [originalData, setOriginalData] = useState(null);
const [errors, setErrors] = useState({});
const [imagePreviewUrls, setImagePreviewUrls] = useState([]);
const [isLoading, setIsLoading] = useState(isEditing);
const [isSubmitting, setIsSubmitting] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
// Categories with icons
const categories = [
"Electronics",
"Furniture",
"Books",
"Kitchen",
"Collectibles",
"Clothing",
"Sports & Outdoors",
"Tools",
"Toys & Games",
"Other",
];
// Condition options
const conditions = ["New", "Like New", "Good", "Fair", "Poor"];
// Status options
const statuses = ["active", "inactive", "sold", "pending"];
// Fetch item data if editing
useEffect(() => {
if (isEditing) {
// This would be an API call in a real app
// Simulating API call with timeout
setTimeout(() => {
// Sample data for item being edited
const itemData = {
id: parseInt(id),
title: "Dell XPS 13 Laptop - 2023 Model",
price: 850,
category: "Electronics",
condition: "Like New",
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",
images: ["/image1.avif", "/image2.avif", "/image3.avif"],
status: "active",
datePosted: "2023-03-02",
};
setFormData(itemData);
setOriginalData(itemData);
setImagePreviewUrls(itemData.images);
setIsLoading(false);
}, 1000);
}
}, [id, isEditing]);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
// Clear error when field is edited
if (errors[name]) {
setErrors({
...errors,
[name]: null,
});
}
};
const handleImageChange = (e) => {
e.preventDefault();
const files = Array.from(e.target.files);
if (formData.images.length + files.length > 5) {
setErrors({
...errors,
images: "Maximum 5 images allowed",
});
return;
}
// Create preview URLs for the images
const newImagePreviewUrls = [...imagePreviewUrls];
const newImages = [...formData.images];
files.forEach((file) => {
const reader = new FileReader();
reader.onloadend = () => {
newImagePreviewUrls.push(reader.result);
setImagePreviewUrls(newImagePreviewUrls);
};
reader.readAsDataURL(file);
newImages.push(file);
});
setFormData({
...formData,
images: newImages,
});
// Clear error if any
if (errors.images) {
setErrors({
...errors,
images: null,
});
}
};
const removeImage = (index) => {
const newImages = [...formData.images];
const newImagePreviewUrls = [...imagePreviewUrls];
newImages.splice(index, 1);
newImagePreviewUrls.splice(index, 1);
setFormData({
...formData,
images: newImages,
});
setImagePreviewUrls(newImagePreviewUrls);
};
const validateForm = () => {
const newErrors = {};
if (!formData.title.trim()) newErrors.title = "Title is required";
if (!formData.price) newErrors.price = "Price is required";
if (isNaN(formData.price) || formData.price <= 0)
newErrors.price = "Price must be a positive number";
if (!formData.category) newErrors.category = "Category is required";
if (!formData.condition) newErrors.condition = "Condition is required";
if (!formData.shortDescription.trim())
newErrors.shortDescription = "Short description is required";
if (!formData.description.trim())
newErrors.description = "Description is required";
if (formData.images.length === 0)
newErrors.images = "At least one image is required";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (!validateForm()) {
// Scroll to the first error
const firstErrorField = Object.keys(errors)[0];
document
.getElementsByName(firstErrorField)[0]
?.scrollIntoView({ behavior: "smooth" });
return;
}
setIsSubmitting(true);
// Simulate API call to post/update the item
setTimeout(() => {
console.log("Form submitted:", formData);
setIsSubmitting(false);
// Show success and redirect to listings
alert(`Item successfully ${isEditing ? "updated" : "created"}!`);
navigate("/selling");
}, 1500);
};
const handleDelete = () => {
setIsSubmitting(true);
// Simulate API call to delete the item
setTimeout(() => {
console.log("Item deleted:", id);
setIsSubmitting(false);
setShowDeleteModal(false);
// Show success and redirect to listings
alert("Item successfully deleted!");
navigate("/selling");
}, 1500);
};
// Show loading state if necessary
if (isLoading) {
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="flex items-center mb-6">
<Link to="/selling" className="text-green-600 hover:text-green-700">
<ArrowLeft className="h-4 w-4 mr-1" />
Back to listings
</Link>
</div>
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/3 mb-8"></div>
<div className="h-64 bg-gray-200 rounded mb-4"></div>
<div className="h-32 bg-gray-200 rounded"></div>
</div>
</div>
);
}
return (
<div className="max-w-4xl mx-auto px-4 py-8">
{/* Breadcrumb & Back Link */}
<div className="mb-6">
<Link
to="/selling"
className="flex items-center text-green-600 hover:text-green-700"
>
<ArrowLeft className="h-4 w-4 mr-1" />
<span>Back to listings</span>
</Link>
</div>
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-800">
{isEditing ? "Edit Item" : "Create New Listing"}
</h1>
{isEditing && (
<button
type="button"
onClick={() => setShowDeleteModal(true)}
className="bg-red-500 hover:bg-red-600 text-white font-medium py-2 px-4 flex items-center"
disabled={isSubmitting}
>
<Trash className="h-5 w-5 mr-1" />
Delete Item
</button>
)}
</div>
<form
onSubmit={handleSubmit}
className="bg-white border border-gray-200 p-6 rounded-md"
>
{/* Title */}
<div className="mb-6">
<label
htmlFor="title"
className="block text-gray-700 font-medium mb-2"
>
Title <span className="text-red-500">*</span>
</label>
<input
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleChange}
className={`w-full px-4 py-2 border ${errors.title ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500`}
placeholder="e.g., Dell XPS 13 Laptop - 2023 Model"
/>
{errors.title && (
<p className="text-red-500 text-sm mt-1">{errors.title}</p>
)}
</div>
{/* Price, Category, Status (side by side on larger screens) */}
<div className="flex flex-col md:flex-row gap-6 mb-6">
<div className="w-full md:w-1/3">
<label
htmlFor="price"
className="block text-gray-700 font-medium mb-2"
>
Price ($) <span className="text-red-500">*</span>
</label>
<input
type="number"
id="price"
name="price"
value={formData.price}
onChange={handleChange}
className={`w-full px-4 py-2 border ${errors.price ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500`}
placeholder="e.g., 850"
min="0"
step="0.01"
/>
{errors.price && (
<p className="text-red-500 text-sm mt-1">{errors.price}</p>
)}
</div>
<div className="w-full md:w-1/3">
<label
htmlFor="category"
className="block text-gray-700 font-medium mb-2"
>
Category <span className="text-red-500">*</span>
</label>
<select
id="category"
name="category"
value={formData.category}
onChange={handleChange}
className={`w-full px-4 py-2 border ${errors.category ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500 bg-white`}
>
<option value="">Select a category</option>
{categories.map((category) => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
{errors.category && (
<p className="text-red-500 text-sm mt-1">{errors.category}</p>
)}
</div>
{isEditing && (
<div className="w-full md:w-1/3">
<label
htmlFor="status"
className="block text-gray-700 font-medium mb-2"
>
Status
</label>
<select
id="status"
name="status"
value={formData.status}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 focus:outline-none focus:border-green-500 bg-white"
>
{statuses.map((status) => (
<option key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</option>
))}
</select>
</div>
)}
</div>
{/* Condition */}
<div className="mb-6">
<label className="block text-gray-700 font-medium mb-2">
Condition <span className="text-red-500">*</span>
</label>
<div className="flex flex-wrap gap-3">
{conditions.map((condition) => (
<label
key={condition}
className={`px-4 py-2 border cursor-pointer ${
formData.condition === condition
? "bg-green-50 border-green-500 text-green-700"
: "border-gray-300 text-gray-700 hover:bg-gray-50"
}`}
>
<input
type="radio"
name="condition"
value={condition}
checked={formData.condition === condition}
onChange={handleChange}
className="sr-only"
/>
{condition}
</label>
))}
</div>
{errors.condition && (
<p className="text-red-500 text-sm mt-1">{errors.condition}</p>
)}
</div>
{/* Short Description */}
<div className="mb-6">
<label
htmlFor="shortDescription"
className="block text-gray-700 font-medium mb-2"
>
Short Description <span className="text-red-500">*</span>
<span className="text-sm font-normal text-gray-500 ml-2">
(Brief summary that appears in listings)
</span>
</label>
<input
type="text"
id="shortDescription"
name="shortDescription"
value={formData.shortDescription}
onChange={handleChange}
className={`w-full px-4 py-2 border ${errors.shortDescription ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500`}
placeholder="e.g., Dell XPS 13 laptop in excellent condition. Intel Core i7, 16GB RAM, 512GB SSD."
maxLength="150"
/>
<p className="text-sm text-gray-500 mt-1">
{formData.shortDescription.length}/150 characters
</p>
{errors.shortDescription && (
<p className="text-red-500 text-sm">{errors.shortDescription}</p>
)}
</div>
{/* Full Description */}
<div className="mb-6">
<label
htmlFor="description"
className="block text-gray-700 font-medium mb-2"
>
Full Description <span className="text-red-500">*</span>
</label>
<textarea
id="description"
name="description"
value={formData.description}
onChange={handleChange}
className={`w-full px-4 py-2 border ${errors.description ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500 h-40`}
placeholder="Describe your item in detail. Include specs, condition, reason for selling, etc."
></textarea>
<p className="text-sm text-gray-500 mt-1">
Use blank lines to separate paragraphs.
</p>
{errors.description && (
<p className="text-red-500 text-sm">{errors.description}</p>
)}
</div>
{/* Image Upload */}
<div className="mb-8">
<label className="block text-gray-700 font-medium mb-2">
Images <span className="text-red-500">*</span>
<span className="text-sm font-normal text-gray-500 ml-2">
(Up to 5 images)
</span>
</label>
{/* Image Preview Area */}
<div className="flex flex-wrap gap-4 mb-4">
{imagePreviewUrls.map((url, index) => (
<div
key={index}
className="relative w-24 h-24 border border-gray-300"
>
<img
src={url}
alt={`Preview ${index + 1}`}
className="w-full h-full object-cover"
/>
<button
type="button"
onClick={() => removeImage(index)}
className="absolute -top-2 -right-2 bg-white rounded-full p-1 shadow-md border border-gray-300"
>
<X className="h-4 w-4 text-gray-600" />
</button>
</div>
))}
{/* Upload Button (only show if less than 5 images) */}
{formData.images.length < 5 && (
<label className="w-24 h-24 border-2 border-dashed border-gray-300 flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:bg-gray-50">
<input
type="file"
accept="image/*"
multiple
onChange={handleImageChange}
className="sr-only"
/>
<Plus className="h-6 w-6 mb-1" />
<span className="text-xs">Add Image</span>
</label>
)}
</div>
{errors.images && (
<p className="text-red-500 text-sm">{errors.images}</p>
)}
</div>
{/* Submit Button */}
<div className="mt-8">
<button
type="submit"
disabled={isSubmitting}
className={`w-full py-3 px-4 text-white font-medium flex items-center justify-center ${
isSubmitting ? "bg-gray-400" : "bg-green-500 hover:bg-green-600"
}`}
>
<Save className="h-5 w-5 mr-2" />
{isSubmitting
? "Saving..."
: isEditing
? "Save Changes"
: "Create Listing"}
</button>
</div>
</form>
{/* Delete Confirmation Modal */}
{showDeleteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-md max-w-md w-full">
<h3 className="text-lg font-medium text-gray-900 mb-2">
Delete Listing
</h3>
<p className="text-gray-600 mb-4">
Are you sure you want to delete <strong>{formData.title}</strong>?
This action cannot be undone.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => setShowDeleteModal(false)}
className="px-4 py-2 border border-gray-300 text-gray-700 font-medium rounded-md hover:bg-gray-50"
disabled={isSubmitting}
>
Cancel
</button>
<button
onClick={handleDelete}
className="px-4 py-2 bg-red-600 text-white font-medium rounded-md hover:bg-red-700 flex items-center"
disabled={isSubmitting}
>
{isSubmitting ? "Deleting..." : "Delete"}
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default ItemForm;

View File

@@ -1,50 +1,188 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useParams, Link } from "react-router-dom"; import { useParams, Link } from "react-router-dom";
import { Heart, ArrowLeft, Tag, User, Calendar } from "lucide-react"; import {
Heart,
ArrowLeft,
Tag,
User,
Calendar,
Star,
Phone,
Mail,
} from "lucide-react";
const ProductDetail = () => { const ProductDetail = () => {
const { id } = useParams(); const { id } = useParams();
const [product, setProduct] = useState(null); const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState({
const [error, setError] = useState(null); product: true,
reviews: true,
submitting: false,
});
const [error, setError] = useState({
product: null,
reviews: null,
submit: null,
});
const [isFavorite, setIsFavorite] = useState(false); const [isFavorite, setIsFavorite] = useState(false);
const [showContactForm, setShowContactForm] = useState(false); const [showContactOptions, setShowContactOptions] = useState(false);
const [message, setMessage] = useState("");
const [currentImage, setCurrentImage] = useState(0); const [currentImage, setCurrentImage] = useState(0);
const [reviews, setReviews] = useState([]);
const [showReviewForm, setShowReviewForm] = useState(false);
const storedUser = JSON.parse(sessionStorage.getItem("user"));
// Fetch product details const [reviewForm, setReviewForm] = useState({
rating: 3,
comment: "",
name: "",
});
// Add this function to handle review input changes
const handleReviewInputChange = (e) => {
const { id, value } = e.target;
setReviewForm((prev) => ({
...prev,
[id]: value,
}));
};
// Add this function to handle star rating selection
const handleRatingChange = (rating) => {
setReviewForm((prev) => ({
...prev,
rating,
}));
};
const handleSubmitReview = async (e) => {
e.preventDefault(); // Prevent form default behavior
try {
setLoading((prev) => ({ ...prev, submitting: true }));
setError((prev) => ({ ...prev, submit: null }));
const reviewData = {
productId: id,
rating: reviewForm.rating,
comment: reviewForm.comment,
userId: storedUser.ID,
};
const response = await fetch(`http://localhost:3030/api/review/add`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(reviewData),
});
const result = await response.json();
// Check if API returned an error message even with 200 status
if (!result.success) {
throw new Error(result.message || "Failed to submit review");
}
alert("Review submitted successfully!");
setReviewForm({
rating: 3,
comment: "",
name: "",
});
setShowReviewForm(false);
try {
setLoading((prev) => ({ ...prev, reviews: true }));
const reviewsResponse = await fetch(
`http://localhost:3030/api/review/${id}`,
);
const reviewsResult = await reviewsResponse.json();
if (reviewsResult.success) {
setReviews(reviewsResult.data || []);
setError((prev) => ({ ...prev, reviews: null }));
} else {
throw new Error(reviewsResult.message || "Error fetching reviews");
}
} catch (reviewsError) {
console.error("Error fetching reviews:", reviewsError);
setError((prev) => ({ ...prev, reviews: reviewsError.message }));
} finally {
setLoading((prev) => ({ ...prev, reviews: false }));
}
} catch (error) {
console.error("Error submitting review:", error);
alert(`Error: ${error.message}`);
setError((prev) => ({
...prev,
submit: error.message,
}));
} finally {
setLoading((prev) => ({ ...prev, submitting: false }));
}
};
// Fetch product data
useEffect(() => { useEffect(() => {
const fetchProduct = async () => { const fetchProduct = async () => {
try { try {
setLoading(true); setLoading((prev) => ({ ...prev, product: true }));
const response = await fetch(`http://localhost:3030/api/product/${id}`); const response = await fetch(`http://localhost:3030/api/product/${id}`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch product"); throw new Error(`HTTP error! Status: ${response.status}`);
} }
const result = await response.json(); const result = await response.json();
console.log(result);
if (result.success) { if (result.success) {
setProduct(result.data); setProduct(result.data);
setError(null); setError((prev) => ({ ...prev, product: null }));
} else { } else {
throw new Error(result.message || "Error fetching product"); throw new Error(result.message || "Error fetching product");
} }
} catch (error) { } catch (error) {
console.error("Error fetching product:", error); console.error("Error fetching product:", error);
setError(error.message); setError((prev) => ({ ...prev, product: error.message }));
setProduct(null);
} finally { } finally {
setLoading(false); setLoading((prev) => ({ ...prev, product: false }));
} }
}; };
fetchProduct(); fetchProduct();
}, [id]); }, [id]);
// Handle favorite toggle // Fetch reviews data
useEffect(() => {
const fetchReviews = async () => {
try {
setLoading((prev) => ({ ...prev, reviews: true }));
const response = await fetch(`http://localhost:3030/api/review/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
setReviews(result.data || []);
setError((prev) => ({ ...prev, reviews: null }));
} else {
throw new Error(result.message || "Error fetching reviews");
}
} catch (error) {
console.error("Error fetching reviews:", error);
setError((prev) => ({ ...prev, reviews: error.message }));
setReviews([]);
} finally {
setLoading((prev) => ({ ...prev, reviews: false }));
}
};
fetchReviews();
}, [id]);
// Handle favorite toggle with error handling
const toggleFavorite = async () => { const toggleFavorite = async () => {
try { try {
const response = await fetch( const response = await fetch(
@@ -61,28 +199,25 @@ const ProductDetail = () => {
}, },
); );
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
setIsFavorite(!isFavorite); setIsFavorite(!isFavorite);
} else {
throw new Error(result.message || "Failed to toggle favorite");
} }
} catch (error) { } catch (error) {
console.error("Error toggling favorite:", error); console.error("Error toggling favorite:", error);
alert(`Failed to add to favorites: ${error.message}`);
} }
}; };
// Handle message submission
const handleSendMessage = (e) => {
e.preventDefault();
// TODO: Implement actual message sending logic
console.log("Message sent:", message);
setMessage("");
setShowContactForm(false);
alert("Message sent to seller!");
};
// Image navigation // Image navigation
const nextImage = () => { const nextImage = () => {
if (product && product.images) { if (product?.images?.length > 0) {
setCurrentImage((prev) => setCurrentImage((prev) =>
prev === product.images.length - 1 ? 0 : prev + 1, prev === product.images.length - 1 ? 0 : prev + 1,
); );
@@ -90,7 +225,7 @@ const ProductDetail = () => {
}; };
const prevImage = () => { const prevImage = () => {
if (product && product.images) { if (product?.images?.length > 0) {
setCurrentImage((prev) => setCurrentImage((prev) =>
prev === 0 ? product.images.length - 1 : prev - 1, prev === 0 ? product.images.length - 1 : prev - 1,
); );
@@ -101,8 +236,22 @@ const ProductDetail = () => {
setCurrentImage(index); setCurrentImage(index);
}; };
// Render loading state // Function to render stars based on rating
if (loading) { const renderStars = (rating) => {
const stars = [];
for (let i = 1; i <= 5; i++) {
stars.push(
<Star
key={i}
className={`h-4 w-4 ${i <= rating ? "text-yellow-400 fill-yellow-400" : "text-gray-300"}`}
/>,
);
}
return stars;
};
// Render loading state for the entire page
if (loading.product) {
return ( return (
<div className="flex justify-center items-center h-screen"> <div className="flex justify-center items-center h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-green-500"></div> <div className="animate-spin rounded-full h-32 w-32 border-t-2 border-green-500"></div>
@@ -110,13 +259,30 @@ const ProductDetail = () => {
); );
} }
// Render error state // Render error state for product
if (error) { if (error.product) {
return ( return (
<div className="flex justify-center items-center h-screen"> <div className="flex justify-center items-center h-screen">
<div className="text-center"> <div className="text-center">
<h2 className="text-2xl text-red-500 mb-4">Error Loading Product</h2> <h2 className="text-2xl text-red-500 mb-4">Error Loading Product</h2>
<p className="text-gray-600">{error}</p> <p className="text-gray-600">{error.product}</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>
);
}
// Safety check for product
if (!product) {
return (
<div className="flex justify-center items-center h-screen">
<div className="text-center">
<h2 className="text-2xl text-red-500 mb-4">Product Not Found</h2>
<Link <Link
to="/" to="/"
className="mt-4 inline-block bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600" className="mt-4 inline-block bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
@@ -145,12 +311,35 @@ const ProductDetail = () => {
<div className="md:w-3/5"> <div className="md:w-3/5">
<div className="bg-white border border-gray-200 mb-4 relative"> <div className="bg-white border border-gray-200 mb-4 relative">
{product.images && product.images.length > 0 ? ( {product.images && product.images.length > 0 ? (
<img <>
src={product.images[currentImage]} <img
alt={product.Name} src={product.images[currentImage]}
className="w-full h-auto object-contain cursor-pointer" alt={product.Name}
onClick={nextImage} className="w-full h-auto object-contain cursor-pointer"
/> onClick={nextImage}
onError={(e) => {
e.target.onerror = null;
e.target.src =
"https://via.placeholder.com/400x300?text=Image+Not+Available";
}}
/>
{product.images.length > 1 && (
<div className="absolute inset-x-0 bottom-0 flex justify-between p-2">
<button
onClick={(e) => {
e.stopPropagation();
prevImage();
}}
className="bg-white/70 p-1 rounded-full"
>
<ArrowLeft className="h-5 w-5" />
</button>
<div className="text-sm bg-white/70 px-2 py-1 rounded">
{currentImage + 1}/{product.images.length}
</div>
</div>
)}
</>
) : ( ) : (
<div className="w-full h-96 flex items-center justify-center bg-gray-200 text-gray-500"> <div className="w-full h-96 flex items-center justify-center bg-gray-200 text-gray-500">
No Image Available No Image Available
@@ -170,6 +359,11 @@ const ProductDetail = () => {
src={image} src={image}
alt={`${product.Name} - view ${index + 1}`} alt={`${product.Name} - view ${index + 1}`}
className="w-full h-auto object-cover" className="w-full h-auto object-cover"
onError={(e) => {
e.target.onerror = null;
e.target.src =
"https://via.placeholder.com/100x100?text=Error";
}}
/> />
</div> </div>
))} ))}
@@ -181,11 +375,14 @@ const ProductDetail = () => {
<div className="bg-white border border-gray-200 p-6 mb-6"> <div className="bg-white border border-gray-200 p-6 mb-6">
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<h1 className="text-2xl font-bold text-gray-800"> <h1 className="text-2xl font-bold text-gray-800">
{product.Name} {product.Name || "Unnamed Product"}
</h1> </h1>
<button <button
onClick={toggleFavorite} onClick={toggleFavorite}
className="p-2 hover:bg-gray-100" className="p-2 hover:bg-gray-100 rounded-full"
aria-label={
isFavorite ? "Remove from favorites" : "Add to favorites"
}
> >
<Heart <Heart
className={`h-6 w-6 ${isFavorite ? "text-red-500 fill-red-500" : "text-gray-400"}`} className={`h-6 w-6 ${isFavorite ? "text-red-500 fill-red-500" : "text-gray-400"}`}
@@ -194,91 +391,68 @@ const ProductDetail = () => {
</div> </div>
<div className="text-2xl font-bold text-green-600 mb-4"> <div className="text-2xl font-bold text-green-600 mb-4">
${product.Price} $
{typeof product.Price === "number"
? product.Price.toFixed(2)
: product.Price}
</div> </div>
<div className="flex flex-wrap gap-x-4 gap-y-2 mb-6 text-sm"> <div className="flex flex-wrap gap-x-4 gap-y-2 mb-6 text-sm">
<div className="flex items-center text-gray-600"> {product.Category && (
<Tag className="h-4 w-4 mr-1" /> <div className="flex items-center text-gray-600">
<span>{product.Category}</span> <Tag className="h-4 w-4 mr-1" />
</div> <span>{product.Category}</span>
<div className="flex items-center text-gray-600"> </div>
<span className="font-medium">Condition:</span> )}
<span className="ml-1">{product.condition}</span>
</div> {product.Date && (
<div className="flex items-center text-gray-600"> <div className="flex items-center text-gray-600">
<Calendar className="h-4 w-4 mr-1" /> <Calendar className="h-4 w-4 mr-1" />
<span>Posted on {product.Date}</span> <span>Posted on {product.Date}</span>
</div> </div>
)}
</div> </div>
<div className="bg-gray-50 p-4 mb-6 border border-gray-200"> <div className="bg-gray-50 p-4 mb-6 border border-gray-200">
<p className="text-gray-700">{product.Description}</p> <p className="text-gray-700">
{product.Description || "No description available"}
</p>
</div> </div>
<button <div className="relative">
onClick={() => setShowContactForm(!showContactForm)} <button
className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3" onClick={() => setShowContactOptions(!showContactOptions)}
> className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3"
Contact Seller >
</button> Contact Seller
</button>
{showContactForm && ( {showContactOptions && (
<div className="border border-gray-200 p-4 mb-4"> <div className="absolute z-10 w-full bg-white border border-gray-200 shadow-md">
<h3 className="font-medium text-gray-800 mb-2"> {product.SellerPhone && (
Contact Seller <a
</h3> href={`tel:${product.SellerPhone}`}
<form onSubmit={handleSendMessage}> className="flex items-center gap-2 p-3 hover:bg-gray-50 border-b border-gray-100"
<div className="mb-3">
<label htmlFor="email" className="block text-gray-700 mb-1">
Email
</label>
<input
type="email"
id="email"
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
/>
</div>
<div className="mb-3">
<label htmlFor="phone" className="block text-gray-700 mb-1">
Phone Number
</label>
<input
type="tel"
id="phone"
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
required
/>
</div>
<div className="mb-3">
<label
htmlFor="contactMessage"
className="block text-gray-700 mb-1"
> >
Message (Optional) <Phone className="h-5 w-5 text-green-500" />
</label> <span>Call Seller</span>
<input </a>
type="text" )}
id="contactMessage"
value={message} {product.SellerEmail && (
onChange={(e) => setMessage(e.target.value)} <a
placeholder="Hi, is this item still available?" href={`mailto:${product.SellerEmail}`}
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500" className="flex items-center gap-2 p-3 hover:bg-gray-50"
/> >
</div> <Mail className="h-5 w-5 text-green-500" />
<button <span>Email Seller</span>
type="submit" </a>
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4" )}
> </div>
Send Contact Info )}
</button> </div>
</form>
</div>
)}
<div className="pt-4 border-t border-gray-200"> <div className="pt-4 border-t border-gray-200">
{/* Seller Info */}
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">
<div className="mr-3"> <div className="mr-3">
<div className="h-12 w-12 rounded-full bg-gray-200 flex items-center justify-center"> <div className="h-12 w-12 rounded-full bg-gray-200 flex items-center justify-center">
@@ -287,31 +461,157 @@ const ProductDetail = () => {
</div> </div>
<div> <div>
<h3 className="font-medium text-gray-800"> <h3 className="font-medium text-gray-800">
{product.UserID || "Unknown Seller"} {product.SellerName || "Unknown Seller"}
</h3> </h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Member since{" "} Member since {product.SellerJoinDate || "N/A"}
{product.seller ? product.seller.memberSince : "N/A"}
</p> </p>
</div> </div>
</div> </div>
<div className="text-sm text-gray-600">
<div>
<span className="font-medium">Rating:</span>{" "}
{product.seller ? `${product.seller.rating}/5` : "N/A"}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* <div className="mt-8"> {/* Reviews Section */}
<h2 className="text-xl font-bold text-gray-800 mb-4">Description</h2> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-800 mb-4">Reviews</h2>
<div className="bg-white border border-gray-200 p-6"> <div className="bg-white border border-gray-200 p-6">
<div className="text-gray-700">{product.Description}</div> {loading.reviews ? (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-green-500"></div>
</div>
) : error.reviews ? (
<div className="text-red-500 mb-4">
Error loading reviews: {error.reviews}
</div>
) : reviews.length === 0 ? (
<div className="text-gray-500 text-center py-6">
No reviews yet for this product
</div>
) : (
<div className="space-y-6">
{reviews.map((review) => (
<div
key={review.id || `review-${Math.random()}`}
className="border-b border-gray-200 pb-6 last:border-0 last:pb-0"
>
<div className="flex justify-between mb-2">
<div className="font-medium text-gray-800">
{review.ReviewerName}
</div>
<div className="text-sm text-gray-500">
{review.ReviewDate
? new Date(review.ReviewDate).toLocaleDateString()
: "Unknown date"}
</div>
</div>
<div className="flex items-center mb-2">
{renderStars(review.Rating || 0)}
<span className="ml-2 text-sm text-gray-600">
{review.Rating || 0}/5
</span>
</div>
<div className="text-gray-700">
{review.Comment || "No comment provided"}
</div>
</div>
))}
</div>
)}
</div> </div>
</div> */}
<div className="mt-4">
<button
onClick={() => setShowReviewForm(true)}
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4 rounded"
>
Write a Review
</button>
</div>
{/* Review Popup Form */}
{showReviewForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-gray-800">
Write a Review
</h3>
<button
onClick={() => setShowReviewForm(false)}
className="text-gray-500 hover:text-gray-700"
>
</button>
</div>
<form onSubmit={handleSubmitReview}>
<div className="mb-4">
<label className="block text-gray-700 mb-1">
Rating <span className="text-red-500">*</span>
</label>
<div className="flex items-center">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => handleRatingChange(star)}
className="focus:outline-none mr-1"
>
<Star
className={`h-6 w-6 ${
star <= reviewForm.rating
? "text-yellow-400 fill-yellow-400"
: "text-gray-300"
}`}
/>
</button>
))}
<span className="ml-2 text-gray-600">
{reviewForm.rating}/5
</span>
</div>
</div>
<div className="mb-4">
<label htmlFor="comment" className="block text-gray-700 mb-1">
Your Review <span className="text-red-500">*</span>
</label>
<textarea
id="comment"
value={reviewForm.comment}
onChange={handleReviewInputChange}
className="w-full p-3 border border-gray-300 rounded focus:outline-none focus:border-green-500"
rows="4"
required
></textarea>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={() => setShowReviewForm(false)}
className="px-4 py-2 border border-gray-300 rounded text-gray-700 hover:bg-gray-100"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
disabled={loading.submitting}
>
{loading.submitting ? "Submitting..." : "Submit Review"}
</button>
</div>
</form>
</div>
</div>
)}
</div>
</div> </div>
); );
}; };

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Filter, Grid, Heart, Tag, X } from "lucide-react"; import { X } from "lucide-react";
import { useLocation, Link } from "react-router-dom"; import { useLocation, Link } from "react-router-dom";
import axios from "axios"; import axios from "axios";
@@ -27,7 +27,7 @@ const SearchPage = () => {
try { try {
const response = await axios.get( const response = await axios.get(
`http://localhost:3030/api/search_products/search`, `http://localhost:3030/api/search/getProduct`,
{ {
params: { name: query }, params: { name: query },
}, },
@@ -96,12 +96,13 @@ const SearchPage = () => {
return ( return (
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<div className="flex flex-col md:flex-row gap-6"> <div className="flex flex-col md:flex-row gap-6">
{/* Filter sidebar */}
<div <div
className={` className={`
fixed inset-0 z-50 bg-white transform transition-transform duration-300 fixed inset-0 z-50 bg-white transform transition-transform duration-300
${isFilterOpen ? "translate-x-0" : "translate-x-full"} ${isFilterOpen ? "translate-x-0" : "translate-x-full"}
md:translate-x-0 md:relative md:block md:w-72 md:translate-x-0 md:relative md:block md:w-72
overflow-y-auto shadow-lg rounded-lg overflow-y-auto shadow-md
`} `}
> >
<div className="md:hidden flex justify-between items-center p-4 border-b"> <div className="md:hidden flex justify-between items-center p-4 border-b">
@@ -110,9 +111,8 @@ const SearchPage = () => {
<X className="text-gray-600" /> <X className="text-gray-600" />
</button> </button>
</div> </div>
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div className="bg-gray-50 rounded-lg p-3"> <div className="bg-gray-50 p-3">
<h3 className="font-semibold text-gray-700 mb-3">Price Range</h3> <h3 className="font-semibold text-gray-700 mb-3">Price Range</h3>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex space-x-2"> <div className="flex space-x-2">
@@ -126,7 +126,7 @@ const SearchPage = () => {
min: Number(e.target.value), min: Number(e.target.value),
})) }))
} }
className="w-full p-2 border rounded text-gray-700" className="w-full p-2 border text-gray-700"
/> />
<input <input
type="number" type="number"
@@ -138,7 +138,7 @@ const SearchPage = () => {
max: Number(e.target.value), max: Number(e.target.value),
})) }))
} }
className="w-full p-2 border rounded text-gray-700" className="w-full p-2 border text-gray-700"
/> />
</div> </div>
</div> </div>
@@ -146,13 +146,13 @@ const SearchPage = () => {
<div className="flex space-x-2"> <div className="flex space-x-2">
<button <button
onClick={applyFilters} onClick={applyFilters}
className="w-full bg-green-500 text-white p-3 rounded-lg hover:bg-green-600 transition-colors" className="w-full bg-green-500 text-white p-3 hover:bg-green-600 transition-colors"
> >
Apply Filters Apply Filters
</button> </button>
<button <button
onClick={resetFilters} onClick={resetFilters}
className="w-full bg-gray-200 text-gray-700 p-3 rounded-lg hover:bg-gray-300 transition-colors" className="w-full bg-gray-200 text-gray-700 p-3 hover:bg-gray-300 transition-colors"
> >
Reset Reset
</button> </button>
@@ -160,6 +160,7 @@ const SearchPage = () => {
</div> </div>
</div> </div>
{/* Main content */}
<div className="flex-1 mt-4 md:mt-0"> <div className="flex-1 mt-4 md:mt-0">
<h2 className="text-2xl font-bold text-gray-800"> <h2 className="text-2xl font-bold text-gray-800">
{filteredProducts.length} Results {filteredProducts.length} Results
@@ -170,18 +171,17 @@ const SearchPage = () => {
</span> </span>
)} )}
</h2> </h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mt-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
{filteredProducts.map((listing) => ( {filteredProducts.map((listing) => (
<Link <Link
key={listing.id} key={listing.id}
to={`/product/${listing.id}`} to={`/product/${listing.id}`}
className="bg-white border border-gray-200 rounded-lg hover:shadow-md transition-shadow block" className="bg-white border border-gray-200 hover:shadow-md transition-shadow block"
> >
<img <img
src={listing.image} src={listing.image}
alt={listing.title} alt={listing.title}
className="w-full h-48 object-cover rounded-t-lg" className="w-full h-48 object-cover"
/> />
<div className="p-4"> <div className="p-4">
<h3 className="text-lg font-medium text-gray-800"> <h3 className="text-lg font-medium text-gray-800">

View File

@@ -1,13 +1,224 @@
import { useState } from 'react'; import { useState, useEffect } from "react";
import { Link } from 'react-router-dom'; import ProductForm from "../components/ProductForm";
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from 'lucide-react';
const Selling = () => { const Selling = () => {
// State to store user's products
const [products, setProducts] = useState([]);
// State to control when editing form is shown
const [showForm, setShowForm] = useState(false);
// State to store the product being edited (or empty for new product)
const [editingProduct, setEditingProduct] = useState({
name: "",
price: "",
description: "",
categories: [],
status: "Unsold",
images: [],
});
// Simulate fetching products from API/database on component mount
useEffect(() => {
// This would be replaced with a real API call
const fetchProducts = async () => {
// Mock data
const mockProducts = [
{
id: "1",
name: "Vintage Camera",
price: "299.99",
description: "A beautiful vintage film camera in excellent condition",
categories: ["Electronics", "Art & Collectibles"],
status: "Unsold",
images: ["/public/Pictures/Dell1.jpg"],
},
{
id: "2",
name: "Leather Jacket",
price: "149.50",
description: "Genuine leather jacket, worn only a few times",
categories: ["Clothing"],
status: "Unsold",
images: [],
},
];
setProducts(mockProducts);
};
fetchProducts();
}, []);
// Handle creating or updating a product
const handleSaveProduct = () => {
if (editingProduct.id) {
// Update existing product
setProducts(
products.map((p) => (p.id === editingProduct.id ? editingProduct : p)),
);
} else {
// Create new product
const newProduct = {
...editingProduct,
id: Date.now().toString(), // Generate a temporary ID
};
setProducts([...products, newProduct]);
}
// Reset form and hide it
setShowForm(false);
setEditingProduct({
name: "",
price: "",
description: "",
categories: [],
status: "Unsold",
images: [],
});
};
// Handle product deletion
const handleDeleteProduct = (productId) => {
if (window.confirm("Are you sure you want to delete this product?")) {
setProducts(products.filter((p) => p.id !== productId));
}
};
// Handle editing a product
const handleEditProduct = (product) => {
setEditingProduct({
...product,
images: product.images || [], // Ensure images array exists
});
setShowForm(true);
};
// Handle adding a new product
const handleAddProduct = () => {
setEditingProduct({
name: "",
price: "",
description: "",
categories: [],
status: "Unsold",
images: [],
});
setShowForm(true);
};
return ( return (
<div> <div className="container mx-auto p-4 max-w-6xl">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">My Listings</h1>
{!showForm && (
<button
onClick={handleAddProduct}
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
>
+ Add New Product
</button>
)}
</div>
{showForm ? (
<ProductForm
editingProduct={editingProduct}
setEditingProduct={setEditingProduct}
onSave={handleSaveProduct}
onCancel={() => setShowForm(false)}
/>
) : (
<>
{products.length === 0 ? (
<div className="text-center py-10 bg-gray-50">
<p className="text-gray-500 mb-4">
You don't have any listings yet
</p>
<button
onClick={handleAddProduct}
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
>
Create Your First Listing
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map((product) => (
<div
key={product.id}
className="border-2 border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
>
<div className="h-48 bg-gray-200 flex items-center justify-center">
{product.images && product.images.length > 0 ? (
<img
src={product.images[0] || ""}
alt={product.name}
className="w-full h-full object-cover"
/>
) : (
<div className="text-gray-400">No image</div>
)}
</div>
<div className="p-4">
<div className="flex justify-between items-start">
<h3 className="text-lg font-semibold text-gray-800">
{product.name}
</h3>
<span
className={`px-2 py-1 text-xs ${
product.status === "Sold"
? "bg-gray-200 text-gray-700"
: "bg-emerald-100 text-emerald-800"
}`}
>
{product.status}
</span>
</div>
<p className="text-emerald-600 font-bold mt-1">
${product.price}
</p>
{product.categories && product.categories.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{product.categories.map((category) => (
<span
key={category}
className="text-xs bg-gray-100 text-gray-600 px-2 py-1 "
>
{category}
</span>
))}
</div>
)}
<p className="text-gray-500 text-sm mt-2 line-clamp-2">
{product.description}
</p>
<div className="mt-4 flex justify-end gap-2">
<button
onClick={() => handleDeleteProduct(product.id)}
className="text-red-600 hover:text-red-800"
>
Delete
</button>
<button
onClick={() => handleEditProduct(product)}
className="text-emerald-600 hover:text-emerald-800 font-medium"
>
Edit
</button>
</div>
</div>
</div>
))}
</div>
)}
</>
)}
</div> </div>
); );
}; };
export default Selling; export default Selling;

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { User, Lock, Trash2, History, Search, Shield } from "lucide-react"; import { User, Lock, Trash2, History, Search, Shield } from "lucide-react";
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
const Settings = () => { const Settings = () => {
const [userData, setUserData] = useState({ const [userData, setUserData] = useState({
@@ -16,6 +17,8 @@ const Settings = () => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [showAlert, setShowAlert] = useState(false);
const storedUser = JSON.parse(sessionStorage.getItem("user"));
// Fetch user data when component mounts // Fetch user data when component mounts
useEffect(() => { useEffect(() => {
@@ -88,6 +91,25 @@ const Settings = () => {
})); }));
}; };
const removeHistory = async () => {
const response = await fetch(
"http://localhost:3030/api/history/delHistory",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: storedUser.ID,
}),
},
);
if (response.ok) {
setShowAlert(true);
}
};
const handleUpdateProfile = async () => { const handleUpdateProfile = async () => {
try { try {
// Ensure userId is present // Ensure userId is present
@@ -159,12 +181,6 @@ const Settings = () => {
} }
}; };
const handleDeleteHistory = (type) => {
// TODO: Delete the specified history
console.log(`Deleting ${type} history`);
alert(`${type} history deleted successfully!`);
};
const handleDeleteAccount = async () => { const handleDeleteAccount = async () => {
if ( if (
window.confirm( window.confirm(
@@ -416,6 +432,12 @@ const Settings = () => {
</div> </div>
{/* Privacy Section */} {/* Privacy Section */}
{showAlert && (
<FloatingAlert
message="Removed Your History! 😉"
onClose={() => setShowAlert(false)}
/>
)}
<div className="bg-white border border-gray-200 mb-6"> <div className="bg-white border border-gray-200 mb-6">
<div className="border-b border-gray-200 p-4"> <div className="border-b border-gray-200 p-4">
<div className="flex items-center"> <div className="flex items-center">
@@ -427,38 +449,17 @@ const Settings = () => {
<div className="p-4"> <div className="p-4">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center pb-4 border-b border-gray-100"> <div className="flex justify-between items-center pb-4 border-b border-gray-100">
<div className="flex items-start">
<Search className="h-5 w-5 text-gray-500 mr-2 mt-0.5" />
<div>
<h3 className="font-medium text-gray-800">Search History</h3>
<p className="text-sm text-gray-500">
Delete all your search history on StudentMarket
</p>
</div>
</div>
<button
onClick={() => handleDeleteHistory("search")}
className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 flex items-center"
>
<Trash2 className="h-4 w-4 mr-1" />
Delete
</button>
</div>
<div className="flex justify-between items-center">
<div className="flex items-start"> <div className="flex items-start">
<History className="h-5 w-5 text-gray-500 mr-2 mt-0.5" /> <History className="h-5 w-5 text-gray-500 mr-2 mt-0.5" />
<div> <div>
<h3 className="font-medium text-gray-800"> <h3 className="font-medium text-gray-800"> History</h3>
Browsing History
</h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Delete all your browsing history on StudentMarket Delete all your history on Market
</p> </p>
</div> </div>
</div> </div>
<button <button
onClick={() => handleDeleteHistory("browsing")} onClick={() => removeHistory()}
className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 flex items-center" className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 flex items-center"
> >
<Trash2 className="h-4 w-4 mr-1" /> <Trash2 className="h-4 w-4 mr-1" />
@@ -472,7 +473,7 @@ const Settings = () => {
{/* Delete Account (Danger Zone) */} {/* Delete Account (Danger Zone) */}
<div className="bg-white border border-red-200 mb-6"> <div className="bg-white border border-red-200 mb-6">
<div className="border-b border-red-200 p-4 bg-red-50"> <div className="border-b border-red-200 p-4 bg-red-50">
<h2 className="text-lg font-medium text-red-700">Danger Zone</h2> <h2 className="text-lg font-medium text-red-700">Danger Zone !!!</h2>
</div> </div>
<div className="p-4"> <div className="p-4">

View File

@@ -1,13 +1,8 @@
import { useState } from 'react'; import { useState } from "react";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from 'lucide-react';
const Transactions = () => { const Transactions = () => {
return ( return <div></div>;
<div>
</div>
);
}; };
export default Transactions; export default Transactions;

View File

@@ -1,5 +1,4 @@
-- Inserting sample data into the Marketplace database -- Inserting sample data into the Marketplace database
-- Clear existing data (if needed)
SET SET
FOREIGN_KEY_CHECKS = 0; FOREIGN_KEY_CHECKS = 0;
@@ -59,33 +58,6 @@ VALUES
'hashedpassword2', 'hashedpassword2',
'555-234-5678', '555-234-5678',
'456 Oak Ave, Calgary, AB' '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 User Roles
@@ -93,10 +65,7 @@ INSERT INTO
UserRole (UserID, Client, Admin) UserRole (UserID, Client, Admin)
VALUES VALUES
(1, TRUE, TRUE), (1, TRUE, TRUE),
(2, TRUE, FALSE), (2, TRUE, FALSE);
(3, TRUE, FALSE),
(4, TRUE, FALSE),
(5, TRUE, FALSE);
-- Insert Categories -- Insert Categories
INSERT INTO INSERT INTO
@@ -161,7 +130,7 @@ VALUES
'HP Laptop', 'HP Laptop',
699.99, 699.99,
1, 1,
2, 1,
'2023 HP Pavilion, 16GB RAM, 512GB SSD', '2023 HP Pavilion, 16GB RAM, 512GB SSD',
2, 2,
'2024-10-10 14:30:00' '2024-10-10 14:30:00'
@@ -171,7 +140,7 @@ VALUES
'Dorm Desk', 'Dorm Desk',
120.00, 120.00,
1, 1,
3, 2,
'Sturdy desk perfect for studying, minor scratches', 'Sturdy desk perfect for studying, minor scratches',
3, 3,
'2024-10-12 09:15:00' '2024-10-12 09:15:00'
@@ -181,7 +150,7 @@ VALUES
'University Hoodie', 'University Hoodie',
35.00, 35.00,
3, 3,
1, 2,
'Size L, university logo, worn twice', 'Size L, university logo, worn twice',
4, 4,
'2024-10-14 16:45:00' '2024-10-14 16:45:00'
@@ -191,7 +160,7 @@ VALUES
'Basketball', 'Basketball',
25.50, 25.50,
1, 1,
4, 2,
'Slightly used indoor basketball', 'Slightly used indoor basketball',
5, 5,
'2024-10-11 11:20:00' '2024-10-11 11:20:00'
@@ -201,7 +170,7 @@ VALUES
'Acoustic Guitar', 'Acoustic Guitar',
175.00, 175.00,
1, 1,
2, 1,
'Beginner acoustic guitar with case', 'Beginner acoustic guitar with case',
6, 6,
'2024-10-09 13:10:00' '2024-10-09 13:10:00'
@@ -211,7 +180,7 @@ VALUES
'Physics Textbook', 'Physics Textbook',
65.00, 65.00,
2, 2,
5, 2,
'University Physics 14th Edition, good condition', 'University Physics 14th Edition, good condition',
1, 1,
'2024-10-08 10:30:00' '2024-10-08 10:30:00'
@@ -221,7 +190,7 @@ VALUES
'Mini Fridge', 'Mini Fridge',
85.00, 85.00,
1, 1,
3, 1,
'Small dorm fridge, works perfectly', 'Small dorm fridge, works perfectly',
8, 8,
'2024-10-13 15:00:00' '2024-10-13 15:00:00'
@@ -231,7 +200,7 @@ VALUES
'PlayStation 5 Controller', 'PlayStation 5 Controller',
55.00, 55.00,
1, 1,
4, 2,
'Extra controller, barely used', 'Extra controller, barely used',
9, 9,
'2024-10-07 17:20:00' '2024-10-07 17:20:00'
@@ -241,7 +210,7 @@ VALUES
'Mountain Bike', 'Mountain Bike',
350.00, 350.00,
1, 1,
5, 1,
'Trek mountain bike, great condition, new tires', 'Trek mountain bike, great condition, new tires',
10, 10,
'2024-10-06 14:00:00' '2024-10-06 14:00:00'
@@ -271,7 +240,7 @@ VALUES
'Graphing Calculator', 'Graphing Calculator',
75.00, 75.00,
1, 1,
3, 1,
'TI-84 Plus, perfect working condition', 'TI-84 Plus, perfect working condition',
12, 12,
'2024-10-03 11:15:00' '2024-10-03 11:15:00'
@@ -281,7 +250,7 @@ VALUES
'Yoga Mat', 'Yoga Mat',
20.00, 20.00,
1, 1,
4, 2,
'Thick yoga mat, barely used', 'Thick yoga mat, barely used',
13, 13,
'2024-10-02 16:00:00' '2024-10-02 16:00:00'
@@ -291,7 +260,7 @@ VALUES
'Winter Jacket', 'Winter Jacket',
120.00, 120.00,
1, 1,
5, 1,
'Columbia winter jacket, size XL, very warm', 'Columbia winter jacket, size XL, very warm',
26, 26,
'2024-10-01 10:20:00' '2024-10-01 10:20:00'
@@ -301,7 +270,7 @@ VALUES
'Computer Science Textbook', 'Computer Science Textbook',
70.00, 70.00,
1, 1,
1, 2,
'Introduction to Algorithms, like new', 'Introduction to Algorithms, like new',
1, 1,
'2024-09-30 14:30:00' '2024-09-30 14:30:00'
@@ -321,7 +290,7 @@ VALUES
'Scientific Calculator', 'Scientific Calculator',
25.00, 25.00,
1, 1,
3, 1,
'Casio scientific calculator', 'Casio scientific calculator',
12, 12,
'2024-09-28 11:30:00' '2024-09-28 11:30:00'
@@ -331,7 +300,7 @@ VALUES
'Bluetooth Speaker', 'Bluetooth Speaker',
45.00, 45.00,
1, 1,
4, 1,
'JBL Bluetooth speaker, great sound', 'JBL Bluetooth speaker, great sound',
23, 23,
'2024-09-27 15:45:00' '2024-09-27 15:45:00'
@@ -341,26 +310,38 @@ VALUES
'Backpack', 'Backpack',
40.00, 40.00,
1, 1,
5, 2,
'North Face backpack, lots of pockets', 'North Face backpack, lots of pockets',
22, 22,
'2024-09-26 09:15:00' '2024-09-26 09:15:00'
); );
-- Insert Image URLs
INSERT INTO INSERT INTO
Image_URL (URL, ProductID) Image_URL (URL, ProductID)
VALUES VALUES
('/image1.avif', 1), ('/Pictures/Dell1.jpg', 1),
('/image1.avif', 2), ('/Pictures/Dell2.jpg', 1),
('/image1.avif', 3), ('/Pictures/Dell3.jpg', 1),
('/image1.avif', 4), ('/Pictures/HP-Laptop1.jpg', 2),
('/image1.avif', 5), ('/Pictures/HP-Laptop1.jpg', 2),
('/image1.avif', 6), ('/Pictures/Dorm-Desk.jpg', 3),
('/image1.avif', 7), ('/Pictures/University-Hoodie.jpg', 4),
('/image1.avif', 8), ('/Pictures/Basketball.jpg', 5),
('/image1.avif', 9), ('/Pictures/Acoustic-Guitar.jpg', 6),
('/image1.avif', 10); ('/Pictures/Physics-Textbook.jpg', 7),
('/Pictures/Mini-Fridge.jpg', 8),
('/Pictures/Controller.jpg', 9),
('/Pictures/Mountain-Bike.jpg', 10),
('/Pictures/Wireless-Mouse.jpg', 11),
('/Pictures/Lab-Coat.jpg', 12),
('/Pictures/Calculator.jpg', 13),
('/Pictures/Yoga-Mat.jpg', 14),
('/Pictures/Winter-Jacket.jpg', 15),
('/Pictures/CS-Textbook.jpg', 16),
('/Pictures/Desk-Lamp.jpg', 17),
('/Pictures/HP-Calculator.jpg', 18),
('/Pictures/Bluetooth-Speaker.jpg', 19),
('/Pictures/Backpack.jpg', 20);
-- Insert Product-Category relationships (products with multiple categories) -- Insert Product-Category relationships (products with multiple categories)
INSERT INTO INSERT INTO
@@ -418,84 +399,21 @@ VALUES
(20, 17), (20, 17),
(20, 24); (20, 24);
-- Backpack: Backpacks & Bags, School Supplies, Dorm Essentials
-- Insert History records -- Insert History records
--
INSERT INTO INSERT INTO
History (HistoryID, UserID, ProductID, Date) History (HistoryID, UserID, ProductID)
VALUES VALUES
(1, 1, 1, '2024-10-15 11:30:00'), (1, 1, 1),
(2, 1, 2, '2024-10-14 13:45:00'), (2, 1, 3),
(3, 1, 5, '2024-10-13 09:20:00'), (3, 1, 5),
(4, 1, 4, '2024-10-12 16:10:00'); (4, 1, 7),
(5, 1, 9),
-- (6, 1, 11),
INSERT INTO (7, 2, 2),
History (HistoryID, UserID, ProductID, Date) (8, 2, 4),
VALUES (9, 2, 5),
(1, 2, 1, '2024-10-15 11:30:00'), -- User 2 viewed Calculus Textbook (10, 1, 15),
(2, 3, 2, '2024-10-14 13:45:00'), -- User 3 viewed HP Laptop (11, 1, 18);
(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 Favorites
INSERT INTO INSERT INTO
@@ -505,9 +423,9 @@ VALUES
(1, 7), -- User 1 likes Physics Textbook (1, 7), -- User 1 likes Physics Textbook
(2, 3), -- User 2 likes Dorm Desk (2, 3), -- User 2 likes Dorm Desk
(2, 10), -- User 2 likes Mountain Bike (2, 10), -- User 2 likes Mountain Bike
(3, 6), -- User 3 likes Acoustic Guitar (1, 6), -- User 3 likes Acoustic Guitar
(4, 5), -- User 4 likes Basketball (1, 5), -- User 4 likes Basketball
(5, 8); (2, 8);
-- User 5 likes Mini Fridge -- User 5 likes Mini Fridge
-- Insert Transactions -- Insert Transactions
@@ -520,242 +438,19 @@ INSERT INTO
PaymentStatus PaymentStatus
) )
VALUES VALUES
(1, 2, 1, '2024-10-16 10:30:00', 'Completed'), (1, 1, 1, '2024-10-16 10:30:00', 'Completed'),
(2, 3, 6, '2024-10-15 15:45:00', 'Completed'), (2, 1, 6, '2024-10-15 15:45:00', 'Completed'),
(3, 4, 8, '2024-10-14 12:20:00', 'Pending'), (3, 1, 8, '2024-10-14 12:20:00', 'Pending'),
(4, 5, 10, '2024-10-13 17:10:00', 'Completed'), (4, 2, 10, '2024-10-13 17:10:00', 'Completed'),
(5, 1, 4, '2024-10-12 14:30:00', 'Completed'); (5, 2, 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 INSERT INTO
Recommendation (RecommendationID_PK, UserID, RecommendedProductID) Review (UserID, ProductID, Comment, Rating, Date)
VALUES
(12, 1, 19),
(13, 1, 9),
(14, 1, 11),
(15, 1, 16),
-- Insert Authentication records
INSERT INTO
AuthVerification (Email, VerificationCode, Authenticated, Date)
VALUES VALUES
( (
'john.doe@example.com', 1,
'123456', 1,
TRUE, 'This is a great fake product! Totally recommend it.',
'2024-10-01 09:00:00' 5,
), NOW ()
(
'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

View File

@@ -51,7 +51,7 @@ CREATE TABLE Image_URL (
-- Fixed Review Entity (Many-to-One with User, Many-to-One with Product) -- Fixed Review Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Review ( CREATE TABLE Review (
ReviewID INT PRIMARY KEY, ReviewID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT, UserID INT,
ProductID INT, ProductID INT,
Comment TEXT, Comment TEXT,
@@ -77,16 +77,17 @@ CREATE TABLE Transaction (
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product) -- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Recommendation ( CREATE TABLE Recommendation (
RecommendationID_PK INT PRIMARY KEY, RecommendationID_PK INT AUTO_INCREMENT PRIMARY KEY,
UserID INT, UserID INT,
RecommendedProductID INT, RecommendedProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID)
); );
-- History Entity (Many-to-One with User, Many-to-One with Product) -- History Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE History ( CREATE TABLE History (
HistoryID INT PRIMARY KEY, HistoryID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT, UserID INT,
ProductID INT, ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -100,7 +101,8 @@ CREATE TABLE Favorites (
UserID INT, UserID INT,
ProductID INT, ProductID INT,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) FOREIGN KEY (ProductID) REFERENCES Product (ProductID),
UNIQUE (UserID, ProductID)
); );
-- Product-Category Junction Table (Many-to-Many) -- Product-Category Junction Table (Many-to-Many)
@@ -120,277 +122,3 @@ CREATE TABLE AuthVerification (
Authenticated BOOLEAN DEFAULT FALSE, Authenticated BOOLEAN DEFAULT FALSE,
Date DATETIME DEFAULT CURRENT_TIMESTAMP Date DATETIME DEFAULT CURRENT_TIMESTAMP
); );
-- -- Insert sample categories
-- INSERT INTO
-- Category (CategoryID, Name)
-- VALUES
-- (1, 'Electronics'),
-- (2, 'Clothing'),
-- (3, 'Books'),
-- (4, 'Home & Garden'),
-- (5, 'Sports & Outdoors');
-- -- -- USER CRUD OPERATIONS
-- -- -- Create User (INSERT)
-- -- INSERT INTO
-- -- User (Name, Email, UCID, Password, Phone, Address)
-- -- VALUES
-- -- (
-- -- 'John Doe',
-- -- 'john@example.com',
-- -- 'UC123456',
-- -- 'hashed_password_here',
-- -- '555-123-4567',
-- -- '123 Main St'
-- -- );
-- -- -- Set user role
-- -- INSERT INTO
-- -- UserRole (UserID, Client, Admin)
-- -- VALUES
-- -- (LAST_INSERT_ID (), TRUE, FALSE);
-- -- -- Read User (SELECT)
-- -- SELECT
-- -- u.*,
-- -- ur.Client,
-- -- ur.Admin
-- -- FROM
-- -- User u
-- -- JOIN UserRole ur ON u.UserID = ur.UserID
-- -- WHERE
-- -- u.UserID = 1;
-- -- -- Update User (UPDATE)
-- -- UPDATE User
-- -- SET
-- -- Name = 'John Smith',
-- -- Phone = '555-987-6543',
-- -- Address = '456 Elm St'
-- -- WHERE
-- -- UserID = 1;
-- -- -- Update User Role
-- -- UPDATE UserRole
-- -- SET
-- -- Admin = TRUE
-- -- WHERE
-- -- UserID = 1;
-- -- -- PRODUCT CRUD OPERATIONS
-- -- -- Create Product (INSERT)
-- -- INSERT INTO
-- -- Product (
-- -- ProductID,
-- -- Name,
-- -- Price,
-- -- StockQuantity,
-- -- UserID,
-- -- Description,
-- -- CategoryID
-- -- )
-- -- VALUES
-- -- (
-- -- 1,
-- -- 'Smartphone',
-- -- 599.99,
-- -- 50,
-- -- 1,
-- -- 'Latest model smartphone with amazing features',
-- -- 1
-- -- );
-- -- -- Add product images with the placeholder URL
-- -- INSERT INTO
-- -- Image_URL (URL, ProductID)
-- -- VALUES
-- -- ('https://picsum.photos/id/237/200/300', 1),
-- -- ('https://picsum.photos/id/237/200/300', 1);
-- -- -- Create another product for recommendations
-- -- INSERT INTO
-- -- Product (
-- -- ProductID,
-- -- Name,
-- -- Price,
-- -- StockQuantity,
-- -- UserID,
-- -- Description,
-- -- CategoryID
-- -- )
-- -- VALUES
-- -- (
-- -- 2,
-- -- 'Tablet',
-- -- 799.99,
-- -- 30,
-- -- 1,
-- -- 'High-performance tablet',
-- -- 1
-- -- );
-- -- -- Add placeholder images for the second product
-- -- INSERT INTO
-- -- Image_URL (URL, ProductID)
-- -- VALUES
-- -- ('https://picsum.photos/id/237/200/300', 2),
-- -- ('https://picsum.photos/id/237/200/300', 2);
-- -- -- Read Product (SELECT)
-- -- SELECT
-- -- p.*,
-- -- c.Name as CategoryName,
-- -- u.Name as SellerName,
-- -- i.URL as ImageURL
-- -- FROM
-- -- Product p
-- -- JOIN Category c ON p.CategoryID = c.CategoryID
-- -- JOIN User u ON p.UserID = u.UserID
-- -- LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
-- -- WHERE
-- -- p.ProductID = 1;
-- -- -- Update Product (UPDATE)
-- -- UPDATE Product
-- -- SET
-- -- Name = 'Premium Smartphone',
-- -- Price = 649.99,
-- -- StockQuantity = 45,
-- -- Description = 'Updated description with new features'
-- -- WHERE
-- -- ProductID = 1;
-- -- -- CATEGORY CRUD OPERATIONS
-- -- -- Create Category (INSERT)
-- -- INSERT INTO
-- -- Category (CategoryID, Name)
-- -- VALUES
-- -- (6, 'Toys & Games');
-- -- -- Read Category (SELECT)
-- -- SELECT
-- -- *
-- -- FROM
-- -- Category
-- -- WHERE
-- -- CategoryID = 6;
-- -- -- Update Category (UPDATE)
-- -- UPDATE Category
-- -- SET
-- -- Name = 'Toys & Children''s Games'
-- -- WHERE
-- -- CategoryID = 6;
-- -- -- REVIEW OPERATIONS
-- -- INSERT INTO
-- -- Review (ReviewID, UserID, ProductID, Comment, Rating)
-- -- VALUES
-- -- (
-- -- 1,
-- -- 1,
-- -- 1,
-- -- 'Great product, very satisfied with the purchase!',
-- -- 5
-- -- );
-- -- -- TRANSACTION OPERATIONS
-- -- INSERT INTO
-- -- Transaction (TransactionID, UserID, ProductID, PaymentStatus)
-- -- VALUES
-- -- (1, 1, 1, 'Completed');
-- -- -- HISTORY OPERATIONS
-- -- INSERT INTO
-- -- History (HistoryID, UserID, ProductID)
-- -- VALUES
-- -- (1, 1, 1);
-- -- -- Read History (SELECT)
-- -- SELECT
-- -- h.*,
-- -- p.Name as ProductName
-- -- FROM
-- -- History h
-- -- JOIN Product p ON h.ProductID = p.ProductID
-- -- WHERE
-- -- h.UserID = 1
-- -- ORDER BY
-- -- h.Date DESC;
-- -- -- FAVORITES OPERATIONS
-- -- INSERT INTO
-- -- Favorites (UserID, ProductID)
-- -- VALUES
-- -- (1, 1);
-- -- -- Read Favorites (SELECT)
-- -- SELECT
-- -- f.*,
-- -- p.Name as ProductName,
-- -- p.Price
-- -- FROM
-- -- Favorites f
-- -- JOIN Product p ON f.ProductID = p.ProductID
-- -- WHERE
-- -- f.UserID = 1;
-- -- -- RECOMMENDATION OPERATIONS
-- -- INSERT INTO
-- -- Recommendation (RecommendationID_PK, UserID, RecommendedProductID)
-- -- VALUES
-- -- (1, 1, 2);
-- -- -- Read Recommendations (SELECT)
-- -- SELECT
-- -- r.*,
-- -- p.Name as RecommendedProductName,
-- -- p.Price,
-- -- p.Description
-- -- FROM
-- -- Recommendation r
-- -- JOIN Product p ON r.RecommendedProductID = p.ProductID
-- -- WHERE
-- -- r.UserID = 1;
-- -- -- Authentication Operations
-- -- -- Create verification code
-- -- INSERT INTO
-- -- AuthVerification (Email, VerificationCode)
-- -- VALUES
-- -- ('new_user@example.com', '123456');
-- -- -- Update authentication status
-- -- UPDATE AuthVerification
-- -- SET
-- -- Authenticated = TRUE
-- -- WHERE
-- -- Email = 'new_user@example.com'
-- -- AND VerificationCode = '123456';
-- -- -- Get top-selling products
-- -- SELECT
-- -- p.ProductID,
-- -- p.Name,
-- -- COUNT(t.TransactionID) as SalesCount,
-- -- SUM(p.Price) as TotalRevenue
-- -- FROM
-- -- Product p
-- -- JOIN Transaction t ON p.ProductID = t.ProductID
-- -- WHERE
-- -- t.PaymentStatus = 'Completed'
-- -- GROUP BY
-- -- p.ProductID,
-- -- p.Name
-- -- ORDER BY
-- -- SalesCount DESC
-- -- LIMIT
-- -- 10;
-- -- -- Get highest-rated products
-- -- SELECT
-- -- p.ProductID,
-- -- p.Name,
-- -- AVG(r.Rating) as AverageRating,
-- -- COUNT(r.ReviewID) as ReviewCount
-- -- FROM
-- -- Product p
-- -- JOIN Review r ON p.ProductID = r.ProductID
-- -- GROUP BY
-- -- p.ProductID,
-- -- p.Name
-- -- HAVING
-- -- ReviewCount >= 5
-- -- ORDER BY
-- -- AverageRating DESC
-- -- LIMIT
-- -- 10;
-- -- -- Get user purchase history with product details
-- -- SELECT
-- -- t.TransactionID,
-- -- t.Date,
-- -- p.Name,
-- -- p.Price,
-- -- t.PaymentStatus
-- -- FROM
-- -- Transaction t
-- -- JOIN Product p ON t.ProductID = p.ProductID
-- -- WHERE
-- -- t.UserID = 1
-- -- ORDER BY
-- -- t.Date DESC;

View File

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

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "Campus-Plug",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

Binary file not shown.

View File

@@ -0,0 +1,163 @@
# pip install mysql.connector
#
import mysql.connector
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import logging
from unittest import result
def database():
db_connection = mysql.connector.connect(
host = "localhost",
port = "3306",
user = "root",
database = "Marketplace"
)
return db_connection
def get_popular_products():
pass
def delete_user_recommendation(userID, Array):
db_con = database()
cursor = db_con.cursor()
try:
for item in Array:
#Product ID starts form index 1
item_value = item + 1
print(item_value)
# Use parameterized queries to prevent SQL injection
cursor.execute(f"INTO Recommendation (UserID, RecommendedProductID) VALUES ({userID}, {item_value});")
db_con.commit()
#results = cursor.fetchall()
#print(results)
except:
pass
def get_all_products():
db_con = database()
cursor = db_con.cursor()
cursor.execute("SELECT CategoryID FROM Category")
categories = cursor.fetchall()
select_clause = "SELECT p.ProductID"
for category in categories:
category_id = category[0]
select_clause += f", MAX(CASE WHEN pc.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`"
final_query = f"""
{select_clause}
FROM Product p
LEFT JOIN Product_Category pc ON p.ProductID = pc.ProductID
LEFT JOIN Category c ON pc.CategoryID = c.CategoryID
GROUP BY p.ProductID;
"""
cursor.execute(final_query)
results = cursor.fetchall()
final = []
for row in results:
text_list = list(row)
text_list.pop(0)
final.append(text_list)
cursor.close()
db_con.close()
return final
def get_user_history(user_id):
db_con = database()
cursor = db_con.cursor()
cursor.execute("SELECT CategoryID FROM Category")
categories = cursor.fetchall()
select_clause = "SELECT p.ProductID"
for category in categories:
category_id = category[0] # get the uid of the catefory and then append that to the new column
select_clause += f", MAX(CASE WHEN pc.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`"
final_query = f"""
{select_clause}
FROM Product p
LEFT JOIN Product_Category pc ON p.ProductID = pc.ProductID
LEFT JOIN Category c ON pc.CategoryID = c.CategoryID
where p.ProductID in (select ProductID from History where UserID = {user_id})
GROUP BY p.ProductID;
"""
cursor.execute(final_query)
results = cursor.fetchall()
final = []
for row in results:
text_list = list(row)
text_list.pop(0)
final.append(text_list)
cursor.close()
db_con.close()
return final
def get_recommendations(user_id, top_n=10):
try:
# Get all products and user history with their category vectors
all_products = get_all_products()
user_history = get_user_history(user_id)
# if not user_history:
# #Cold start: return popular products
# return get_popular_products(top_n)
# Calculate similarity between all products and user history
user_profile = np.mean(user_history, axis=0) # Average user preferences
similarities = cosine_similarity([user_profile], all_products)
# finds the indices of the top N products that have the highest
# cosine similarity with the user's profile and sorted from most similar to least similar.
product_indices = similarities[0].argsort()[-top_n:][::-1]
print("product", product_indices)
# Get the recommended product IDs
recommended_products = [all_products[i][0] for i in product_indices] # Product IDs
# Upload the recommendations to the database
history_upload(user_id, product_indices) # Pass the indices directly to history_upload
# Return recommended product IDs
return recommended_products
except Exception as e:
logging.error(f"Recommendation error for user {user_id}: {str(e)}")
# return get_popular_products(top_n) # Fallback to popular products
def history_upload(userID, anrr):
db_con = database()
cursor = db_con.cursor()
try:
for item in anrr:
#Product ID starts form index 1
item_value = item + 1
print(item_value)
# Use parameterized queries to prevent SQL injection
cursor.execute(f"INSERT INTO Recommendation (UserID, RecommendedProductID) VALUES ({userID}, {item_value});")
# Commit the changes
db_con.commit()
# If you need results, you'd typically fetch them after a SELECT query
#results = cursor.fetchall()
#print(results)
except Exception as e:
print(f"Error: {e}")
db_con.rollback()
finally:
# Close the cursor and connection
cursor.close()
db_con.close()

View File

@@ -1,48 +0,0 @@
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

@@ -1,5 +1,11 @@
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
from flask_cors import CORS from flask_cors import CORS
from app import get_recommendations
app = Flask(__name__) app = Flask(__name__)
CORS(app) # Enable CORS for all routes CORS(app) # Enable CORS for all routes
@@ -15,6 +21,8 @@ def handle_session_data():
if not user_id or not email or is_authenticated is None: if not user_id or not email or is_authenticated is None:
return jsonify({'error': 'Invalid data'}), 400 return jsonify({'error': 'Invalid data'}), 400
get_recommendations(user_id)
print(f"Received session data: User ID: {user_id}, Email: {email}, Authenticated: {is_authenticated}") print(f"Received session data: User ID: {user_id}, Email: {email}, Authenticated: {is_authenticated}")
return jsonify({'message': 'Session data received successfully'}) return jsonify({'message': 'Session data received successfully'})