Base UI design, May contain bugs
@@ -1,8 +0,0 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="./public/icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
<title>Campus Plug</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
929
frontend/package-lock.json
generated
@@ -10,19 +10,25 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.9",
|
||||
"lucide-react": "^0.477.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.18",
|
||||
"globals": "^15.14.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.9",
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
frontend/public/Profile.jpg
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
frontend/public/icon.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
frontend/public/image1.avif
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
frontend/public/image2.avif
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
frontend/public/image3.avif
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
frontend/public/market.png
Normal file
|
After Width: | Height: | Size: 923 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,42 +0,0 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
@@ -1,35 +1,398 @@
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import './App.css'
|
||||
import { useState, useEffect } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import Navbar from './components/Navbar';
|
||||
import Home from './pages/Home';
|
||||
import Settings from './pages/Settings';
|
||||
import Selling from './pages/Selling';
|
||||
import Transactions from './pages/Transactions';
|
||||
import Favorites from './pages/Favorites';
|
||||
import ProductDetail from './pages/ProductDetail';
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
// Authentication state
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
// UI state for login/signup form
|
||||
const [isSignUp, setIsSignUp] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
ucid: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showImage, setShowImage] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Fake users database
|
||||
const fakeUsers = [
|
||||
{ email: 'john1@ucalgary.ca', password: 'password123', name: 'John Doe' },
|
||||
{ email: 'jane@ucalgary.ca', password: 'password456', name: 'Jane Smith' }
|
||||
];
|
||||
|
||||
// Auto-hide image on smaller screens
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
setShowImage(false);
|
||||
} else {
|
||||
setShowImage(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial check
|
||||
handleResize();
|
||||
|
||||
// Listen for window resize
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Cleanup
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { id, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[id]: value
|
||||
}));
|
||||
|
||||
// Clear any error when user starts typing again
|
||||
if (error) setError('');
|
||||
};
|
||||
|
||||
const handleLogin = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate email and password
|
||||
if (!formData.email || !formData.password) {
|
||||
setError('Email and password are required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSignUp) {
|
||||
// Handle Sign Up
|
||||
console.log('Sign Up Form Data:', formData);
|
||||
|
||||
// Simulate saving new user to database
|
||||
const newUser = {
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
ucid: formData.ucid,
|
||||
phone: formData.phone
|
||||
};
|
||||
|
||||
// Set authenticated user
|
||||
setUser(newUser);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
console.log('New user registered:', newUser);
|
||||
} else {
|
||||
// Handle Login
|
||||
console.log('Login Attempt:', { email: formData.email, password: formData.password });
|
||||
|
||||
// Check against fake user database
|
||||
const foundUser = fakeUsers.find(
|
||||
user => user.email === formData.email && user.password === formData.password
|
||||
);
|
||||
|
||||
if (foundUser) {
|
||||
// Set authenticated user
|
||||
setUser({
|
||||
name: foundUser.name,
|
||||
email: foundUser.email
|
||||
});
|
||||
setIsAuthenticated(true);
|
||||
console.log('Login successful for:', foundUser.name);
|
||||
} else {
|
||||
setError('Invalid email or password');
|
||||
console.log('Login failed: Invalid credentials');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
console.log('User logged out');
|
||||
|
||||
// Reset form data
|
||||
setFormData({
|
||||
name: '',
|
||||
ucid: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
});
|
||||
};
|
||||
|
||||
const toggleAuthMode = () => {
|
||||
setIsSignUp(!isSignUp);
|
||||
setError(''); // Clear errors when switching modes
|
||||
|
||||
// Reset form data when switching modes
|
||||
setFormData({
|
||||
name: '',
|
||||
ucid: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
});
|
||||
};
|
||||
|
||||
// Login component
|
||||
const LoginComponent = () => (
|
||||
<div className="flex h-screen bg-white">
|
||||
{/* Image Section - Automatically hidden on mobile */}
|
||||
{showImage && (
|
||||
<div className="w-1/2 relative">
|
||||
<img
|
||||
src="../market.png"
|
||||
alt="auth illustration"
|
||||
className="w-full h-full object-cover opacity-75"
|
||||
/>
|
||||
<div className="absolute inset-0"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Auth Form Section */}
|
||||
<div className={`${showImage ? 'w-1/2' : 'w-full'} bg-white p-8 flex items-center justify-center`}>
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-800">
|
||||
{isSignUp ? 'Create Account' : 'Welcome Back'}
|
||||
</h2>
|
||||
<p className="mt-2 text-gray-600">
|
||||
{isSignUp ? 'Set up your new account' : 'Sign in to your account'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-200 shadow-sm p-6">
|
||||
{error && (
|
||||
<div className="mb-4 p-2 bg-red-50 text-red-600 text-sm border border-red-200 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
{/* Name field - only for signup */}
|
||||
{isSignUp && (
|
||||
<div>
|
||||
<label htmlFor="name" className="block mb-1 text-sm font-medium text-gray-800">
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
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"
|
||||
required={isSignUp}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSignUp && (
|
||||
<div>
|
||||
<label htmlFor="ucid" className="block mb-1 text-sm font-medium text-gray-800">
|
||||
UCID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="ucid"
|
||||
value={formData.ucid}
|
||||
onChange={handleInputChange}
|
||||
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"
|
||||
required={isSignUp}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block mb-1 text-sm font-medium text-gray-800">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
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"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isSignUp && (
|
||||
<div>
|
||||
<label htmlFor="phone" className="block mb-1 text-sm font-medium text-gray-800">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
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"
|
||||
required={isSignUp}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block mb-1 text-sm font-medium text-gray-800">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
id="password"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
placeholder={isSignUp ? "Create a secure 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"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-sm text-gray-500 hover:text-green-500"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
{isSignUp ? 'Create Account' : 'Sign In'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-gray-500">
|
||||
<p>
|
||||
{isSignUp ? 'Already have an account?' : "Don't have an account?"}
|
||||
{' '}
|
||||
<button
|
||||
onClick={toggleAuthMode}
|
||||
className="text-green-500 font-medium hover:text-green-700"
|
||||
>
|
||||
{isSignUp ? 'Sign in' : 'Sign up'}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Protected route component
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
<Router>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Only show navbar when authenticated */}
|
||||
{isAuthenticated && <Navbar onLogout={handleLogout} userName={user?.name} />}
|
||||
|
||||
<Routes>
|
||||
{/* Public routes */}
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
isAuthenticated ?
|
||||
<Navigate to="/" /> :
|
||||
<LoginComponent />
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Protected routes */}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<Home />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/product/:id"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ProductDetail />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<Settings />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/selling"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<Selling />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/transactions"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<Transactions />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/favorites"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<Favorites />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Redirect to login for any unmatched routes */}
|
||||
<Route path="*" element={<Navigate to={isAuthenticated ? "/" : "/login"} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.jsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
63
frontend/src/components/Navbar.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import UserDropdown from './UserDropdown';
|
||||
import { Search, Heart } from 'lucide-react';
|
||||
|
||||
const Navbar = ({ onLogout, userName }) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const handleSearchChange = (e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
};
|
||||
|
||||
const handleSearchSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
console.log('Searching for:', searchQuery);
|
||||
// TODO: Implement search functionality
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<div className="flex-shrink-0">
|
||||
<Link to="/" className="flex items-center">
|
||||
<span className="text-green-600 font-bold text-xl">Campus Plug</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="flex-1 max-w-2xl px-4">
|
||||
<form onSubmit={handleSearchSubmit} className="w-full">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for books, electronics, furniture..."
|
||||
className="w-full p-2 pl-10 pr-4 border border-gray-300 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Search className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* User Navigation */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Favorites Button */}
|
||||
<Link to="/favorites" className="p-2 text-gray-600 hover:text-green-600">
|
||||
<Heart className="h-6 w-6" />
|
||||
</Link>
|
||||
{/* User Profile */}
|
||||
<UserDropdown onLogout={onLogout} userName={userName} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
106
frontend/src/components/UserDropdown.jsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { User, Settings, ShoppingBag, DollarSign, LogOut } from 'lucide-react';
|
||||
|
||||
const UserDropdown = ({ onLogout, userName }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Use passed userName or fallback to default
|
||||
const displayName = userName || 'User';
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
// Close the dropdown
|
||||
setIsOpen(false);
|
||||
|
||||
// Call the onLogout function from props
|
||||
if (onLogout) {
|
||||
onLogout();
|
||||
console.log("Logout successful");
|
||||
}
|
||||
|
||||
// Navigate to login page (this may be redundant as App.jsx should handle redirection)
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
className="flex items-center focus:outline-none"
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
<div className="h-8 w-8 rounded-full bg-green-100 flex items-center justify-center">
|
||||
<User className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white border border-gray-200 shadow-md z-10">
|
||||
{/* User Info */}
|
||||
<div className="px-4 py-3 border-b border-gray-100">
|
||||
<p className="text-sm font-medium text-gray-900">{displayName}</p>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="py-1">
|
||||
<Link
|
||||
to="/selling"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<ShoppingBag className="h-4 w-4 mr-2 text-gray-500" />
|
||||
My Listings
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/transactions"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<DollarSign className="h-4 w-4 mr-2 text-gray-500" />
|
||||
Transactions
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/settings"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-2 text-gray-500" />
|
||||
Settings
|
||||
</Link>
|
||||
|
||||
<button
|
||||
className="flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-2 text-gray-500" />
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserDropdown;
|
||||
@@ -1,68 +1,4 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@import "tailwindcss";
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
167
frontend/src/pages/Favorites.jsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Heart, Tag, Trash2, Filter, ChevronDown } from 'lucide-react';
|
||||
|
||||
const Favorites = () => {
|
||||
const [favorites, setFavorites] = useState([
|
||||
{
|
||||
id: 0,
|
||||
title: 'Dell XPS 16 Laptop',
|
||||
price: 850,
|
||||
category: 'Electronics',
|
||||
image: '/image1.avif',
|
||||
condition: 'Like New',
|
||||
seller: 'Michael T.',
|
||||
datePosted: '5d ago',
|
||||
dateAdded: '2023-03-08',
|
||||
},
|
||||
|
||||
]);
|
||||
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [sortBy, setSortBy] = useState('dateAdded');
|
||||
const [filterCategory, setFilterCategory] = useState('All');
|
||||
|
||||
// Function to remove item from favorites
|
||||
const removeFromFavorites = (id) => {
|
||||
setFavorites(favorites.filter(item => item.id !== id));
|
||||
};
|
||||
|
||||
// Available categories for filtering
|
||||
const categories = ['All', 'Electronics', 'Textbooks', 'Furniture', 'Kitchen', 'Other'];
|
||||
|
||||
// Sort favorites based on selected sort option
|
||||
const sortedFavorites = [...favorites].sort((a, b) => {
|
||||
if (sortBy === 'dateAdded') {
|
||||
return new Date(b.dateAdded) - new Date(a.dateAdded);
|
||||
} else if (sortBy === 'priceHigh') {
|
||||
return b.price - a.price;
|
||||
} else if (sortBy === 'priceLow') {
|
||||
return a.price - b.price;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Filter favorites based on selected category
|
||||
const filteredFavorites = filterCategory === 'All'
|
||||
? sortedFavorites
|
||||
: sortedFavorites.filter(item => item.category === filterCategory);
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-800">My Favorites</h1>
|
||||
<button
|
||||
className="flex items-center text-gray-600 hover:text-gray-800"
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
>
|
||||
<Filter className="h-5 w-5 mr-1" />
|
||||
<span>Filter & Sort</span>
|
||||
<ChevronDown className={`h-4 w-4 ml-1 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters and Sorting */}
|
||||
{showFilters && (
|
||||
<div className="bg-white border border-gray-200 p-4 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Sort by
|
||||
</label>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
||||
>
|
||||
<option value="dateAdded">Recently Added</option>
|
||||
<option value="priceHigh">Price (High to Low)</option>
|
||||
<option value="priceLow">Price (Low to High)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
value={filterCategory}
|
||||
onChange={(e) => setFilterCategory(e.target.value)}
|
||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
||||
>
|
||||
{categories.map((category) => (
|
||||
<option key={category} value={category}>{category}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Favorites List */}
|
||||
{filteredFavorites.length === 0 ? (
|
||||
<div className="bg-white border border-gray-200 p-8 text-center">
|
||||
<Heart className="h-12 w-12 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-medium text-gray-700 mb-2">No favorites yet</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Items you save will appear here. Start browsing to add items to your favorites.
|
||||
</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-block bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
|
||||
>
|
||||
Browse Listings
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredFavorites.map((item) => (
|
||||
<div key={item.id} className="bg-white border border-gray-200 hover:shadow-md transition-shadow relative">
|
||||
<button
|
||||
onClick={() => removeFromFavorites(item.id)}
|
||||
className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm text-red-500 hover:bg-red-50"
|
||||
title="Remove from favorites"
|
||||
>
|
||||
<Trash2 className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<Link to={`/product/${item.id}`}>
|
||||
<img src={item.image} alt={item.title} className="w-full h-48 object-cover" />
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-lg font-medium text-gray-800 leading-tight">
|
||||
{item.title}
|
||||
</h3>
|
||||
<span className="font-semibold text-green-600">${item.price}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-sm text-gray-500 mb-3">
|
||||
<Tag className="h-4 w-4 mr-1" />
|
||||
<span>{item.category}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>{item.condition}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-2 border-t border-gray-100">
|
||||
<span className="text-xs text-gray-500">Listed {item.datePosted}</span>
|
||||
<span className="text-sm font-medium text-gray-700">{item.seller}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show count if there are favorites */}
|
||||
{filteredFavorites.length > 0 && (
|
||||
<div className="mt-6 text-sm text-gray-500">
|
||||
Showing {filteredFavorites.length} {filteredFavorites.length === 1 ? 'item' : 'items'}
|
||||
{filterCategory !== 'All' && ` in ${filterCategory}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Favorites;
|
||||
137
frontend/src/pages/Home.jsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from 'lucide-react';
|
||||
|
||||
const Home = () => {
|
||||
const navigate = useNavigate();
|
||||
// Same categories
|
||||
const categories = [
|
||||
{ id: 1, name: 'Textbooks', icon: <Book className="h-5 w-5" /> },
|
||||
{ id: 2, name: 'Electronics', icon: <Laptop className="h-5 w-5" /> },
|
||||
{ id: 3, name: 'Furniture', icon: <Sofa className="h-5 w-5" /> },
|
||||
{ id: 4, name: 'Kitchen', icon: <Utensils className="h-5 w-5" /> },
|
||||
{ id: 5, name: 'Other', icon: <Gift className="h-5 w-5" /> },
|
||||
];
|
||||
|
||||
// Same listings data
|
||||
const [listings, setListings] = useState([
|
||||
{
|
||||
id: 0,
|
||||
title: 'Dell XPS 16 Laptop',
|
||||
price: 850,
|
||||
category: 'Electronics',
|
||||
image: 'image1.avif',
|
||||
condition: 'Good',
|
||||
seller: 'Michael T.',
|
||||
datePosted: '5d ago',
|
||||
isFavorite: true,
|
||||
},
|
||||
]);
|
||||
|
||||
// Toggle favorite status
|
||||
const toggleFavorite = (id, e) => {
|
||||
e.preventDefault(); // Prevent navigation when clicking the heart icon
|
||||
setListings(
|
||||
listings.map((listing) =>
|
||||
listing.id === id ? { ...listing, isFavorite: !listing.isFavorite } : listing
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelling = () => {
|
||||
navigate('/selling');
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Hero Section */}
|
||||
<div className="bg-green-100 py-8 px-4 mb-8 shadow-2xs">
|
||||
<div className="max-w-3xl mx-auto text-center">
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-4">
|
||||
Buy and Sell on Campus
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
The marketplace exclusively for university students. Find everything you need or sell what you don't.
|
||||
</p>
|
||||
|
||||
<button onClick={handleSelling}
|
||||
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-green-400">
|
||||
Post an Item
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">Categories</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{categories.map((category) => (
|
||||
|
||||
<button
|
||||
key={category.id}
|
||||
className="flex flex-col items-center justify-center p-4 bg-white border border-gray-200 hover:border-green-500 hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-center w-12 h-12 bg-green-50 text-green-600 rounded-full mb-2">
|
||||
{category.icon}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700">{category.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Listings */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">Recent Listings</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{listings.map((listing) => (
|
||||
<Link
|
||||
key={listing.id}
|
||||
to={`/product/${listing.id}`}
|
||||
className="bg-white border border-gray-200 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="relative">
|
||||
<img src={listing.image} alt={listing.title} className="w-full h-48 object-cover" />
|
||||
<button
|
||||
onClick={(e) => toggleFavorite(listing.id, e)}
|
||||
className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm"
|
||||
>
|
||||
<Heart
|
||||
className={`h-5 w-5 ${
|
||||
listing.isFavorite ? 'text-red-500 fill-red-500' : 'text-gray-400'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-lg font-medium text-gray-800 leading-tight">
|
||||
{listing.title}
|
||||
</h3>
|
||||
<span className="font-semibold text-green-600">${listing.price}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-sm text-gray-500 mb-3">
|
||||
<Tag className="h-4 w-4 mr-1" />
|
||||
<span>{listing.category}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>{listing.condition}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-2 border-t border-gray-100">
|
||||
<span className="text-xs text-gray-500">{listing.datePosted}</span>
|
||||
<span className="text-sm font-medium text-gray-700">{listing.seller}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
237
frontend/src/pages/ProductDetail.jsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import { useState } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { Heart, ArrowLeft, Tag, User, Calendar, Share, Flag } from 'lucide-react';
|
||||
|
||||
const ProductDetail = () => {
|
||||
const { id } = useParams();
|
||||
const [isFavorite, setIsFavorite] = useState(false);
|
||||
const [showContactForm, setShowContactForm] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [currentImage, setCurrentImage] = useState(0);
|
||||
|
||||
// Sample data for demonstration
|
||||
const product = [
|
||||
{
|
||||
id: 0,
|
||||
title: 'Dell XPS 13 Laptop - 2023 Model',
|
||||
price: 850,
|
||||
shortDescription: 'Dell XPS 13 laptop in excellent condition. Intel Core i7, 16GB RAM, 512GB SSD. Includes charger and original box.',
|
||||
description: 'Selling my Dell XPS 13 laptop. Only 6 months old and in excellent condition. Intel Core i7 processor, 16GB RAM, 512GB SSD. Battery life is still excellent (around 10 hours of regular use). Comes with original charger and box. Selling because I\'m upgrading to a MacBook for design work.\n\nSpecs:\n- Intel Core i7 11th Gen\n- 16GB RAM\n- 512GB NVMe SSD\n- 13.4" FHD+ Display (1920x1200)\n- Windows 11 Pro\n- Backlit Keyboard\n- Thunderbolt 4 ports',
|
||||
condition: 'Like New',
|
||||
category: 'Electronics',
|
||||
datePosted: '2023-03-02',
|
||||
images: [
|
||||
'/image1.avif',
|
||||
'/image2.avif',
|
||||
'/image3.avif'
|
||||
],
|
||||
seller: {
|
||||
name: 'Michael T.',
|
||||
rating: 4.8,
|
||||
memberSince: 'January 2022',
|
||||
avatar: '/Profile.jpg'
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
console.log(product[id])
|
||||
|
||||
const toggleFavorite = () => {
|
||||
setIsFavorite(!isFavorite);
|
||||
};
|
||||
|
||||
const handleSendMessage = (e) => {
|
||||
e.preventDefault();
|
||||
// TODO: this would send the message to the seller
|
||||
console.log('Message sent:', message);
|
||||
setMessage('');
|
||||
setShowContactForm(false);
|
||||
// Show confirmation or success message
|
||||
alert('Message sent to seller!');
|
||||
};
|
||||
|
||||
// Function to split description into paragraphs
|
||||
const formatDescription = (text) => {
|
||||
return text.split('\n\n').map((paragraph, index) => (
|
||||
<p key={index} className="mb-4">
|
||||
{paragraph.split('\n').map((line, i) => (
|
||||
<span key={i}>
|
||||
{line}
|
||||
{i < paragraph.split('\n').length - 1 && <br />}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
));
|
||||
};
|
||||
|
||||
// Handle image navigation
|
||||
const nextImage = () => {
|
||||
setCurrentImage((prev) => (prev === product.images.length - 1 ? 0 : prev + 1));
|
||||
};
|
||||
|
||||
const prevImage = () => {
|
||||
setCurrentImage((prev) => (prev === 0 ? product.images.length - 1 : prev - 1));
|
||||
};
|
||||
|
||||
const selectImage = (index) => {
|
||||
setCurrentImage(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 py-8">
|
||||
{/* Breadcrumb & Back Link */}
|
||||
<div className="mb-6">
|
||||
<Link to="/" className="flex items-center text-green-600 hover:text-green-700">
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
<span>Back to listings</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
{/* Left Column - Images */}
|
||||
<div className="md:w-3/5">
|
||||
{/* Main Image */}
|
||||
<div className="bg-white border border-gray-200 mb-4 relative">
|
||||
<img
|
||||
src={product[id].images[currentImage]}
|
||||
alt={product[id].title}
|
||||
className="w-full h-auto object-contain cursor-pointer"
|
||||
onClick={nextImage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Thumbnail Images */}
|
||||
{product[id].images.length > 1 && (
|
||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
||||
{product[id].images.map((image, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`bg-white border ${currentImage === index ? 'border-green-500' : 'border-gray-200'} min-w-[100px] cursor-pointer`}
|
||||
onClick={() => selectImage(index)}
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={`${product[id].title} - view ${index + 1}`}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column - Details */}
|
||||
<div className="md:w-2/5">
|
||||
{/* Product Info Card */}
|
||||
<div className="bg-white border border-gray-200 p-6 mb-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h1 className="text-2xl font-bold text-gray-800">{product[id].title}</h1>
|
||||
<button
|
||||
onClick={toggleFavorite}
|
||||
className="p-2 hover:bg-gray-100"
|
||||
>
|
||||
<Heart
|
||||
className={`h-6 w-6 ${isFavorite ? 'text-red-500 fill-red-500' : 'text-gray-400'}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-2xl font-bold text-green-600 mb-4">
|
||||
${product[id].price}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2 mb-6 text-sm">
|
||||
<div className="flex items-center text-gray-600">
|
||||
<Tag className="h-4 w-4 mr-1" />
|
||||
<span>{product[id].category}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-gray-600">
|
||||
<span className="font-medium">Condition:</span>
|
||||
<span className="ml-1">{product[id].condition}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-gray-600">
|
||||
<Calendar className="h-4 w-4 mr-1" />
|
||||
<span>Posted on {product[id].datePosted}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Short Description */}
|
||||
<div className="bg-gray-50 p-4 mb-6 border border-gray-200">
|
||||
<p className="text-gray-700">{product[id].shortDescription}</p>
|
||||
</div>
|
||||
|
||||
{/* Contact Button */}
|
||||
<button
|
||||
onClick={() => setShowContactForm(!showContactForm)}
|
||||
className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3"
|
||||
>
|
||||
Contact Seller
|
||||
</button>
|
||||
|
||||
{/* TODO:Contact Form */}
|
||||
{showContactForm && (
|
||||
<div className="border border-gray-200 p-4 mb-4">
|
||||
<h3 className="font-medium text-gray-800 mb-2">Message Seller</h3>
|
||||
<form onSubmit={handleSendMessage}>
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Hi, is this item still available?"
|
||||
className="w-full p-3 border border-gray-300 h-32 mb-3 focus:outline-none focus:border-green-500"
|
||||
required
|
||||
></textarea>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Seller Info */}
|
||||
<div className="pt-4 border-t border-gray-200">
|
||||
<div className="flex items-center mb-3">
|
||||
<div className="mr-3">
|
||||
{product[id].seller.avatar ? (
|
||||
<img
|
||||
src={product[id].seller.avatar}
|
||||
alt="Seller"
|
||||
className="h-12 w-12 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-12 w-12 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
<User className="h-6 w-6 text-gray-600" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-800">{product[id].seller.name}</h3>
|
||||
<p className="text-sm text-gray-500">Member since {product[id].seller.memberSince}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
<div>
|
||||
<span className="font-medium">Rating:</span> {product[id].seller.rating}/5
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description Section */}
|
||||
<div className="mt-8">
|
||||
<h2 className="text-xl font-bold text-gray-800 mb-4">Description</h2>
|
||||
<div className="bg-white border border-gray-200 p-6">
|
||||
<div className="text-gray-700">
|
||||
{formatDescription(product[id].description)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetail;
|
||||
13
frontend/src/pages/Selling.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from 'lucide-react';
|
||||
|
||||
const Selling = () => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Selling;
|
||||
281
frontend/src/pages/Settings.jsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import { useState } from 'react';
|
||||
import { User, Lock, Trash2, History, Search, Shield } from 'lucide-react';
|
||||
|
||||
const Settings = () => {
|
||||
const [userData, setUserData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
ucid: '',
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { id, value } = e.target;
|
||||
setUserData(prevData => ({
|
||||
...prevData,
|
||||
[id]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleProfileUpdate = (e) => {
|
||||
e.preventDefault();
|
||||
// TODO: updated profile data to a server
|
||||
console.log('Profile updated:', userData);
|
||||
alert('Profile updated successfully!');
|
||||
};
|
||||
|
||||
const handlePasswordUpdate = (e) => {
|
||||
e.preventDefault();
|
||||
// TODO: validate and update password
|
||||
if (userData.newPassword !== userData.confirmPassword) {
|
||||
alert('New passwords do not match!');
|
||||
return;
|
||||
}
|
||||
console.log('Password updated');
|
||||
// Reset password fields
|
||||
setUserData(prevData => ({
|
||||
...prevData,
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
}));
|
||||
alert('Password updated successfully!');
|
||||
};
|
||||
|
||||
const handleDeleteHistory = (type) => {
|
||||
// TODO: Delete the specified history
|
||||
console.log(`Deleting ${type} history`);
|
||||
alert(`${type} history deleted successfully!`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-6">Account Settings</h1>
|
||||
|
||||
{/* Profile Information Section */}
|
||||
<div className="bg-white border border-gray-200 mb-6">
|
||||
<div className="border-b border-gray-200 p-4">
|
||||
<div className="flex items-center">
|
||||
<User className="h-5 w-5 text-gray-500 mr-2" />
|
||||
<h2 className="text-lg font-medium text-gray-800">Profile Information</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<form onSubmit={handleProfileUpdate}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={userData.name}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={userData.email}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
value={userData.phone}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="ucid" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
UCID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="ucid"
|
||||
value={userData.ucid}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
|
||||
>
|
||||
Update Profile
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security Section */}
|
||||
<div className="bg-white border border-gray-200 mb-6">
|
||||
<div className="border-b border-gray-200 p-4">
|
||||
<div className="flex items-center">
|
||||
<Lock className="h-5 w-5 text-gray-500 mr-2" />
|
||||
<h2 className="text-lg font-medium text-gray-800">Password</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<form onSubmit={handlePasswordUpdate}>
|
||||
<div className="space-y-4 mb-4">
|
||||
<div>
|
||||
<label htmlFor="currentPassword" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Current Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="currentPassword"
|
||||
value={userData.currentPassword}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="newPassword" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="newPassword"
|
||||
value={userData.newPassword}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
value={userData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
|
||||
>
|
||||
Change Password
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Privacy Section */}
|
||||
<div className="bg-white border border-gray-200 mb-6">
|
||||
<div className="border-b border-gray-200 p-4">
|
||||
<div className="flex items-center">
|
||||
<Shield className="h-5 w-5 text-gray-500 mr-2" />
|
||||
<h2 className="text-lg font-medium text-gray-800">Privacy</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center pb-4 border-b border-gray-100">
|
||||
<div className="flex items-start">
|
||||
<Search className="h-5 w-5 text-gray-500 mr-2 mt-0.5" />
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-800">Search History</h3>
|
||||
<p className="text-sm text-gray-500">Delete all your search history on StudentMarket</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteHistory('search')}
|
||||
className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 flex items-center"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-start">
|
||||
<History className="h-5 w-5 text-gray-500 mr-2 mt-0.5" />
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-800">Browsing History</h3>
|
||||
<p className="text-sm text-gray-500">Delete all your browsing history on StudentMarket</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteHistory('browsing')}
|
||||
className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 flex items-center"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Account (Danger Zone) */}
|
||||
<div className="bg-white border border-red-200 mb-6">
|
||||
<div className="border-b border-red-200 p-4 bg-red-50">
|
||||
<h2 className="text-lg font-medium text-red-700">Danger Zone</h2>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-800">Delete Account</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
Once you delete your account, there is no going back. Please be certain.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="bg-red-500 hover:bg-red-600 text-white font-medium py-2 px-4"
|
||||
onClick={() => {
|
||||
if (window.confirm('Are you sure you want to delete your account? This action cannot be undone.')) {
|
||||
console.log('Account deletion requested');
|
||||
alert('Account deletion request submitted. You will receive a confirmation email.');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
13
frontend/src/pages/Transactions.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from 'lucide-react';
|
||||
|
||||
const Transactions = () => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Transactions;
|
||||
@@ -1,7 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
})
|
||||
|
||||