Merge branch 'main' into mann-Branch

This commit is contained in:
Mann Patel
2025-04-21 01:04:03 -06:00
23 changed files with 1282 additions and 165 deletions

View File

@@ -1,25 +1,28 @@
### 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 ```Your-Name Branch-Name```. 2. Make a brach with the following naming conventionp, prefix it with your name ```Your-Name Branch-Name```.
--- ---
### Frontend ### Frontend
1. `cd frontend` into the dir and then type command 1. `cd frontend` into the dir and then type command
```Bash ```Bash
#Install the needed lib with the command bellow 1. npm install #Installs the needed packages
npm install 2. npm run dev #Start The Server
#Start The Server
npm run dev
``` ```
--- ---
### Backend ### Backend
1. `cd backend` into the dir and then type command 1. `cd backend` into the dir and then type command
```Bash ```Bash
#Install the needed lib with the command bellow 1. npm install #Installs the needed packages
npm install 2. npm run dev #Start The Server
#Start The Server ```
npm run dev ---
### Recommendation
1. `cd recommendation-engine` into the dir and then type command
```Bash
1. python3 server.py #Start The Server
``` ```
--- ---
### Recommendation system ### Recommendation system
@@ -34,7 +37,6 @@
2. To Create the DataBase use the command bellow: 2. To Create the DataBase use the command bellow:
```Bash ```Bash
1. mysql -u root 1. mysql -u root
2. use Marketplace; 2. \. PathToYour/Schema.sql
3. \. PathToYour/Schema.sql
3. \. PathToYour/Init-Data.sql 3. \. PathToYour/Init-Data.sql
``` ```

View File

@@ -1,5 +1,51 @@
const db = require("../utils/database"); 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) => { exports.getAllCategory = async (req, res) => {
try { try {
const [data, fields] = await db.execute(`SELECT * FROM Category`); const [data, fields] = await db.execute(`SELECT * FROM Category`);

View File

@@ -345,3 +345,62 @@ exports.getProductById = async (req, res) => {
}); });
} }
}; };
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.removeAnyProduct = async (req, res) => {
const { id } = req.params;
console.log(id);
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,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

@@ -13,13 +13,13 @@ exports.sendVerificationCode = async (req, res) => {
// Generate a random 6-digit code // Generate a random 6-digit code
const verificationCode = crypto.randomInt(100000, 999999).toString(); const verificationCode = crypto.randomInt(100000, 999999).toString();
console.log( console.log(
`Generated verification code for ${email}: ${verificationCode}`, `Generated verification code for ${email}: ${verificationCode}`
); );
// Check if email already exists in verification table // Check if email already exists in verification table
const [results, fields] = await db.execute( const [results, fields] = await db.execute(
"SELECT * FROM AuthVerification WHERE Email = ?", "SELECT * FROM AuthVerification WHERE Email = ?",
[email], [email]
); );
if (results.length > 0) { if (results.length > 0) {
@@ -27,7 +27,7 @@ exports.sendVerificationCode = async (req, res) => {
const [result] = await db.execute( const [result] = await db.execute(
`UPDATE AuthVerification SET VerificationCode = ?, Authenticated = FALSE, Date = CURRENT_TIMESTAMP `UPDATE AuthVerification SET VerificationCode = ?, Authenticated = FALSE, Date = CURRENT_TIMESTAMP
WHERE Email = ?`, WHERE Email = ?`,
[verificationCode, email], [verificationCode, email]
); );
// Send email and respond // Send email and respond
@@ -37,7 +37,7 @@ exports.sendVerificationCode = async (req, res) => {
// Insert new record // Insert new record
const [result] = await db.execute( const [result] = await db.execute(
"INSERT INTO AuthVerification (Email, VerificationCode, Authenticated) VALUES (?, ?, FALSE)", "INSERT INTO AuthVerification (Email, VerificationCode, Authenticated) VALUES (?, ?, FALSE)",
[email, verificationCode], [email, verificationCode]
); );
// Send email and respond // Send email and respond
await sendVerificationEmail(email, verificationCode); await sendVerificationEmail(email, verificationCode);
@@ -62,7 +62,7 @@ exports.verifyCode = async (req, res) => {
// Check verification code // Check verification code
const [results, fields] = await db.execute( const [results, fields] = await db.execute(
"SELECT * FROM AuthVerification WHERE Email = ? AND VerificationCode = ? AND Authenticated = 0 AND Date > DATE_SUB(NOW(), INTERVAL 15 MINUTE)", "SELECT * FROM AuthVerification WHERE Email = ? AND VerificationCode = ? AND Authenticated = 0 AND Date > DATE_SUB(NOW(), INTERVAL 15 MINUTE)",
[email, code], [email, code]
); );
if (results.length === 0) { if (results.length === 0) {
console.log(`Invalid or expired verification code for email ${email}`); console.log(`Invalid or expired verification code for email ${email}`);
@@ -76,7 +76,7 @@ exports.verifyCode = async (req, res) => {
// Mark as authenticated // Mark as authenticated
const [result] = await db.execute( const [result] = await db.execute(
"UPDATE AuthVerification SET Authenticated = TRUE WHERE Email = ?", "UPDATE AuthVerification SET Authenticated = TRUE WHERE Email = ?",
[email], [email]
); );
res.json({ res.json({
success: true, success: true,
@@ -95,7 +95,7 @@ exports.completeSignUp = async (req, res) => {
try { try {
const [results, fields] = await db.execute( const [results, fields] = await db.execute(
`SELECT * FROM AuthVerification WHERE Email = ? AND Authenticated = 1;`, `SELECT * FROM AuthVerification WHERE Email = ? AND Authenticated = 1;`,
[data.email], [data.email]
); );
if (results.length === 0) { if (results.length === 0) {
@@ -105,20 +105,20 @@ exports.completeSignUp = async (req, res) => {
// Create the user // Create the user
const [createResult] = await db.execute( const [createResult] = await db.execute(
`INSERT INTO User (Name, Email, UCID, Password, Phone, Address) `INSERT INTO User (Name, Email, UCID, Password, Phone, Address)
VALUES ('${data.name}', '${data.email}', '${data.UCID}', '${data.password}', '${data.phone}', '${data.address}')`, VALUES ('${data.name}', '${data.email}', '${data.UCID}', '${data.password}', '${data.phone}', '${data.address}')`
); );
// Insert role using the user's ID // Insert role using the user's ID
const [insertResult] = await db.execute( const [insertResult] = await db.execute(
`INSERT INTO UserRole (UserID, Client, Admin) `INSERT INTO UserRole (UserID, Client, Admin)
VALUES (LAST_INSERT_ID(), ${data.client || true}, ${ VALUES (LAST_INSERT_ID(), ${data.client || true}, ${
data.admin || false data.admin || false
})`, })`
); );
// Delete verification record // Delete verification record
const [deleteResult] = await db.execute( const [deleteResult] = await db.execute(
`DELETE FROM AuthVerification WHERE Email = '${data.email}'`, `DELETE FROM AuthVerification WHERE Email = '${data.email}'`
); );
res.json({ res.json({
@@ -310,7 +310,7 @@ exports.deleteUser = async (req, res) => {
// Delete from UserRole first (assuming foreign key constraint) // Delete from UserRole first (assuming foreign key constraint)
const [result1] = await db.execute( const [result1] = await db.execute(
"DELETE FROM UserRole WHERE UserID = ?", "DELETE FROM UserRole WHERE UserID = ?",
[userId], [userId]
); );
// Then delete from User table // Then delete from User table
@@ -328,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

@@ -9,7 +9,8 @@ const searchRouter = require("./routes/search");
const recommendedRouter = require("./routes/recommendation"); const recommendedRouter = require("./routes/recommendation");
const history = require("./routes/history"); const history = require("./routes/history");
const review = require("./routes/review"); const review = require("./routes/review");
const category = require("./routes/category"); const categoryRouter = require("./routes/category");
const transactionRouter = require("./routes/transaction");
const { generateEmailTransporter } = require("./utils/mail"); const { generateEmailTransporter } = require("./utils/mail");
const { const {
@@ -43,7 +44,9 @@ app.use("/api/search", searchRouter);
app.use("/api/engine", recommendedRouter); app.use("/api/engine", recommendedRouter);
app.use("/api/history", history); app.use("/api/history", history);
app.use("/api/review", review); app.use("/api/review", review);
app.use("/api/category", category); 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
clean_up_time = 30 * 60 * 1000; clean_up_time = 30 * 60 * 1000;

View File

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

View File

@@ -7,8 +7,10 @@ const {
getAllProducts, getAllProducts,
getProductById, getProductById,
addProduct, addProduct,
myProduct,
removeProduct, removeProduct,
removeAnyProduct,
getProductWithPagination,
myProduct,
updateProduct, updateProduct,
} = require("../controllers/product"); } = require("../controllers/product");
const router = express.Router(); const router = express.Router();
@@ -27,6 +29,12 @@ router.post("/delProduct", removeProduct);
router.post("/myProduct", myProduct); router.post("/myProduct", myProduct);
router.post("/addProduct", addProduct); router.post("/addProduct", addProduct);
router.get("/getProduct", getAllProducts); router.get("/getProduct", getAllProducts);
//Remove product
router.delete("/any/:id", removeAnyProduct);
//Get products with pagination
router.get("/getProductWithPagination", getProductWithPagination);
router.get("/:id", getProductById); // Simplified route router.get("/:id", getProductById); // Simplified route
router.put("/update/:productId", updateProduct); router.put("/update/:productId", updateProduct);

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

@@ -8,6 +8,8 @@ const {
updateUser, updateUser,
deleteUser, deleteUser,
doLogin, doLogin,
isAdmin,
getUsersWithPagination,
} = require("../controllers/user"); } = require("../controllers/user");
const router = express.Router(); const router = express.Router();
@@ -36,4 +38,10 @@ 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

@@ -13,6 +13,7 @@
"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": {
@@ -4369,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

@@ -15,6 +15,7 @@
"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": {

View File

@@ -4,6 +4,7 @@ import {
Routes, Routes,
Route, Route,
Navigate, Navigate,
useLocation,
} from "react-router-dom"; } from "react-router-dom";
import Navbar from "./components/Navbar"; import Navbar from "./components/Navbar";
import Home from "./pages/Home"; import Home from "./pages/Home";
@@ -13,6 +14,9 @@ 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 SearchPage from "./pages/SearchPage"; import SearchPage from "./pages/SearchPage";
import Dashboard from "./pages/Dashboard"; // The single consolidated dashboard component
import DashboardNav from "./components/DashboardNav";
import { verifyIsAdmin } from "./api/admin";
function App() { function App() {
// Authentication state - initialize from localStorage if available // Authentication state - initialize from localStorage if available
@@ -35,6 +39,18 @@ function App() {
useState(false); useState(false);
const [recommendations, setRecommendations] = useState([]); const [recommendations, setRecommendations] = useState([]);
// Admin state
const [isAdmin, setIsAdmin] = useState(false);
const [showAdminDashboard, setShowAdminDashboard] = useState(false);
// Check URL to determine if we're in admin mode
useEffect(() => {
// If URL contains /admin, set showAdminDashboard to true
if (window.location.pathname.includes("/admin")) {
setShowAdminDashboard(true);
}
}, []);
// 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);
@@ -120,6 +136,28 @@ function App() {
} }
}; };
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);
// Update URL without reloading page
window.history.pushState({}, "", "/admin");
};
const handleCloseAdminDashboard = () => {
setShowAdminDashboard(false);
// Update URL without reloading page
window.history.pushState({}, "", "/");
};
// Send verification code // Send verification code
const sendVerificationCode = async (userData) => { const sendVerificationCode = async (userData) => {
try { try {
@@ -409,6 +447,7 @@ function App() {
setVerificationStep("initial"); setVerificationStep("initial");
setTempUserData(null); setTempUserData(null);
setRecommendations([]); setRecommendations([]);
setShowAdminDashboard(false);
// Clear localStorage // Clear localStorage
sessionStorage.removeItem("user"); sessionStorage.removeItem("user");
@@ -749,96 +788,117 @@ function App() {
return ( return (
<Router> <Router>
<div className="min-h-screen bg-gray-50"> {/* If admin dashboard should be shown */}
{/* Show loading overlay when generating recommendations */} {showAdminDashboard ? (
{isGeneratingRecommendations && <LoadingOverlay />} <div className="flex">
<DashboardNav handleCloseAdminDashboard={handleCloseAdminDashboard} />
<Routes>
{/* Single admin route for consolidated dashboard */}
<Route path="/admin/*" element={<Dashboard />} />
{/* Any other path in admin mode should go to dashboard */}
<Route path="*" element={<Navigate to="/admin" />} />
</Routes>
</div>
) : (
/* Normal user interface */
<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}
<Routes> onLogout={handleLogout}
{/* Public routes */} userName={user?.name}
<Route handleShowAdminDashboard={handleShowAdminDashboard}
path="/login" />
element={isAuthenticated ? <Navigate to="/" /> : <LoginComponent />} )}
/> <Routes>
{/* Protected routes */} {/* Public routes */}
<Route <Route
path="/" path="/login"
element={ element={
<ProtectedRoute> isAuthenticated ? <Navigate to="/" /> : <LoginComponent />
<div className="container mx-auto px-4 py-6"> }
<Home recommendations={recommendations} /> />
</div> {/* Protected routes */}
</ProtectedRoute> <Route
} path="/"
/> element={
<Route <ProtectedRoute>
path="/product/:id" <div className="container mx-auto px-4 py-6">
element={ <Home recommendations={recommendations} />
<ProtectedRoute> </div>
<ProductDetail /> </ProtectedRoute>
</ProtectedRoute> }
} />
/> <Route
<Route path="/product/:id"
path="/search" element={
element={ <ProtectedRoute>
<ProtectedRoute> <ProductDetail />
<div className="container mx-auto px-4 py-6"> </ProtectedRoute>
<SearchPage /> }
</div> />
</ProtectedRoute> <Route
} path="/search"
/> element={
<Route <ProtectedRoute>
path="/settings" <div className="container mx-auto px-4 py-6">
element={ <SearchPage />
<ProtectedRoute> </div>
<div className="container mx-auto px-4 py-6"> </ProtectedRoute>
<Settings /> }
</div> />
</ProtectedRoute> <Route
} path="/settings"
/> element={
<Route <ProtectedRoute>
path="/selling" <div className="container mx-auto px-4 py-6">
element={ <Settings />
<ProtectedRoute> </div>
<div className="container mx-auto px-4 py-6"> </ProtectedRoute>
<Selling /> }
</div> />
</ProtectedRoute> <Route
} path="/selling"
/> element={
<Route <ProtectedRoute>
path="/transactions" <div className="container mx-auto px-4 py-6">
element={ <Selling />
<ProtectedRoute> </div>
<div className="container mx-auto px-4 py-6"> </ProtectedRoute>
<Transactions /> }
</div> />
</ProtectedRoute> <Route
} path="/transactions"
/> element={
<Route <ProtectedRoute>
path="/favorites" <div className="container mx-auto px-4 py-6">
element={ <Transactions />
<ProtectedRoute> </div>
<div className="container mx-auto px-4 py-6"> </ProtectedRoute>
<Favorites /> }
</div> />
</ProtectedRoute> <Route
} path="/favorites"
/> element={
{/* Redirect to login for any unmatched routes */} <ProtectedRoute>
<Route <div className="container mx-auto px-4 py-6">
path="*" <Favorites />
element={<Navigate to={isAuthenticated ? "/" : "/login"} />} </div>
/> </ProtectedRoute>
</Routes> }
</div> />
{/* Redirect to login for any unmatched routes */}
<Route
path="*"
element={<Navigate to={isAuthenticated ? "/" : "/login"} />}
/>
</Routes>
</div>
)}
</Router> </Router>
); );
} }

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

@@ -0,0 +1,119 @@
// api.js
import axios from "axios";
const client = axios.create({
baseURL: "http://localhost:3030/api",
});
// Users
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) {
return handleError(error);
}
};
export const removeUser = async (id) => {
try {
const { data } = await client.post(`/user/delete`, { userId: id });
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
export const verifyIsAdmin = async (id) => {
try {
const { data } = await client.get(`/user/isAdmin/${id}`);
return { isAdmin: data.isAdmin };
} catch (error) {
return handleError(error);
}
};
// Products
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) {
return handleError(error);
}
};
export const removeProduct = async (id) => {
try {
const { data } = await client.delete(`/product/any/${id}`);
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
// Categories
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) {
return handleError(error);
}
};
export const addCategory = async (name) => {
try {
const { data } = await client.post(`/category/addCategory`, { name });
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
export const removeCategory = async (id) => {
try {
const { data } = await client.delete(`/category/${id}`);
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
// Transactions
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) {
return handleError(error);
}
};
export const removeTransaction = async (id) => {
try {
const { data } = await client.delete(`/transaction/${id}`);
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
// Shared Error Handler
const handleError = (error) => {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
};
// Optional: export client if you want to use it elsewhere
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,15 @@
import { FaArrowLeft } from "react-icons/fa";
export default function DashboardNav({ handleCloseAdminDashboard }) {
return (
<div className="w-48 min-w-[12rem] bg-gray-100 text-emerald-600 flex flex-col p-4 shadow-md">
<button
onClick={handleCloseAdminDashboard}
className="flex items-center gap-2 text-sm font-medium hover:text-emerald-700 underline underline-offset-4 transition"
>
<FaArrowLeft className="text-xs mt-[1px]" />
Back to User Page
</button>
</div>
);
}

View File

@@ -3,7 +3,7 @@ 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 navigate = useNavigate();
@@ -76,7 +76,12 @@ const Navbar = ({ onLogout, userName }) => {
</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,8 +1,14 @@
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();
@@ -89,6 +95,20 @@ const UserDropdown = ({ onLogout, userName }) => {
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}

View File

@@ -0,0 +1,362 @@
import { useEffect, useState, useCallback } from "react";
import {
getUsers,
removeUser,
getTransactions,
removeTransaction,
getProducts,
removeProduct,
getCategories,
removeCategory,
} from "../api/admin";
import { MdDelete } from "react-icons/md";
import { IoAddCircleSharp } from "react-icons/io5";
import { FaHome } from "react-icons/fa";
import Pagination from "../components/Pagination";
import CategoryForm from "../components/CategoryForm";
import DashboardNav from "../components/DashboardNav";
import { useNavigate } from "react-router-dom";
// Spinner Component
const Spinner = () => (
<div className="flex justify-center items-center h-40 w-full">
<div className="w-12 h-12 border-4 border-green-500 border-dashed rounded-full animate-spin"></div>
</div>
);
// Empty State Component
const EmptyState = () => (
<div className="flex flex-col items-center justify-center bg-gray-50 py-10 rounded-lg w-full">
<svg
className="w-16 h-16 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
></path>
</svg>
<p className="mt-4 text-lg font-medium text-gray-600">No data found</p>
<p className="mt-1 text-sm text-gray-500">
No records are currently available.
</p>
</div>
);
// Generic Dashboard Component
const Dashboard = ({
fetchDataFn,
deleteFn,
columns,
idKey,
refreshKey = 0,
headerAction = null,
}) => {
const navigate = useNavigate();
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(true);
const pageLimit = 10;
const fetchItems = useCallback(
(page = 1, limit = 10) => {
setLoading(true);
fetchDataFn(page, limit)
.then((res) => {
const data =
res.users || res.products || res.transactions || res.data || [];
setItems(data);
setTotal(res.total);
})
.catch((error) => {
console.error("Error fetching data:", error);
setItems([]);
setTotal(0);
})
.finally(() => setLoading(false));
},
[fetchDataFn],
);
const handleRemove = (id) => {
if (window.confirm("Are you sure you want to delete this item?")) {
setLoading(true);
deleteFn(id)
.then(() => fetchItems(currentPage))
.catch((error) => {
console.error("Error removing item:", error);
setLoading(false);
});
}
};
useEffect(() => {
fetchItems(currentPage);
}, [fetchItems, currentPage, refreshKey]);
const onChangePage = (page) => {
setCurrentPage(page);
};
if (loading) return <Spinner />;
return (
<div className="w-full mt-6">
<div className="flex flex-col md:flex-row justify-between items-center mb-4 w-full">
<h2 className="text-xl font-semibold text-gray-700 mb-2 md:mb-0">
Total: <span className="text-green-600">{total}</span>
</h2>
{headerAction && <div>{headerAction}</div>}
</div>
{items.length > 0 ? (
<div className="w-full overflow-x-auto bg-white rounded-lg shadow">
<table className="min-w-full divide-y divide-gray-200 table-fixed">
<thead className="bg-green-600">
<tr>
{columns.map((col) => (
<th
key={col.label}
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider"
>
{col.label}
</th>
))}
<th
scope="col"
className="px-6 py-3 text-center text-xs font-medium text-white uppercase tracking-wider w-20"
>
Action
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{items.map((item) => (
<tr
key={item[idKey]}
className="hover:bg-gray-50 transition-colors"
>
{columns.map((col) => (
<td
key={`${item[idKey]}-${col.key}`}
className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 overflow-hidden text-ellipsis"
>
{item[col.key] || "—"}
</td>
))}
<td className="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
<button
onClick={() => handleRemove(item[idKey])}
className="text-red-500 hover:text-red-700 transition-colors"
title="Delete"
>
<MdDelete size={20} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<EmptyState />
)}
{total > pageLimit && (
<div className="mt-4 w-full">
<Pagination
pageNum={Math.ceil(total / pageLimit)}
onChange={onChangePage}
/>
</div>
)}
</div>
);
};
// Main Admin Tabs
export default function AdminDashboardTabs() {
const [activeTab, setActiveTab] = useState(0);
const [tabData, setTabData] = useState({
users: { loaded: false, data: null },
products: { loaded: false, data: null },
transactions: { loaded: false, data: null },
categories: { loaded: false, data: null },
});
const [visible, setVisible] = useState(false);
const [categoryRefreshKey, setCategoryRefreshKey] = useState(0);
const toggleForm = () => setVisible((v) => !v);
// Preload all tab data
useEffect(() => {
const tabKeys = ["users", "products", "transactions", "categories"];
const loadTabData = async (index) => {
if (index === tabKeys.length) return;
setTabData((prev) => ({
...prev,
[tabKeys[index]]: { ...prev[tabKeys[index]], loaded: true },
}));
// Load next tab after a short delay
setTimeout(() => loadTabData(index + 1), 100);
};
loadTabData(0);
}, []);
const tabs = [
{
title: "Users",
icon: "👥",
key: "users",
component: () => (
<Dashboard
fetchDataFn={getUsers}
deleteFn={removeUser}
idKey="UserID"
columns={[
{ label: "ID", key: "UserID" },
{ label: "UCID", key: "UCID" },
{ label: "Name", key: "Name" },
{ label: "Email", key: "Email" },
{ label: "Phone", key: "Phone" },
{ label: "Address", key: "Address" },
]}
/>
),
},
{
title: "Products",
icon: "📦",
key: "products",
component: () => (
<Dashboard
fetchDataFn={getProducts}
deleteFn={removeProduct}
idKey="ProductID"
columns={[
{ label: "ID", key: "ProductID" },
{ label: "Name", key: "ProductName" },
{ label: "Price", key: "Price" },
{ label: "Category", key: "Category" },
{ label: "Seller", key: "SellerName" },
]}
/>
),
},
{
title: "Transactions",
icon: "💰",
key: "transactions",
component: () => (
<Dashboard
fetchDataFn={getTransactions}
deleteFn={removeTransaction}
idKey="TransactionID"
columns={[
{ label: "ID", key: "TransactionID" },
{ label: "User", key: "UserName" },
{ label: "Product", key: "ProductName" },
{ label: "Date", key: "Date" },
{ label: "Status", key: "PaymentStatus" },
]}
/>
),
},
{
title: "Categories",
icon: "🏷️",
key: "categories",
component: () => (
<>
<Dashboard
fetchDataFn={getCategories}
deleteFn={removeCategory}
idKey="CategoryID"
columns={[
{ label: "ID", key: "CategoryID" },
{ label: "Name", key: "Name" },
]}
refreshKey={categoryRefreshKey}
headerAction={
<button
onClick={toggleForm}
className="flex items-center bg-green-500 rounded-md px-4 py-2 text-white text-sm hover:bg-green-600 transition-colors font-medium shadow"
>
<IoAddCircleSharp className="mr-1" size={18} />
Add Category
</button>
}
/>
<CategoryForm
visible={visible}
onAddCategory={() => {
setCategoryRefreshKey((prev) => prev + 1);
toggleForm();
}}
/>
</>
),
},
];
return (
<div className="w-full min-h-screen bg-gray-50">
<div className="w-full px-4 py-8">
<div className="mb-8 flex justify-between items-center w-full">
<h1 className="text-2xl md:text-3xl font-bold text-gray-800">
Admin Dashboard
</h1>
</div>
{/* Mobile Tabs */}
<div className="md:hidden w-full mb-4">
<select
className="w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring focus:ring-green-500 focus:ring-opacity-50 p-2"
value={activeTab}
onChange={(e) => setActiveTab(parseInt(e.target.value))}
>
{tabs.map((tab, index) => (
<option key={tab.key} value={index}>
{tab.icon} {tab.title}
</option>
))}
</select>
</div>
{/* Desktop Tabs */}
<div className="hidden md:flex space-x-1 border-b border-gray-200 w-full overflow-x-auto">
{tabs.map((tab, index) => (
<button
key={tab.key}
className={`px-6 py-3 font-medium text-sm rounded-t-lg transition-colors ${
index === activeTab
? "text-green-700 bg-white border-l border-t border-r border-gray-200 border-b-0"
: "text-gray-600 hover:text-green-700 bg-gray-50"
}`}
onClick={() => setActiveTab(index)}
>
<span className="inline-block mr-2">{tab.icon}</span>
{tab.title}
</button>
))}
</div>
<div className="bg-white p-4 md:p-6 rounded-lg shadow-sm border border-gray-200 mt-4 w-full">
<div className="w-full">
{tabData[tabs[activeTab].key].loaded && tabs[activeTab].component()}
</div>
</div>
</div>
</div>
);
}

124
frontend/src/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 CASCADE,
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 CASCADE,
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 CASCADE
);
-- 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
);

View File

@@ -67,40 +67,42 @@ VALUES
(1, TRUE, TRUE), (1, TRUE, TRUE),
(2, TRUE, FALSE); (2, TRUE, FALSE);
-- Insert Categories
-- Insert Categories -- Insert Categories
INSERT INTO INSERT INTO
Category (CategoryID, Name) Category (Name)
VALUES VALUES
(1, 'Textbooks'), ('Textbooks'),
(2, 'Electronics'), ('Electronics'),
(3, 'Furniture'), ('Furniture'),
(4, 'Clothing'), ('Clothing'),
(5, 'Sports Equipment'), ('Sports Equipment'),
(6, 'Musical Instruments'), ('Musical Instruments'),
(7, 'Art Supplies'), ('Art Supplies'),
(8, 'Kitchen Appliances'), ('Kitchen Appliances'),
(9, 'Gaming'), ('Gaming'),
(10, 'Bicycles'), ('Bicycles'),
(11, 'Computer Accessories'), ('Computer Accessories'),
(12, 'Stationery'), ('Stationery'),
(13, 'Fitness Equipment'), ('Fitness Equipment'),
(14, 'Winter Sports'), ('Winter Sports'),
(15, 'Lab Equipment'), ('Lab Equipment'),
(16, 'Camping Gear'), ('Camping Gear'),
(17, 'School Supplies'), ('School Supplies'),
(18, 'Office Furniture'), ('Office Furniture'),
(19, 'Books (Non-textbook)'), ('Books (-textbook)'),
(20, 'Math & Science Resources'), ('Math & Science Resources'),
(21, 'Engineering Tools'), ('Engineering Tools'),
(22, 'Backpacks & Bags'), ('Backpacks & Bags'),
(23, 'Audio Equipment'), ('Audio Equipment'),
(24, 'Dorm Essentials'), ('Dorm Essentials'),
(25, 'Smartphones & Tablets'), ('Smartphones & Tablets'),
(26, 'Winter Clothing'), ('Winter Clothing'),
(27, 'Photography Equipment'), ('Photography Equipment'),
(28, 'Event Tickets'), ('Event Tickets'),
(29, 'Software Licenses'), ('Software Licenses'),
(30, 'Transportation (Car Pool)'); ('Transportation (Car Pool)'),
('Other');
-- Insert Products -- Insert Products
INSERT INTO INSERT INTO

View File

@@ -24,11 +24,10 @@ CREATE TABLE UserRole (
-- Category Entity (must be created before Product or else error) -- Category Entity (must be created before Product or else error)
CREATE TABLE Category ( CREATE TABLE Category (
CategoryID INT PRIMARY KEY, CategoryID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL Name VARCHAR(255) NOT NULL
); );
-- Product Entity
CREATE TABLE Product ( CREATE TABLE Product (
ProductID INT AUTO_INCREMENT PRIMARY KEY, ProductID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL, Name VARCHAR(255) NOT NULL,
@@ -42,11 +41,25 @@ CREATE TABLE Product (
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
); );
-- 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,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
);
-- Fixed Image_URL table -- Fixed Image_URL table
CREATE TABLE Image_URL ( CREATE TABLE Image_URL (
URL VARCHAR(255), URL VARCHAR(255),
ProductID INT, ProductID INT,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
); );
-- Fixed Review Entity (Many-to-One with User, Many-to-One with Product) -- Fixed Review Entity (Many-to-One with User, Many-to-One with Product)
@@ -60,8 +73,8 @@ CREATE TABLE Review (
AND Rating <= 5 AND Rating <= 5
), ),
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
); );
-- Transaction Entity (Many-to-One with User, Many-to-One with Product) -- Transaction Entity (Many-to-One with User, Many-to-One with Product)
@@ -71,8 +84,8 @@ CREATE TABLE Transaction (
ProductID INT, ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
PaymentStatus VARCHAR(50), PaymentStatus VARCHAR(50),
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE SET NULL
); );
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product) -- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
@@ -81,8 +94,8 @@ CREATE TABLE Recommendation (
UserID INT, UserID INT,
RecommendedProductID INT, RecommendedProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
); );
-- History Entity (Many-to-One with User, Many-to-One with Product) -- History Entity (Many-to-One with User, Many-to-One with Product)
@@ -91,8 +104,8 @@ CREATE TABLE History (
UserID INT, UserID INT,
ProductID INT, ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
); );
-- Favorites Entity (Many-to-One with User, Many-to-One with Product) -- Favorites Entity (Many-to-One with User, Many-to-One with Product)
@@ -100,8 +113,8 @@ CREATE TABLE Favorites (
FavoriteID INT AUTO_INCREMENT PRIMARY KEY, FavoriteID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT, UserID INT,
ProductID INT, ProductID INT,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID), FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
UNIQUE (UserID, ProductID) UNIQUE (UserID, ProductID)
); );
@@ -110,8 +123,8 @@ CREATE TABLE Product_Category (
ProductID INT, ProductID INT,
CategoryID INT, CategoryID INT,
PRIMARY KEY (ProductID, CategoryID), PRIMARY KEY (ProductID, CategoryID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID), FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE CASCADE
); );
-- Login Authentication table -- Login Authentication table