Base UI design, May contain bugs

This commit is contained in:
Mann Patel
2025-03-05 22:30:52 -07:00
parent 4bedeed33c
commit 49929a85da
28 changed files with 2208 additions and 367 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,50 +1,8 @@
# Campus-Plug # Campus-Plug
The ultimate plug for student deals The ultimate plug for student deals
## Features Breakdown
### Buying Features:
- [ ] **Browsing & Searching for Products:**
- [ ] Categories (e.g., electronics, textbooks, clothing)
- [ ] Keyword-based search
- [ ] Advanced filters (price range, condition, seller rating, location)
- [ ] **Viewing Product Details:**
- [ ] Product descriptions
- [ ] Pricing details
- [ ] Seller contact information
- [ ] Product condition (new, like new, used)
- [ ] Multiple images
- [ ] **Contacting Seller:**
- [ ] Seller's contact details (email/phone)
- [ ] Direct communication without in-app chat
- [ ] **Reviews & Ratings:**
- [ ] 5-star rating system
- [ ] Review text for seller feedback
### Selling Features:
- [ ] **Add/Edit Product Listings:**
- [ ] Dashboard for sellers to add, edit, and delete listings
- [ ] Upload images, set prices, and update availability
- [ ] **Sales Tracking:**
- [ ] Dashboard for sales tracking (total sales, revenue)
- [ ] **Track Order History & Customer Info:**
- [ ] Access to records of transactions, order details, and customer contact info
### Authentication & Security:
- [ ] **Sign up and Sign in with UCalgary Email:**
- [ ] Email-based authentication (using @ucalgary.ca) with OPT
### Recommendation System:
- [ ] **Product Recommendations:**
- [ ] Suggest products based on user browsing and buying history
## some ground rules ## some ground rules
1. Add both node_modules from client and server to your `gitignore` file 1. Add both node_modules from Slient and Server to your `gitignore` file
2. Do not use `.ENV` variables 2. Do not use `.ENV` variables
3. For any functionality make a brach with the prefix of your name `Name-<some branch name>` use this namign convention 3. For any functionality make a brach with the prefix of your name `Name-<some branch name>` use this namign convention
4. For all method added a comment as to what it does 4. For all method added a comment as to what it does
@@ -66,6 +24,6 @@ The ultimate plug for student deals
``` ```
3. To start the server, cd into server dir and then type command `npm run start` 3. To start the server, cd into server dir and then type command `npm run start`
## Database ### Database
- Use only SQL database - Use only mySQL database
- Loading of initian database will be done

View File

@@ -2,16 +2,10 @@ import express, { json } from "express";
import cors from "cors"; import cors from "cors";
const app = express(); const app = express();
// Middleware
//uses the cors and json middleware
//cors is used to allow cross-origin requests
//json is used to parse the request body
app.use(cors()); app.use(cors());
app.use(json()); app.use(json());
// Sample Route // sample route
//This is a sample route that sends a JSON response
//when a GET request is made to /api/test
app.get("/api/test", (req, res) => { app.get("/api/test", (req, res) => {
res.json({ message: "Hello from server!" }); res.json({ message: "Hello from server!" });
}); });

10
backend/user.sql Normal file
View File

@@ -0,0 +1,10 @@
CREATE TABLE users
(
User_Id int NOT NULL,
Name varchar(255) NOT NULL,
UCID int NOT NULL,
Email varchar(255) NOT NULL,
Phone varchar(255),
Address varchar(255),
--TODO: Add the ROle section
)

View File

@@ -1 +0,0 @@
just a placeholder file

View File

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

View File

@@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title> <title>Campus Plug</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

File diff suppressed because it is too large Load Diff

View File

@@ -10,19 +10,25 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.0.9",
"lucide-react": "^0.477.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"react-router-dom": "^7.2.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.19.0",
"@types/react": "^19.0.8", "@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.19.0", "eslint": "^9.19.0",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18", "eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0", "globals": "^15.14.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.9",
"vite": "^6.1.0" "vite": "^6.1.0"
} }
} }

BIN
frontend/public/Profile.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
frontend/public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
frontend/public/image1.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
frontend/public/image2.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
frontend/public/image3.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
frontend/public/market.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 KiB

View File

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

View File

@@ -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;
}

View File

@@ -1,35 +1,398 @@
import { useState } from 'react' import { useState, useEffect } from 'react';
import reactLogo from './assets/react.svg' import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import viteLogo from '/vite.svg' import Navbar from './components/Navbar';
import './App.css' 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() { function App() {
const [count, setCount] = useState(0) // Authentication state
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);
return ( // UI state for login/signup form
<> const [isSignUp, setIsSignUp] = useState(false);
<div> const [formData, setFormData] = useState({
<a href="https://vite.dev" target="_blank"> name: '',
<img src={viteLogo} className="logo" alt="Vite logo" /> ucid: '',
</a> email: '',
<a href="https://react.dev" target="_blank"> phone: '',
<img src={reactLogo} className="logo react" alt="React logo" /> password: '',
</a> });
</div> const [showPassword, setShowPassword] = useState(false);
<h1>Vite + React</h1> const [showImage, setShowImage] = useState(true);
<div className="card"> const [error, setError] = useState('');
<button onClick={() => setCount((count) => count + 1)}>
count is {count} // Fake users database
</button> const fakeUsers = [
<p> { email: 'john1@ucalgary.ca', password: 'password123', name: 'John Doe' },
Edit <code>src/App.jsx</code> and save to test HMR { email: 'jane@ucalgary.ca', password: 'password456', name: 'Jane Smith' }
</p> ];
</div>
<p className="read-the-docs"> // Auto-hide image on smaller screens
Click on the Vite and React logos to learn more useEffect(() => {
</p> 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;
} }
export default App 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 (
<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>
</Router>
);
}
export default App;

View 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;

View 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;

View File

@@ -1,68 +1,4 @@
:root { @import "tailwindcss";
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; @tailwind base;
line-height: 1.5; @tailwind components;
font-weight: 400; @tailwind utilities;
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;
}
}

View 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
View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -1,7 +1,11 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [
react(),
tailwindcss(),
],
}) })