74 Commits

Author SHA1 Message Date
estherdev03
15eddb8d13 Fix clearn up expired code 2025-04-20 22:14:04 -06:00
estherdev03
2e98a88db9 Merge branch 'esther2' of https://github.com/MannPatel0/Campus-Plug into esther2 2025-04-20 22:08:38 -06:00
Esther Tran
bcb912b6ce Merge pull request #5 from MannPatel0/main
merge
2025-04-20 22:08:03 -06:00
Mann Patel
8347689f6c uploads 2025-04-20 22:07:40 -06:00
estherdev03
e83b3640a5 Merge branch 'esther2' of https://github.com/MannPatel0/Campus-Plug into esther2 2025-04-20 22:05:42 -06:00
estherdev03
bd69bed934 Merge branch 'main' of https://github.com/MannPatel0/Campus-Plug into esther2 2025-04-20 22:03:32 -06:00
estherdev03
51dffdae30 fix clean up expired code 2025-04-20 22:00:19 -06:00
Mann Patel
1c8a7522e2 Merge branch 'mann-Branch' 2025-04-20 21:52:15 -06:00
Mann Patel
77a35810fd home and selligns page polished 2025-04-20 21:42:34 -06:00
Esther Tran
8dcaff3d6d Merge pull request #4 from MannPatel0/main
Merge pull request #3 from MannPatel0/esther2
2025-04-20 21:01:54 -06:00
Esther Tran
51cd90a409 Merge pull request #3 from MannPatel0/esther2
Finish admin
2025-04-20 20:59:36 -06:00
estherdev03
444b436983 Add transaction section to admin dashboard 2025-04-20 20:56:14 -06:00
Mann Patel
bcf849611f selling's page is now complete 2025-04-20 20:46:27 -06:00
Mann Patel
f223f3717d updating work 2025-04-20 19:09:02 -06:00
Mann Patel
0c08dbc5ce updating products 2025-04-20 17:46:00 -06:00
Mann Patel
6ef4a22e9f update on category 2025-04-20 12:50:46 -06:00
estherdev03
26cd50ab6f Finish admin dashboard and update sql code 2025-04-20 07:50:57 -06:00
estherdev03
7a2250369e Finish admin dashboard and update sql code 2025-04-20 07:48:20 -06:00
Mann Patel
d169c9ba58 update 2025-04-19 10:22:16 -06:00
Mann Patel
dee6e3ce10 getCategory update 2025-04-18 22:09:04 -06:00
Mann Patel
e97f80aee1 Merge branch 'mannBranch' 2025-04-18 20:25:13 -06:00
Mann Patel
fd43001374 add prod mylistings 2025-04-18 20:25:02 -06:00
Mann Patel
121316a8d4 Fixing a bug again (Noah) 2025-04-18 19:39:18 -06:00
Mann Patel
649dad75cb update to the color 2025-04-18 19:05:20 -06:00
Mann Patel
47786b04f4 Update README.md 2025-04-18 11:30:40 -06:00
Mann Patel
fa43c91cc5 Update README.md 2025-04-18 11:29:39 -06:00
Mann Patel
5b4332a847 Update README.md 2025-04-18 11:29:06 -06:00
Mann Patel
067a5c3b0e color theme update emerald 2025-04-18 10:43:41 -06:00
Mann Patel
a8417a3697 updating frontend code 2025-04-18 10:37:19 -06:00
Mann Patel
d1aed0602d update 2025-04-17 14:10:18 -06:00
Mann Patel
e5493ad59f Update review.js 2025-04-15 14:54:16 -06:00
Mann Patel
06e045fbff fav product from prodDetail page 2025-04-15 00:18:19 -06:00
Mann Patel
fdf63f4e6a code clean up 2025-04-14 22:18:56 -06:00
Mann Patel
b142610d50 Merge branch 'mannBranch' 2025-04-14 12:09:19 -06:00
Mann Patel
635f73c1be update to listingspage 2025-04-14 12:09:06 -06:00
Mann Patel
521c3af00b Merge branch 'mannBranch' of https://github.com/MannPatel0/Campus-Plug into mannBranch 2025-04-14 11:08:45 -06:00
Mann Patel
63c594e041 Update Settings.jsx 2025-04-14 11:08:43 -06:00
Mann Patel
a52f6ce563 added products images and fixed db problems 2025-04-14 11:06:59 -06:00
Mann Patel
0c82952927 Update Favorites.jsx 2025-04-13 12:59:09 -06:00
Mann Patel
2ef05ac3af History (add/ remove) favorites add remove done 2025-04-13 12:52:21 -06:00
Mann Patel
3bdb8877a6 update server for recom... 2025-04-12 21:57:53 -06:00
Mann Patel
814c24c83f selling Pg UI and Fav is done 2025-04-12 18:33:13 -06:00
Mann Patel
0f8bb622a4 add review and read review now done 2025-04-12 13:10:17 -06:00
Mann Patel
0e32389482 update fav 2025-04-12 11:29:46 -06:00
Mann Patel
10f0469b56 added review functionality 2025-04-12 11:27:27 -06:00
Mann Patel
d8ed58f572 Update recommendation.js 2025-04-04 00:21:10 -06:00
Mann Patel
75c7675601 added Review Feature 2025-04-04 00:02:04 -06:00
Mann Patel
643b9e357c recommendation engine 75% polished 2025-04-03 18:56:39 -06:00
Mann Patel
3537e698b1 Update README.md 2025-04-03 14:05:26 -06:00
Mann Patel
ac099da486 server/client recom.. connection 👍 2025-04-03 12:15:32 -06:00
Mann Patel
99f12319d5 Cosine Sim Calc now Working properly 2025-04-03 11:59:25 -06:00
Mann Patel
e7580c36f5 update 2025-04-02 19:53:42 -06:00
Mann Patel
a1ca7304eb updatepush 2025-04-02 13:56:48 -06:00
Mann Patel
755069d279 Create example.py 2025-04-02 09:16:28 -06:00
Mann Patel
ff8b7f2081 Delete client.js 2025-03-30 00:27:40 -06:00
Mann Patel
c3cab2776d Merge branch 'mannBranch' 2025-03-30 00:25:41 -06:00
Mann Patel
71a90265d9 update to engin 2025-03-30 00:20:42 -06:00
Mann Patel
2e77ef49f4 search bar now working 2025-03-29 17:28:09 -06:00
Mann Patel
91ec43627a updated sql wit example dataset now 2025-03-29 16:13:22 -06:00
Mann Patel
7a87fc1e49 Initial Server and Client for recommond engine 2025-03-25 14:48:28 -06:00
Mann Patel
f52693dfc2 Homepg & ProductDet.. now updated 2025-03-25 14:47:54 -06:00
Mann Patel
e7a6e1dd8b Bug fix: Products No image 2025-03-24 23:04:12 -06:00
Mann Patel
fc125cc76a Merge branch 'mannBranch' of https://github.com/MannPatel0/Campus-Plug into mannBranch 2025-03-24 23:03:18 -06:00
Mann Patel
148fe95a11 update 2025-03-23 20:16:19 -06:00
Mann Patel
d53a487aea Update .gitignore 2025-03-23 20:02:11 -06:00
Mann Patel
30482760dd Merge branch 'main' into mannBranch 2025-03-23 18:31:10 -06:00
Mann Patel
ee6df0f674 Merge branch 'main' into mannBranch 2025-03-23 16:30:35 -06:00
Mann Patel
904c908249 init local db with python 2025-03-23 16:29:38 -06:00
Mann Patel
48668be540 reverting back to local db 2025-03-23 14:49:10 -06:00
Mann Patel
c829b30350 Merge branch 'main' into mannBranch 2025-03-22 14:19:38 -06:00
Mann Patel
01d5e1b67b Moving To db to aws 2025-03-22 14:10:37 -06:00
noahnghg
8000ed18bf update product sql 2025-03-19 20:07:06 -06:00
Mann Patel
656801238c Merge pull request #1 from MannPatel0/estherBranch
Update backend and frontend
2025-03-19 13:01:43 -06:00
Mann Patel
6a7fa61fcd backend continuous update 2025-03-19 01:06:53 -06:00
88 changed files with 5678 additions and 1845 deletions

2
.gitignore vendored
View File

@@ -2,8 +2,6 @@
*/node_modules */node_modules
.DS_Store .DS_Store
*~/backend/index.js
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea

View File

@@ -1,25 +1,42 @@
### 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, prefix 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
npm install 1. npm install #Installs the needed packages
2. npm run dev #Start The Server
``` ```
2. **Start The Server**, `cd frontend` into the dir and then type command ---
### Backend
1. `cd backend` into the dir and then type command
```Bash ```Bash
npm run dev 1. npm install #Installs the needed packages
2. npm run dev #Start The Server
``` ```
### `backend` ---
1. Install the needed lib with the command bellow
### Recommendation
1. `cd recommendation-engine` into the dir and then type command
```Bash ```Bash
npm install 1. python3 server.py #Start The Server
``` ```
2. **Start The Server**, `cd backend` into the dir and then type command ---
### Recommendation system
1. Install the dependencies
```Bash ```Bash
npm run dev pip install mysql.connector
``` ```
### Database ### Database
- MySql Version 9.2.0 1. MySql Version 9.2.0
2. To Create the DataBase use the command bellow:
```Bash
1. mysql -u root
2. \. PathToYour/Schema.sql
3. \. PathToYour/Init-Data.sql
```

View File

