4 Commits

Author SHA1 Message Date
Mann Patel
bbddc8566a Enforce @ucalgary.ca emails for registration & require login after account creation 2025-04-22 12:18:10 -06:00
Mann Patel
4ba6dfa7be UI Color update 2025-04-21 22:46:39 -06:00
estherdev03
0a1db869f7 fix delete category 2025-04-21 17:03:09 -06:00
estherdev03
a8745ed94c Fix admin 2025-04-21 16:53:51 -06:00
21 changed files with 258 additions and 508 deletions

View File

@@ -25,12 +25,13 @@
1. python3 server.py #Start The Server 1. python3 server.py #Start The Server
``` ```
--- ---
### Recommendation system ### Recommendation system
1. Install the dependencies 1. Install the dependencies
```Bash ```Bash
pip install mysql.connector pip install mysql.connector
``` ```
---
### Database ### Database
1. MySql Version 9.2.0 1. MySql Version 9.2.0

View File

@@ -7,7 +7,7 @@ exports.getAllCategoriesWithPagination = async (req, res) => {
try { try {
const [data, _] = await db.execute( const [data, _] = await db.execute(
"SELECT * FROM Category C ORDER BY C.CategoryID ASC LIMIT ? OFFSET ?", "SELECT * FROM Category C ORDER BY C.CategoryID ASC LIMIT ? OFFSET ?",
[limit.toString(), offset.toString()], [limit.toString(), offset.toString()]
); );
const [result] = await db.execute("SELECT COUNT(*) AS count FROM Category"); const [result] = await db.execute("SELECT COUNT(*) AS count FROM Category");
@@ -24,7 +24,7 @@ exports.addCategory = async (req, res) => {
try { try {
const [result] = await db.execute( const [result] = await db.execute(
"INSERT INTO Category (Name) VALUES (?)", "INSERT INTO Category (Name) VALUES (?)",
[name], [name]
); );
res.json({ message: "Adding new category successfully!" }); res.json({ message: "Adding new category successfully!" });
} catch (error) { } catch (error) {
@@ -34,15 +34,23 @@ exports.addCategory = async (req, res) => {
exports.removeCategory = async (req, res) => { exports.removeCategory = async (req, res) => {
const { id } = req.params; const { id } = req.params;
try { try {
if (id == "1") throw Error("You're not allowed to delete this category!");
const [updateResult] = await db.execute(
"UPDATE Product SET CategoryID = 1 WHERE CategoryID = ?",
[id]
);
const [result] = await db.execute( const [result] = await db.execute(
`DELETE FROM Category WHERE CategoryID = ?`, `DELETE FROM Category WHERE CategoryID = ?`,
[id], [id]
); );
res.json({ message: "Delete category successfully!" }); res.json({ message: "Delete category successfully!" });
} catch (error) { } catch (error) {
res.json({ error: "Cannot remove category from database!" }); res.json({
error: error.message || "Cannot remove category from database!",
});
} }
}; };

View File

@@ -292,7 +292,8 @@ function App() {
// Set authenticated user // Set authenticated user
setUser(newUser); setUser(newUser);
setIsAuthenticated(true); setIsSignUp(false);
//setIsAuthenticated(true);
// Save to localStorage to persist across refreshes // Save to localStorage to persist across refreshes
sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("isAuthenticated", "true");
@@ -338,12 +339,11 @@ 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
@@ -517,7 +517,7 @@ function App() {
// Loading overlay component // Loading overlay component
const LoadingOverlay = () => ( const LoadingOverlay = () => (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <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 className="animate-spin rounded-full h-12 w-12 border-4 border-emerald-600 border-t-transparent"></div>
</div> </div>
); );
@@ -584,7 +584,7 @@ function App() {
id="name" id="name"
name="name" name="name"
placeholder="Enter your name" placeholder="Enter your name"
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" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -603,7 +603,7 @@ function App() {
id="ucid" id="ucid"
name="ucid" name="ucid"
placeholder="1234567" placeholder="1234567"
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" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -621,7 +621,7 @@ function App() {
id="email" id="email"
name="email" name="email"
placeholder="your.email@ucalgary.ca" placeholder="your.email@ucalgary.ca"
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" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required required
/> />
</div> </div>
@@ -639,7 +639,7 @@ function App() {
id="phone" id="phone"
name="phone" name="phone"
placeholder="+1(123)456 7890" placeholder="+1(123)456 7890"
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" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -658,7 +658,7 @@ function App() {
id="address" id="address"
name="address" name="address"
placeholder="Your 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" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -680,7 +680,7 @@ function App() {
? "Create a secure password" ? "Create a secure password"
: "Enter your password" : "Enter your password"
} }
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" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required required
/> />
</div> </div>
@@ -689,7 +689,7 @@ function App() {
<button <button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className="w-full px-6 py-2 text-base font-medium text-white bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2 transition-colors disabled:bg-green-300" className="w-full px-6 py-2 text-base font-medium text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400 focus:ring-offset-2 transition-colors disabled:bg-emerald-300"
> >
{isLoading {isLoading
? "Please wait..." ? "Please wait..."
@@ -716,7 +716,7 @@ function App() {
id="verificationCode" id="verificationCode"
name="verificationCode" name="verificationCode"
placeholder="Enter the 6-digit code" placeholder="Enter the 6-digit code"
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" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required required
/> />
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">
@@ -728,7 +728,7 @@ function App() {
<button <button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className="w-full px-6 py-2 text-base font-medium text-white bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2 transition-colors disabled:bg-green-300" className="w-full px-6 py-2 text-base font-medium text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400 focus:ring-offset-2 transition-colors disabled:bg-emerald-300"
> >
{isLoading ? "Please wait..." : "Verify Code"} {isLoading ? "Please wait..." : "Verify Code"}
</button> </button>
@@ -746,7 +746,7 @@ function App() {
type="button" type="button"
onClick={handleResendCode} onClick={handleResendCode}
disabled={isLoading} disabled={isLoading}
className="text-sm text-green-500 hover:text-green-700" className="text-sm text-emerald-600 hover:text-emerald-700"
> >
Resend code Resend code
</button> </button>
@@ -764,7 +764,7 @@ function App() {
<button <button
onClick={toggleAuthMode} onClick={toggleAuthMode}
type="button" type="button"
className="text-green-500 font-medium hover:text-green-700" className="text-emerald-600 font-medium hover:text-emerald-700"
> >
{isSignUp ? "Sign in" : "Sign up"} {isSignUp ? "Sign in" : "Sign up"}
</button> </button>

View File

@@ -7,10 +7,10 @@ const client = axios.create({
}); });
// Users // Users
export const getUsers = async (page, limit = 10) => { export const getUsers = async (page = 1, limit = 10) => {
try { try {
const { data } = await client.get( const { data } = await client.get(
`/user/getUserWithPagination?page=${page}&limit=${limit}`, `/user/getUserWithPagination?page=${page}&limit=${limit}`
); );
return { users: data.users, total: data.total }; return { users: data.users, total: data.total };
} catch (error) { } catch (error) {
@@ -37,10 +37,10 @@ export const verifyIsAdmin = async (id) => {
}; };
// Products // Products
export const getProducts = async (page, limit = 10) => { export const getProducts = async (page = 1, limit = 10) => {
try { try {
const { data } = await client.get( const { data } = await client.get(
`/product/getProductWithPagination?limit=${limit}&page=${page}`, `/product/getProductWithPagination?limit=${limit}&page=${page}`
); );
return { products: data.products, total: data.totalProd }; return { products: data.products, total: data.totalProd };
} catch (error) { } catch (error) {
@@ -61,7 +61,7 @@ export const removeProduct = async (id) => {
export const getCategories = async (page, limit = 10) => { export const getCategories = async (page, limit = 10) => {
try { try {
const { data } = await client.get( const { data } = await client.get(
`/category/getCategories?page=${page}&limit=${limit}`, `/category/getCategories?page=${page}&limit=${limit}`
); );
return { data: data.data, total: data.total }; return { data: data.data, total: data.total };
} catch (error) { } catch (error) {
@@ -81,6 +81,7 @@ export const addCategory = async (name) => {
export const removeCategory = async (id) => { export const removeCategory = async (id) => {
try { try {
const { data } = await client.delete(`/category/${id}`); const { data } = await client.delete(`/category/${id}`);
if (data.error) throw Error(data.error);
return { message: data.message }; return { message: data.message };
} catch (error) { } catch (error) {
return handleError(error); return handleError(error);
@@ -88,10 +89,10 @@ export const removeCategory = async (id) => {
}; };
// Transactions // Transactions
export const getTransactions = async (page, limit = 10) => { export const getTransactions = async (page = 1, limit = 10) => {
try { try {
const { data } = await client.get( const { data } = await client.get(
`/transaction/getTransactions?limit=${limit}&page=${page}`, `/transaction/getTransactions?limit=${limit}&page=${page}`
); );
return { transactions: data.data, total: data.total }; return { transactions: data.data, total: data.total };
} catch (error) { } catch (error) {
@@ -112,7 +113,7 @@ export const removeTransaction = async (id) => {
const handleError = (error) => { const handleError = (error) => {
const { response } = error; const { response } = error;
if (response?.data) return response.data; if (response?.data) return response.data;
return { error: error.message || error }; return alert(error.message || error);
}; };
// Optional: export client if you want to use it elsewhere // Optional: export client if you want to use it elsewhere

View File

@@ -22,7 +22,7 @@ export default function CategoryForm({ visible, onAddCategory }) {
.classList.remove("opacity-0", "bg-red-200", "text-red-500"); .classList.remove("opacity-0", "bg-red-200", "text-red-500");
document document
.getElementById("noti") .getElementById("noti")
.classList.add("bg-green-200", "text-green-800"); .classList.add("bg-emerald-200", "text-emerald-800");
document.getElementById("noti").innerHTML = `${message.message}`; document.getElementById("noti").innerHTML = `${message.message}`;
setCategory(""); setCategory("");
onAddCategory(); onAddCategory();
@@ -42,18 +42,18 @@ export default function CategoryForm({ visible, onAddCategory }) {
return ( return (
<form onSubmit={handleSubmit} action="" className="flex p-2 items-center"> <form onSubmit={handleSubmit} action="" className="flex p-2 items-center">
<label htmlFor="category" className="text-green-700"> <label htmlFor="category" className="text-emerald-700">
Category: Category:
</label> </label>
<input <input
type="text" type="text"
className="border border-green-700 ml-2 rounded-sm focus:bg-green-100 text-green-900" className="border border-emerald-700 ml-2 rounded-sm focus:bg-emerald-100 text-emerald-900"
name="category" name="category"
id="category" id="category"
onChange={handleChange} onChange={handleChange}
value={category} value={category}
/> />
<button type="submit" className="text-2xl pl-1 text-green-700"> <button type="submit" className="text-2xl pl-1 text-emerald-700">
<MdAddBox className="text-3xl" /> <MdAddBox className="text-3xl" />
</button> </button>
<p <p

View File

@@ -3,6 +3,7 @@ import { FaArrowLeft } from "react-icons/fa";
export default function DashboardNav({ handleCloseAdminDashboard }) { export default function DashboardNav({ handleCloseAdminDashboard }) {
return ( return (
<div className="w-48 min-w-[12rem] bg-gray-100 text-emerald-600 flex flex-col p-4 shadow-md"> <div className="w-48 min-w-[12rem] bg-gray-100 text-emerald-600 flex flex-col p-4 shadow-md">
<h2 className="text-lg font-semibold mb-4">Admin Dashboard</h2>
<button <button
onClick={handleCloseAdminDashboard} onClick={handleCloseAdminDashboard}
className="flex items-center gap-2 text-sm font-medium hover:text-emerald-700 underline underline-offset-4 transition" className="flex items-center gap-2 text-sm font-medium hover:text-emerald-700 underline underline-offset-4 transition"

View File

@@ -11,7 +11,7 @@ const FloatingAlert = ({ message, onClose, duration = 3000 }) => {
}, [onClose, duration]); }, [onClose, duration]);
return ( 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"> <div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-emerald-600 text-white px-4 py-2 rounded-xl shadow-lg z-50 text-center">
{message} {message}
</div> </div>
); );

View File

@@ -35,7 +35,7 @@ const Navbar = ({ onLogout, userName, isAdmin, handleShowAdminDashboard }) => {
alt="Campus Plug" alt="Campus Plug"
className="h-8 px-2" className="h-8 px-2"
/> />
<span className="hidden md:block text-emerald-600 font-bold text-xl"> <span className="hidden md:block text-emerald-700 font-bold text-xl">
Campus Plug Campus Plug
</span> </span>
</Link> </Link>

View File

@@ -32,7 +32,7 @@ export default function Pagination({ pageNum, onChange }) {
onClick={() => { onClick={() => {
handleTogglePage("previous"); 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" 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-emerald-700 border border-gray-300 hover:bg-emerald-700 hover:text-white"
> >
<span className="sr-only">Previous</span> <span className="sr-only">Previous</span>
<svg <svg
@@ -56,9 +56,9 @@ export default function Pagination({ pageNum, onChange }) {
<li key={page}> <li key={page}>
<NavLink <NavLink
className={`${ className={`${
currentPage == page ? "bg-green-600" : "bg-green-700" currentPage == page ? "bg-emerald-700" : "bg-emerald-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"`} " flex items-center justify-center px-3 h-8 leading-tight text-white border border-gray-300 hover:bg-emerald-700 hover:text-white"`}
onClick={() => { onClick={() => {
handleClick(page); handleClick(page);
}} }}
@@ -72,7 +72,7 @@ export default function Pagination({ pageNum, onChange }) {
onClick={() => { onClick={() => {
handleTogglePage("next"); 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" className="flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 rounded-e-lg text-white bg-emerald-700 border border-gray-300 hover:bg-emerald-700 hover:text-white"
> >
<span className="sr-only">Next</span> <span className="sr-only">Next</span>
<svg <svg

View File

@@ -55,7 +55,7 @@ const UserDropdown = ({
onClick={toggleDropdown} onClick={toggleDropdown}
> >
<div className="h-8 w-8 rounded-full bg-emerald-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-emerald-600" /> <User className="h-5 w-5 text-emerald-700" />
</div> </div>
</button> </button>

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback, useRef } from "react";
import { import {
getUsers, getUsers,
removeUser, removeUser,
@@ -11,16 +11,14 @@ import {
} from "../api/admin"; } from "../api/admin";
import { MdDelete } from "react-icons/md"; import { MdDelete } from "react-icons/md";
import { IoAddCircleSharp } from "react-icons/io5"; import { IoAddCircleSharp } from "react-icons/io5";
import { FaHome } from "react-icons/fa";
import Pagination from "../components/Pagination"; import Pagination from "../components/Pagination";
import CategoryForm from "../components/CategoryForm"; import CategoryForm from "../components/CategoryForm";
import DashboardNav from "../components/DashboardNav";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
// Spinner Component // Spinner Component
const Spinner = () => ( const Spinner = () => (
<div className="flex justify-center items-center h-40 w-full"> <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 className="w-12 h-12 border-4 border-emerald-600 border-dashed rounded-full animate-spin"></div>
</div> </div>
); );
@@ -64,18 +62,26 @@ const Dashboard = ({
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const pageLimit = 10; const pageLimit = 10;
const currentTab = useRef();
//Reset the current page to 1 whenever we switch between tab
useEffect(() => {
if (currentTab.current != idKey) setCurrentPage(1);
currentTab.current = idKey;
}, [idKey]);
const fetchItems = useCallback( const fetchItems = useCallback(
(page = 1, limit = 10) => { (page = 1, limit = 10) => {
setLoading(true);
fetchDataFn(page, limit) fetchDataFn(page, limit)
.then((res) => { .then((res) => {
console.log(res);
const data = const data =
res.users || res.products || res.transactions || res.data || []; res.users || res.products || res.transactions || res.data || [];
setItems(data); setItems(data);
setTotal(res.total); setTotal(res.total);
}) })
.catch((error) => { .catch((error) => {
console.error("Error fetching data:", error);
setItems([]); setItems([]);
setTotal(0); setTotal(0);
}) })
@@ -110,7 +116,7 @@ const Dashboard = ({
<div className="w-full mt-6"> <div className="w-full mt-6">
<div className="flex flex-col md:flex-row justify-between items-center mb-4 w-full"> <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"> <h2 className="text-xl font-semibold text-gray-700 mb-2 md:mb-0">
Total: <span className="text-green-600">{total}</span> Total: <span className="text-emerald-700">{total}</span>
</h2> </h2>
{headerAction && <div>{headerAction}</div>} {headerAction && <div>{headerAction}</div>}
</div> </div>
@@ -118,7 +124,7 @@ const Dashboard = ({
{items.length > 0 ? ( {items.length > 0 ? (
<div className="w-full overflow-x-auto bg-white rounded-lg shadow"> <div className="w-full overflow-x-auto bg-white rounded-lg shadow">
<table className="min-w-full divide-y divide-gray-200 table-fixed"> <table className="min-w-full divide-y divide-gray-200 table-fixed">
<thead className="bg-green-600"> <thead className="bg-emerald-700">
<tr> <tr>
{columns.map((col) => ( {columns.map((col) => (
<th <th
@@ -290,7 +296,7 @@ export default function AdminDashboardTabs() {
headerAction={ headerAction={
<button <button
onClick={toggleForm} 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" className="flex items-center bg-emerald-600 rounded-md px-4 py-2 text-white text-sm hover:bg-emerald-700 transition-colors font-medium shadow"
> >
<IoAddCircleSharp className="mr-1" size={18} /> <IoAddCircleSharp className="mr-1" size={18} />
Add Category Add Category
@@ -312,18 +318,14 @@ export default function AdminDashboardTabs() {
return ( return (
<div className="w-full min-h-screen bg-gray-50"> <div className="w-full min-h-screen bg-gray-50">
<div className="w-full px-4 py-8"> <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 */} {/* Mobile Tabs */}
<div className="md:hidden w-full mb-4"> <div className="md:hidden w-full mb-4">
<select <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" className="w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-600 focus:ring focus:ring-emerald-600 focus:ring-opacity-50 p-2"
value={activeTab} value={activeTab}
onChange={(e) => setActiveTab(parseInt(e.target.value))} onChange={(e) => {
setActiveTab(parseInt(e.target.value));
}}
> >
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
<option key={tab.key} value={index}> <option key={tab.key} value={index}>
@@ -340,10 +342,12 @@ export default function AdminDashboardTabs() {
key={tab.key} key={tab.key}
className={`px-6 py-3 font-medium text-sm rounded-t-lg transition-colors ${ className={`px-6 py-3 font-medium text-sm rounded-t-lg transition-colors ${
index === activeTab index === activeTab
? "text-green-700 bg-white border-l border-t border-r border-gray-200 border-b-0" ? "text-emerald-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" : "text-gray-600 hover:text-emerald-700 bg-gray-50"
}`} }`}
onClick={() => setActiveTab(index)} onClick={() => {
setActiveTab(index);
}}
> >
<span className="inline-block mr-2">{tab.icon}</span> <span className="inline-block mr-2">{tab.icon}</span>
{tab.title} {tab.title}

View File

@@ -119,7 +119,7 @@ const Favorites = () => {
</p> </p>
<Link <Link
to="/" to="/"
className="inline-block bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4" className="inline-block bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4"
> >
Browse Listings Browse Listings
</Link> </Link>
@@ -161,7 +161,7 @@ const Favorites = () => {
</button> </button>
</div> </div>
<p className="text-emerald-600 font-bold mt-1"> <p className="text-emerald-700 font-bold mt-1">
${product.price.toFixed(2)} ${product.price.toFixed(2)}
</p> </p>
@@ -200,56 +200,8 @@ const Favorites = () => {
)} )}
<footer className="bg-gray-800 text-white py-6 mt-12"> <footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4"> <div className="border-t border-gray-700 text-center text-sm text-gray-400">
<div className="flex flex-col md:flex-row justify-between items-center"> <p>© 2025 Campus Marketplace. All rights reserved.</p>
<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> </div>
</footer> </footer>
</div> </div>

View File

@@ -219,7 +219,7 @@ const Home = () => {
// Loading indicator component // Loading indicator component
const LoadingSection = () => ( const LoadingSection = () => (
<div className="flex justify-center items-center h-48"> <div className="flex justify-center items-center h-48">
<Loader className="animate-spin text-emerald-600 h-8 w-8" /> <Loader className="animate-spin text-emerald-700 h-8 w-8" />
</div> </div>
); );
@@ -243,7 +243,7 @@ const Home = () => {
e.preventDefault(); e.preventDefault();
toggleFavorite(product.id); toggleFavorite(product.id);
}} }}
className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm" className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-600 transition shadow-sm"
> >
<Bookmark className="text-white w-5 h-5" /> <Bookmark className="text-white w-5 h-5" />
</button> </button>
@@ -253,7 +253,7 @@ const Home = () => {
<h3 className="text-lg font-medium text-gray-800 leading-tight"> <h3 className="text-lg font-medium text-gray-800 leading-tight">
{product.title} {product.title}
</h3> </h3>
<span className="font-semibold text-emerald-600 block mt-1"> <span className="font-semibold text-emerald-700 block mt-1">
${product.price} ${product.price}
</span> </span>
@@ -353,7 +353,7 @@ const Home = () => {
</p> </p>
<button <button
onClick={handleSelling} onClick={handleSelling}
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" className="bg-emerald-700 hover:bg-emerald-700 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-emerald-400 transition-colors"
> >
Post an Item Post an Item
</button> </button>
@@ -405,59 +405,9 @@ const Home = () => {
</ScrollableProductList> </ScrollableProductList>
)} )}
</div> </div>
{/* Footer */}
<footer className="bg-gray-800 text-white py-6 mt-12"> <footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4"> <div className="border-t border-gray-700 text-center text-sm text-gray-400">
<div className="flex flex-col md:flex-row justify-between items-center"> <p>© 2025 Campus Marketplace. All rights reserved.</p>
<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> </div>
</footer> </footer>
</div> </div>

View File

@@ -247,7 +247,7 @@ const ProductDetail = () => {
if (loading.product) { if (loading.product) {
return ( return (
<div className="flex justify-center items-center h-screen"> <div className="flex justify-center items-center h-screen">
<div className="animate-spin h-32 w-32 border-t-2 border-emerald-600"></div> <div className="animate-spin h-32 w-32 border-t-2 border-emerald-700"></div>
</div> </div>
); );
} }
@@ -261,7 +261,7 @@ const ProductDetail = () => {
<p className="text-gray-600">{error.product}</p> <p className="text-gray-600">{error.product}</p>
<Link <Link
to="/" to="/"
className="mt-4 inline-block bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700" className="mt-4 inline-block bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
Back to Listings Back to Listings
</Link> </Link>
@@ -278,7 +278,7 @@ const ProductDetail = () => {
<h2 className="text-2xl text-red-500 mb-4">Product Not Found</h2> <h2 className="text-2xl text-red-500 mb-4">Product Not Found</h2>
<Link <Link
to="/" to="/"
className="mt-4 inline-block bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700" className="mt-4 inline-block bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
Back to Listings Back to Listings
</Link> </Link>
@@ -356,7 +356,7 @@ const ProductDetail = () => {
{product.images.map((image, index) => ( {product.images.map((image, index) => (
<div <div
key={index} key={index}
className={`bg-white border ${currentImage === index ? "border-emerald-600 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`} className={`bg-white border ${currentImage === index ? "border-emerald-700 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`}
onClick={() => selectImage(index)} onClick={() => selectImage(index)}
> >
<img <img
@@ -386,7 +386,7 @@ const ProductDetail = () => {
e.preventDefault(); e.preventDefault();
toggleFavorite(product.ProductID); toggleFavorite(product.ProductID);
}} }}
className="top-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-600 transition shadow-sm" className="top-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-700 transition shadow-sm"
> >
<Bookmark className="text-white w-5 h-5" /> <Bookmark className="text-white w-5 h-5" />
</button> </button>
@@ -455,7 +455,7 @@ const ProductDetail = () => {
alert(`Error: ${error.message}`); alert(`Error: ${error.message}`);
} }
}} }}
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-600 hover:bg-emerald-700 text-white font-medium py-3 px-4 mb-3"
> >
Contact Seller Contact Seller
</button> </button>
@@ -467,7 +467,7 @@ const ProductDetail = () => {
href={`tel:${product.SellerPhone}`} href={`tel:${product.SellerPhone}`}
className="flex items-center gap-2 p-3 hover:bg-gray-50 border-b border-gray-100" className="flex items-center gap-2 p-3 hover:bg-gray-50 border-b border-gray-100"
> >
<Phone className="h-5 w-5 text-emerald-600" /> <Phone className="h-5 w-5 text-emerald-700" />
<span>Call Seller</span> <span>Call Seller</span>
</a> </a>
)} )}
@@ -477,7 +477,7 @@ const ProductDetail = () => {
href={`mailto:${product.SellerEmail}`} href={`mailto:${product.SellerEmail}`}
className="flex items-center gap-2 p-3 hover:bg-gray-50" className="flex items-center gap-2 p-3 hover:bg-gray-50"
> >
<Mail className="h-5 w-5 text-emerald-600" /> <Mail className="h-5 w-5 text-emerald-700" />
<span>Email Seller</span> <span>Email Seller</span>
</a> </a>
)} )}
@@ -498,7 +498,10 @@ const ProductDetail = () => {
{product.SellerName || "Unknown Seller"} {product.SellerName || "Unknown Seller"}
</h3> </h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Member since {product.SellerJoinDate || "N/A"} Product listed since{" "}
{product.Date
? new Date(product.Date).toLocaleDateString()
: "N/A"}
</p> </p>
</div> </div>
</div> </div>
@@ -514,7 +517,7 @@ const ProductDetail = () => {
<div className="bg-white border border-gray-200 p-6"> <div className="bg-white border border-gray-200 p-6">
{loading.reviews ? ( {loading.reviews ? (
<div className="flex justify-center py-8"> <div className="flex justify-center py-8">
<div className="animate-spin h-8 w-8 border-t-2 border-emerald-600"></div> <div className="animate-spin h-8 w-8 border-t-2 border-emerald-700"></div>
</div> </div>
) : error.reviews ? ( ) : error.reviews ? (
<div className="text-red-500 mb-4"> <div className="text-red-500 mb-4">
@@ -561,7 +564,7 @@ const ProductDetail = () => {
<div className="mt-4"> <div className="mt-4">
<button <button
onClick={() => setShowReviewForm(true)} onClick={() => setShowReviewForm(true)}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4" className="bg-emerald-700 hover:bg-emerald-700 text-white font-medium py-2 px-4"
> >
Write a Review Write a Review
</button> </button>
@@ -619,7 +622,7 @@ const ProductDetail = () => {
id="comment" id="comment"
value={reviewForm.comment} value={reviewForm.comment}
onChange={handleReviewInputChange} onChange={handleReviewInputChange}
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-emerald-600" className="w-full p-3 border border-gray-300 focus:outline-none focus:border-emerald-700"
rows="4" rows="4"
required required
></textarea> ></textarea>
@@ -635,7 +638,7 @@ const ProductDetail = () => {
</button> </button>
<button <button
type="submit" type="submit"
className="px-4 py-2 bg-emerald-600 text-white hover:bg-emerald-700" className="px-4 py-2 bg-emerald-700 text-white hover:bg-emerald-700"
disabled={loading.submitting} disabled={loading.submitting}
> >
{loading.submitting ? "Submitting..." : "Submit Review"} {loading.submitting ? "Submitting..." : "Submit Review"}

View File

@@ -146,7 +146,7 @@ const SearchPage = () => {
<div className="flex space-x-2"> <div className="flex space-x-2">
<button <button
onClick={applyFilters} onClick={applyFilters}
className="w-full bg-emerald-500 text-white p-3 hover:bg-emerald-600 transition-colors" className="w-full bg-emerald-600 text-white p-3 hover:bg-emerald-700 transition-colors"
> >
Apply Filters Apply Filters
</button> </button>
@@ -187,7 +187,7 @@ const SearchPage = () => {
<h3 className="text-lg font-medium text-gray-800"> <h3 className="text-lg font-medium text-gray-800">
{listing.title} {listing.title}
</h3> </h3>
<p className="text-emerald-600 font-semibold"> <p className="text-emerald-700 font-semibold">
${Number(listing.price).toFixed(2)} ${Number(listing.price).toFixed(2)}
</p> </p>
</div> </div>
@@ -196,58 +196,9 @@ const SearchPage = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Footer - Added here */}
<footer className="bg-gray-800 text-white py-6 mt-12"> <footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4"> <div className="border-t border-gray-700 text-center text-sm text-gray-400">
<div className="flex flex-col md:flex-row justify-between items-center"> <p>© 2025 Campus Marketplace. All rights reserved.</p>
<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> </div>
</footer> </footer>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useLocation, Link } from "react-router-dom"; import { useLocation, Link } from "react-router-dom";
import { X, ChevronLeft, Plus, Trash2 } from "lucide-react"; import { X, ChevronLeft, Trash2 } from "lucide-react";
const Selling = () => { const Selling = () => {
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
@@ -8,14 +8,13 @@ const Selling = () => {
const storedUser = JSON.parse(sessionStorage.getItem("user")); const storedUser = JSON.parse(sessionStorage.getItem("user"));
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState([]);
const [categoryMapping, setCategoryMapping] = useState({}); const [categoryMapping, setCategoryMapping] = useState({});
const [selectedCategory, setSelectedCategory] = useState("");
const [originalProduct, setOriginalProduct] = useState(null); const [originalProduct, setOriginalProduct] = useState(null);
const [editingProduct, setEditingProduct] = useState({ const [editingProduct, setEditingProduct] = useState({
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
images: [], images: [],
}); });
@@ -59,7 +58,7 @@ const Selling = () => {
fetchCategories(); fetchCategories();
}, []); }, []);
// Simulate fetching products from API/database on component mount // Fetch products from API/database on component mount
useEffect(() => { useEffect(() => {
const fetchProducts = async () => { const fetchProducts = async () => {
try { try {
@@ -89,61 +88,87 @@ const Selling = () => {
}; };
fetchProducts(); fetchProducts();
}, []); // Add userId to dependency array if it might change });
// When editing a product, save the original product properly
const handleEditProduct = (product) => { const handleEditProduct = (product) => {
// Save the original product completely
setOriginalProduct(product); setOriginalProduct(product);
// Convert category ID to category name if needed
const categoryName = getCategoryNameById(product.CategoryID); const categoryName = getCategoryNameById(product.CategoryID);
setEditingProduct({ setEditingProduct({
...product, ...product,
categories: categoryName ? [categoryName] : [], category: categoryName || "", // Single category string
images: product.images || [], // Ensure images array exists images: product.images || [],
}); });
setShowForm(true); setShowForm(true);
}; };
// Then update the handleSaveProduct function to properly merge values // Upload images to server and get their paths
const uploadImages = async (images) => {
console.log(images);
const uploadedImagePaths = [];
// Filter out only File objects (new images to upload)
const filesToUpload = images.filter((img) => img instanceof File);
for (const file of filesToUpload) {
// Create a FormData object to send the file
const formData = new FormData();
formData.append("image", file);
try {
// Send the file to your upload endpoint
const response = await fetch("http://localhost:3030/api/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Failed to upload image: ${file.name}`);
}
const result = await response.json();
// Assuming the server returns the path where the file was saved
uploadedImagePaths.push(`/public/uploads/${file.name}`);
} catch (error) {
console.error("Error uploading image:", error);
// If upload fails, still add the expected path (this is a fallback)
uploadedImagePaths.push(`/public/uploads/${file.name}`);
}
}
// Also include any existing image URLs that are strings, not File objects
const existingImages = images.filter((img) => typeof img === "string");
if (existingImages.length > 0) {
uploadedImagePaths.push(...existingImages);
}
return uploadedImagePaths;
};
// Handle saving product with updated image logic
const handleSaveProduct = async () => { const handleSaveProduct = async () => {
if (!(editingProduct.categories || []).length) { if (!editingProduct.category) {
alert("Please select at least one category"); alert("Please select a category");
return; return;
} }
try { try {
const imagePaths = []; let imagePaths = [];
// Handle images properly // Handle image uploads and get their paths
if (editingProduct.images && editingProduct.images.length > 0) { if (editingProduct.images && editingProduct.images.length > 0) {
// If there are new images uploaded (File objects) imagePaths = await uploadImages(editingProduct.images);
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) { } else if (originalProduct?.image_url) {
// If no new images but there was an original image URL // If no new images but there was an original image URL
imagePaths.push(originalProduct.image_url); imagePaths = [originalProduct.image_url];
} }
const categoryName = (editingProduct.categories || [])[0];
const categoryID = const categoryID =
categoryMapping[categoryName] || originalProduct?.CategoryID || 1; categoryMapping[editingProduct.category] ||
originalProduct?.CategoryID ||
1;
// Create payload with proper fallback to original values // Create payload with proper fallback to original values
const payload = { const payload = {
@@ -166,12 +191,7 @@ const Selling = () => {
originalProduct?.Description || originalProduct?.Description ||
"", "",
category: categoryID, category: categoryID,
images: images: imagePaths.length > 0 ? imagePaths : [],
imagePaths.length > 0
? imagePaths
: originalProduct?.image_url
? [originalProduct.image_url]
: [],
}; };
console.log("Sending payload:", payload); console.log("Sending payload:", payload);
@@ -206,7 +226,7 @@ const Selling = () => {
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
images: [], images: [],
}); });
@@ -243,7 +263,7 @@ const Selling = () => {
throw new Error("Network response was not ok"); throw new Error("Network response was not ok");
} }
} catch (error) { } catch (error) {
console.error("Error fetching products:", error); console.error("Error deleting product:", error);
// You might want to set an error state here // You might want to set an error state here
} }
}; };
@@ -267,53 +287,18 @@ const Selling = () => {
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
images: [], images: [],
}); });
setShowForm(true); setShowForm(true);
}; };
const addCategory = () => { // Handle category change
if ( const handleCategoryChange = (e) => {
selectedCategory && setEditingProduct({
!(editingProduct.categories || []).includes(selectedCategory) ...editingProduct,
) { category: e.target.value,
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 (
@@ -323,7 +308,7 @@ const Selling = () => {
{!showForm && ( {!showForm && (
<button <button
onClick={handleAddProduct} onClick={handleAddProduct}
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700" className="bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
+ Add New Product + Add New Product
</button> </button>
@@ -335,7 +320,7 @@ const Selling = () => {
{/* Back Button */} {/* Back Button */}
<button <button
onClick={() => setShowForm(false)} onClick={() => setShowForm(false)}
className="mb-4 text-emerald-600 hover:text-emerald-800 flex items-center gap-1" className="mb-4 text-emerald-700 hover:text-emerald-800 flex items-center gap-1"
> >
<ChevronLeft size={16} /> <ChevronLeft size={16} />
<span>Back to Listings</span> <span>Back to Listings</span>
@@ -363,7 +348,7 @@ const Selling = () => {
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" className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
/> />
</div> </div>
@@ -382,78 +367,33 @@ const Selling = () => {
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" className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
/> />
</div> </div>
{/* Sold Status */} {/* Category - Single Selection Dropdown */}
<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"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Categories Category
</label> </label>
<div className="flex gap-2">
<select <select
value={selectedCategory} value={editingProduct.category || ""}
onChange={(e) => setSelectedCategory(e.target.value)} onChange={handleCategoryChange}
className="flex-1 px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none" className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
required
> >
<option value="" disabled> <option value="" disabled>
Select a category Select a category
</option> </option>
{categories {categories.map((category, index) => (
.filter(
(cat) => !(editingProduct.categories || []).includes(cat),
)
.map((category, index) => (
<option key={index} value={category}> <option key={index} value={category}>
{category} {category}
</option> </option>
))} ))}
</select> </select>
<button {!editingProduct.category && (
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"> <p className="text-xs text-gray-500 mt-1">
Please select at least one category Please select a category
</p> </p>
)} )}
</div> </div>
@@ -475,7 +415,7 @@ const Selling = () => {
}) })
} }
rows="4" rows="4"
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none" className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
placeholder="Describe your product in detail..." placeholder="Describe your product in detail..."
></textarea> ></textarea>
</div> </div>
@@ -504,8 +444,8 @@ const Selling = () => {
htmlFor="image-upload" htmlFor="image-upload"
className="block w-full p-3 border border-gray-300 bg-gray-50 text-center cursor-pointer hover:bg-gray-100" 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"> <span className="text-emerald-700 font-medium">
Click to upload images Click to upload images (will be saved to /public/uploads)
</span> </span>
</label> </label>
@@ -535,7 +475,11 @@ const Selling = () => {
className="relative w-20 h-20 border border-gray-200 overflow-hidden" className="relative w-20 h-20 border border-gray-200 overflow-hidden"
> >
<img <img
src={URL.createObjectURL(img)} src={
typeof img === "string"
? img
: URL.createObjectURL(img)
}
alt={`Product ${idx + 1}`} alt={`Product ${idx + 1}`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
@@ -559,7 +503,8 @@ const Selling = () => {
)} )}
{/* Show current image if editing */} {/* Show current image if editing */}
{editingProduct.image_url && ( {editingProduct.image_url &&
!(editingProduct.images || []).length && (
<div className="mt-3"> <div className="mt-3">
<p className="text-sm text-gray-600 mb-2">Current image:</p> <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"> <div className="relative w-20 h-20 border border-gray-200 overflow-hidden">
@@ -583,22 +528,9 @@ const Selling = () => {
Cancel Cancel
</button> </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 <button
onClick={handleSaveProduct} onClick={handleSaveProduct}
className="bg-emerald-600 text-white px-6 py-2 hover:bg-emerald-700 rounded-md" className="bg-emerald-700 text-white px-6 py-2 hover:bg-emerald-700 rounded-md"
> >
{editingProduct.ProductID ? "Update Product" : "Add Product"} {editingProduct.ProductID ? "Update Product" : "Add Product"}
</button> </button>
@@ -613,7 +545,7 @@ const Selling = () => {
</p> </p>
<button <button
onClick={handleAddProduct} onClick={handleAddProduct}
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700" className="bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
Create Your First Listing Create Your First Listing
</button> </button>
@@ -645,7 +577,7 @@ const Selling = () => {
</h3> </h3>
</div> </div>
<p className="text-emerald-600 font-bold mt-1"> <p className="text-emerald-700 font-bold mt-1">
${product.Price} ${product.Price}
</p> </p>
@@ -679,7 +611,7 @@ const Selling = () => {
e.preventDefault(); e.preventDefault();
handleEditProduct(product); handleEditProduct(product);
}} }}
className="text-emerald-600 hover:text-emerald-800 font-medium" className="text-emerald-700 hover:text-emerald-800 font-medium"
> >
Edit Edit
</button> </button>
@@ -692,6 +624,11 @@ const Selling = () => {
)} )}
</> </>
)} )}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
</div>
</footer>
</div> </div>
); );
}; };

View File

@@ -190,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-emerald-500"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-emerald-600"></div>
</div> </div>
); );
} }
@@ -234,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-emerald-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
required required
/> />
</div> </div>
@@ -251,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-emerald-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
required required
readOnly // Email is often used as identifier and not changeable readOnly // Email is often used as identifier and not changeable
/> />
@@ -269,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-emerald-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
/> />
</div> </div>
@@ -285,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-emerald-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
required required
/> />
</div> </div>
@@ -302,7 +302,7 @@ 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-emerald-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
/> />
</div> </div>
<div> <div>
@@ -317,14 +317,14 @@ const Settings = () => {
id="password" id="password"
value={userData.password} value={userData.password}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
/> />
</div> </div>
</div> </div>
<button <button
type="submit" type="submit"
className="bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4" className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4"
> >
Update Profile Update Profile
</button> </button>
@@ -395,6 +395,11 @@ const Settings = () => {
</div> </div>
</div> </div>
</div> </div>
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
</div>
</footer>
</div> </div>
); );
}; };

View File

@@ -14,7 +14,7 @@ const Transactions = () => {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userID: 1 }), // replace with actual userID body: JSON.stringify({ userID: 1 }), // replace with actual userID
} },
); );
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { transactions: txData } = await response.json(); const { transactions: txData } = await response.json();
@@ -29,7 +29,7 @@ const Transactions = () => {
image: tx.Image_URL || "/default-image.jpg", image: tx.Image_URL || "/default-image.jpg",
date: tx.Date, date: tx.Date,
status: tx.PaymentStatus, status: tx.PaymentStatus,
})) })),
); );
} catch (error) { } catch (error) {
console.error("Failed to fetch transactions:", error); console.error("Failed to fetch transactions:", error);
@@ -47,7 +47,7 @@ const Transactions = () => {
method: "DELETE", method: "DELETE",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ transactionID: id }), body: JSON.stringify({ transactionID: id }),
} },
); );
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
@@ -86,7 +86,7 @@ const Transactions = () => {
</p> </p>
<Link <Link
to="/" to="/"
className="inline-block bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4" className="inline-block bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4"
> >
Browse Listings Browse Listings
</Link> </Link>
@@ -127,7 +127,7 @@ const Transactions = () => {
{tx.name} {tx.name}
</h3> </h3>
{tx.price !== null && ( {tx.price !== null && (
<p className="text-emerald-600 font-bold mt-1"> <p className="text-emerald-700 font-bold mt-1">
${tx.price.toFixed(2)} ${tx.price.toFixed(2)}
</p> </p>
)} )}
@@ -152,58 +152,8 @@ const Transactions = () => {
)} )}
<footer className="bg-gray-800 text-white py-6 mt-12"> <footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4"> <div className="border-t border-gray-700 text-center text-sm text-gray-400">
<div className="flex flex-col md:flex-row justify-between items-center"> <p>© 2025 Campus Marketplace. All rights reserved.</p>
<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> </div>
</footer> </footer>
</div> </div>

View File

@@ -44,7 +44,7 @@ VALUES
( (
1, 1,
'John Doe', 'John Doe',
'john.doe@example.com', 'john.doe@ucalgary.ca',
'U123456', 'U123456',
'hashedpassword1', 'hashedpassword1',
'555-123-4567', '555-123-4567',
@@ -53,7 +53,7 @@ VALUES
( (
2, 2,
'Jane Smith', 'Jane Smith',
'jane.smith@example.com', 'jane.smith@ucalgary.ca',
'U234567', 'U234567',
'hashedpassword2', 'hashedpassword2',
'555-234-5678', '555-234-5678',
@@ -72,6 +72,7 @@ VALUES
INSERT INTO INSERT INTO
Category (Name) Category (Name)
VALUES VALUES
('Other'),
('Textbooks'), ('Textbooks'),
('Electronics'), ('Electronics'),
('Furniture'), ('Furniture'),
@@ -100,9 +101,7 @@ VALUES
('Winter Clothing'), ('Winter Clothing'),
('Photography Equipment'), ('Photography Equipment'),
('Event Tickets'), ('Event Tickets'),
('Software Licenses'), ('Software Licenses');
('Transportation (Car Pool)'),
('Other');
-- Insert Products -- Insert Products
INSERT INTO INSERT INTO
@@ -454,4 +453,5 @@ VALUES
1, 1,
'This is a great fake product! Totally recommend it.', 'This is a great fake product! Totally recommend it.',
5, 5,
); '2024-10-02 16:00:00'
)

View File

@@ -28,19 +28,6 @@ CREATE TABLE Category (
Name VARCHAR(255) NOT NULL Name VARCHAR(255) NOT NULL
); );
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),
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
);
-- Product Entity -- Product Entity
CREATE TABLE Product ( CREATE TABLE Product (
ProductID INT AUTO_INCREMENT PRIMARY KEY, ProductID INT AUTO_INCREMENT PRIMARY KEY,
@@ -51,7 +38,7 @@ CREATE TABLE Product (
Description TEXT, Description TEXT,
CategoryID INT, CategoryID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL, FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
); );
@@ -73,7 +60,7 @@ 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) ON DELETE SET NULL, FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
); );
@@ -85,7 +72,7 @@ CREATE TABLE Transaction (
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
PaymentStatus VARCHAR(50), PaymentStatus VARCHAR(50),
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE, FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE SET NULL FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
); );
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product) -- Recommendation Entity (Many-to-One with User, Many-to-One with Product)