@@ -1,423 +0,0 @@
-- MySql Version 9.2.0
CREATE DATABASE Marketplace;
USE Marketplace;
-- User Entity
CREATE TABLE User (
UserID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(100) NOT NULL,
Email VARCHAR(100) UNIQUE NOT NULL,
UCID VARCHAR(20) UNIQUE NOT NULL,
Password VARCHAR(255) NOT NULL,
Phone VARCHAR(20),
Address VARCHAR(255)
);
CREATE TABLE UserRole (
UserID INT,
Client BOOLEAN DEFAULT True,
Admin BOOLEAN DEFAULT FALSE,
PRIMARY KEY (UserID),
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE
);
-- Category Entity (must be created before Product or else error)
CREATE TABLE Category (
CategoryID INT PRIMARY KEY,
Name VARCHAR(255) NOT NULL
);
-- Product Entity
CREATE TABLE Product (
ProductID INT PRIMARY KEY,
Name VARCHAR(255) NOT NULL,
Price DECIMAL(10, 2) NOT NULL,
StockQuantity INT,
UserID INT,
Description TEXT,
CategoryID INT NOT NULL,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
);
-- Fixed Image_URL table
CREATE TABLE Image_URL (
URL VARCHAR(255),
ProductID INT,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
);
-- Fixed Review Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Review (
ReviewID INT PRIMARY KEY,
UserID INT,
ProductID INT,
Comment TEXT,
Rating INT CHECK (
Rating >= 1
AND Rating <= 5
),
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
);
-- Transaction Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Transaction (
TransactionID INT PRIMARY KEY,
UserID INT,
ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
PaymentStatus VARCHAR(50),
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
);
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Recommendation (
RecommendationID_PK INT PRIMARY KEY,
UserID INT,
RecommendedProductID INT,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID)
);
-- History Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE History (
HistoryID INT PRIMARY KEY,
UserID INT,
ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
);
-- Favorites Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Favorites (
FavoriteID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
ProductID INT,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
);
-- Product-Category Junction Table (Many-to-Many)
CREATE TABLE Product_Category (
ProductID INT,
CategoryID INT,
PRIMARY KEY (ProductID, CategoryID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID),
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
);
-- Login Authentication table
CREATE TABLE AuthVerification (
UserID INT AUTO_INCREMENT PRIMARY KEY,
Email VARCHAR(100) UNIQUE NOT NULL,
VerificationCode VARCHAR(6) NOT NULL,
Authenticated BOOLEAN DEFAULT FALSE,
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

@@ -0,0 +1,70 @@
const db = require("../utils/database");
exports.getAllCategoriesWithPagination = async (req, res) => {
const limit = +req.query?.limit;
const page = +req.query?.page;
const offset = (page - 1) * limit;
try {
const [data, _] = await db.execute(
"SELECT * FROM Category C ORDER BY C.CategoryID ASC LIMIT ? OFFSET ?",
[limit.toString(), offset.toString()],
);
const [result] = await db.execute("SELECT COUNT(*) AS count FROM Category");
const { count: total } = result[0];
return res.json({ data, total });
} catch (error) {
res.json({ error: "Cannot fetch categories from database!" });
}
};
exports.addCategory = async (req, res) => {
const { name } = req.body;
try {
const [result] = await db.execute(
"INSERT INTO Category (Name) VALUES (?)",
[name],
);
res.json({ message: "Adding new category successfully!" });
} catch (error) {
res.json({ error: "Cannot add new category!" });
}
};
exports.removeCategory = async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute(
`DELETE FROM Category WHERE CategoryID = ?`,
[id],
);
res.json({ message: "Delete category successfully!" });
} catch (error) {
res.json({ error: "Cannot remove category from database!" });
}
};
exports.getAllCategory = async (req, res) => {
try {
const [data, fields] = await db.execute(`SELECT * FROM Category`);
const formattedData = {};
data.forEach((row) => {
formattedData[row.CategoryID] = row.Name;
});
res.json({
success: true,
message: "Categories fetched successfully",
data: formattedData,
});
} catch (error) {
console.error("Error fetching categories:", error);
return res.status(500).json({
success: false,
error: "Database error occurred",
});
}
};

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,83 @@
const db = require("../utils/database"); const db = require("../utils/database");
exports.addToFavorite = async (req, res) => { exports.addProduct = async (req, res) => {
const { userID, productsID } = req.body; const { userID, name, price, qty, description, category, images } = req.body;
try { try {
// 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 Product (Name, Price, StockQuantity, UserID, Description, CategoryID) VALUES (?, ?, ?, ?, ?, ?)`,
[userID, productsID] [name, price, qty, userID, description, category],
);
const productID = result.insertId;
if (images && images.length > 0) {
const imageInsertPromises = images.map((imagePath) =>
db.execute(`INSERT INTO Image_URL (URL, ProductID) VALUES (?, ?)`, [
imagePath,
productID,
]),
);
await Promise.all(imageInsertPromises); //perallel
}
res.json({
success: true,
message: "Product and images added successfully",
});
} catch (error) {
console.error("Error adding product or images:", error);
console.log(error);
return res.json({ error: "Could not add product or images" });
}
};
exports.removeProduct = async (req, res) => {
const { userID, productID } = req.body;
console.log(userID);
try {
// First delete images
await db.execute(`DELETE FROM Image_URL WHERE ProductID = ?`, [productID]);
await db.execute(`DELETE FROM History WHERE ProductID = ?`, [productID]);
await db.execute(`DELETE FROM Favorites WHERE ProductID = ?`, [productID]);
await db.execute(`DELETE FROM Product_Category WHERE ProductID = ?`, [
productID,
]);
await db.execute(`DELETE FROM Product_Category WHERE ProductID = ?`, [
productID,
]);
await db.execute(`DELETE FROM Transaction WHERE ProductID = ?`, [
productID,
]);
await db.execute(
`DELETE FROM Recommendation WHERE RecommendedProductID = ?`,
[productID],
);
// Then delete the product
await db.execute(`DELETE FROM Product WHERE UserID = ? AND ProductID = ?`, [
userID,
productID,
]);
res.json({
success: true,
message: "Product removed successfully",
});
} catch (error) {
console.error("Error removing product:", error);
return res.json({ error: "Could not remove product" });
}
};
exports.addFavorite = async (req, res) => {
const { userID, productID } = req.body;
console.log(userID);
try {
const [result] = await db.execute(
`INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`,
[userID, productID],
); );
res.json({ res.json({
@@ -20,18 +90,199 @@ exports.addToFavorite = async (req, res) => {
} }
}; };
//Get all products exports.removeFavorite = async (req, res) => {
exports.getAllProducts = async (req, res) => { const { userID, productID } = req.body;
console.log(userID);
try { try {
const [data, fields] = await db.execute("SELECT * FROM Product"); // Use parameterized query to prevent SQL injection
const [result] = await db.execute(
`DELETE FROM Favorites WHERE UserID = ? AND ProductID = ?`,
[userID, productID],
);
res.json({ res.json({
success: true, success: true,
message: "Product added to favorites successfully", 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.updateProduct = async (req, res) => {
const { productId } = req.params;
const { name, description, price, category, images } = req.body;
console.log(productId);
const connection = await db.getConnection();
try {
await connection.beginTransaction();
// Step 1: Check if the product exists
const [checkProduct] = await connection.execute(
"SELECT * FROM Product WHERE ProductID = ?",
[productId],
);
if (checkProduct.length === 0) {
await connection.rollback();
return res.status(404).json({ error: "Product not found" });
}
// Step 2: Update the product
await connection.execute(
`
UPDATE Product
SET Name = ?, Description = ?, Price = ?, CategoryID = ?
WHERE ProductID = ?
`,
[name, description, price, category, productId],
);
// Step 3: Delete existing images
await connection.execute(`DELETE FROM Image_URL WHERE ProductID = ?`, [
productId,
]);
// Step 4: Insert new image URLs
for (const imageUrl of images) {
await connection.execute(
`INSERT INTO Image_URL (ProductID, URL) VALUES (?, ?)`,
[productId, imageUrl],
);
}
await connection.commit();
res.json({ success: true, message: "Product updated successfully" });
} catch (error) {
await connection.rollback();
console.error("Update product error:", error);
res.status(500).json({ error: "Failed to update product" });
} finally {
connection.release();
}
};
exports.myProduct = async (req, res) => {
const { userID } = req.body;
try {
const [result] = await db.execute(
`
SELECT
p.ProductID,
p.Name,
p.Description,
p.Price,
p.CategoryID,
p.UserID,
p.Date,
u.Name AS SellerName,
MIN(i.URL) AS image_url
FROM Product p
JOIN User u ON p.UserID = u.UserID
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
WHERE p.UserID = ?
GROUP BY
p.ProductID,
p.Name,
p.Description,
p.Price,
p.CategoryID,
p.UserID,
p.Date,
u.Name;
`,
[userID],
);
res.json({
success: true,
data: result,
});
} catch (error) {
console.error("Error retrieving favorites:", error);
res.status(500).json({ error: "Could not retrieve favorite products" });
}
};
exports.getFavorites = async (req, res) => {
const { userID } = req.body;
try {
const [favorites] = await db.execute(
`
SELECT
p.ProductID,
p.Name,
p.Description,
p.Price,
p.CategoryID,
p.UserID,
p.Date,
u.Name AS SellerName,
MIN(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 = ?
GROUP BY
p.ProductID,
p.Name,
p.Description,
p.Price,
p.CategoryID,
p.UserID,
p.Date,
u.Name;
`,
[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
exports.getAllProducts = async (req, res) => {
try {
const [data, fields] = await db.execute(`
SELECT
P.ProductID,
P.Name AS ProductName,
P.Price,
P.Date AS DateUploaded,
U.Name AS SellerName,
MIN(I.URL) AS ProductImage,
C.Name AS Category
FROM Product P
JOIN Image_URL I ON P.ProductID = I.ProductID
JOIN User U ON P.UserID = U.UserID
JOIN Category C ON P.CategoryID = C.CategoryID
GROUP BY
P.ProductID,
P.Name,
P.Price,
P.Date,
U.Name,
C.Name;
`);
res.json({
success: true,
message: "Products fetched successfully",
data, data,
}); });
} catch (error) { } catch (error) {
console.error("Error finding user:", error); console.error("Error finding products:", error);
return res.status(500).json({ return res.status(500).json({
found: false, found: false,
error: "Database error occurred", error: "Database error occurred",
@@ -39,32 +290,117 @@ exports.getAllProducts = async (req, res) => {
} }
}; };
// db_con.query( exports.getProductById = async (req, res) => {
// "SELECT ProductID FROM product WHERE ProductID = ?", const { id } = req.params;
// [productID], console.log("Received Product ID:", id);
// (err, results) => {
// if (err) {
// console.error("Error checking product:", err);
// return res.json({ error: "Database error" });
// }
// if (results.length === 0) { try {
// return res.json({ error: "Product does not exist" }); const [data] = await db.execute(
// } `
// }, SELECT p.*,U.Name AS SellerName,U.Email as SellerEmail,U.Phone as SellerPhone, i.URL AS image_url
// ); FROM Product p
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
JOIN User U ON p.UserID = U.UserID
WHERE p.ProductID = ?
`,
[id],
);
// db_con.query( // Log raw data for debugging
// "INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)", console.log("Raw Database Result:", data);
// [userID, productID],
// (err, result) => { if (data.length === 0) {
// if (err) { console.log("No product found with ID:", id);
// console.error("Error adding favorite product:", err); return res.status(404).json({
// return res.json({ error: "Could not add favorite product" }); success: false,
// } message: "Product not found",
// res.json({ });
// success: true, }
// message: "Product added to favorites successfully",
// }); // Collect all image URLs
// }, const images = data
// ); .map((row) => row.image_url)
.filter((url) => url !== null);
// Create product object with all details from first row and collected images
const product = {
...data[0], // Base product details
images: images, // Collected image URLs
};
// Log processed product for debugging
console.log("Processed Product:", product);
res.json({
success: true,
message: "Product fetched successfully",
data: product,
});
} catch (error) {
console.error("Full Error Details:", error);
return res.status(500).json({
success: false,
message: "Database error occurred",
error: error.message,
});
}
};
exports.getProductWithPagination = async (req, res) => {
const limit = +req.query.limit;
const page = +req.query.page;
const offset = (page - 1) * limit;
try {
const [data, fields] = await db.execute(
`
SELECT
P.ProductID,
P.Name AS ProductName,
P.Price,
P.Date AS DateUploaded,
U.Name AS SellerName,
MIN(I.URL) AS ProductImage,
C.Name AS Category
FROM Product P
LEFT JOIN Image_URL I ON P.ProductID = I.ProductID
LEFT JOIN User U ON P.UserID = U.UserID
LEFT JOIN Category C ON P.CategoryID = C.CategoryID
GROUP BY
P.ProductID,
P.Name,
P.Price,
P.Date,
U.Name,
C.Name
ORDER BY P.ProductID ASC
LIMIT ? OFFSET ?
`,
[limit.toString(), offset.toString()],
);
const [result] = await db.execute(
`SELECT COUNT(*) AS totalProd FROM Product`,
);
const { totalProd } = result[0];
return res.json({ totalProd, products: data });
} catch (error) {
res.json({ error: "Error fetching products!" });
}
};
exports.removeProduct = async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute(
`DELETE FROM Product WHERE ProductID = ?`,
[id],
);
res.json({ message: "Delete product successfully!" });
} catch (error) {
res.json({ error: "Cannot remove product from database!" });
}
};

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

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

View File

@@ -0,0 +1,40 @@
const db = require("../utils/database");
exports.getTransactionWithPagination = async (req, res) => {
const limit = +req.query?.limit;
const page = +req.query?.page;
const offset = (page - 1) * limit;
try {
const [data, _] = await db.execute(
`SELECT T.TransactionID, DATE_FORMAT(T.Date, '%b-%d-%Y %h:%i %p') as Date, T.PaymentStatus, U.Name as UserName, P.Name as ProductName
FROM Transaction T
LEFT JOIN User U ON T.UserID = U.UserID
LEFT JOIN Product P ON T.ProductID = P.ProductID
ORDER BY T.TransactionID ASC LIMIT ? OFFSET ?`,
[limit.toString(), offset.toString()]
);
const [result] = await db.execute(
"SELECT COUNT(*) AS count FROM Transaction"
);
const { count: total } = result[0];
return res.json({ data, total });
} catch (error) {
res.json({ error: "Cannot fetch transactions from database!" });
}
};
exports.removeTransation = async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute(
"DELETE FROM Transaction WHERE TransactionID = ?;",
[id.toString()]
);
return res.json({ message: "Remove transaction successfully!" });
} catch (error) {
return res
.status(500)
.json({ error: "Cannot remove transactions from database!" });
}
};

View File

@@ -134,6 +134,62 @@ exports.completeSignUp = async (req, res) => {
} }
}; };
exports.doLogin = async (req, res) => {
const { email, password } = req.body;
// Input validation
if (!email || !password) {
return res.status(400).json({
found: false,
error: "Email and password are required",
});
}
try {
// Query to find user with matching email
const query = "SELECT * FROM User WHERE email = ?";
const [data, fields] = await db.execute(query, [email]);
// Check if user was found
if (data && data.length > 0) {
const user = data[0];
// Verify password match
if (user.Password === password) {
// Consider using bcrypt for secure password comparison
// Return user data without password
return res.json({
found: true,
userID: user.UserID,
name: user.Name,
email: user.Email,
UCID: user.UCID,
phone: user.Phone,
address: user.Address,
});
} else {
// Password doesn't match
return res.json({
found: false,
error: "Invalid email or password",
});
}
} else {
// User not found
return res.json({
found: false,
error: "Invalid email or password",
});
}
} catch (error) {
console.error("Error logging in:", error);
return res.status(500).json({
found: false,
error: "Database error occurred",
});
}
};
exports.getAllUser = async (req, res) => { exports.getAllUser = async (req, res) => {
try { try {
const [users, fields] = await db.execute("SELECT * FROM User;"); const [users, fields] = await db.execute("SELECT * FROM User;");
@@ -174,6 +230,7 @@ exports.findUserByEmail = async (req, res) => {
UCID: user.UCID, UCID: user.UCID,
phone: user.Phone, phone: user.Phone,
address: user.Address, address: user.Address,
password: user.Password,
// Include any other fields your user might have // Include any other fields your user might have
// Make sure the field names match exactly with your database column names // Make sure the field names match exactly with your database column names
}); });
@@ -194,37 +251,47 @@ exports.findUserByEmail = async (req, res) => {
}; };
exports.updateUser = async (req, res) => { exports.updateUser = async (req, res) => {
const { userId, ...updateData } = req.body; try {
const userId = req.body?.userId;
const name = req.body?.name;
const email = req.body?.email;
const phone = req.body?.phone;
const UCID = req.body?.UCID;
const address = req.body?.address;
const password = req.body?.password;
if (!userId) {
return res.status(400).json({ error: "User ID is required" });
}
if (!userId) { // Build updateData manually
return res.status(400).json({ error: "User ID is required" }); const updateData = {};
} if (name) updateData.name = name;
if (email) updateData.email = email;
if (phone) updateData.phone = phone;
if (UCID) updateData.UCID = UCID;
if (address) updateData.address = address;
if (password) updateData.password = password;
if (Object.keys(updateData).length === 0) {
return res.status(400).json({ error: "No valid fields to update" });
}
//query dynamically based on provided fields const updateFields = [];
const updateFields = []; const values = [];
const values = [];
Object.entries(updateData).forEach(([key, value]) => { Object.entries(updateData).forEach(([key, value]) => {
// Only include fields that are actually in the User table
if (["Name", "Email", "Password", "Phone", "UCID"].includes(key)) {
updateFields.push(`${key} = ?`); updateFields.push(`${key} = ?`);
values.push(value); values.push(value);
} });
});
if (updateFields.length === 0) { values.push(userId);
return res.status(400).json({ error: "No valid fields to update" });
}
// Add userId to values array const query = `UPDATE User SET ${updateFields.join(", ")} WHERE userId = ?`;
values.push(userId);
try {
const query = `UPDATE User SET ${updateFields.join(", ")} WHERE UserID = ?`;
const [updateResult] = await db.execute(query, values); const [updateResult] = await db.execute(query, values);
if (updateResult.affectedRows === 0) { if (updateResult.affectedRows === 0) {
return res.status(404).json({ error: "User not found" }); return res.status(404).json({ error: "User not found" });
} }
res.json({ success: true, message: "User updated successfully" }); res.json({ success: true, message: "User updated successfully" });
} catch (error) { } catch (error) {
console.error("Error updating user:", error); console.error("Error updating user:", error);
@@ -261,3 +328,38 @@ exports.deleteUser = async (req, res) => {
return res.status(500).json({ error: "Could not delete user!" }); return res.status(500).json({ error: "Could not delete user!" });
} }
}; };
exports.getUsersWithPagination = async (req, res) => {
const limit = +req.query.limit;
const page = +req.query.page;
const offset = (page - 1) * limit;
try {
const [users, fields] = await db.execute(
"SELECT * FROM User LIMIT ? OFFSET ?",
[limit.toString(), offset.toString()]
);
const [result] = await db.execute("SELECT COUNT(*) AS count FROM User");
const { count: total } = result[0];
res.json({ users, total });
} catch (error) {
console.error("Errors: ", error);
return res.status(500).json({ error: "\nCould not fetch users!" });
}
};
exports.isAdmin = async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute(
"SELECT R.Admin FROM marketplace.userrole R WHERE R.UserID = ?",
[id]
);
const { Admin } = result[0];
res.json({ isAdmin: Admin });
} catch (error) {
res.json({ error: "Cannot verify admin status!" });
}
};

View File

@@ -1,10 +1,17 @@
const express = require("express"); const express = require("express");
const cors = require("cors"); const cors = require("cors");
//Get the db connection
const db = require("./utils/database"); 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 recommendedRouter = require("./routes/recommendation");
const history = require("./routes/history");
const review = require("./routes/review");
const categoryRouter = require("./routes/category");
const transactionRouter = require("./routes/transaction");
const { generateEmailTransporter } = require("./utils/mail"); const { generateEmailTransporter } = require("./utils/mail");
const { const {
cleanupExpiredCodes, cleanupExpiredCodes,
@@ -28,15 +35,22 @@ transporter
console.error("Email connection failed:", error); console.error("Email connection failed:", error);
}); });
//Check database connection
checkDatabaseConnection(db); checkDatabaseConnection(db);
//Routes //Routes
app.use("/api/user", userRouter); //prefix with /api/user app.use("/api/user", userRouter);
app.use("/api/product", productRouter); //prefix with /api/product app.use("/api/product", productRouter);
app.use("/api/search", searchRouter);
app.use("/api/engine", recommendedRouter);
app.use("/api/history", history);
app.use("/api/review", review);
app.use("/api/category", categoryRouter);
app.use("/api/transaction", transactionRouter);
app.use("/api/category", categoryRouter);
// Set up a scheduler to run cleanup every hour // Set up a scheduler to run cleanup every hour
setInterval(cleanupExpiredCodes, 60 * 60 * 1000); clean_up_time = 30 * 60 * 1000;
setInterval(cleanupExpiredCodes, clean_up_time);
app.listen(3030, () => { app.listen(3030, () => {
console.log(`Running Backend on http://localhost:3030/`); console.log(`Running Backend on http://localhost:3030/`);

View File

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

View File

@@ -0,0 +1,16 @@
const express = require("express");
const {
getAllCategoriesWithPagination,
addCategory,
removeCategory,
getAllCategory,
} = require("../controllers/category");
const router = express.Router();
router.get("/getCategories", getAllCategoriesWithPagination);
router.post("/addCategory", addCategory);
router.delete("/:id", removeCategory);
router.get("/", getAllCategory);
module.exports = router;

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,10 +1,41 @@
// routes/product.js
const express = require("express"); const express = require("express");
const { addToFavorite, getAllProducts } = require("../controllers/product"); const {
addFavorite,
getFavorites,
removeFavorite,
getAllProducts,
getProductById,
addProduct,
removeProduct,
getProductWithPagination,
myProduct,
updateProduct,
} = require("../controllers/product");
const router = express.Router(); const router = express.Router();
router.post("/add_fav_product", addToFavorite); // Add detailed logging middleware
router.use((req, res, next) => {
console.log(`Incoming ${req.method} request to ${req.path}`);
next();
});
router.get("/get_product", getAllProducts); router.post("/addFavorite", addFavorite);
router.post("/getFavorites", getFavorites);
router.post("/delFavorite", removeFavorite);
router.post("/delProduct", removeProduct);
router.post("/myProduct", myProduct);
router.post("/addProduct", addProduct);
router.get("/getProduct", getAllProducts);
//Remove product
router.delete("/:id", removeProduct);
//Get products with pagination
router.get("/getProductWithPagination", getProductWithPagination);
router.get("/:id", getProductById); // Simplified route
router.put("/update/:productId", updateProduct);
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("/addReview", submitReview);
module.exports = router;

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

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

View File

@@ -0,0 +1,12 @@
const express = require("express");
const {
getTransactionWithPagination,
removeTransation,
} = require("../controllers/transaction");
const router = express.Router();
router.get("/getTransactions", getTransactionWithPagination);
router.delete("/:id", removeTransation);
module.exports = router;

View File

@@ -7,6 +7,9 @@ const {
findUserByEmail, findUserByEmail,
updateUser, updateUser,
deleteUser, deleteUser,
doLogin,
isAdmin,
getUsersWithPagination,
} = require("../controllers/user"); } = require("../controllers/user");
const router = express.Router(); const router = express.Router();
@@ -26,10 +29,19 @@ router.get("/fetch_all_users", getAllUser);
//Fetch One user Data with all fields: //Fetch One user Data with all fields:
router.post("/find_user", findUserByEmail); router.post("/find_user", findUserByEmail);
//Fetch One user Data with all fields:
router.post("/do_login", doLogin);
//Update A uses Data: //Update A uses Data:
router.post("/update", updateUser); router.post("/update", updateUser);
//Delete A uses Data: //Delete A uses Data:
router.post("/delete", deleteUser); router.post("/delete", deleteUser);
//Check admin status
router.get("/isAdmin/:id", isAdmin);
//Fetch user with pagination
router.get("/getUserWithPagination", getUsersWithPagination);
module.exports = router; module.exports = router;

View File

@@ -1,12 +1,14 @@
const mysql = require("mysql2"); const mysql = require("mysql2");
//Create a pool of connections to allow multiple query happen at the same time
const pool = mysql.createPool({ const pool = mysql.createPool({
host: "localhost", host: "localhost",
user: "root", user: "root",
database: "marketplace", database: "Marketplace",
password: "12345678", password: "12345678",
}); });
//Export a promise for promise-based query // const pool = mysql.createPool(
// "singlestore://mann-619d0:<mann-619d0 Password>@svc-3482219c-a389-4079-b18b-d50662524e8a-shared-dml.aws-virginia-6.svc.singlestore.com:3333/db_mann_48ba9?ssl={}",
// );
module.exports = pool.promise(); module.exports = pool.promise();

View File

@@ -1,4 +1,5 @@
const { generateEmailTransporter } = require("./mail"); const { generateEmailTransporter } = require("./mail");
const db = require("../utils/database");
// Helper function to send email // Helper function to send email
async function sendVerificationEmail(email, verificationCode) { async function sendVerificationEmail(email, verificationCode) {
@@ -17,18 +18,17 @@ async function sendVerificationEmail(email, verificationCode) {
} }
// Clean up expired verification codes (run this periodically) // Clean up expired verification codes (run this periodically)
function cleanupExpiredCodes() { const cleanupExpiredCodes = () => {
db_con.query( db.execute(
"DELETE FROM AuthVerification WHERE Date < DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND Authenticated = 0", "DELETE FROM AuthVerification WHERE Date < DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND Authenticated = 0"
(err, result) => { )
if (err) { .then((res) => {
console.error("Error cleaning up expired codes:", err); console.log(`Cleaned up expired verification codes`);
} else { })
console.log(`Cleaned up ${result} expired verification codes`); .catch((err) => {
} console.error("Error cleaning up expired codes:", err);
} });
); };
}
const checkDatabaseConnection = async (db) => { const checkDatabaseConnection = async (db) => {
try { try {

View File

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

View File

@@ -11,9 +11,11 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.0.9", "@tailwindcss/vite": "^4.0.9",
"axios": "^1.8.4",
"lucide-react": "^0.477.0", "lucide-react": "^0.477.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.2.0" "react-router-dom": "^7.2.0"
}, },
"devDependencies": { "devDependencies": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

View File

@@ -12,7 +12,14 @@ 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 Dashboard from "./pages/Dashboard";
import UserDashboard from "./pages/UserDashboard";
import ProductDashboard from "./pages/ProductDashboard";
import DashboardNav from "./components/DashboardNav";
import CategoryDashboard from "./pages/CategoryDashboard";
import { verifyIsAdmin } from "./api/admin";
import TransactionDashboard from "./pages/TransactionDashboard";
function App() { function App() {
// Authentication state - initialize from localStorage if available // Authentication state - initialize from localStorage if available
@@ -30,6 +37,11 @@ function App() {
const [error, setError] = useState(""); const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// Product recommendation states
const [isGeneratingRecommendations, setIsGeneratingRecommendations] =
useState(false);
const [recommendations, setRecommendations] = useState([]);
// New verification states // New verification states
const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying' const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying'
const [tempUserData, setTempUserData] = useState(null); const [tempUserData, setTempUserData] = useState(null);
@@ -50,6 +62,91 @@ function App() {
return () => window.removeEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize);
}, []); }, []);
useEffect(() => {
if (isAuthenticated && user) {
sendSessionDataToServer();
}
}, [isAuthenticated, user]);
// Generate product recommendations when user logs in
useEffect(() => {
if (isAuthenticated && user) {
generateProductRecommendations();
}
}, [isAuthenticated, user]);
// Generate product recommendations
const generateProductRecommendations = async () => {
try {
setIsGeneratingRecommendations(true);
// Add a short delay to simulate calculation time
await new Promise((resolve) => setTimeout(resolve, 500));
console.log("Generating product recommendations for user:", user.ID);
// Make API call to get recommendations
const response = await fetch(
"http://localhost:3030/api/recommendations/generate",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user.ID,
}),
},
);
if (!response.ok) {
throw new Error("Failed to generate recommendations");
}
const result = await response.json();
if (result.success) {
console.log(
"Recommendations generated successfully:",
result.recommendations,
);
setRecommendations(result.recommendations);
// Store recommendations in session storage for access across the app
sessionStorage.setItem(
"userRecommendations",
JSON.stringify(result.recommendations),
);
} else {
console.error("Error generating recommendations:", result.message);
}
} catch (err) {
console.error("Error generating product recommendations:", err);
} finally {
setIsGeneratingRecommendations(false);
}
};
const [isAdmin, setIsAdmin] = useState(false);
const [showAdminDashboard, setShowAdminDashboard] = useState(false);
useEffect(() => {
const userInfo = sessionStorage.getItem("user")
? JSON.parse(sessionStorage.getItem("user"))
: "";
const id = userInfo?.ID;
verifyIsAdmin(id).then((data) => {
setIsAdmin(data.isAdmin);
});
}, [user]);
const handleShowAdminDashboard = () => {
setShowAdminDashboard(true);
};
const handleCloseAdminDashboard = () => {
setShowAdminDashboard(false);
};
// Send verification code // Send verification code
const sendVerificationCode = async (userData) => { const sendVerificationCode = async (userData) => {
try { try {
@@ -70,7 +167,7 @@ function App() {
email: userData.email, email: userData.email,
// Add any other required fields // Add any other required fields
}), }),
} },
); );
if (!response.ok) { if (!response.ok) {
@@ -119,7 +216,7 @@ function App() {
email: tempUserData.email, email: tempUserData.email,
code: code, code: code,
}), }),
} },
); );
if (!response.ok) { if (!response.ok) {
@@ -163,7 +260,7 @@ function App() {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(userData), body: JSON.stringify(userData),
} },
); );
if (!response.ok) { if (!response.ok) {
@@ -176,6 +273,7 @@ function App() {
if (result.success) { if (result.success) {
// Create user object from API response // Create user object from API response
const newUser = { const newUser = {
ID: result.userID || result.ID,
name: result.name || userData.name, name: result.name || userData.name,
email: result.email || userData.email, email: result.email || userData.email,
UCID: result.UCID || userData.ucid, UCID: result.UCID || userData.ucid,
@@ -189,11 +287,18 @@ function App() {
sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("isAuthenticated", "true");
sessionStorage.setItem("user", JSON.stringify(newUser)); sessionStorage.setItem("user", JSON.stringify(newUser));
// After successful signup, send session data to server
sendSessionDataToServer();
// Reset verification steps // Reset verification steps
setVerificationStep("initial"); setVerificationStep("initial");
setTempUserData(null); setTempUserData(null);
console.log("Signup completed successfully"); console.log("Signup completed successfully");
// Generate recommendations for the new user
generateProductRecommendations();
return true; return true;
} else { } else {
setError(result.message || "Failed to complete signup"); setError(result.message || "Failed to complete signup");
@@ -222,11 +327,12 @@ function App() {
setError("Email and password are required"); setError("Email and password are required");
setIsLoading(false); setIsLoading(false);
return; return;
} else if (!formValues.email.endsWith("@ucalgary.ca")) {
setError("Please use your UCalgary email address (@ucalgary.ca)");
setIsLoading(false);
return;
} }
// else if (!formValues.email.endsWith("@ucalgary.ca")) {
// setError("Please use your UCalgary email address (@ucalgary.ca)");
// setIsLoading(false);
// return;
// }
try { try {
if (isSignUp) { if (isSignUp) {
// Handle Sign Up with verification // Handle Sign Up with verification
@@ -239,7 +345,7 @@ function App() {
UCID: formValues.ucid, UCID: formValues.ucid,
phone: formValues.phone, phone: formValues.phone,
password: formValues.password, // This will be needed for the final signup password: formValues.password, // This will be needed for the final signup
address: "NOT_GIVEN", address: formValues.address, // Add this line
client: 1, client: 1,
admin: 0, admin: 0,
}; };
@@ -255,7 +361,7 @@ function App() {
// Make API call to localhost:3030/find_user // Make API call to localhost:3030/find_user
const response = await fetch( const response = await fetch(
"http://localhost:3030/api/user/find_user", "http://localhost:3030/api/user/do_login",
{ {
method: "POST", method: "POST",
headers: { headers: {
@@ -265,7 +371,7 @@ function App() {
email: formValues.email, email: formValues.email,
password: formValues.password, password: formValues.password,
}), }),
} },
); );
if (!response.ok) { if (!response.ok) {
@@ -291,9 +397,11 @@ function App() {
sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("isAuthenticated", "true");
sessionStorage.setItem("user", JSON.stringify(userObj)); sessionStorage.setItem("user", JSON.stringify(userObj));
sessionStorage.getItem("user");
console.log("Login successful for:", userData.email); console.log("Login successful for:", userData.email);
// Start generating recommendations with a slight delay
// This will happen in the useEffect, but we set a loading state to show to the user
setIsGeneratingRecommendations(true);
} else { } else {
// Show error message for invalid credentials // Show error message for invalid credentials
setError("Invalid email or password"); setError("Invalid email or password");
@@ -327,11 +435,12 @@ function App() {
setUser(null); setUser(null);
setVerificationStep("initial"); setVerificationStep("initial");
setTempUserData(null); setTempUserData(null);
setRecommendations([]);
// Clear localStorage // Clear localStorage
//
sessionStorage.removeItem("user"); sessionStorage.removeItem("user");
sessionStorage.removeItem("isAuthenticated"); sessionStorage.removeItem("isAuthenticated");
sessionStorage.removeItem("userRecommendations");
console.log("User logged out"); console.log("User logged out");
}; };
@@ -355,6 +464,51 @@ function App() {
setError(""); setError("");
}; };
const sendSessionDataToServer = async () => {
try {
// Retrieve data from sessionStorage
const user = JSON.parse(sessionStorage.getItem("user"));
if (!user || !isAuthenticated) {
console.log("User is not authenticated");
return;
}
// Prepare the data to send
const requestData = {
userId: user.ID, // or user.ID depending on your user structure
email: user.email,
isAuthenticated,
};
// Send data to Python server (replace with your actual server URL)
const response = await fetch("http://0.0.0.0:5000/api/user/session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestData),
});
// Check the response
if (response.ok) {
const result = await response.json();
console.log("Server response:", result);
} else {
console.error("Failed to send session data to the server");
}
} catch (error) {
console.error("Error sending session data:", error);
}
};
// Loading overlay component
const LoadingOverlay = () => (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-green-500 border-t-transparent"></div>
</div>
);
// Login component // Login component
const LoginComponent = () => ( const LoginComponent = () => (
<div className="flex h-screen bg-white"> <div className="flex h-screen bg-white">
@@ -479,6 +633,25 @@ function App() {
</div> </div>
)} )}
{isSignUp && (
<div>
<label
htmlFor="address"
className="block mb-1 text-sm font-medium text-gray-800"
>
Address
</label>
<input
type="text"
id="address"
name="address"
placeholder="Your address"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
required={isSignUp}
/>
</div>
)}
<div> <div>
<label <label
htmlFor="password" htmlFor="password"
@@ -509,8 +682,8 @@ function App() {
{isLoading {isLoading
? "Please wait..." ? "Please wait..."
: isSignUp : isSignUp
? "Create Account" ? "Create Account"
: "Sign In"} : "Sign In"}
</button> </button>
</div> </div>
</form> </form>
@@ -601,12 +774,43 @@ function App() {
return children; return children;
}; };
// If user is admin, show admin naviagtion
if (showAdminDashboard) {
return (
<Router>
<div className="flex">
<DashboardNav handleCloseAdminDashboard={handleCloseAdminDashboard} />
<Routes>
{/* Admin routes */}
<Route path="/admin" element={<Dashboard />} />
<Route path="/admin/user" element={<UserDashboard />} />
<Route path="/admin/product" element={<ProductDashboard />} />
<Route path="/admin/category" element={<CategoryDashboard />} />
<Route
path="/admin/transaction"
element={<TransactionDashboard />}
/>
<Route path="*" element={<Dashboard />} />
</Routes>
</div>
</Router>
);
}
return ( return (
<Router> <Router>
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* Show loading overlay when generating recommendations */}
{isGeneratingRecommendations && <LoadingOverlay />}
{/* Only show navbar when authenticated */} {/* Only show navbar when authenticated */}
{isAuthenticated && ( {isAuthenticated && (
<Navbar onLogout={handleLogout} userName={user?.name} /> <Navbar
isAdmin={isAdmin}
onLogout={handleLogout}
userName={user?.name}
handleShowAdminDashboard={handleShowAdminDashboard}
/>
)} )}
<Routes> <Routes>
{/* Public routes */} {/* Public routes */}
@@ -620,7 +824,7 @@ function App() {
element={ element={
<ProtectedRoute> <ProtectedRoute>
<div className="container mx-auto px-4 py-6"> <div className="container mx-auto px-4 py-6">
<Home /> <Home recommendations={recommendations} />
</div> </div>
</ProtectedRoute> </ProtectedRoute>
} }
@@ -633,6 +837,16 @@ function App() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route
path="/search"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<SearchPage />
</div>
</ProtectedRoute>
}
/>
<Route <Route
path="/settings" path="/settings"
element={ element={
@@ -653,27 +867,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={

120
frontend/src/api/admin.js Normal file
View File

@@ -0,0 +1,120 @@
import client from "./client";
export const getUsers = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/user/getUserWithPagination?page=${page}&limit=${limit}`
);
return { users: data.users, total: data.total };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const getProducts = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/product/getProductWithPagination?limit=${limit}&page=${page}`
);
return { products: data.products, total: data.totalProd };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const getCategories = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/category/getCategories?page=${page}&limit=${limit}`
);
return { data: data.data, total: data.total };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const getTransactions = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/transaction/getTransactions?limit=${limit}&page=${page}`
);
return { transactions: data.data, total: data.total };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const addCategory = async (name) => {
try {
const { data } = await client.post(`/category/addCategory`, { name: name });
return { message: data.message };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const removeCategory = async (id) => {
try {
const { data } = await client.delete(`/category/${id}`);
return { message: data.message };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const removeUser = async (id) => {
try {
const { data } = await client.post(`/user/delete`, { userId: id });
return { message: data.message };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const removeProduct = async (id) => {
try {
const { data } = await client.delete(`/product/${id}`);
return { message: data.message };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const verifyIsAdmin = async (id) => {
try {
const { data } = await client.get(`/user/isAdmin/${id}`);
return { isAdmin: data.isAdmin };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const removeTransaction = async (id) => {
try {
const { data } = await client.delete(`/transaction/${id}`);
return { message: data.message };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};

View File

@@ -0,0 +1,3 @@
import axios from "axios";
const client = axios.create({ baseURL: "http://localhost:3030/api" });
export default client;

View File

@@ -0,0 +1,65 @@
import { useState } from "react";
import { MdAddBox } from "react-icons/md";
import { addCategory } from "../api/admin";
export default function CategoryForm({ visible, onAddCategory }) {
const [category, setCategory] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!category.trim()) {
document.getElementById("noti").innerHTML = "Category name is missing!";
document
.getElementById("noti")
.classList.add("bg-red-200", "text-red-500");
document.getElementById("noti").classList.remove("opacity-0");
return;
}
addCategory(category)
.then((message) => {
document
.getElementById("noti")
.classList.remove("opacity-0", "bg-red-200", "text-red-500");
document
.getElementById("noti")
.classList.add("bg-green-200", "text-green-800");
document.getElementById("noti").innerHTML = `${message.message}`;
setCategory("");
onAddCategory();
})
.catch((err) => {
console.log(err);
});
};
const handleChange = ({ target }) => {
setCategory(target.value);
if (target.value.trim())
document.getElementById("noti").classList.add("opacity-0");
};
if (!visible) return;
return (
<form onSubmit={handleSubmit} action="" className="flex p-2 items-center">
<label htmlFor="category" className="text-green-700">
Category:
</label>
<input
type="text"
className="border border-green-700 ml-2 rounded-sm focus:bg-green-100 text-green-900"
name="category"
id="category"
onChange={handleChange}
value={category}
/>
<button type="submit" className="text-2xl pl-1 text-green-700">
<MdAddBox className="text-3xl" />
</button>
<p
id="noti"
className="text-red-500 bg-red-200 px-2 rounded-sm opacity-0 mx-2"
></p>
</form>
);
}

View File

@@ -0,0 +1,98 @@
import { Link, NavLink } from "react-router-dom";
import { FaUserTag } from "react-icons/fa";
import { FaBoxArchive } from "react-icons/fa6";
import { MdOutlineCategory } from "react-icons/md";
import { FaArrowLeft } from "react-icons/fa";
import { FaMoneyBillTransfer } from "react-icons/fa6";
export default function DashboardNav({ handleCloseAdminDashboard }) {
const handleClick = () => {
handleCloseAdminDashboard();
};
return (
<div>
<div className="w-3xs h-screen bg-green-700 border border-green-700">
<ul>
<li>
<div className="flex-shrink-0 p-6 bg-green-200">
<Link to="/admin" className="flex items-center">
<img
src="/icon/icon-512.png"
alt="Campus Plug"
className="h-8 px-2 "
/>
<span className="hidden md:block text-green-700 font-bold text-2xl">
Campus Plug
</span>
</Link>
</div>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/user"
className={({ isActive }) =>
(isActive
? " text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5 "
}
>
<FaUserTag />
<span className="pl-3">Users</span>
</NavLink>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/product"
className={({ isActive }) =>
(isActive
? "text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5"
}
>
<FaBoxArchive />
<span className="pl-3">Products</span>
</NavLink>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/category"
className={({ isActive }) =>
(isActive
? "text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5"
}
>
<MdOutlineCategory />
<span className="pl-3">Categories</span>
</NavLink>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/transaction"
className={({ isActive }) =>
(isActive
? "text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5"
}
>
<FaMoneyBillTransfer />
<span className="pl-3">Transaction</span>
</NavLink>
</li>
</ul>
<div
onClick={handleClick}
className=" text-center my-8 underline text-white underline-offset-4 flex justify-center items-center hover:cursor-pointer h-fit w-fit mx-auto hover:text-green-200 transition"
>
<FaArrowLeft className="text-sm mx-2 mt-1" />
<span>Go back to user page</span>
</div>
</div>
</div>
);
}

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-emerald-500 text-white px-4 py-2 rounded-xl shadow-lg z-50 text-center">
{message}
</div>
);
};
export default FloatingAlert;

View File

@@ -1,10 +1,11 @@
import { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import UserDropdown from "./UserDropdown"; import UserDropdown from "./UserDropdown";
import { Search, Heart } from "lucide-react"; import { Search, Heart } from "lucide-react";
const Navbar = ({ onLogout, userName }) => { const Navbar = ({ onLogout, userName, isAdmin, handleShowAdminDashboard }) => {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const navigate = useNavigate();
const handleSearchChange = (e) => { const handleSearchChange = (e) => {
setSearchQuery(e.target.value); setSearchQuery(e.target.value);
@@ -12,8 +13,14 @@ const Navbar = ({ onLogout, userName }) => {
const handleSearchSubmit = (e) => { const handleSearchSubmit = (e) => {
e.preventDefault(); e.preventDefault();
console.log("Searching for:", searchQuery);
// TODO: Implement search functionality // if (!searchQuery.trim()) return;
// Navigate to search page with query
navigate({
pathname: "/search",
search: `?name=${encodeURIComponent(searchQuery)}`,
});
}; };
return ( return (
@@ -28,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 text-emerald-600 font-bold text-xl">
Campus Plug Campus Plug
</span> </span>
</Link> </Link>
@@ -40,14 +47,20 @@ const Navbar = ({ onLogout, userName }) => {
<div className="relative"> <div className="relative">
<input <input
type="text" type="text"
placeholder="Search for books, electronics, furniture..." placeholder="Search for anything..."
className="w-full p-2 pl-10 pr-4 border border-gray-300 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500" className="w-full p-2 pl-10 pr-4 border border-gray-300 focus:outline-none focus:border-[#ed7f30]-500 focus:ring-1 focus:ring-[#ed7f30]-500"
value={searchQuery} value={searchQuery}
onChange={handleSearchChange} onChange={handleSearchChange}
/> />
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="h-5 w-5 text-gray-400" /> <Search className="h-5 w-5 text-gray-400" />
</div> </div>
<button
type="submit"
className="absolute inset-y-0 right-0 flex items-center px-3 text-gray-500 hover:text-[#ed7f30]-500"
>
Search
</button>
</div> </div>
</form> </form>
</div> </div>
@@ -57,12 +70,18 @@ const Navbar = ({ onLogout, userName }) => {
{/* Favorites Button */} {/* Favorites Button */}
<Link <Link
to="/favorites" to="/favorites"
className="p-2 text-gray-600 hover:text-green-600" className="p-2 text-gray-600 hover:text-[#ed7f30]-600"
> >
<Heart className="h-6 w-6" /> <Heart className="h-6 w-6" />
</Link> </Link>
{/* User Profile */} {/* User Profile */}
<UserDropdown onLogout={onLogout} userName={userName} /> <UserDropdown
isAdmin={isAdmin}
onLogout={onLogout}
userName={userName}
handleShowAdminDashboard={handleShowAdminDashboard}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,99 @@
import { useState } from "react";
import { NavLink } from "react-router-dom";
export default function Pagination({ pageNum, onChange }) {
const [currentPage, setCurrentPage] = useState(1);
const pages = [];
for (let i = 1; i <= pageNum; i++) {
pages.push(i);
}
const handleClick = (page) => {
setCurrentPage(page);
onChange(page);
};
const handleTogglePage = (type) => {
let current = currentPage;
if (type == "next")
current = current + 1 <= pageNum ? current + 1 : current;
else current = current - 1 >= 1 ? current - 1 : current;
setCurrentPage(current);
onChange(current);
};
return (
<>
<nav aria-label="Page navigation" className="flex justify-end">
<ul className="flex items-center -space-x-px h-8 text-sm mt-4 pr-0 font-bold">
<li>
<NavLink
onClick={() => {
handleTogglePage("previous");
}}
className=" flex items-center justify-center px-3 h-8 ms-0 leading-tight border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 text-white bg-green-700 border border-gray-300 hover:bg-green-600 hover:text-white"
>
<span className="sr-only">Previous</span>
<svg
className="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 1 1 5l4 4"
/>
</svg>
</NavLink>
</li>
{pages.map((page) => (
<li key={page}>
<NavLink
className={`${
currentPage == page ? "bg-green-600" : "bg-green-700"
} +
" flex items-center justify-center px-3 h-8 leading-tight text-white border border-gray-300 hover:bg-green-600 hover:text-white"`}
onClick={() => {
handleClick(page);
}}
>
{page}
</NavLink>
</li>
))}
<li>
<NavLink
onClick={() => {
handleTogglePage("next");
}}
className="flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 rounded-e-lg text-white bg-green-700 border border-gray-300 hover:bg-green-600 hover:text-white"
>
<span className="sr-only">Next</span>
<svg
className="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m1 9 4-4-4-4"
/>
</svg>
</NavLink>
</li>
</ul>
</nav>
</>
);
}

View File

@@ -1,19 +1,25 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from "react";
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from "react-router-dom";
import { User, Settings, ShoppingBag, DollarSign, LogOut } from 'lucide-react'; import { User, Settings, ShoppingBag, DollarSign, LogOut } from "lucide-react";
import { RiAdminLine } from "react-icons/ri";
const UserDropdown = ({ onLogout, userName }) => { const UserDropdown = ({
onLogout,
userName,
isAdmin,
handleShowAdminDashboard,
}) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
const navigate = useNavigate(); const navigate = useNavigate();
// Use passed userName or fallback to default // Use passed userName or fallback to default
const displayName = userName || 'User'; const displayName = userName || "User";
const toggleDropdown = () => { const toggleDropdown = () => {
setIsOpen(!isOpen); setIsOpen(!isOpen);
}; };
// Close dropdown when clicking outside // Close dropdown when clicking outside
useEffect(() => { useEffect(() => {
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
@@ -21,45 +27,45 @@ const UserDropdown = ({ onLogout, userName }) => {
setIsOpen(false); setIsOpen(false);
} }
}; };
document.addEventListener('mousedown', handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener("mousedown", handleClickOutside);
}; };
}, []); }, []);
const handleLogout = () => { const handleLogout = () => {
// Close the dropdown // Close the dropdown
setIsOpen(false); setIsOpen(false);
// Call the onLogout function from props // Call the onLogout function from props
if (onLogout) { if (onLogout) {
onLogout(); onLogout();
console.log("Logout successful"); console.log("Logout successful");
} }
// Navigate to login page (this may be redundant as App.jsx should handle redirection) // Navigate to login page (this may be redundant as App.jsx should handle redirection)
navigate('/login'); navigate("/login");
}; };
return ( return (
<div className="relative" ref={dropdownRef}> <div className="relative" ref={dropdownRef}>
<button <button
className="flex items-center focus:outline-none" className="flex items-center focus:outline-none"
onClick={toggleDropdown} onClick={toggleDropdown}
> >
<div className="h-8 w-8 rounded-full bg-green-100 flex items-center justify-center"> <div className="h-8 w-8 rounded-full bg-emerald-100 flex items-center justify-center">
<User className="h-5 w-5 text-green-600" /> <User className="h-5 w-5 text-emerald-600" />
</div> </div>
</button> </button>
{isOpen && ( {isOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white border border-gray-200 shadow-md z-10"> <div className="absolute right-0 mt-2 w-48 bg-white border border-gray-200 shadow-md z-10">
{/* User Info */} {/* User Info */}
<div className="px-4 py-3 border-b border-gray-100"> <div className="px-4 py-3 border-b border-gray-100">
<p className="text-sm font-medium text-gray-900">{displayName}</p> <p className="text-sm font-medium text-gray-900">{displayName}</p>
</div> </div>
{/* Navigation Links */} {/* Navigation Links */}
<div className="py-1"> <div className="py-1">
<Link <Link
@@ -70,7 +76,7 @@ const UserDropdown = ({ onLogout, userName }) => {
<ShoppingBag className="h-4 w-4 mr-2 text-gray-500" /> <ShoppingBag className="h-4 w-4 mr-2 text-gray-500" />
My Listings My Listings
</Link> </Link>
<Link <Link
to="/transactions" to="/transactions"
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
@@ -79,7 +85,7 @@ const UserDropdown = ({ onLogout, userName }) => {
<DollarSign className="h-4 w-4 mr-2 text-gray-500" /> <DollarSign className="h-4 w-4 mr-2 text-gray-500" />
Transactions Transactions
</Link> </Link>
<Link <Link
to="/settings" to="/settings"
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
@@ -88,7 +94,21 @@ const UserDropdown = ({ onLogout, userName }) => {
<Settings className="h-4 w-4 mr-2 text-gray-500" /> <Settings className="h-4 w-4 mr-2 text-gray-500" />
Settings Settings
</Link> </Link>
{isAdmin ? (
<Link
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => {
handleShowAdminDashboard();
}}
>
<RiAdminLine className="h-4 w-4 mr-2 text-gray-500" />
Admin
</Link>
) : (
<></>
)}
<button <button
className="flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" className="flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={handleLogout} onClick={handleLogout}
@@ -103,4 +123,4 @@ const UserDropdown = ({ onLogout, userName }) => {
); );
}; };
export default UserDropdown; export default UserDropdown;

View File

@@ -1,4 +1,4 @@
@import "tailwindcss"; @import "tailwindcss";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@@ -0,0 +1,105 @@
import { useEffect, useState } from "react";
import { getCategories, removeCategory } from "../api/admin";
import { MdDelete } from "react-icons/md";
import Pagination from "../components/Pagination";
import { IoAddCircleSharp } from "react-icons/io5";
import CategoryForm from "../components/CategoryForm";
export default function CategoryDashboard() {
const [categories, setCategories] = useState([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
let pageLimit = 10;
const [visible, setVisible] = useState(false);
const onChangePage = (page, limit = 10) => {
setCurrentPage(page);
fetchCategory(page, limit);
};
const fetchCategory = (page = 1, limit = 10) => {
getCategories(page, limit).then(({ data, total }) => {
setCategories(data);
setTotal(total);
});
};
const notiChange = () => {
fetchCategory(currentPage);
};
const handleToggleForm = () => {
setVisible((curr) => !curr);
};
const handleRemove = (id) => {
removeCategory(id)
.then(() => {
fetchCategory(currentPage);
})
.catch((err) => {
console.log(err);
});
};
//Get user when initialize the component
useEffect(fetchCategory, []);
return (
<div className="pt-10 p-20 w-full">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
CATEGORIES
</h1>
<button
onClick={handleToggleForm}
className="flex justify-end items-center bg-blue-500 rounded-md px-2 text-white text-sm p-1 mb-1 ml-auto hover:cursor-pointer hover:bg-blue-600 transtion"
>
<span className="pr-1 text-xs">Add</span>
<IoAddCircleSharp />
</button>
<CategoryForm onAddCategory={notiChange} visible={visible} />
{categories.length > 0 ? (
<>
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>CategoryID</th>
<th>Name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{categories.map((category) => (
<tr
key={category.UserID}
className="border border-green-600 h-10"
>
<td>{category.CategoryID}</td>
<td>{category.Name}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemove(category.CategoryID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
pageNum={Math.ceil(total / pageLimit)}
onChange={onChangePage}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1 w-fit">
No category exists!
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function Dashboard() {
return (
<div className="text-3xl font-bold p-3 text-green-800">
Welcome to admin dashboard
</div>
);
}

View File

@@ -1,151 +1,190 @@
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, Trash2 } from "lucide-react";
const Favorites = () => { const Favorites = () => {
const [favorites, setFavorites] = useState([ const [favorites, setFavorites] = useState([]);
{ const [sortBy, setSortBy] = useState("dateAdded");
id: 0, const storedUser = JSON.parse(sessionStorage.getItem("user"));
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); function reloadPage() {
const [sortBy, setSortBy] = useState('dateAdded'); const docTimestamp = new Date(performance.timing.domLoading).getTime();
const [filterCategory, setFilterCategory] = useState('All'); const now = Date.now();
if (now > docTimestamp) {
location.reload();
}
}
// Function to remove item from favorites const mapCategory = (id) => {
const removeFromFavorites = (id) => { return id || "Other";
setFavorites(favorites.filter(item => item.id !== id));
}; };
// Available categories for filtering const removeFromFavorites = async (itemID) => {
const categories = ['All', 'Electronics', 'Textbooks', 'Furniture', 'Kitchen', 'Other']; 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,
}),
},
);
// Sort favorites based on selected sort option const data = await response.json();
const sortedFavorites = [...favorites].sort((a, b) => { if (data.success) {
if (sortBy === 'dateAdded') { reloadPage();
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;
} }
if (!response.ok) throw new Error("Failed to remove from favorites");
};
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,
name: item.Name,
price: parseFloat(item.Price),
categories: [mapCategory(item.Category)],
image: item.image_url || "/default-image.jpg",
description: item.Description || "",
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 = filterCategory === 'All'
? sortedFavorites
: sortedFavorites.filter(item => item.category === filterCategory);
return ( return (
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">My Favorites</h1> <h1 className="text-2xl font-bold text-gray-800">My Favorites</h1>
<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 */} {sortedFavorites.length === 0 ? (
{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 */}
{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-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4"
> >
Browse Listings Browse Listings
</Link> </Link>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredFavorites.map((item) => ( {sortedFavorites.map((product) => (
<div key={item.id} className="bg-white border border-gray-200 hover:shadow-md transition-shadow relative"> <div
<button key={product.id}
onClick={() => removeFromFavorites(item.id)} className="border-2 border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm text-red-500 hover:bg-red-50" >
title="Remove from favorites" <Link to={`/product/${product.id}`}>
> <div className="h-48 bg-gray-200 flex items-center justify-center">
<Trash2 className="h-5 w-5" /> {product.image ? (
</button> <img
src={product.image}
<Link to={`/product/${item.id}`}> alt={product.name}
<img src={item.image} alt={item.title} className="w-full h-48 object-cover" /> className="w-full h-full object-cover"
/>
) : (
<div className="text-gray-400">No image</div>
)}
</div>
<div className="p-4"> <div className="p-4">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start">
<h3 className="text-lg font-medium text-gray-800 leading-tight"> <h3 className="text-lg font-semibold text-gray-800">
{item.title} {product.name}
</h3> </h3>
<span className="font-semibold text-green-600">${item.price}</span> <button
</div> onClick={(e) => {
e.stopPropagation();
<div className="flex items-center text-sm text-gray-500 mb-3"> e.preventDefault();
<Tag className="h-4 w-4 mr-1" /> removeFromFavorites(product.id);
<span>{item.category}</span> }}
<span className="mx-2"></span> className="text-red-500 hover:text-red-600"
<span>{item.condition}</span> >
</div> <Trash2 size={24} />
</button>
<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-sm font-medium text-gray-700">{item.seller}</span>
</div> </div>
<p className="text-emerald-600 font-bold mt-1">
${product.price.toFixed(2)}
</p>
{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>
<p className="text-gray-400 text-xs mt-2">
Posted {product.datePosted}
</p>
</div> </div>
</Link> </Link>
</div> </div>
@@ -153,15 +192,68 @@ const Favorites = () => {
</div> </div>
)} )}
{/* Show count if there are favorites */} {sortedFavorites.length > 0 && (
{filteredFavorites.length > 0 && (
<div className="mt-6 text-sm text-gray-500"> <div className="mt-6 text-sm text-gray-500">
Showing {filteredFavorites.length} {filteredFavorites.length === 1 ? 'item' : 'items'} Showing {sortedFavorites.length}{" "}
{filterCategory !== 'All' && ` in ${filterCategory}`} {sortedFavorites.length === 1 ? "item" : "items"}
</div> </div>
)} )}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<h3 className="text-lg font-semibold mb-2">Campus Marketplace</h3>
<p className="text-gray-400 text-sm">
Your trusted university trading platform
</p>
</div>
<div className="flex space-x-6">
<div>
<h4 className="font-medium mb-2">Quick Links</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">
<Link to="/" className="hover:text-white transition">
Home
</Link>
</li>
<li className="mb-1">
<Link to="/selling" className="hover:text-white transition">
Sell an Item
</Link>
</li>
<li className="mb-1">
<Link
to="/favorites"
className="hover:text-white transition"
>
My Favorites
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Contact</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">support@campusmarket.com</li>
<li className="mb-1">University of Calgary</li>
</ul>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-6 pt-6 text-center text-sm text-gray-400">
<p>
© {new Date().getFullYear()} Campus Marketplace. All rights
reserved.
</p>
</div>
</div>
</footer>
</div> </div>
); );
}; };
export default Favorites; export default Favorites;

View File

@@ -1,35 +1,149 @@
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 { useState, useEffect, useRef } from "react";
import { Tag, ChevronLeft, ChevronRight, Bookmark, Loader } 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 [showAlert, setShowAlert] = useState(false);
const [isLoading, setIsLoading] = useState({
recommendations: true,
listings: true,
history: true,
});
const recommendationsFetched = useRef(false);
const historyFetched = useRef(false);
//After user data storing the session.
const storedUser = JSON.parse(sessionStorage.getItem("user"));
const toggleFavorite = async (id) => {
try {
const response = await fetch(
"http://localhost:3030/api/product/addFavorite",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: storedUser.ID,
productID: id,
}),
},
);
const data = await response.json();
if (data.success) {
setShowAlert(true);
// Close alert after 3 seconds
setTimeout(() => setShowAlert(false), 3000);
}
console.log(`Add Product -> Favorites: ${id}`);
} catch (error) {
console.error("Error adding favorite:", error);
}
};
const addHistory = async (id) => {
try {
await fetch("http://localhost:3030/api/history/addHistory", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: storedUser.ID,
productID: id,
}),
});
} catch (error) {
console.error("Error adding to history:", error);
}
};
// Fetch recommended products
useEffect(() => {
const fetchRecommendedProducts = async () => {
// Skip if already fetched or no user data
if (recommendationsFetched.current || !storedUser || !storedUser.ID)
return;
setIsLoading((prev) => ({ ...prev, recommendations: true }));
try {
recommendationsFetched.current = true; // Mark as fetched before the API call
const response = await fetch(
"http://localhost:3030/api/engine/recommended",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: storedUser.ID,
}),
},
);
if (!response.ok) throw new Error("Failed to fetch recommendations");
const data = await response.json();
if (data.success) {
setRecommended(
data.data.map((product) => ({
id: product.ProductID,
title: product.ProductName,
price: product.Price,
category: product.Category,
image: product.ProductImage,
seller: product.SellerName,
datePosted: product.DateUploaded,
isFavorite: false,
})),
);
} else {
throw new Error(data.message || "Error fetching recommendations");
}
} catch (error) {
console.error("Error fetching recommendations:", error);
setError(error.message);
// Reset the flag if there's an error so it can try again
recommendationsFetched.current = false;
} finally {
setIsLoading((prev) => ({ ...prev, recommendations: false }));
}
};
fetchRecommendedProducts();
}, [storedUser]); // Keep dependency
// Fetch all products
useEffect(() => { useEffect(() => {
const fetchProducts = async () => { const fetchProducts = async () => {
setIsLoading((prev) => ({ ...prev, listings: true }));
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");
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
setListings( setListings(
data.data.map((product) => ({ data.data.map((product) => ({
id: product.ProductID, id: product.ProductID,
title: product.Name, title: product.ProductName,
price: product.Price, price: product.Price,
category: product.CategoryID, category: product.Category,
image: product.ImageURL, image: product.ProductImage,
condition: "New", // Modify based on actual data seller: product.SellerName,
seller: "Unknown", // Modify if seller info is available datePosted: product.DateUploaded,
datePosted: "Just now",
isFavorite: false, isFavorite: false,
})) })),
); );
} else { } else {
throw new Error(data.message || "Error fetching products"); throw new Error(data.message || "Error fetching products");
@@ -37,142 +151,315 @@ const Home = () => {
} catch (error) { } catch (error) {
console.error("Error fetching products:", error); console.error("Error fetching products:", error);
setError(error.message); setError(error.message);
} finally {
setIsLoading((prev) => ({ ...prev, listings: false }));
} }
}; };
fetchProducts(); fetchProducts();
}, []); }, []);
// Toggle favorite status // Fetch user history
const toggleFavorite = (id, e) => { useEffect(() => {
e.preventDefault(); // Prevent navigation when clicking the heart icon const fetchUserHistory = async () => {
setListings((prevListings) => // Skip if already fetched or no user data
prevListings.map((listing) => if (historyFetched.current || !storedUser || !storedUser.ID) return;
listing.id === id
? { ...listing, isFavorite: !listing.isFavorite } setIsLoading((prev) => ({ ...prev, history: true }));
: listing try {
) historyFetched.current = true; // Mark as fetched before the API call
);
}; const response = await fetch(
"http://localhost:3030/api/history/getHistory",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: storedUser.ID,
}),
},
);
if (!response.ok) throw new Error("Failed to fetch history");
const data = await response.json();
if (data.success) {
setHistory(
data.data.map((product) => ({
id: product.ProductID,
title: product.ProductName,
price: product.Price,
category: product.Category,
image: product.ProductImage,
seller: product.SellerName,
datePosted: product.DateUploaded,
})),
);
} else {
throw new Error(data.message || "Error fetching history");
}
} catch (error) {
console.error("Error fetching history:", error);
setError(error.message);
// Reset the flag if there's an error so it can try again
historyFetched.current = false;
} finally {
setIsLoading((prev) => ({ ...prev, history: false }));
}
};
fetchUserHistory();
}, [storedUser]); // Keep dependency
const handleSelling = () => { const handleSelling = () => {
navigate("/selling"); navigate("/selling");
}; };
// Loading indicator component
const LoadingSection = () => (
<div className="flex justify-center items-center h-48">
<Loader className="animate-spin text-emerald-600 h-8 w-8" />
</div>
);
// Product card component to reduce duplication
const ProductCard = ({ product, addToHistory = false }) => (
<Link
key={product.id}
to={`/product/${product.id}`}
onClick={addToHistory ? () => addHistory(product.id) : undefined}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative"
>
<div className="relative">
<img
src={product.image}
alt={product.title}
className="w-full h-48 object-cover"
/>
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
toggleFavorite(product.id);
}}
className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm"
>
<Bookmark className="text-white w-5 h-5" />
</button>
</div>
<div className="p-4">
<h3 className="text-lg font-medium text-gray-800 leading-tight">
{product.title}
</h3>
<span className="font-semibold text-emerald-600 block mt-1">
${product.price}
</span>
<div className="flex items-center text-sm text-gray-500 mt-2">
<Tag className="h-4 w-4 mr-1" />
<span>{product.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">{product.datePosted}</span>
<span className="text-sm font-medium text-gray-700">
{product.seller}
</span>
</div>
</div>
</Link>
);
// Scrollable product list component to reduce duplication
const ScrollableProductList = ({
containerId,
products,
children,
isLoading,
addToHistory = false,
}) => (
<div className="relative py-4">
{children}
<div className="relative">
<button
onClick={() =>
document
.getElementById(containerId)
.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"
>
<ChevronLeft size={24} />
</button>
<div
id={containerId}
className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0 rounded min-h-[250px]"
>
{isLoading ? (
<LoadingSection />
) : products.length > 0 ? (
products.map((product) => (
<ProductCard
key={product.id}
product={product}
addToHistory={addToHistory}
/>
))
) : (
<div className="flex justify-center items-center w-full h-48 text-gray-500">
No products available
</div>
)}
</div>
<button
onClick={() =>
document
.getElementById(containerId)
.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"
>
<ChevronRight size={24} />
</button>
</div>
</div>
);
return ( return (
<div> <div className="flex flex-col min-h-screen">
{/* Hero Section with School Background */} <div className="flex-grow">
<div className="relative py-12 px-4 mb-8 shadow-sm"> {/* Hero Section with School Background */}
{/* Background Image - Positioned at bottom */} <div className="relative py-12 px-4 mb-8 shadow-sm">
<div className="absolute inset-0 z-0 overflow-hidden bg-black bg-opacity-100"> <div className="absolute inset-0 z-0 overflow-hidden bg-black bg-opacity-100">
<img <img
src="../public/Ucalgary.png" src="../public/Ucalgary.png"
alt="University of Calgary" alt="University of Calgary"
className="w-full h-full object-cover object-bottom opacity-50" className="w-full h-full object-cover object-bottom opacity-45"
/> />
{/* Dark overlay for better text readability */} </div>
</div>
{/* Content */} <div className="max-w-2xl mx-auto text-center relative z-1">
<div className="max-w-2xl mx-auto text-center relative z-1"> <h1 className="text-3xl font-bold text-white mb-4">
<h1 className="text-3xl font-bold text-white mb-4"> Buy and Sell on Campus
Buy and Sell on Campus </h1>
</h1> <p className="text-white mb-6">
<p className="text-white mb-6"> The marketplace exclusively for university students. Find
The marketplace exclusively for university students. Find everything everything you need or sell what you don't.
you need or sell what you don't. </p>
</p>
<button
onClick={handleSelling}
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-green-400 transition-colors"
>
Post an Item
</button>
</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 <button
key={category.id} onClick={handleSelling}
className="flex flex-col items-center justify-center p-4 bg-white border border-gray-200 hover:border-green-500 hover:shadow-sm" className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-emerald-400 transition-colors"
> >
<div className="flex items-center justify-center w-12 h-12 bg-green-50 text-green-600 rounded-full mb-2"> Post an Item
{category.icon}
</div>
<span className="text-sm font-medium text-gray-700">
{category.name}
</span>
</button> </button>
))} </div>
</div> </div>
</div> */}
{/* Recent Listings */} {/* Floating Alert */}
<div> {showAlert && (
<h2 className="text-xl font-semibold text-gray-800 mb-4"> <FloatingAlert
Recent Listings message="Product added to favorites!"
</h2> onClose={() => setShowAlert(false)}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6"> />
{listings.map((listing) => ( )}
<Link
key={listing.id}
to={`/product/${listing.id}`}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow"
>
<div className="relative">
<img
src={listing.image}
alt={listing.title}
className="w-full h-48 object-cover"
/>
<button
onClick={(e) => toggleFavorite(listing.id, e)}
className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm"
>
<Heart
className={`h-5 w-5 ${
listing.isFavorite
? "text-red-500 fill-red-500"
: "text-gray-400"
}`}
/>
</button>
</div>
<div className="p-4"> {/* Recommendations Section */}
<div className="flex justify-between items-start mb-2"> <ScrollableProductList
<h3 className="text-lg font-medium text-gray-800 leading-tight"> containerId="RecomContainer"
{listing.title} products={recommended}
</h3> isLoading={isLoading.recommendations}
<span className="font-semibold text-green-600"> addToHistory={true}
${listing.price} >
</span> <h2 className="text-xl font-semibold text-gray-800 mb-4">
</div> Recommended For You
</h2>
</ScrollableProductList>
<div className="flex items-center text-sm text-gray-500 mb-3"> {/* Recent Listings Section */}
<Tag className="h-4 w-4 mr-1" /> <ScrollableProductList
<span>{listing.category}</span> containerId="listingsContainer"
<span className="mx-2"></span> products={listings}
<span>{listing.condition}</span> isLoading={isLoading.listings}
</div> addToHistory={true}
>
<h2 className="text-xl font-semibold text-gray-800 mb-4">
Recent Listings
</h2>
</ScrollableProductList>
<div className="flex justify-between items-center pt-2 border-t border-gray-100"> {/* History Section */}
<span className="text-xs text-gray-500"> {(history.length > 0 || isLoading.history) && (
{listing.datePosted} <ScrollableProductList
</span> containerId="HistoryContainer"
<span className="text-sm font-medium text-gray-700"> products={history}
{listing.seller} isLoading={isLoading.history}
</span> >
</div> <h2 className="text-xl font-semibold text-gray-800 mb-4">
</div> Your Browsing History
</Link> </h2>
))} </ScrollableProductList>
</div> )}
</div> </div>
{/* Footer */}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<h3 className="text-lg font-semibold mb-2">Campus Marketplace</h3>
<p className="text-gray-400 text-sm">
Your trusted university trading platform
</p>
</div>
<div className="flex space-x-6">
<div>
<h4 className="font-medium mb-2">Quick Links</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">
<Link to="/" className="hover:text-white transition">
Home
</Link>
</li>
<li className="mb-1">
<Link to="/selling" className="hover:text-white transition">
Sell an Item
</Link>
</li>
<li className="mb-1">
<Link
to="/favorites"
className="hover:text-white transition"
>
My Favorites
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Contact</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">support@campusmarket.com</li>
<li className="mb-1">University of Calgary</li>
</ul>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-6 pt-6 text-center text-sm text-gray-400">
<p>
© {new Date().getFullYear()} Campus Marketplace. All rights
reserved.
</p>
</div>
</div>
</footer>
</div> </div>
); );
}; };

View File

@@ -1,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

@@ -0,0 +1,91 @@
import { useEffect, useState } from "react";
import { getProducts, removeProduct } from "../api/admin";
import { MdDelete } from "react-icons/md";
import Pagination from "../components/Pagination";
export default function ProductDashboard() {
const [products, setProducts] = useState([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
let pageLimit = 10;
const onChangePage = (page, limit = 10) => {
setCurrentPage(page);
fetchProducts(page, limit);
};
const fetchProducts = (page = 1, limit = 10) => {
getProducts(page, limit).then(({ products, total }) => {
setTotal(total);
setProducts(products);
});
};
const handleRemoveProduct = (id) => {
removeProduct(id)
.then((res) => {
fetchProducts(currentPage);
})
.catch((err) => {
console.log(err);
});
};
//Get user when initialize the component
useEffect(fetchProducts, []);
return (
<div className="pt-10 p-20">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
PRODUCTS
</h1>
{products.length > 0 ? (
<>
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>ProductID</th>
<th>Name</th>
<th>Price</th>
<th>Category</th>
<th>Seller</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr
key={product.ProductID}
className="border border-green-600 h-10"
>
<td>{product.ProductID}</td>
<td>{product.ProductName}</td>
<td>{product.Price}</td>
<td>{product.Category ? product.Category : "N/A"}</td>
<td>{product.SellerName ? product.SellerName : "N/A"}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemoveProduct(product.ProductID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
onChange={onChangePage}
pageNum={Math.ceil(total / pageLimit)}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1 w-fit">
No product exists!
</p>
)}
</div>
);
}

View File

@@ -1,298 +1,610 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useParams, Link } from "react-router-dom"; import { useParams, Link } from "react-router-dom";
import { import {
Heart,
ArrowLeft, ArrowLeft,
Tag, Tag,
User, User,
Calendar, Calendar,
Share, Star,
Flag, Phone,
Mail,
Bookmark,
} from "lucide-react"; } from "lucide-react";
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
const ProductDetail = () => { const ProductDetail = () => {
const { id } = useParams(); const { id } = useParams();
const [isFavorite, setIsFavorite] = useState(false); const [product, setProduct] = useState(null);
const [showContactForm, setShowContactForm] = useState(false); const [loading, setLoading] = useState({
const [message, setMessage] = useState(""); product: true,
reviews: true,
submitting: false,
});
const [error, setError] = useState({
product: null,
reviews: null,
submit: null,
});
const [showContactOptions, setShowContactOptions] = useState(false);
const [currentImage, setCurrentImage] = useState(0); const [currentImage, setCurrentImage] = useState(0);
const [reviews, setReviews] = useState([]);
const [showReviewForm, setShowReviewForm] = useState(false);
const [showAlert, setShowAlert] = useState(false);
const storedUser = JSON.parse(sessionStorage.getItem("user"));
// Sample data for demonstration const toggleFavorite = async (id) => {
const product = [ const response = await fetch(
{ "http://localhost:3030/api/product/addFavorite",
id: 0, {
title: "Dell XPS 13 Laptop - 2023 Model", method: "POST",
price: 850, headers: {
shortDescription: "Content-Type": "application/json",
"Dell XPS 13 laptop in excellent condition. Intel Core i7, 16GB RAM, 512GB SSD. Includes charger and original box.", },
description: body: JSON.stringify({
"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", userID: storedUser.ID,
condition: "Like New", productID: id,
category: }),
"Electronics, Electronics, Electronics, Electronics , Electronics , Electronics, Electronicss",
datePosted: "2023-03-02",
images: [
"/image1.avif",
"/image2.avif",
"/image3.avif",
"/image3.avif",
"/image3.avif",
],
seller: {
name: "Michael T.",
rating: 4.8,
memberSince: "January 2022",
avatar: "/Profile.jpg",
}, },
},
];
console.log(product[id]);
const toggleFavorite = () => {
setIsFavorite(!isFavorite);
};
const handleSendMessage = (e) => {
e.preventDefault();
// TODO: this would send the message to the seller
console.log("Message sent:", message);
setMessage("");
setShowContactForm(false);
// Show confirmation or success message
alert("Message sent to seller!");
};
// Function to split description into paragraphs
const formatDescription = (text) => {
return text.split("\n\n").map((paragraph, index) => (
<p key={index} className="mb-4">
{paragraph.split("\n").map((line, i) => (
<span key={i}>
{line}
{i < paragraph.split("\n").length - 1 && <br />}
</span>
))}
</p>
));
};
// image navigation
const nextImage = () => {
setCurrentImage((prev) =>
prev === product.images.length - 1 ? 0 : prev + 1,
); );
const data = await response.json();
if (data.success) {
setShowAlert(true);
}
};
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/addReview`,
{
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(() => {
const fetchProduct = async () => {
try {
setLoading((prev) => ({ ...prev, product: true }));
const response = await fetch(`http://localhost:3030/api/product/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
setProduct(result.data);
setError((prev) => ({ ...prev, product: null }));
} else {
throw new Error(result.message || "Error fetching product");
}
} catch (error) {
console.error("Error fetching product:", error);
setError((prev) => ({ ...prev, product: error.message }));
} finally {
setLoading((prev) => ({ ...prev, product: false }));
}
};
fetchProduct();
}, [id]);
// 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]);
// Image navigation
const nextImage = () => {
if (product?.images?.length > 0) {
setCurrentImage((prev) =>
prev === product.images.length - 1 ? 0 : prev + 1,
);
}
}; };
const prevImage = () => { const prevImage = () => {
setCurrentImage((prev) => if (product?.images?.length > 0) {
prev === 0 ? product.images.length - 1 : prev - 1, setCurrentImage((prev) =>
); prev === 0 ? product.images.length - 1 : prev - 1,
);
}
}; };
const selectImage = (index) => { const selectImage = (index) => {
setCurrentImage(index); setCurrentImage(index);
}; };
// Function to render stars based on rating
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 (
<div className="flex justify-center items-center h-screen">
<div className="animate-spin h-32 w-32 border-t-2 border-emerald-600"></div>
</div>
);
}
// Render error state for product
if (error.product) {
return (
<div className="flex justify-center items-center h-screen">
<div className="text-center">
<h2 className="text-2xl text-red-500 mb-4">Error Loading Product</h2>
<p className="text-gray-600">{error.product}</p>
<Link
to="/"
className="mt-4 inline-block bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
>
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
to="/"
className="mt-4 inline-block bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
>
Back to Listings
</Link>
</div>
</div>
);
}
// Render product details
return ( return (
<div className="max-w-6xl mx-auto px-4 py-8"> <div className="max-w-6xl mx-auto px-4 py-8">
{/* Breadcrumb & Back Link */} {/* <div className="mb-6">
<div className="mb-6">
<Link <Link
to="/" to="/search"
className="flex items-center text-green-600 hover:text-green-700" className="flex items-center text-emerald-700 hover:text-emerald-700"
> >
<ArrowLeft className="h-4 w-4 mr-1" /> <ArrowLeft className="h-4 w-4 mr-1" />
<span>Back to listings</span> <span>Back</span>
</Link> </Link>
</div> </div> */}
{showAlert && (
<FloatingAlert
message="Product added to favorites!"
onClose={() => setShowAlert(false)}
/>
)}
<div className="flex flex-col md:flex-row gap-8"> <div className="flex flex-col md:flex-row gap-8">
{/* Left Column - Images */}
<div className="md:w-3/5"> <div className="md:w-3/5">
{/* Main Image */}
<div className="bg-white border border-gray-200 mb-4 relative"> <div className="bg-white border border-gray-200 mb-4 relative">
<img {product.images && product.images.length > 0 ? (
src={product[id].images[currentImage]} <>
alt={product[id].title} <img
className="w-full h-auto object-contain cursor-pointer" src={product.images[currentImage]}
onClick={nextImage} alt={product.Name}
/> 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";
}}
/>
{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"
>
<ArrowLeft className="h-5 w-5" />
</button>
<div className="text-sm bg-white/70 px-2 py-1 ">
{currentImage + 1}/{product.images.length}
</div>
</div>
)}
</>
) : (
<div className="w-full h-96 flex items-center justify-center bg-gray-200 text-gray-500">
No Image Available
</div>
)}
</div> </div>
{/* Thumbnail Images */} {product.images && product.images.length > 1 && (
{product[id].images.length > 1 && (
<div className="flex gap-2 overflow-x-auto pb-2"> <div className="flex gap-2 overflow-x-auto pb-2">
{product[id].images.map((image, index) => ( {product.images.map((image, index) => (
<div <div
key={index} key={index}
className={`bg-white border ${currentImage === index ? "border-green-500" : "border-gray-200"} min-w-[100px] cursor-pointer`} className={`bg-white border ${currentImage === index ? "border-emerald-600 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`}
onClick={() => selectImage(index)} onClick={() => selectImage(index)}
> >
<img <img
src={image} src={image}
alt={`${product[id].title} - 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>
))} ))}
</div> </div>
)} )}
</div> </div>
{/* Right Column - Details */}
<div className="md:w-2/5"> <div className="md:w-2/5">
{/* Product Info Card */}
<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[id].title} {product.Name || "Unnamed Product"}
</h1> </h1>
<button <button
onClick={toggleFavorite} onClick={(e) => {
className="p-2 hover:bg-gray-100" e.stopPropagation();
e.preventDefault();
toggleFavorite(product.ProductID);
}}
className="top-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-600 transition shadow-sm"
> >
<Heart <Bookmark className="text-white w-5 h-5" />
className={`h-6 w-6 ${isFavorite ? "text-red-500 fill-red-500" : "text-gray-400"}`}
/>
</button> </button>
</div> </div>
<div className="text-2xl font-bold text-green-600 mb-4"> <div className="text-2xl font-bold text-emerald-700 mb-4">
${product[id].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[id].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[id].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[id].datePosted}</span> <span>Posted on {product.Date}</span>
</div> </div>
)}
</div> </div>
{/* Short Description */}
<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[id].shortDescription}</p> <p className="text-gray-700">
{product.Description || "No description available"}
</p>
</div> </div>
{/* Contact Button */} <div className="relative">
<button <button
onClick={() => setShowContactForm(!showContactForm)} onClick={() => setShowContactOptions(!showContactOptions)}
className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3" className="w-full bg-emerald-700 hover:bg-emerald-700 text-white font-medium py-3 px-4 mb-3"
> >
Contact Seller Contact Seller
</button> </button>
{/* TODO:Contact Form */} {showContactOptions && (
{showContactForm && ( <div className="absolute z-10 w-full bg-white border border-gray-200 shadow-md">
<div className="border border-gray-200 p-4 mb-4"> {product.SellerPhone && (
<h3 className="font-medium text-gray-800 mb-2"> <a
Contact Seller href={`tel:${product.SellerPhone}`}
</h3> className="flex items-center gap-2 p-3 hover:bg-gray-50 border-b border-gray-100"
<form onSubmit={handleSendMessage}>
<div className="mb-3">
<label htmlFor="email" className="block text-gray-700 mb-1">
Email
</label>
<input
type="email"
id="email"
value={product[id].User.email}
onChange={(e) => setEmail(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"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
required
/>
</div>
<div className="mb-3">
<label
htmlFor="contactMessage"
className="block text-gray-700 mb-1"
> >
Message (Optional) <Phone className="h-5 w-5 text-emerald-600" />
</label> <span>Call Seller</span>
<input </a>
type="text" )}
id="contactMessage"
value={contactMessage} {product.SellerEmail && (
onChange={(e) => setContactMessage(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-emerald-600" />
<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>
)}
{/* Seller Info */}
<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">
{product[id].seller.avatar ? ( <div className="h-12 w-12 bg-gray-200 flex items-center justify-center">
<img <User className="h-6 w-6 text-gray-600" />
src={product[id].seller.avatar} </div>
alt="Seller"
className="h-12 w-12 rounded-full"
/>
) : (
<div className="h-12 w-12 rounded-full bg-gray-200 flex items-center justify-center">
<User className="h-6 w-6 text-gray-600" />
</div>
)}
</div> </div>
<div> <div>
<h3 className="font-medium text-gray-800"> <h3 className="font-medium text-gray-800">
{product[id].seller.name} {product.SellerName || "Unknown Seller"}
</h3> </h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Member since {product[id].seller.memberSince} Member since {product.SellerJoinDate || "N/A"}
</p> </p>
</div> </div>
</div> </div>
<div className="text-sm text-gray-600">
<div>
<span className="font-medium">Rating:</span>{" "}
{product[id].seller.rating}/5
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Description Section */} {/* Reviews Section */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-800 mb-4">Description</h2> <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"> {loading.reviews ? (
{formatDescription(product[id].description)} <div className="flex justify-center py-8">
</div> <div className="animate-spin h-8 w-8 border-t-2 border-emerald-600"></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 className="mt-4">
<button
onClick={() => setShowReviewForm(true)}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4"
>
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 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 focus:outline-none focus:border-emerald-600"
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 text-gray-700 hover:bg-gray-100"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-emerald-600 text-white hover:bg-emerald-700"
disabled={loading.submitting}
>
{loading.submitting ? "Submitting..." : "Submit Review"}
</button>
</div>
</form>
</div>
</div>
)}
</div> </div>
</div> </div>
); );

View File

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

View File

@@ -1,13 +1,699 @@
import { useState } from 'react'; import { useState, useEffect } from "react";
import { Link } from 'react-router-dom'; import { useLocation, Link } from "react-router-dom";
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from 'lucide-react'; import { X, ChevronLeft, Plus, Trash2 } from "lucide-react";
const Selling = () => { const Selling = () => {
const [products, setProducts] = useState([]);
const [showForm, setShowForm] = useState(false);
const storedUser = JSON.parse(sessionStorage.getItem("user"));
const [categories, setCategories] = useState([]);
const [categoryMapping, setCategoryMapping] = useState({});
const [selectedCategory, setSelectedCategory] = useState("");
const [originalProduct, setOriginalProduct] = useState(null);
const [editingProduct, setEditingProduct] = useState({
name: "",
price: "",
description: "",
categories: [],
images: [],
});
function reloadPage() {
var doctTimestamp = new Date(performance.timing.domLoading).getTime();
var now = Date.now();
if (now > doctTimestamp) {
location.reload();
}
}
// Fetch categories from API
useEffect(() => {
const fetchCategories = async () => {
try {
const response = await fetch("http://localhost:3030/api/category");
if (!response.ok) throw new Error("Failed to fetch categories");
const responseJson = await response.json();
const data = responseJson.data;
// Create an array of category names for the dropdown
const categoryNames = [];
const mapping = {};
// Process the data properly to avoid rendering objects
Object.entries(data).forEach(([id, name]) => {
// Make sure each category name is a string
const categoryName = String(name);
categoryNames.push(categoryName);
mapping[categoryName] = parseInt(id);
});
setCategories(categoryNames);
setCategoryMapping(mapping);
} catch (error) {
console.error("Error fetching categories:", error);
}
};
fetchCategories();
}, []);
// Simulate fetching products from API/database on component mount
useEffect(() => {
const fetchProducts = async () => {
try {
// Replace with your actual API endpoint
const response = await fetch(
"http://localhost:3030/api/product/myProduct",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: storedUser.ID,
}),
},
);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const datajson = await response.json();
setProducts(datajson.data);
} catch (error) {
console.error("Error fetching products:", error);
// You might want to set an error state here
}
};
fetchProducts();
}, []); // Add userId to dependency array if it might change
// When editing a product, save the original product properly
const handleEditProduct = (product) => {
// Save the original product completely
setOriginalProduct(product);
// Convert category ID to category name if needed
const categoryName = getCategoryNameById(product.CategoryID);
setEditingProduct({
...product,
categories: categoryName ? [categoryName] : [],
images: product.images || [], // Ensure images array exists
});
setShowForm(true);
};
// Then update the handleSaveProduct function to properly merge values
const handleSaveProduct = async () => {
if (!(editingProduct.categories || []).length) {
alert("Please select at least one category");
return;
}
try {
const imagePaths = [];
// Handle images properly
if (editingProduct.images && editingProduct.images.length > 0) {
// If there are new images uploaded (File objects)
const newImages = editingProduct.images.filter(
(img) => img instanceof File,
);
newImages.forEach((file) => {
const simulatedPath = `/public/uploads/${file.name}`;
imagePaths.push(simulatedPath);
});
// Also include any existing image URLs that are strings, not File objects
const existingImages = editingProduct.images.filter(
(img) => typeof img === "string",
);
if (existingImages.length > 0) {
imagePaths.push(...existingImages);
}
} else if (originalProduct?.image_url) {
// If no new images but there was an original image URL
imagePaths.push(originalProduct.image_url);
}
const categoryName = (editingProduct.categories || [])[0];
const categoryID =
categoryMapping[categoryName] || originalProduct?.CategoryID || 1;
// Create payload with proper fallback to original values
const payload = {
name:
editingProduct.Name ||
editingProduct.name ||
originalProduct?.Name ||
"",
price: parseFloat(
editingProduct.Price ||
editingProduct.price ||
originalProduct?.Price ||
0,
),
qty: 1,
userID: storedUser.ID,
description:
editingProduct.Description ||
editingProduct.description ||
originalProduct?.Description ||
"",
category: categoryID,
images:
imagePaths.length > 0
? imagePaths
: originalProduct?.image_url
? [originalProduct.image_url]
: [],
};
console.log("Sending payload:", payload);
const endpoint = editingProduct.ProductID
? `http://localhost:3030/api/product/update/${editingProduct.ProductID}`
: "http://localhost:3030/api/product/addProduct";
const method = editingProduct.ProductID ? "PUT" : "POST";
const response = await fetch(endpoint, {
method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorData = await response.text();
throw new Error(
`${editingProduct.ProductID ? "Failed to update" : "Failed to add"} product: ${errorData}`,
);
}
const data = await response.json();
console.log("Product saved:", data);
// Reset form and hide it
setShowForm(false);
setEditingProduct({
name: "",
price: "",
description: "",
categories: [],
images: [],
});
setOriginalProduct(null); // reset original as well
reloadPage();
} catch (error) {
console.error("Error saving product:", error);
alert(`Error saving product: ${error.message}`);
}
};
// Handle product deletion
const handleDeleteProduct = async (productId) => {
try {
// Replace with your actual API endpoint
const response = await fetch(
"http://localhost:3030/api/product/delProduct",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: storedUser.ID,
productID: productId,
}),
},
);
reloadPage();
console.log("deleteproodidt");
if (!response.ok) {
throw new Error("Network response was not ok");
}
} catch (error) {
console.error("Error fetching products:", error);
// You might want to set an error state here
}
};
// Helper function to get category name from ID
const getCategoryNameById = (categoryId) => {
if (!categoryId || !categoryMapping) return null;
// Find the category name by ID
for (const [name, id] of Object.entries(categoryMapping)) {
if (id === categoryId) {
return name;
}
}
return null;
};
// Handle adding a new product
const handleAddProduct = () => {
setEditingProduct({
name: "",
price: "",
description: "",
categories: [],
images: [],
});
setShowForm(true);
};
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,
),
}));
};
const markAsSold = async () => {
// This would call an API to move the product to the transaction table
try {
// API call would go here
console.log(
"Moving product to transaction table:",
editingProduct.ProductID,
);
// Toggle the sold status in the UI
setEditingProduct((prev) => ({
...prev,
isSold: !prev.isSold,
}));
// You would add your API call here to update the backend
} catch (error) {
console.error("Error marking product as sold:", error);
}
};
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 ? (
<div className="bg-white border border-gray-200 shadow-md p-6">
{/* Back Button */}
<button
onClick={() => setShowForm(false)}
className="mb-4 text-emerald-600 hover:text-emerald-800 flex items-center gap-1"
>
<ChevronLeft size={16} />
<span>Back to Listings</span>
</button>
<h3 className="text-xl font-bold text-gray-800 mb-6 border-b border-gray-200 pb-3">
{editingProduct?.ProductID
? "Edit Your Product"
: "List a New Product"}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Product Name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Product Name
</label>
<input
type="text"
value={editingProduct.Name || editingProduct.name || ""}
onChange={(e) =>
setEditingProduct({
...editingProduct,
Name: e.target.value,
name: e.target.value,
})
}
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
/>
</div>
{/* Price */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Price ($)
</label>
<input
type="number"
value={editingProduct.Price || editingProduct.price || ""}
onChange={(e) =>
setEditingProduct({
...editingProduct,
Price: e.target.value,
price: e.target.value,
})
}
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
/>
</div>
{/* Sold Status */}
<div className="md:col-span-2">
<div className="flex items-center mt-2">
{editingProduct.isSold && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
Sold
</span>
)}
</div>
</div>
{/* Categories */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Categories
</label>
<div className="flex gap-2">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
>
<option value="" disabled>
Select a category
</option>
{categories
.filter(
(cat) => !(editingProduct.categories || []).includes(cat),
)
.map((category, index) => (
<option key={index} value={category}>
{category}
</option>
))}
</select>
<button
type="button"
onClick={addCategory}
disabled={!selectedCategory}
className="px-3 py-2 bg-emerald-600 text-white hover:bg-emerald-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center gap-1"
>
<Plus size={16} />
<span>Add</span>
</button>
</div>
{/* Selected Categories */}
{(editingProduct.categories || []).length > 0 ? (
<div className="mt-2 flex flex-wrap gap-2">
{(editingProduct.categories || []).map((category, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-1 bg-emerald-100 text-emerald-800"
>
{category}
<button
type="button"
onClick={() => removeCategory(category)}
className="ml-1 text-emerald-600 hover:text-emerald-800"
>
<X size={14} />
</button>
</span>
))}
</div>
) : (
<p className="text-xs text-gray-500 mt-1">
Please select at least one category
</p>
)}
</div>
{/* Description */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
value={
editingProduct.Description || editingProduct.description || ""
}
onChange={(e) =>
setEditingProduct({
...editingProduct,
Description: e.target.value,
description: e.target.value,
})
}
rows="4"
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
placeholder="Describe your product in detail..."
></textarea>
</div>
{/* Image Upload */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Product Images <span className="text-gray-500">(Max 5)</span>
</label>
<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 border-gray-300 bg-gray-50 text-center cursor-pointer hover:bg-gray-100"
>
<span className="text-emerald-600 font-medium">
Click to upload images
</span>
</label>
{/* Image previews */}
{(editingProduct.images || []).length > 0 && (
<div className="mt-3">
<div className="flex justify-between items-center mb-2">
<p className="text-sm text-gray-600">
{editingProduct.images.length}{" "}
{editingProduct.images.length === 1 ? "image" : "images"}{" "}
selected
</p>
<button
onClick={() =>
setEditingProduct((prev) => ({ ...prev, images: [] }))
}
className="text-sm text-red-600 hover:text-red-800 flex items-center gap-1"
>
<Trash2 size={14} />
<span>Clear all</span>
</button>
</div>
<div className="flex flex-wrap gap-2">
{editingProduct.images.map((img, idx) => (
<div
key={idx}
className="relative w-20 h-20 border border-gray-200 overflow-hidden"
>
<img
src={URL.createObjectURL(img)}
alt={`Product ${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"
>
<X size={14} />
</button>
</div>
))}
</div>
</div>
)}
{/* Show current image if editing */}
{editingProduct.image_url && (
<div className="mt-3">
<p className="text-sm text-gray-600 mb-2">Current image:</p>
<div className="relative w-20 h-20 border border-gray-200 overflow-hidden">
<img
src={editingProduct.image_url}
alt="Current product"
className="w-full h-full object-cover"
/>
</div>
</div>
)}
</div>
</div>
{/* Actions */}
<div className="mt-6 flex justify-end gap-3 border-t border-gray-200 pt-4">
<button
onClick={() => setShowForm(false)}
className="bg-gray-100 text-gray-700 px-4 py-2 hover:bg-gray-200 rounded-md"
>
Cancel
</button>
{editingProduct.ProductID && (
<button
onClick={markAsSold}
className={`px-4 py-2 rounded-md transition-colors ${
editingProduct.isSold
? "bg-green-600 text-white hover:bg-green-700"
: "bg-red-600 text-white hover:bg-red-700"
}`}
>
Mark as {editingProduct.isSold ? "Available" : "Sold"}
</button>
)}
<button
onClick={handleSaveProduct}
className="bg-emerald-600 text-white px-6 py-2 hover:bg-emerald-700 rounded-md"
>
{editingProduct.ProductID ? "Update Product" : "Add Product"}
</button>
</div>
</div>
) : (
<>
{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) => (
<Link
key={product.ProductID}
to={`/product/${product.ProductID}`}
>
<div 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.image_url && product.image_url.length > 0 ? (
<img
src={product.image_url || ""}
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>
</div>
<p className="text-emerald-600 font-bold mt-1">
${product.Price}
</p>
{product.CategoryID && (
<div className="mt-2 flex flex-wrap gap-1">
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1">
{getCategoryNameById(product.CategoryID) ||
product.CategoryID}
</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={(e) => {
e.stopPropagation();
e.preventDefault();
handleDeleteProduct(product.ProductID);
}}
className="text-red-600 hover:text-red-800"
>
Delete
</button>
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleEditProduct(product);
}}
className="text-emerald-600 hover:text-emerald-800 font-medium"
>
Edit
</button>
</div>
</div>
</div>
</Link>
))}
</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, 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({
@@ -9,13 +10,13 @@ const Settings = () => {
phone: "", phone: "",
UCID: "", UCID: "",
address: "", address: "",
currentPassword: "", password: "",
newPassword: "",
confirmPassword: "",
}); });
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(() => {
@@ -43,7 +44,7 @@ const Settings = () => {
body: JSON.stringify({ body: JSON.stringify({
email: storedUser.email, email: storedUser.email,
}), }),
} },
); );
const data = await response.json(); const data = await response.json();
@@ -53,16 +54,13 @@ const Settings = () => {
// Update state with fetched data // Update state with fetched data
setUserData((prevData) => ({ setUserData((prevData) => ({
...prevData, ...prevData,
userId: data.userId || storedUser.id || "", // Try both sources userId: storedUser.ID, // Try both sources
name: data.name || storedUser.name || "", name: data.name || storedUser.name || "",
email: data.email || storedUser.email || "", email: data.email || storedUser.email || "",
UCID: data.UCID || storedUser.UCID || "", UCID: data.UCID || storedUser.UCID || "",
phone: data.phone || storedUser.phone || "", phone: data.phone || storedUser.phone || "",
address: data.address || storedUser.address || "", address: data.address || storedUser.address || "",
// Reset password fields password: data.password,
currentPassword: "",
newPassword: "",
confirmPassword: "",
})); }));
} else { } else {
throw new Error(data.error || "Failed to retrieve user data"); throw new Error(data.error || "Failed to retrieve user data");
@@ -70,7 +68,7 @@ const Settings = () => {
} catch (error) { } catch (error) {
console.error("Error fetching user data:", error); console.error("Error fetching user data:", error);
setError( setError(
error.message || "An error occurred while loading your profile" error.message || "An error occurred while loading your profile",
); );
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -88,75 +86,65 @@ const Settings = () => {
})); }));
}; };
const handleProfileUpdate = async (e) => { const removeHistory = async () => {
e.preventDefault(); 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 () => {
try { try {
// TODO: Implement the actual update API call // Ensure userId is present
console.log("Profile updated:", userData); if (!userData.userId) {
throw new Error("User ID is missing. Unable to update profile.");
}
// Update localStorage with new user data setIsLoading(true);
const storedUser = JSON.parse(localStorage.getItem("user")); setError(null);
const updatedUser = {
...storedUser,
name: userData.name,
phone: userData.phone,
UCID: userData.UCID,
address: userData.address,
};
localStorage.setItem("user", JSON.stringify(updatedUser));
const response = await fetch("http://localhost:3030/api/user/update", {
method: "POST", // or "PUT" if your backend supports it
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(userData),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || "Failed to update profile");
}
console.log("Profile updated successfully:", result);
alert("Profile updated successfully!"); alert("Profile updated successfully!");
} catch (error) { } catch (error) {
console.error("Error updating profile:", error); console.error("Error updating profile:", error);
alert("Failed to update profile: " + error.message); setError(
error.message || "An error occurred while updating your profile.",
);
} finally {
setIsLoading(false);
} }
}; };
const handlePasswordUpdate = async (e) => {
e.preventDefault();
try {
// Validate passwords match
if (userData.newPassword !== userData.confirmPassword) {
alert("New passwords do not match!");
return;
}
// TODO: Implement the actual password update API call
console.log("Password updated");
// Update password in localStorage
const storedUser = JSON.parse(localStorage.getItem("user"));
const updatedUser = {
...storedUser,
password: userData.newPassword,
};
localStorage.setItem("user", JSON.stringify(updatedUser));
// Reset password fields
setUserData((prevData) => ({
...prevData,
currentPassword: "",
newPassword: "",
confirmPassword: "",
}));
alert("Password updated successfully!");
} catch (error) {
console.error("Error updating password:", error);
alert("Failed to update password: " + error.message);
}
};
const 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(
"Are you sure you want to delete your account? This action cannot be undone." "Are you sure you want to delete your account? This action cannot be undone.",
) )
) { ) {
try { try {
@@ -202,7 +190,7 @@ const Settings = () => {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex justify-center items-center h-64"> <div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-emerald-500"></div>
</div> </div>
); );
} }
@@ -232,7 +220,7 @@ const Settings = () => {
</div> </div>
<div className="p-4"> <div className="p-4">
<form onSubmit={handleProfileUpdate}> <form onSubmit={handleUpdateProfile}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div> <div>
<label <label
@@ -246,7 +234,7 @@ const Settings = () => {
id="name" id="name"
value={userData.name} value={userData.name}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
required required
/> />
</div> </div>
@@ -263,7 +251,7 @@ const Settings = () => {
id="email" id="email"
value={userData.email} value={userData.email}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
required required
readOnly // Email is often used as identifier and not changeable readOnly // Email is often used as identifier and not changeable
/> />
@@ -281,7 +269,7 @@ const Settings = () => {
id="phone" id="phone"
value={userData.phone} value={userData.phone}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
/> />
</div> </div>
@@ -297,7 +285,7 @@ const Settings = () => {
id="UCID" id="UCID"
value={userData.UCID} value={userData.UCID}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
required required
/> />
</div> </div>
@@ -314,14 +302,29 @@ const Settings = () => {
id="address" id="address"
value={userData.address} value={userData.address}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
/>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 mb-1"
>
password
</label>
<input
type="text"
id="password"
value={userData.password}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
/> />
</div> </div>
</div> </div>
<button <button
type="submit" type="submit"
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4" className="bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4"
> >
Update Profile Update Profile
</button> </button>
@@ -329,81 +332,13 @@ const Settings = () => {
</div> </div>
</div> </div>
{/* Security Section */}
<div className="bg-white border border-gray-200 mb-6">
<div className="border-b border-gray-200 p-4">
<div className="flex items-center">
<Lock className="h-5 w-5 text-gray-500 mr-2" />
<h2 className="text-lg font-medium text-gray-800">Password</h2>
</div>
</div>
<div className="p-4">
<form onSubmit={handlePasswordUpdate}>
<div className="space-y-4 mb-4">
<div>
<label
htmlFor="currentPassword"
className="block text-sm font-medium text-gray-700 mb-1"
>
Current Password
</label>
<input
type="password"
id="currentPassword"
value={userData.currentPassword}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
required
/>
</div>
<div>
<label
htmlFor="newPassword"
className="block text-sm font-medium text-gray-700 mb-1"
>
New Password
</label>
<input
type="password"
id="newPassword"
value={userData.newPassword}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
required
/>
</div>
<div>
<label
htmlFor="confirmPassword"
className="block text-sm font-medium text-gray-700 mb-1"
>
Confirm New Password
</label>
<input
type="password"
id="confirmPassword"
value={userData.confirmPassword}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
required
/>
</div>
</div>
<button
type="submit"
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
>
Change Password
</button>
</form>
</div>
</div>
{/* Privacy Section */} {/* Privacy Section */}
{showAlert && (
<FloatingAlert
message="We 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">
@@ -414,39 +349,18 @@ 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 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 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" />
@@ -460,7 +374,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

@@ -0,0 +1,91 @@
import { useEffect, useState } from "react";
import { getTransactions, removeTransaction } from "../api/admin";
import { MdDelete } from "react-icons/md";
import Pagination from "../components/Pagination";
export default function TransactionDashboard() {
const [transactions, setTransactions] = useState([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
let pageLimit = 10;
const onChangePage = (page, limit = 10) => {
setCurrentPage(page);
fetchTransactions(page, limit);
};
const fetchTransactions = (page = 1, limit = 10) => {
getTransactions(page, limit).then(({ transactions, total }) => {
setTotal(total);
setTransactions(transactions);
});
};
const handleRemoveTransaction = (id) => {
removeTransaction(id)
.then(() => {
fetchTransactions(currentPage);
})
.catch((err) => {
console.log(err);
});
};
//Get user when initialize the component
useEffect(fetchTransactions, []);
return (
<div className="pt-10 p-20">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
TRANSACTIONS
</h1>
{transactions.length > 0 ? (
<>
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>TransactionID</th>
<th>User</th>
<th>Product</th>
<th>Date</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{transactions.map((t) => (
<tr
key={t.TransactionID}
className="border border-green-600 h-10"
>
<td>{t.TransactionID}</td>
<td>{t.UserName ? t.UserName : "N/A"}</td>
<td>{t.ProductName ? t.ProductName : "N/A"}</td>
<td>{t.Date}</td>
<td>{t.PaymentStatus}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemoveTransaction(t.TransactionID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
onChange={onChangePage}
pageNum={Math.ceil(total / pageLimit)}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1 w-fit">
No transaction exists!
</p>
)}
</div>
);
}

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

@@ -0,0 +1,91 @@
import { useEffect, useState } from "react";
import { getUsers, removeUser } from "../api/admin";
import { MdDelete } from "react-icons/md";
import Pagination from "../components/Pagination";
export default function UserDashboard() {
const [users, setUsers] = useState([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
let pageLimit = 10;
const onChangePage = (page, limit = 10) => {
setCurrentPage(page);
fetchUsers(page, limit);
};
const fetchUsers = (page = 1, limit = 10) => {
getUsers(page, limit).then(({ users, total }) => {
setUsers(users);
setTotal(total);
});
};
const handleRemoveUser = (id) => {
removeUser(id)
.then((res) => {
fetchUsers(currentPage);
})
.catch((err) => {
console.log(err);
});
};
//Get user when initialize the component
useEffect(fetchUsers, []);
return (
<div className="pt-10 p-20">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
USERS
</h1>
{users.length > 0 ? (
<>
{" "}
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>UserID</th>
<th>UCID</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Address</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.UserID} className="border border-green-600 h-10">
<td>{user.UserID}</td>
<td>{user.UCID}</td>
<td>{user.Name}</td>
<td>{user.Email}</td>
<td>{user.Phone}</td>
<td>{user.Address}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemoveUser(user.UserID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
pageNum={Math.ceil(total / pageLimit)}
onChange={onChangePage}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1">
No user exists!
</p>
)}
</div>
);
}

456
mysql-code/Init-Data.sql Normal file
View File

@@ -0,0 +1,456 @@
-- Inserting sample data into the Marketplace database
SET
FOREIGN_KEY_CHECKS = 0;
TRUNCATE TABLE Product_Category;
TRUNCATE TABLE Favorites;
TRUNCATE TABLE History;
TRUNCATE TABLE Recommendation;
TRUNCATE TABLE Transaction;
TRUNCATE TABLE Review;
TRUNCATE TABLE Image_URL;
TRUNCATE TABLE Product;
TRUNCATE TABLE Category;
TRUNCATE TABLE UserRole;
TRUNCATE TABLE User;
TRUNCATE TABLE AuthVerification;
SET
FOREIGN_KEY_CHECKS = 1;
-- Insert Users
INSERT INTO
User (
UserID,
Name,
Email,
UCID,
Password,
Phone,
Address
)
VALUES
(
1,
'John Doe',
'john.doe@example.com',
'U123456',
'hashedpassword1',
'555-123-4567',
'123 Main St, Calgary, AB'
),
(
2,
'Jane Smith',
'jane.smith@example.com',
'U234567',
'hashedpassword2',
'555-234-5678',
'456 Oak Ave, Calgary, AB'
);
-- Insert User Roles
INSERT INTO
UserRole (UserID, Client, Admin)
VALUES
(1, TRUE, TRUE),
(2, TRUE, FALSE);
-- Insert Categories
-- Insert Categories
INSERT INTO
Category (Name)
VALUES
('Textbooks'),
('Electronics'),
('Furniture'),
('Clothing'),
('Sports Equipment'),
('Musical Instruments'),
('Art Supplies'),
('Kitchen Appliances'),
('Gaming'),
('Bicycles'),
('Computer Accessories'),
('Stationery'),
('Fitness Equipment'),
('Winter Sports'),
('Lab Equipment'),
('Camping Gear'),
('School Supplies'),
('Office Furniture'),
('Books (-textbook)'),
('Math & Science Resources'),
('Engineering Tools'),
('Backpacks & Bags'),
('Audio Equipment'),
('Dorm Essentials'),
('Smartphones & Tablets'),
('Winter Clothing'),
('Photography Equipment'),
('Event Tickets'),
('Software Licenses'),
('Transportation (Car Pool)');
-- Insert Products
INSERT INTO
Product (
ProductID,
Name,
Price,
StockQuantity,
UserID,
Description,
CategoryID,
Date
)
VALUES
(
1,
'Calculus Textbook 8th Edition',
79.99,
5,
1,
'Like new calculus textbook, minor highlighting',
1,
'2024-10-15 10:00:00'
),
(
2,
'HP Laptop',
699.99,
1,
1,
'2023 HP Pavilion, 16GB RAM, 512GB SSD',
2,
'2024-10-10 14:30:00'
),
(
3,
'Dorm Desk',
120.00,
1,
2,
'Sturdy desk perfect for studying, minor scratches',
3,
'2024-10-12 09:15:00'
),
(
4,
'University Hoodie',
35.00,
3,
2,
'Size L, university logo, worn twice',
4,
'2024-10-14 16:45:00'
),
(
5,
'Basketball',
25.50,
1,
2,
'Slightly used indoor basketball',
5,
'2024-10-11 11:20:00'
),
(
6,
'Acoustic Guitar',
175.00,
1,
1,
'Beginner acoustic guitar with case',
6,
'2024-10-09 13:10:00'
),
(
7,
'Physics Textbook',
65.00,
2,
2,
'University Physics 14th Edition, good condition',
1,
'2024-10-08 10:30:00'
),
(
8,
'Mini Fridge',
85.00,
1,
1,
'Small dorm fridge, works perfectly',
8,
'2024-10-13 15:00:00'
),
(
9,
'PlayStation 5 Controller',
55.00,
1,
2,
'Extra controller, barely used',
9,
'2024-10-07 17:20:00'
),
(
10,
'Mountain Bike',
350.00,
1,
1,
'Trek mountain bike, great condition, new tires',
10,
'2024-10-06 14:00:00'
),
(
11,
'Wireless Mouse',
22.99,
3,
1,
'Logitech wireless mouse with battery',
11,
'2024-10-05 09:30:00'
),
(
12,
'Chemistry Lab Coat',
30.00,
2,
2,
'Size M, required for chem labs',
15,
'2024-10-04 13:45:00'
),
(
13,
'Graphing Calculator',
75.00,
1,
1,
'TI-84 Plus, perfect working condition',
12,
'2024-10-03 11:15:00'
),
(
14,
'Yoga Mat',
20.00,
1,
2,
'Thick yoga mat, barely used',
13,
'2024-10-02 16:00:00'
),
(
15,
'Winter Jacket',
120.00,
1,
1,
'Columbia winter jacket, size XL, very warm',
26,
'2024-10-01 10:20:00'
),
(
16,
'Computer Science Textbook',
70.00,
1,
2,
'Introduction to Algorithms, like new',
1,
'2024-09-30 14:30:00'
),
(
17,
'Desk Lamp',
15.00,
2,
2,
'LED desk lamp with adjustable brightness',
24,
'2024-09-29 12:00:00'
),
(
18,
'Scientific Calculator',
25.00,
1,
1,
'Casio scientific calculator',
12,
'2024-09-28 11:30:00'
),
(
19,
'Bluetooth Speaker',
45.00,
1,
1,
'JBL Bluetooth speaker, great sound',
23,
'2024-09-27 15:45:00'
),
(
20,
'Backpack',
40.00,
1,
2,
'North Face backpack, lots of pockets',
22,
'2024-09-26 09:15:00'
);
INSERT INTO
Image_URL (URL, ProductID)
VALUES
('/Uploads/Dell1.jpg', 1),
('/Uploads/Dell2.jpg', 1),
('/Uploads/Dell3.jpg', 1),
('/Uploads/HP-Laptop1.jpg', 2),
('/Uploads/HP-Laptop1.jpg', 2),
('/Uploads/Dorm-Desk.jpg', 3),
('/Uploads/University-Hoodie.jpg', 4),
('/Uploads/Basketball.jpg', 5),
('/Uploads/Acoustic-Guitar.jpg', 6),
('/Uploads/Physics-Textbook.jpg', 7),
('/Uploads/Mini-Fridge.jpg', 8),
('/Uploads/Controller.jpg', 9),
('/Uploads/Mountain-Bike.jpg', 10),
('/Uploads/Wireless-Mouse.jpg', 11),
('/Uploads/Lab-Coat.jpg', 12),
('/Uploads/Calculator.jpg', 13),
('/Uploads/Yoga-Mat.jpg', 14),
('/Uploads/Winter-Jacket.jpg', 15),
('/Uploads/CS-Textbook.jpg', 16),
('/Uploads/Desk-Lamp.jpg', 17),
('/Uploads/HP-Calculator.jpg', 18),
('/Uploads/Bluetooth-Speaker.jpg', 19),
('/Uploads/Backpack.jpg', 20);
-- Insert Product-Category relationships (products with multiple categories)
INSERT INTO
Product_Category (ProductID, CategoryID)
VALUES
(1, 1),
(1, 17),
(1, 20), -- Calculus book: Textbooks, School Supplies, Math Resources
(2, 2),
(2, 11),
(2, 25), -- Laptop: Electronics, Computer Accessories, Smartphones & Tablets
(3, 3),
(3, 18),
(3, 24), -- Desk: Furniture, Office Furniture, Dorm Essentials
(4, 4),
(4, 26), -- Hoodie: Clothing, Winter Clothing
(5, 5),
(5, 13), -- Basketball: Sports Equipment, Fitness Equipment
(6, 6),
(6, 23), -- Guitar: Musical Instruments, Audio Equipment
(7, 1),
(7, 15),
(7, 20), -- Physics book: Textbooks, Lab Equipment, Math & Science Resources
(8, 8),
(8, 24), -- Mini Fridge: Kitchen Appliances, Dorm Essentials
(9, 9),
(9, 2), -- PS5 Controller: Gaming, Electronics
(10, 10),
(10, 5),
(10, 13), -- Mountain Bike: Bicycles, Sports Equipment, Fitness Equipment
(11, 11),
(11, 2), -- Mouse: Computer Accessories, Electronics
(12, 15),
(12, 17), -- Lab Coat: Lab Equipment, School Supplies
(13, 12),
(13, 17),
(13, 20), -- Calculator: Stationery, School Supplies, Math & Science Resources
(14, 13),
(14, 5), -- Yoga Mat: Fitness Equipment, Sports Equipment
(15, 26),
(15, 4),
(15, 14), -- Winter Jacket: Winter Clothing, Clothing, Winter Sports
(16, 1),
(16, 17),
(16, 19), -- CS Book: Textbooks, School Supplies, Books (Non-textbook)
(17, 24),
(17, 2), -- Desk Lamp: Dorm Essentials, Electronics
(18, 12),
(18, 17),
(18, 20), -- Scientific Calculator: Stationery, School Supplies, Math & Science
(19, 23),
(19, 2),
(19, 24), -- Bluetooth Speaker: Audio Equipment, Electronics, Dorm Essentials
(20, 22),
(20, 17),
(20, 24);
-- Insert History records
INSERT INTO
History (HistoryID, UserID, ProductID)
VALUES
(1, 1, 1),
(2, 1, 3),
(3, 1, 5),
(4, 1, 7),
(5, 1, 9),
(6, 1, 11),
(7, 2, 2),
(8, 2, 4),
(9, 2, 5),
(10, 1, 15),
(11, 1, 18);
-- Insert Favorites
INSERT INTO
Favorites (UserID, ProductID)
VALUES
(1, 2), -- User 1 likes HP Laptop
(1, 7), -- User 1 likes Physics Textbook
(2, 3), -- User 2 likes Dorm Desk
(2, 10), -- User 2 likes Mountain Bike
(1, 6), -- User 3 likes Acoustic Guitar
(1, 5), -- User 4 likes Basketball
(2, 8);
-- User 5 likes Mini Fridge
-- Insert Transactions
INSERT INTO
Transaction (
TransactionID,
UserID,
ProductID,
Date,
PaymentStatus
)
VALUES
(1, 1, 1, '2024-10-16 10:30:00', 'Completed'),
(2, 1, 6, '2024-10-15 15:45:00', 'Completed'),
(3, 1, 8, '2024-10-14 12:20:00', 'Pending'),
(4, 2, 10, '2024-10-13 17:10:00', 'Completed'),
(5, 2, 4, '2024-10-12 14:30:00', 'Completed');
INSERT INTO
Review (UserID, ProductID, Comment, Rating, Date)
VALUES
(
1,
1,
'This is a great fake product! Totally recommend it.',
5,
);

124
mysql-code/Schema.sql Normal file
View File

@@ -0,0 +1,124 @@
-- MySql Version 9.2.0
CREATE DATABASE Marketplace;
USE Marketplace;
-- User Entity
CREATE TABLE User (
UserID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(100) NOT NULL,
Email VARCHAR(100) UNIQUE NOT NULL,
UCID VARCHAR(20) UNIQUE NOT NULL,
Password VARCHAR(255) NOT NULL,
Phone VARCHAR(20),
Address VARCHAR(255)
);
CREATE TABLE UserRole (
UserID INT,
Client BOOLEAN DEFAULT True,
Admin BOOLEAN DEFAULT FALSE,
PRIMARY KEY (UserID),
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE
);
-- Category Entity (must be created before Product or else error)
CREATE TABLE Category (
CategoryID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL
);
-- Product Entity
CREATE TABLE Product (
ProductID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL,
Price DECIMAL(10, 2) NOT NULL,
StockQuantity INT,
UserID INT,
Description TEXT,
CategoryID INT NOT NULL,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE SET NULL
);
-- Fixed Image_URL table
CREATE TABLE Image_URL (
URL VARCHAR(255),
ProductID INT,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- Fixed Review Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Review (
ReviewID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
ProductID INT,
Comment TEXT,
Rating INT CHECK (
Rating >= 1
AND Rating <= 5
),
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- Transaction Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Transaction (
TransactionID INT PRIMARY KEY,
UserID INT,
ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
PaymentStatus VARCHAR(50),
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE SET NULL
);
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Recommendation (
RecommendationID_PK INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
RecommendedProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- History Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE History (
HistoryID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- Favorites Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Favorites (
FavoriteID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
ProductID INT,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
UNIQUE (UserID, ProductID)
);
-- Product-Category Junction Table (Many-to-Many)
CREATE TABLE Product_Category (
ProductID INT,
CategoryID INT,
PRIMARY KEY (ProductID, CategoryID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE CASCADE
);
-- Login Authentication table
CREATE TABLE AuthVerification (
UserID INT AUTO_INCREMENT PRIMARY KEY,
Email VARCHAR(100) UNIQUE NOT NULL,
VerificationCode VARCHAR(6) NOT NULL,
Authenticated BOOLEAN DEFAULT FALSE,
Date DATETIME DEFAULT CURRENT_TIMESTAMP
);

Binary file not shown.

View File

@@ -0,0 +1,272 @@
# pip install mysql.connector
import mysql.connector
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import logging
import random
def database():
db_connection = mysql.connector.connect(
host = "localhost",
port = "3306",
user = "root",
database = "Marketplace"
)
return db_connection
def delete_user_recommendations(user_id):
db_con = database()
cursor = db_con.cursor()
try:
cursor.execute("DELETE FROM Recommendation WHERE UserID = %s", (user_id,))
db_con.commit()
print(f"Deleted existing recommendations for user {user_id}")
logging.info(f"Deleted existing recommendations for user {user_id}")
return True
except Exception as e:
logging.error(f"Error deleting recommendations for user {user_id}: {str(e)}")
db_con.rollback()
return False
finally:
cursor.close()
db_con.close()
def get_random_products(count=10, exclude_list=None):
"""Get random products from the database, excluding any in the exclude_list"""
db_con = database()
cursor = db_con.cursor()
try:
if exclude_list and len(exclude_list) > 0:
# Convert exclude_list to string for SQL IN clause
exclude_str = ', '.join(map(str, exclude_list))
cursor.execute(f"SELECT ProductID FROM Product WHERE ProductID NOT IN ({exclude_str}) ORDER BY RAND() LIMIT {count}")
else:
cursor.execute(f"SELECT ProductID FROM Product ORDER BY RAND() LIMIT {count}")
random_products = [row[0] for row in cursor.fetchall()]
return random_products
except Exception as e:
logging.error(f"Error getting random products: {str(e)}")
return []
finally:
cursor.close()
db_con.close()
def get_popular_products(count=10):
"""Get popular products based on history table frequency"""
db_con = database()
cursor = db_con.cursor()
try:
# Get products that appear most frequently in history
cursor.execute("""
SELECT ProductID, COUNT(*) as count
FROM History
GROUP BY ProductID
ORDER BY count DESC
LIMIT %s
""", (count,))
popular_products = [row[0] for row in cursor.fetchall()]
# If not enough popular products, supplement with random ones
if len(popular_products) < count:
random_products = get_random_products(count - len(popular_products), popular_products)
popular_products.extend(random_products)
return popular_products
except Exception as e:
logging.error(f"Error getting popular products: {str(e)}")
return get_random_products(count) # Fallback to random products
finally:
cursor.close()
db_con.close()
def has_user_history_or_recommendations(user_id):
"""Check if user exists in History or Recommendation table"""
db_con = database()
cursor = db_con.cursor()
try:
# Check if user has history
cursor.execute("SELECT COUNT(*) FROM History WHERE UserID = %s", (user_id,))
history_count = cursor.fetchone()[0]
# Check if user has recommendations
cursor.execute("SELECT COUNT(*) FROM Recommendation WHERE UserID = %s", (user_id,))
recommendation_count = cursor.fetchone()[0]
return history_count > 0 or recommendation_count > 0
except Exception as e:
logging.error(f"Error checking user history/recommendations: {str(e)}")
return False
finally:
cursor.close()
db_con.close()
def get_all_products():
db_con = database()
cursor = db_con.cursor()
try:
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 = []
product_ids = []
for row in results:
text_list = list(row)
product_id = text_list.pop(0) # Save the product ID before removing it
final.append(text_list)
product_ids.append(product_id)
cursor.close()
db_con.close()
return final, product_ids # Return both feature vectors and product IDs
except Exception as e:
logging.error(f"Error getting all products: {str(e)}")
cursor.close()
db_con.close()
return [], []
def get_user_history(user_id):
db_con = database()
cursor = db_con.cursor()
try:
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 category 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
except Exception as e:
logging.error(f"Error getting user history: {str(e)}")
cursor.close()
db_con.close()
return []
def get_recommendations(user_id, top_n=5):
try:
# Always delete existing recommendations first
delete_user_recommendations(user_id)
# Check if user has history or recommendations
if not has_user_history_or_recommendations(user_id):
# Cold start: return random products
random_recs = get_random_products(top_n)
# Store these random recommendations
history_upload(user_id, random_recs)
# Add 5 more unique random products
additional_random = get_random_products(5, random_recs)
history_upload(user_id, additional_random)
return random_recs + additional_random
# Get all products and user history with their category vectors
all_product_features, all_product_ids = get_all_products()
user_history = get_user_history(user_id)
if not user_history:
# User exists but has no history yet
popular_recs = get_popular_products(top_n)
history_upload(user_id, popular_recs)
# Add 5 more unique random products
additional_random = get_random_products(5, popular_recs)
history_upload(user_id, additional_random)
return popular_recs + additional_random
# 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_product_features)
print(similarities)
# Get indices of the top N products sorted by similarity
product_indices = similarities[0].argsort()[-top_n:][::-1]
# Get the actual product IDs using the indices
recommended_product_ids = [all_product_ids[i] for i in product_indices]
# Upload the core recommendations to the database
history_upload(user_id, recommended_product_ids)
# Add 5 more unique random products that aren't in the recommendations
additional_random = get_random_products(5, recommended_product_ids)
history_upload(user_id, additional_random)
# Return both the similarity-based recommendations and the random ones
return recommended_product_ids + additional_random
except Exception as e:
logging.error(f"Recommendation error for user {user_id}: {str(e)}")
# Fallback to random products
random_products = get_random_products(top_n + 5)
return random_products
def history_upload(userID, products):
"""Upload product recommendations to the database"""
db_con = database()
cursor = db_con.cursor()
try:
for product_id in products:
# Use parameterized queries to prevent SQL injection
cursor.execute("INSERT INTO Recommendation (UserID, RecommendedProductID) VALUES (%s, %s)",
(userID, product_id))
# Commit the changes
db_con.commit()
except Exception as e:
logging.error(f"Error uploading recommendations: {str(e)}")
db_con.rollback()
finally:
# Close the cursor and connection
cursor.close()
db_con.close()

View File

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