Initial commit

This commit is contained in:
2025-11-25 00:43:00 +05:30
commit ce133723fd
48 changed files with 22361 additions and 0 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
REACT_APP_API_URL=172.232.124.96:5370/api

169
.gitignore vendored Normal file
View File

@@ -0,0 +1,169 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Production builds
dist/
build/
*.tgz
*.tar.gz
# Vite
.vite/
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs/
*.log
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output/
# Dependency directories
jspm_packages/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache/
# next.js build output
.next/
# nuxt.js build output
.nuxt/
# vuepress build output
.vuepress/dist/
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# Temporary folders
tmp/
temp/
# Archive files
*.zip
*.rar
*.7z
# Backend data files (keep structure but ignore data)
backend/data.json
backend/db.json
# Config files with sensitive data
config/local.json
config/production.json
# Build artifacts
*.cache
.parcel-cache
# Testing
coverage/
.nyc_output/
# Storybook build outputs
storybook-static
# Local development
.local
# API keys and secrets
.env.keys
secrets.json
# User-specific files
*.user.js
*.user.css
# Lock files (optional - comment out if you want to track them)
# package-lock.json
# yarn.lock
# pnpm-lock.yaml
# Vite specific
.vite/
# React specific
.eslintcache
# Development server
.eslintcache
# Custom ignores for this project
# Keep the final index.html but ignore build files
dist/
build/
# Ignore compiled/bundled JS files but keep source
src/**/*.bundle.js
src/**/*.min.js
# Ignore source maps in production
*.map
src/**/*.map
# Keep important files
!index.html
!package.json
!package-lock.json
!vite.config.js
!eslint.config.js

BIN
media/logo/wraffle_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

17462
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "wraffle-ecommerce-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"axios": "^1.13.1",
"react-router-dom": "^7.9.5"
},
"proxy": "http://172.232.124.96:5370",
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
public/images/hoodie1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

BIN
public/images/hoodie2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

20
public/index.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Wraffle - Premium hoodies for every style and occasion"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Wraffle Ecommerce</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

25
public/manifest.json Normal file
View File

@@ -0,0 +1,25 @@
{
"short_name": "Wraffle",
"name": "Wraffle Ecommerce",
"icons": [
{
"src": "wraffle_logo.png",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/png"
},
{
"src": "wraffle_logo.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "wraffle_logo.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#FFB400",
"background_color": "#ffffff"
}

181
public/verify-email.css Normal file
View File

@@ -0,0 +1,181 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #000000 0%, #333333 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.verify-container {
width: 100%;
max-width: 500px;
}
.verify-card {
background: #000000;
border: 2px solid #FFB400;
border-radius: 15px;
padding: 40px;
text-align: center;
box-shadow: 0 10px 30px rgba(255, 215, 0, 0.3);
}
/* Hidden class */
.hidden {
display: none !important;
}
/* Verifying state */
.verifying {
color: #ffffff;
}
.verifying h2 {
color: #FFD700;
margin-bottom: 20px;
font-size: 2rem;
font-weight: bold;
}
/* Spinner */
.spinner {
width: 50px;
height: 50px;
border: 4px solid #333333;
border-top: 4px solid #FFD700;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Success state */
.success h2 {
color: #FFD700;
margin-bottom: 20px;
font-size: 2rem;
font-weight: bold;
}
.success p {
color: #ffffff;
margin-bottom: 20px;
font-size: 1.1rem;
line-height: 1.5;
}
.redirect-message {
color: #FFD700 !important;
font-weight: bold;
margin-bottom: 30px !important;
}
/* Success icon */
.success-icon {
width: 80px;
height: 80px;
background: #FFD700;
color: #000000;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: bold;
margin: 0 auto 20px;
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.4);
}
/* Error state */
.error h2 {
color: #ff4444;
margin-bottom: 20px;
font-size: 2rem;
font-weight: bold;
}
.error p {
color: #ffffff;
margin-bottom: 20px;
font-size: 1.1rem;
line-height: 1.5;
}
/* Error icon */
.error-icon {
width: 80px;
height: 80px;
background: #ff4444;
color: #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: bold;
margin: 0 auto 20px;
box-shadow: 0 5px 15px rgba(255, 68, 68, 0.4);
}
/* Buttons */
.home-btn, .retry-btn {
background: #FFD700;
color: #000000;
border: none;
padding: 12px 30px;
border-radius: 8px;
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.home-btn:hover, .retry-btn:hover {
background: #ffed4e;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.4);
}
.retry-btn {
background: #ff4444;
color: #ffffff;
}
.retry-btn:hover {
background: #ff6666;
box-shadow: 0 5px 15px rgba(255, 102, 102, 0.4);
}
/* Responsive design */
@media (max-width: 768px) {
.verify-card {
padding: 30px 20px;
margin: 20px;
}
.verifying h2, .success h2, .error h2 {
font-size: 1.5rem;
}
.success p, .error p {
font-size: 1rem;
}
.home-btn, .retry-btn {
padding: 10px 25px;
font-size: 1rem;
}
}

103
public/verify-email.html Normal file
View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Verification - Wraffle</title>
<link rel="stylesheet" href="verify-email.css">
</head>
<body>
<div class="verify-container">
<div class="verify-card">
<div id="verifying" class="verifying">
<div class="spinner"></div>
<h2>Verifying your email...</h2>
</div>
<div id="success" class="success hidden">
<div class="success-icon"></div>
<h2>Email Verified Successfully!</h2>
<p>Your email has been verified. You can now access all features of your account.</p>
<p class="redirect-message">Redirecting to home page in <span id="countdown">10</span> seconds...</p>
<button class="home-btn" onclick="goHome()">Go to Home Now</button>
</div>
<div id="error" class="error hidden">
<div class="error-icon"></div>
<h2>Verification Failed</h2>
<p id="error-message">Verification failed</p>
<p class="redirect-message">Redirecting to login page in <span id="countdown-error">10</span> seconds...</p>
<button class="retry-btn" onclick="goLogin()">Back to Login</button>
</div>
</div>
</div>
<script>
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
let countdownInterval;
async function verifyEmail() {
if (!token) {
showError('No verification token provided');
return;
}
try {
const response = await fetch(`http://172.232.124.96:5370/api/auth/verify-email?token=${token}`);
const data = await response.json();
if (response.ok) {
showSuccess();
} else {
showError(data.message || 'Verification failed');
}
} catch (error) {
showError('Network error occurred');
}
}
function showSuccess() {
document.getElementById('verifying').classList.add('hidden');
document.getElementById('success').classList.remove('hidden');
startCountdown('countdown', () => window.location.href = '/');
}
function showError(message) {
document.getElementById('verifying').classList.add('hidden');
document.getElementById('error').classList.remove('hidden');
document.getElementById('error-message').textContent = message;
startCountdown('countdown-error', () => window.location.href = '/login');
}
function startCountdown(elementId, callback) {
let timeLeft = 10;
const countdownElement = document.getElementById(elementId);
countdownElement.textContent = timeLeft;
countdownInterval = setInterval(() => {
timeLeft--;
countdownElement.textContent = timeLeft;
if (timeLeft <= 0) {
clearInterval(countdownInterval);
callback();
}
}, 1000);
}
function goHome() {
clearInterval(countdownInterval);
window.location.href = '/';
}
function goLogin() {
clearInterval(countdownInterval);
window.location.href = '/login';
}
// Start verification on page load
verifyEmail();
</script>
</body>
</html>

BIN
public/wraffle_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

3
src/App.css Normal file
View File

@@ -0,0 +1,3 @@
main {
flex: 1;
}

75
src/App.jsx Normal file
View File

@@ -0,0 +1,75 @@
import React, { useState, useEffect } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import Header from './components/Header';
import Footer from './components/Footer';
import Home from './pages/Home';
import Shop from './pages/Shop';
import About from './pages/About';
import Contact from './pages/Contact';
import Auth from './pages/Auth';
import VerifyEmail from './pages/VerifyEmail';
import VerifyEmailSuccess from './pages/VerifyEmailSuccess';
import ResetPassword from './pages/ResetPassword';
import authService from './services/authService';
import './App.css';
// import admin from './pages/Admin';
import Product from './pages/ProductDetails';
import ProductDetails from './pages/ProductDetails';
import Cart from './pages/Cart';
import Admin from './pages/Admin';
function App() {
const [user, setUser] = useState(null);
const [isAdmin, setIsAdmin] = useState(false);
const fetchProfile = async () => {
try {
const response = await authService.getProfile();
setUser(response.user);
setIsAdmin(response.user.role === 'admin');
} catch (error) {
console.error('Failed to fetch profile:', error);
setUser(null);
setIsAdmin(false);
}
};
useEffect(() => {
fetchProfile();
}, []);
const handleLogin = async () => {
await fetchProfile();
};
const handleLogout = () => {
authService.logout();
setUser(null);
setIsAdmin(false);
};
return (
<div className="App">
<Header user={user} onLogout={handleLogout} isAdmin={isAdmin} />
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/shop" element={<Shop />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/auth" element={user ? <Navigate to="/" replace /> : <Auth onLogin={handleLogin} />} />
<Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/verify-email-success" element={<VerifyEmailSuccess />} />
<Route path="/reset-password" element={<ResetPassword />} />
{/* <Route path="/admin" element={<admin />} /> */}
<Route path="/productdetail" element={<ProductDetails/>}/>
<Route path="/cart" element={<Cart/>} />
<Route path="/admin" element={isAdmin ? <Admin/> : <Navigate to="/auth" replace />} />
</Routes>
</main>
<Footer />
</div>
);
}
export default App;

71
src/components/Footer.css Normal file
View File

@@ -0,0 +1,71 @@
.footer {
background-color: #000;
color: #FFB400;
padding: 2rem 0 1rem;
margin-top: auto;
}
.footer-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.footer-section h3 {
color: #FFB400;
margin-bottom: 1rem;
font-size: 1.2rem;
}
.footer-section p {
color: #ccc;
line-height: 1.6;
}
.footer-logo {
width: 70px;
height: 50px;
margin-bottom: 1rem;
filter: brightness(0) invert(1);
}
.footer-section ul {
list-style: none;
padding: 0;
}
.footer-section ul li {
margin-bottom: 0.5rem;
}
.footer-section ul li a {
color: #ccc;
text-decoration: none;
transition: color 0.3s ease;
}
.footer-section ul li a:hover {
color: #FFB400;
}
.footer-bottom {
text-align: center;
padding-top: 2rem;
margin-top: 2rem;
border-top: 1px solid #333;
color: #ccc;
}
@media (max-width: 768px) {
.footer-container {
grid-template-columns: 1fr;
text-align: center;
}
.footer-section {
margin-bottom: 1.5rem;
}
}

47
src/components/Footer.jsx Normal file
View File

@@ -0,0 +1,47 @@
import React from 'react';
import './Footer.css';
const Footer = () => {
return (
<footer className="footer">
<div className="footer-container">
<div className="footer-section">
<img src="/wraffle_logo.png" alt="Wraffle" className="footer-logo" />
<p>Made to Fit Your Vibe.</p>
</div>
<div className="footer-section">
<h3>Quick Links</h3>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/shop">Shop</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div className="footer-section">
<h3>Customer Service</h3>
<ul>
<li><a href="/orders">My Orders</a></li>
<li><a href="/contact">Support</a></li>
<li><a href="/contact">FAQ</a></li>
</ul>
</div>
<div className="footer-section">
<h3>Contact Info</h3>
<p>Email: info@wraffle.com</p>
<p>Phone: (555) 123-4567</p>
<p>Address: 123 Food St, City, State 12345</p>
</div>
</div>
<div className="footer-bottom">
<p>&copy; 2024 Wraffle. All rights reserved.</p>
</div>
</footer>
);
};
export default Footer;

131
src/components/Header.css Normal file
View File

@@ -0,0 +1,131 @@
.header {
background-color: #000;
color: #FFB400;
padding: 1rem 0;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.header-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
text-decoration: none;
color: #FFB400;
font-size: 1.5rem;
font-weight: bold;
}
.logo-img {
width: 40px;
height: 40px;
margin-right: 0.5rem;
filter: brightness(0) invert(1);
object-fit: contain;
}
.logo-text {
color: #FFB400;
}
.nav {
display: flex;
align-items: center;
gap: 1.5rem;
}
.nav-link {
color: #FFB400;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 5px;
transition: background-color 0.3s ease;
}
.nav-link:hover,
.nav-link.signup {
background-color: #FFB400;
color: #000;
}
.nav-user {
color: #FFB400;
font-weight: bold;
}
.nav-button {
background-color: #FFB400;
color: #000;
border: none;
padding: 0.5rem 1rem;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.3s ease;
}
.nav-button:hover {
background-color: #FFC107;
}
.menu-toggle {
display: none;
flex-direction: column;
background: none;
border: none;
cursor: pointer;
}
.menu-bar {
width: 25px;
height: 3px;
background-color: #FFB400;
margin: 3px 0;
transition: 0.3s;
}
@media (max-width: 768px) {
.nav {
position: fixed;
top: 100%;
left: 0;
right: 0;
background-color: #000;
flex-direction: column;
padding: 1rem;
transform: translateY(-100%);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.nav.nav-open {
transform: translateY(0);
opacity: 1;
visibility: visible;
}
.menu-toggle {
display: flex;
}
.nav-link,
.nav-user,
.nav-button {
width: 100%;
text-align: center;
margin: 0.5rem 0;
}
}

59
src/components/Header.jsx Normal file
View File

@@ -0,0 +1,59 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import './Header.css';
const Header = ({ user, onLogout, isAdmin }) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const navigate = useNavigate();
const handleLogout = () => {
onLogout();
navigate('/');
};
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
};
return (
<header className="header">
<div className="header-container">
<Link to="/" className="logo">
<img src="/wraffle_logo.png" alt="Wraffle" className="logo-img" />
<span className="logo-text">Wraffle</span>
</Link>
<nav className={`nav ${isMenuOpen ? 'nav-open' : ''}`}>
<Link to="/" className="nav-link" onClick={() => setIsMenuOpen(false)}>Home</Link>
<Link to="/shop" className="nav-link" onClick={() => setIsMenuOpen(false)}>Shop</Link>
<Link to="/about" className="nav-link" onClick={() => setIsMenuOpen(false)}>About</Link>
<Link to="/contact" className="nav-link" onClick={() => setIsMenuOpen(false)}>Contact</Link>
<Link to="/cart" className="nav-link" onClick={() => setIsMenuOpen(false)}>Cart</Link>
{user ? (
<>
<Link to="/orders" className="nav-link" onClick={() => setIsMenuOpen(false)}>My Orders</Link>
{isAdmin && (
<Link to="/admin" className="nav-link" onClick={() => setIsMenuOpen(false)}>Admin</Link>
)}
<span className="nav-user">Welcome, {user.first_name}</span>
<button className="nav-button logout" onClick={handleLogout}>Logout</button>
</>
) : (
<>
<Link to="/auth" className="nav-link" onClick={() => setIsMenuOpen(false)}>Login</Link>
</>
)}
</nav>
<button className="menu-toggle" onClick={toggleMenu}>
<span className="menu-bar"></span>
<span className="menu-bar"></span>
<span className="menu-bar"></span>
</button>
</div>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,65 @@
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.spinner-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.spinner-logo {
max-width: 60px;
height: auto;
animation: spin 2s linear infinite;
filter: brightness(0) invert(1);
}
.loading-spinner.yellow .spinner-logo {
filter: none;
}
.spinner-ring {
position: absolute;
top: -10px;
left: -10px;
width: 80px;
height: 80px;
border: 3px solid transparent;
border-top: 3px solid #FFB400;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-spinner.small .spinner-logo {
max-width: 40px;
height: auto;
}
.loading-spinner.small .spinner-ring {
width: 60px;
height: 60px;
top: -10px;
left: -10px;
}
.loading-spinner.large .spinner-logo {
max-width: 100px;
height: auto;
}
.loading-spinner.large .spinner-ring {
width: 120px;
height: 120px;
top: -10px;
left: -10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import './LoadingSpinner.css';
const LoadingSpinner = ({ size = 'medium', color = 'yellow' }) => {
return (
<div className={`loading-spinner ${size} ${color}`}>
<div className="spinner-container">
<img
src="/wraffle_logo.png"
alt="Wraffle Logo"
className="spinner-logo"
/>
<div className="spinner-ring"></div>
</div>
</div>
);
};
export default LoadingSpinner;

4
src/config/api.jsx Normal file
View File

@@ -0,0 +1,4 @@
// API Configuration
const API_BASE_URL = 'http://172.232.124.96:5370/api';
export default API_BASE_URL;

23
src/index.css Normal file
View File

@@ -0,0 +1,23 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
* {
box-sizing: border-box;
}
.App {
min-height: 100vh;
display: flex;
flex-direction: column;
}

14
src/index.js Normal file
View File

@@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>
);

178
src/pages/About.css Normal file
View File

@@ -0,0 +1,178 @@
.about {
min-height: calc(100vh - 160px);
padding: 2rem 1rem;
}
.about-header {
text-align: center;
margin-bottom: 4rem;
}
.about-header h1 {
color: #000;
font-size: 2.5rem;
margin-bottom: 1rem;
}
.about-header p {
color: #666;
font-size: 1.1rem;
}
.about-content {
max-width: 1200px;
margin: 0 auto;
}
/* Story Section */
.story-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
margin-bottom: 4rem;
align-items: center;
}
.story-content h2 {
color: #000;
font-size: 2rem;
margin-bottom: 1.5rem;
}
.story-content p {
color: #666;
line-height: 1.6;
margin-bottom: 1rem;
font-size: 1.1rem;
}
.story-image {
display: flex;
justify-content: center;
align-items: center;
}
.story-logo {
max-width: 250px;
height: auto;
animation: pulse 4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
/* Values Section */
.values-section {
margin-bottom: 4rem;
}
.values-section h2 {
color: #000;
font-size: 2rem;
text-align: center;
margin-bottom: 3rem;
}
.values-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.value-card {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
text-align: center;
transition: transform 0.3s ease;
}
.value-card:hover {
transform: translateY(-5px);
}
.value-card h3 {
color: #FFB400;
font-size: 1.3rem;
margin-bottom: 1rem;
}
.value-card p {
color: #666;
line-height: 1.6;
}
/* Mission Section */
.mission-section {
background-color: #000;
color: #FFB400;
padding: 4rem 2rem;
border-radius: 10px;
text-align: center;
}
.mission-section h2 {
font-size: 2rem;
margin-bottom: 2rem;
}
.mission-section p {
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 3rem;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.mission-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 2rem;
max-width: 600px;
margin: 0 auto;
}
.stat h3 {
color: #FFB400;
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.stat p {
color: #ccc;
font-size: 1rem;
}
@media (max-width: 768px) {
.story-section {
grid-template-columns: 1fr;
gap: 2rem;
text-align: center;
}
.story-logo {
max-width: 200px;
height: auto;
}
.values-grid {
grid-template-columns: 1fr;
}
.mission-stats {
grid-template-columns: 1fr;
}
.about-header h1 {
font-size: 2rem;
}
.story-content h2,
.values-section h2 {
font-size: 1.5rem;
}
}

92
src/pages/About.jsx Normal file
View File

@@ -0,0 +1,92 @@
import React from 'react';
import './About.css';
const About = () => {
return (
<div className="about">
<div className="about-header">
<h1>About Wraffle</h1>
<p>Your premium hoodie destination</p>
</div>
<div className="about-content">
<section className="story-section">
<div className="story-content">
<h2>Our Story</h2>
<p>
Wraffle was born from a passion for quality streetwear and a desire to create
hoodies that stand out from the crowd. We believe that everyone deserves to
wear comfortable, stylish clothing that expresses their unique personality.
</p>
<p>
Founded in 2024, we've quickly become a go-to destination for fashion enthusiasts
who appreciate premium quality, unique designs, and exceptional comfort.
</p>
</div>
<div className="story-image">
<img src="/wraffle_logo.png" alt="Wraffle Story" className="story-logo" />
</div>
</section>
<section className="values-section">
<h2>Our Values</h2>
<div className="values-grid">
<div className="value-card">
<h3>Quality First</h3>
<p>
We use only the finest materials and pay attention to every detail
in our manufacturing process to ensure lasting quality.
</p>
</div>
<div className="value-card">
<h3>Unique Designs</h3>
<p>
Our designs are created by talented artists and designers who push
the boundaries of streetwear fashion.
</p>
</div>
<div className="value-card">
<h3>Sustainability</h3>
<p>
We're committed to reducing our environmental impact through
sustainable practices and eco-friendly materials.
</p>
</div>
<div className="value-card">
<h3>Customer Focus</h3>
<p>
Your satisfaction is our top priority. We strive to provide
exceptional service and support to every customer.
</p>
</div>
</div>
</section>
<section className="mission-section">
<h2>Our Mission</h2>
<p>
To empower individuals to express themselves through fashion while maintaining
the highest standards of quality, comfort, and style. We believe that great
clothing should be accessible to everyone, regardless of their budget or style preferences.
</p>
<div className="mission-stats">
<div className="stat">
<h3>1000+</h3>
<p>Happy Customers</p>
</div>
<div className="stat">
<h3>50+</h3>
<p>Unique Designs</p>
</div>
<div className="stat">
<h3>24/7</h3>
<p>Customer Support</p>
</div>
</div>
</section>
</div>
</div>
);
};
export default About;

259
src/pages/Admin.css Normal file
View File

@@ -0,0 +1,259 @@
:root {
--primary-color: #2563eb;
--secondary-color: #1e40af;
--bg-color: #f3f4f6;
--sidebar-bg: #1f2937;
--sidebar-text: #e5e7eb;
--card-bg: #ffffff;
--text-color: #111827;
--border-color: #e5e7eb;
--danger-color: #ef4444;
}
.admin-container {
display: flex;
min-height: 100vh;
background-color: var(--bg-color);
font-family: 'Inter', sans-serif;
}
/* Sidebar */
.admin-sidebar {
width: 260px;
background-color: var(--sidebar-bg);
color: var(--sidebar-text);
display: flex;
flex-direction: column;
padding: 20px;
flex-shrink: 0;
}
.sidebar-header {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 40px;
color: #fff;
display: flex;
align-items: center;
gap: 10px;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 10px;
}
.nav-item {
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: var(--sidebar-text);
text-decoration: none;
display: flex;
align-items: center;
gap: 12px;
}
.nav-item:hover, .nav-item.active {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
/* Main Content */
.admin-main {
flex: 1;
padding: 32px;
overflow-y: auto;
}
.page-header {
margin-bottom: 32px;
}
.page-title {
font-size: 1.875rem;
font-weight: 700;
color: var(--text-color);
margin: 0;
}
/* Cards */
.admin-card {
background: var(--card-bg);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 24px;
margin-bottom: 32px;
}
.card-header {
margin-bottom: 24px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 16px;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-color);
margin: 0;
}
/* Form */
.admin-form {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group.full-width {
grid-column: span 2;
}
.form-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-color);
}
.form-input, .form-textarea, .form-select {
padding: 10px 14px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.95rem;
transition: border-color 0.2s;
width: 100%;
}
.form-input:focus, .form-textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-textarea {
min-height: 100px;
resize: vertical;
}
.file-upload-wrapper {
border: 2px dashed var(--border-color);
padding: 20px;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background: #f9fafb;
}
.file-upload-wrapper:hover {
border-color: var(--primary-color);
background: #eff6ff;
}
.image-preview {
margin-top: 10px;
max-height: 150px;
border-radius: 6px;
object-fit: cover;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
padding: 12px 24px;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
width: fit-content;
}
.btn-primary:hover {
background-color: var(--secondary-color);
}
/* Table */
.table-container {
overflow-x: auto;
}
.admin-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
.admin-table th {
background-color: #f9fafb;
font-weight: 600;
text-align: left;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
color: #6b7280;
}
.admin-table td {
padding: 16px;
border-bottom: 1px solid var(--border-color);
color: var(--text-color);
vertical-align: middle;
}
.product-thumb {
width: 48px;
height: 48px;
border-radius: 6px;
object-fit: cover;
background: #f3f4f6;
}
.action-btn {
padding: 6px 12px;
border-radius: 4px;
border: 1px solid var(--border-color);
background: white;
cursor: pointer;
font-size: 0.875rem;
color: var(--danger-color);
transition: all 0.2s;
}
.action-btn:hover {
background: #fee2e2;
border-color: #fecaca;
}
.admin-error {
background-color: #fee2e2;
color: #991b1b;
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
border: 1px solid #fecaca;
}
@media (max-width: 768px) {
.admin-container {
flex-direction: column;
}
.admin-sidebar {
width: 100%;
padding: 16px;
}
.admin-form {
grid-template-columns: 1fr;
}
}

251
src/pages/Admin.jsx Normal file
View File

@@ -0,0 +1,251 @@
import React, { useEffect, useState } from 'react';
import productService from '../services/productService';
import './Admin.css';
const Admin = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [form, setForm] = useState({
name: '',
description: '',
price: '',
stockQuantity: 0,
category: '',
imageUrl: '',
imageUrl1: ''
});
const [imagePreview, setImagePreview] = useState('');
const [imagePreview1, setImagePreview1] = useState('');
const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState('products');
useEffect(() => {
fetchProducts();
}, []);
const fetchProducts = async () => {
try {
setLoading(true);
const data = await productService.getProducts({ limit: 200 });
setProducts(data.products || []);
} catch (err) {
console.error(err);
setError('Failed to load products or you may not have admin access');
} finally {
setLoading(false);
}
};
const handleInput = (e) => setForm({ ...form, [e.target.name]: e.target.value });
const handleImageUpload = (e, field) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setForm(prev => ({ ...prev, [field]: reader.result }));
if (field === 'imageUrl') setImagePreview(reader.result);
if (field === 'imageUrl1') setImagePreview1(reader.result);
};
reader.readAsDataURL(file);
}
};
const handleAddProduct = async (e) => {
e.preventDefault();
if (!form.name || !form.price) {
setError('Name and price are required');
return;
}
const payload = {
name: form.name,
description: form.description,
price: parseFloat(form.price),
stockQuantity: parseInt(form.stockQuantity, 10) || 0,
imageUrl: form.imageUrl,
imageUrl1: form.imageUrl1, // Sending second image
category: form.category,
};
try {
await productService.addProduct(payload);
setForm({ name: '', description: '', price: '', stockQuantity: 0, category: '', imageUrl: '', imageUrl1: '' });
setImagePreview('');
setImagePreview1('');
fetchProducts();
setError(null);
alert('Product added successfully!');
} catch (err) {
console.error(err);
setError(err.response?.data?.message || 'Failed to add product');
}
};
const handleUpdateStock = async (id, newQty) => {
try {
await productService.updateProduct(id, { stockQuantity: parseInt(newQty, 10) });
fetchProducts();
} catch (err) {
console.error(err);
setError('Failed to update stock');
}
};
const handleDelete = async (id) => {
if (!window.confirm('Delete this product?')) return;
try {
await productService.deleteProduct(id);
fetchProducts();
} catch (err) {
console.error(err);
setError('Failed to delete product');
}
};
return (
<div className="admin-container">
{/* Sidebar */}
<aside className="admin-sidebar">
<div className="sidebar-header">
<span>Wraffle Admin</span>
</div>
<nav className="sidebar-nav">
<a
href="#"
className={`nav-item ${activeTab === 'products' ? 'active' : ''}`}
onClick={() => setActiveTab('products')}
>
📦 Products
</a>
<a
href="#"
className={`nav-item ${activeTab === 'orders' ? 'active' : ''}`}
onClick={() => setActiveTab('orders')}
>
🛒 Orders (Coming Soon)
</a>
<a href="/" className="nav-item">
🏠 Back to Site
</a>
</nav>
</aside>
{/* Main Content */}
<main className="admin-main">
<div className="page-header">
<h1 className="page-title">Product Management</h1>
</div>
{error && <div className="admin-error">{error}</div>}
{/* Add Product Form */}
<section className="admin-card">
<div className="card-header">
<h2 className="card-title">Add New Product</h2>
</div>
<form onSubmit={handleAddProduct} className="admin-form">
<div className="form-group">
<label className="form-label">Product Name</label>
<input className="form-input" name="name" placeholder="e.g. Summer T-Shirt" value={form.name} onChange={handleInput} />
</div>
<div className="form-group">
<label className="form-label">Price ($)</label>
<input className="form-input" name="price" type="number" step="0.01" placeholder="0.00" value={form.price} onChange={handleInput} />
</div>
<div className="form-group">
<label className="form-label">Stock Quantity</label>
<input className="form-input" name="stockQuantity" type="number" placeholder="0" value={form.stockQuantity} onChange={handleInput} />
</div>
<div className="form-group">
<label className="form-label">Category</label>
<input className="form-input" name="category" placeholder="e.g. Men, Women" value={form.category} onChange={handleInput} />
</div>
<div className="form-group full-width">
<label className="form-label">Description</label>
<textarea className="form-textarea" name="description" placeholder="Product details..." value={form.description} onChange={handleInput} />
</div>
{/* Image Uploads */}
<div className="form-group">
<label className="form-label">Main Image</label>
<div className="file-upload-wrapper">
<input type="file" accept="image/*" onChange={(e) => handleImageUpload(e, 'imageUrl')} />
{imagePreview && <img src={imagePreview} alt="Preview" className="image-preview" />}
</div>
</div>
<div className="form-group">
<label className="form-label">Second Image</label>
<div className="file-upload-wrapper">
<input type="file" accept="image/*" onChange={(e) => handleImageUpload(e, 'imageUrl1')} />
{imagePreview1 && <img src={imagePreview1} alt="Preview" className="image-preview" />}
</div>
</div>
<div className="form-group full-width">
<button type="submit" className="btn-primary">Add Product</button>
</div>
</form>
</section>
{/* Product List */}
<section className="admin-card">
<div className="card-header">
<h2 className="card-title">Product List</h2>
</div>
{loading ? <p>Loading...</p> : (
<div className="table-container">
<table className="admin-table">
<thead>
<tr>
<th>Image</th>
<th>Name</th>
<th>Price</th>
<th>Stock</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{products.map(p => (
<tr key={p.id}>
<td>
{p.image_url ? (
<img src={p.image_url} alt={p.name} className="product-thumb" />
) : (
<div className="product-thumb" style={{display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ccc'}}>No Img</div>
)}
</td>
<td>{p.name}</td>
<td>${p.price}</td>
<td>
<input
type="number"
className="form-input"
style={{width: '80px'}}
defaultValue={p.stock_quantity}
onBlur={(e) => handleUpdateStock(p.id, e.target.value)}
/>
</td>
<td>{p.category}</td>
<td>
<button className="action-btn" onClick={() => handleDelete(p.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
</main>
</div>
);
};
export default Admin;

169
src/pages/Auth.css Normal file
View File

@@ -0,0 +1,169 @@
.auth-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #000000 0%, #333333 100%);
padding: 20px;
}
.auth-card {
background: #000;
border-radius: 12px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5);
width: 100%;
max-width: 450px;
padding: 40px;
animation: slideIn 0.5s ease-out;
border: 2px solid #FFB400;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.auth-header {
text-align: center;
margin-bottom: 30px;
}
.auth-logo {
text-align: center;
margin-bottom: 20px;
}
.auth-logo-img {
max-width: 60px;
height: auto;
filter: brightness(0) invert(1);
}
.auth-header h2 {
color: #FFB400;
font-size: 28px;
font-weight: 600;
margin: 0;
}
.auth-form {
margin-bottom: 20px;
}
.form-row {
display: flex;
gap: 15px;
}
.form-row .form-group {
flex: 1;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #FFB400;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #FFB400;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s ease, background-color 0.3s ease;
background-color: #111;
color: #FFB400;
}
.form-group input:focus {
outline: none;
border-color: #FFC107;
box-shadow: 0 0 0 3px rgba(255, 215, 0, 0.1);
background-color: #222;
}
.error-message {
background: #ff4444;
color: #fff;
padding: 10px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
text-align: center;
}
.auth-button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #FFB400 0%, #FFC107 100%);
color: #000;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.auth-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.3);
}
.auth-button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.auth-toggle {
text-align: center;
margin-top: 20px;
}
.auth-toggle p {
color: #FFB400;
margin: 0;
font-size: 14px;
}
.toggle-button {
background: none;
border: none;
color: #FFB400;
font-weight: 600;
cursor: pointer;
text-decoration: underline;
margin-left: 5px;
transition: color 0.2s ease;
}
.toggle-button:hover {
color: #FFC107;
}
@media (max-width: 480px) {
.auth-container {
padding: 10px;
}
.auth-card {
padding: 30px 20px;
}
.form-row {
flex-direction: column;
gap: 0;
}
}

273
src/pages/Auth.jsx Normal file
View File

@@ -0,0 +1,273 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import authService from '../services/authService';
import productService from '../services/productService';
import './Auth.css';
const Auth = ({ onLogin }) => {
const [isLogin, setIsLogin] = useState(true);
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: ''
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [emailNotVerified, setEmailNotVerified] = useState(false);
const [showForgotPassword, setShowForgotPassword] = useState(false);
const navigate = useNavigate();
const handleInputChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
setEmailNotVerified(false);
try {
if (isLogin) {
const response = await authService.login({
email: formData.email,
password: formData.password
});
// If backend returned a token (helpful in dev over HTTP), set it in memory
if (response && response.token) {
authService.setToken(response.token);
productService.setToken(response.token);
}
// Inform parent to refresh profile and update UI
onLogin();
navigate('/');
} else {
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
setLoading(false);
return;
}
await authService.register({
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
password: formData.password
});
setIsLogin(true);
setError('Registration successful! Please check your email for verification and then login.');
}
} catch (err) {
const errorMessage = err.response?.data?.message || 'An error occurred';
setError(errorMessage);
if (errorMessage === 'Please verify your email before logging in') {
setEmailNotVerified(true);
}
} finally {
setLoading(false);
}
};
const handleResendVerification = async () => {
setError('');
setLoading(true);
try {
await authService.resendVerificationEmail({ email: formData.email });
setError('A new verification email has been sent. Please check your inbox.');
} catch (err) {
setError(err.response?.data?.message || 'An error occurred while resending the verification email.');
} finally {
setLoading(false);
}
};
const handleForgotPasswordSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await authService.forgotPassword(formData.email);
setError('If an account with that email exists, a password reset link has been sent.');
setShowForgotPassword(false);
} catch (err) {
setError(err.response?.data?.message || 'An error occurred.');
} finally {
setLoading(false);
}
};
const toggleMode = () => {
setIsLogin(!isLogin);
setError('');
setFormData({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: ''
});
setEmailNotVerified(false);
setShowForgotPassword(false);
};
if (showForgotPassword) {
return (
<div className="auth-container">
<div className="auth-card">
<div className="auth-header">
<div className="auth-logo">
<img src="/wraffle_logo.png" alt="Wraffle" className="auth-logo-img" />
</div>
<h2>Forgot Password</h2>
</div>
<form onSubmit={handleForgotPasswordSubmit} className="auth-form">
<p>Enter your email address and we'll send you a link to reset your password.</p>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
/>
</div>
{error && <div className="error-message">{error}</div>}
<button type="submit" className="auth-button" disabled={loading}>
{loading ? 'Sending...' : 'Send Reset Link'}
</button>
</form>
<div className="auth-toggle">
<p>
Remember your password?
<button type="button" onClick={() => setShowForgotPassword(false)} className="toggle-button">
Login
</button>
</p>
</div>
</div>
</div>
);
}
return (
<div className="auth-container">
<div className="auth-card">
<div className="auth-header">
<div className="auth-logo">
<img src="/wraffle_logo.png" alt="Wraffle" className="auth-logo-img" />
</div>
<h2>{isLogin ? 'Login' : 'Sign Up'}</h2>
</div>
<form onSubmit={handleSubmit} className="auth-form">
{!isLogin && (
<div className="form-row">
<div className="form-group">
<label htmlFor="firstName">First Name</label>
<input
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleInputChange}
required={!isLogin}
/>
</div>
<div className="form-group">
<label htmlFor="lastName">Last Name</label>
<input
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleInputChange}
required={!isLogin}
/>
</div>
</div>
)}
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
required
/>
{isLogin && (
<div className="forgot-password-link">
<button type="button" onClick={() => setShowForgotPassword(true)} className="toggle-button">
Forgot Password?
</button>
</div>
)}
</div>
{!isLogin && (
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
required={!isLogin}
/>
</div>
)}
{error && <div className="error-message">{error}</div>}
{emailNotVerified && (
<div className="resend-verification">
<p>Your email is not verified.</p>
<button type="button" onClick={handleResendVerification} className="toggle-button" disabled={loading}>
{loading ? 'Sending...' : 'Resend Verification Link'}
</button>
</div>
)}
<button type="submit" className="auth-button" disabled={loading}>
{loading ? 'Please wait...' : (isLogin ? 'Login' : 'Sign Up')}
</button>
</form>
<div className="auth-toggle">
<p>
{isLogin ? "Don't have an account?" : 'Already have an account?'}
<button type="button" onClick={toggleMode} className="toggle-button">
{isLogin ? 'Sign Up' : 'Login'}
</button>
</p>
</div>
</div>
</div>
);
};
export default Auth;

41
src/pages/Cart.css Normal file
View File

@@ -0,0 +1,41 @@
.cart-page {
padding: 24px;
}
.cart-table {
width: 100%;
border-collapse: collapse;
}
.cart-table th, .cart-table td {
padding: 12px;
border-bottom: 1px solid #eee;
text-align: left;
}
.cart-thumb {
width: 72px;
height: 72px;
object-fit: cover;
margin-right: 12px;
}
.cart-product {
display: flex;
align-items: center;
}
.cart-summary {
margin-top: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.cart-actions button {
margin-left: 8px;
}
.cart-remove {
background: transparent;
color: #c00;
border: 1px solid #c00;
padding: 6px 10px;
cursor: pointer;
}
.cart-error { color: #c00; margin-bottom: 12px; }
.cart-empty { padding: 24px; }
.cart-loading { padding: 24px; }

121
src/pages/Cart.jsx Normal file
View File

@@ -0,0 +1,121 @@
import React, { useEffect, useState } from 'react';
import cartService from '../services/cartService';
import './Cart.css';
import { useNavigate } from 'react-router-dom';
const Cart = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const navigate = useNavigate();
const fetchCart = async () => {
try {
setLoading(true);
const data = await cartService.getCart();
setItems(data.items || []);
setError(null);
} catch (err) {
console.error('Fetch cart error', err);
if (err.response && (err.response.status === 401 || err.response.status === 403)) {
// go to login
navigate('/auth');
} else {
setError('Failed to load cart');
}
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchCart();
}, []);
const handleQuantityChange = async (itemId, qty) => {
if (qty < 0) return;
try {
await cartService.updateItem(itemId, qty);
fetchCart();
} catch (err) {
console.error('Update item error', err);
setError('Failed to update item');
}
};
const handleRemove = async (itemId) => {
if (!window.confirm('Remove this item from cart?')) return;
try {
await cartService.removeItem(itemId);
fetchCart();
} catch (err) {
console.error('Remove item error', err);
setError('Failed to remove item');
}
};
const subtotal = items.reduce((sum, it) => sum + (it.price || 0) * (it.quantity || 0), 0);
if (loading) return <div className="cart-loading">Loading cart...</div>;
return (
<div className="cart-page">
<h1>Your Cart</h1>
{error && <div className="cart-error">{error}</div>}
{items.length === 0 ? (
<div className="cart-empty">Your cart is empty.</div>
) : (
<div className="cart-list">
<table className="cart-table">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
<th>Total</th>
<th></th>
</tr>
</thead>
<tbody>
{items.map(item => (
<tr key={item.cartItemId}>
<td className="cart-product">
<img src={item.imageUrl || '/placeholder.jpg'} alt={item.name} className="cart-thumb" />
<div className="cart-prod-info">
<div className="cart-prod-name">{item.name}</div>
<div className="cart-prod-stock">{item.stockQuantity > 0 ? `${item.stockQuantity} in stock` : 'Out of stock'}</div>
</div>
</td>
<td> {item.price}</td>
<td>
<input
type="number"
min="0"
value={item.quantity}
onChange={(e) => handleQuantityChange(item.cartItemId, parseInt(e.target.value || '0', 10))}
/>
</td>
<td> {(item.price * item.quantity).toFixed(2)}</td>
<td>
<button className="cart-remove" onClick={() => handleRemove(item.cartItemId)}>Remove</button>
</td>
</tr>
))}
</tbody>
</table>
<div className="cart-summary">
<div>Subtotal: <strong> {subtotal.toFixed(2)}</strong></div>
<div className="cart-actions">
<button onClick={() => navigate('/shop')}>Continue Shopping</button>
<button onClick={() => navigate('/orders')}>Proceed to Checkout</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Cart;

190
src/pages/Contact.css Normal file
View File

@@ -0,0 +1,190 @@
.contact {
min-height: calc(100vh - 160px);
padding: 2rem 1rem;
}
.contact-header {
text-align: center;
margin-bottom: 4rem;
}
.contact-header h1 {
color: #000;
font-size: 2.5rem;
margin-bottom: 1rem;
}
.contact-header p {
color: #666;
font-size: 1.1rem;
}
.contact-content {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
}
.contact-info {
display: flex;
flex-direction: column;
gap: 3rem;
}
.info-section h2,
.social-section h2 {
color: #000;
font-size: 1.8rem;
margin-bottom: 2rem;
}
.contact-details {
display: grid;
gap: 1.5rem;
}
.contact-item h3 {
color: #FFB400;
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.contact-item p {
color: #666;
line-height: 1.5;
}
.social-links {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.social-link {
background-color: #000;
color: #FFB400;
padding: 0.75rem 1.5rem;
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s ease;
font-weight: bold;
}
.social-link:hover {
background-color: #333;
}
.contact-form {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.contact-form h2 {
color: #000;
margin-bottom: 2rem;
text-align: center;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
color: #333;
font-weight: bold;
margin-bottom: 0.5rem;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s ease;
box-sizing: border-box;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #FFB400;
}
.form-group textarea {
resize: vertical;
min-height: 120px;
}
.submit-button {
background-color: #FFB400;
color: #000;
border: none;
padding: 1rem 2rem;
border-radius: 5px;
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
width: 100%;
transition: background-color 0.3s ease;
}
.submit-button:hover:not(:disabled) {
background-color: #FFC107;
}
.submit-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.submit-message {
margin-top: 1rem;
padding: 1rem;
border-radius: 5px;
text-align: center;
font-weight: bold;
}
.submit-message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.submit-message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@media (max-width: 768px) {
.contact-content {
grid-template-columns: 1fr;
gap: 2rem;
}
.contact-header h1 {
font-size: 2rem;
}
.info-section h2,
.social-section h2,
.contact-form h2 {
font-size: 1.5rem;
}
.social-links {
justify-content: center;
}
.contact-form {
padding: 1.5rem;
}
}

151
src/pages/Contact.jsx Normal file
View File

@@ -0,0 +1,151 @@
import React, { useState } from 'react';
import './Contact.css';
const Contact = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitMessage, setSubmitMessage] = useState('');
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
// Simulate form submission
try {
// In a real app, this would send to your backend
await new Promise(resolve => setTimeout(resolve, 1000));
setSubmitMessage('Thank you for your message! We\'ll get back to you soon.');
setFormData({ name: '', email: '', subject: '', message: '' });
} catch (error) {
setSubmitMessage('Sorry, there was an error sending your message. Please try again.');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="contact">
<div className="contact-header">
<h1>Contact Us</h1>
<p>Get in touch with the Wraffle team</p>
</div>
<div className="contact-content">
<div className="contact-info">
<div className="info-section">
<h2>Get In Touch</h2>
<div className="contact-details">
<div className="contact-item">
<h3>Email</h3>
<p>support@wraffle.com</p>
</div>
<div className="contact-item">
<h3>Phone</h3>
<p>+1 (555) 123-4567</p>
</div>
<div className="contact-item">
<h3>Address</h3>
<p>123 Fashion Street<br />Style City, SC 12345</p>
</div>
<div className="contact-item">
<h3>Hours</h3>
<p>Mon-Fri: 9AM-6PM<br />Sat-Sun: 10AM-4PM</p>
</div>
</div>
</div>
<div className="social-section">
<h2>Follow Us</h2>
<div className="social-links">
<a href="#" className="social-link">Facebook</a>
<a href="#" className="social-link">Instagram</a>
<a href="#" className="social-link">Twitter</a>
<a href="#" className="social-link">TikTok</a>
</div>
</div>
</div>
<div className="contact-form">
<h2>Send us a Message</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Name *</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="email">Email *</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="subject">Subject *</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="message">Message *</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
rows="6"
required
></textarea>
</div>
<button
type="submit"
className="submit-button"
disabled={isSubmitting}
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
{submitMessage && (
<div className={`submit-message ${submitMessage.includes('error') ? 'error' : 'success'}`}>
{submitMessage}
</div>
)}
</form>
</div>
</div>
</div>
);
};
export default Contact;

234
src/pages/Home.css Normal file
View File

@@ -0,0 +1,234 @@
.home {
min-height: calc(100vh - 160px);
}
.hero {
background: linear-gradient(135deg, #000 0%, #333 100%);
color: #FFB400;
padding: 4rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 60vh;
}
.hero-content {
flex: 1;
max-width: 500px;
}
.hero h1 {
font-size: 3rem;
margin-bottom: 1rem;
color: #FFB400;
}
.hero p {
font-size: 1.2rem;
margin-bottom: 2rem;
color: #ccc;
}
.cta-button {
background-color: #FFB400;
color: #000;
padding: 1rem 2rem;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
font-size: 1.1rem;
transition: background-color 0.3s ease;
display: inline-block;
}
.cta-button:hover {
background-color: #FFC107;
}
.hero-image {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.hero-logo {
max-width: 200px;
height: auto;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
/* Featured Products */
.featured-products {
padding: 4rem 1rem;
background-color: #f9f9f9;
text-align: center;
}
.featured-products h2 {
color: #000;
font-size: 2.5rem;
margin-bottom: 3rem;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.product-card {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.product-card:hover {
transform: translateY(-5px);
}
.product-image {
height: 250px;
overflow: hidden;
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-info {
padding: 1.5rem;
}
.product-info h3 {
color: #000;
margin-bottom: 0.5rem;
font-size: 1.2rem;
}
.product-price {
color: #FFB400;
font-size: 1.3rem;
font-weight: bold;
margin-bottom: 1rem;
}
.view-product {
background-color: #000;
color: #FFB400;
padding: 0.5rem 1rem;
text-decoration: none;
border-radius: 5px;
display: inline-block;
transition: background-color 0.3s ease;
}
.view-product:hover {
background-color: #333;
}
.view-all {
margin-top: 3rem;
}
.view-all-button {
background-color: #FFB400;
color: #000;
padding: 1rem 2rem;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
display: inline-block;
transition: background-color 0.3s ease;
}
.view-all-button:hover {
background-color: #FFC107;
}
/* About Preview */
.about-preview {
padding: 4rem 1rem;
background-color: #000;
color: #FFB400;
}
.about-content {
max-width: 1200px;
margin: 0 auto;
text-align: center;
}
.about-content h2 {
font-size: 2.5rem;
margin-bottom: 3rem;
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
.feature h3 {
color: #FFB400;
margin-bottom: 1rem;
font-size: 1.3rem;
}
.feature p {
color: #ccc;
line-height: 1.6;
}
.learn-more {
background-color: #FFB400;
color: #000;
padding: 1rem 2rem;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
display: inline-block;
transition: background-color 0.3s ease;
}
.learn-more:hover {
background-color: #FFC107;
}
@media (max-width: 768px) {
.hero {
flex-direction: column;
text-align: center;
padding: 2rem 1rem;
}
.hero h1 {
font-size: 2rem;
}
.hero-logo {
max-width: 150px;
height: auto;
}
.products-grid {
grid-template-columns: 1fr;
}
.features {
grid-template-columns: 1fr;
}
}

91
src/pages/Home.jsx Normal file
View File

@@ -0,0 +1,91 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import productService from '../services/productService';
import LoadingSpinner from '../components/LoadingSpinner';
import './Home.css';
const Home = () => {
const [featuredProducts, setFeaturedProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchFeaturedProducts = async () => {
try {
const response = await productService.getProducts({ limit: 6 });
setFeaturedProducts(response.products || []);
} catch (error) {
console.error('Error fetching products:', error);
} finally {
setLoading(false);
}
};
fetchFeaturedProducts();
}, []);
if (loading) {
return <LoadingSpinner />;
}
return (
<div className="home">
{/* Hero Section */}
<section className="hero">
<div className="hero-content">
<h1>Welcome to Wraffle</h1>
<p>Premium hoodies for every style and occasion</p>
<Link to="/shop" className="cta-button">Shop Now</Link>
</div>
<div className="hero-image">
<img src="/wraffle_logo.png" alt="Wraffle Logo" className="hero-logo" />
</div>
</section>
{/* Featured Products */}
<section className="featured-products">
<h2>Featured Hoodies</h2>
<div className="products-grid">
{featuredProducts.map((product) => (
<div key={product.id} className="product-card">
<div className="product-image">
<img src={product.image_url || '/placeholder.jpg'} alt={product.name} />
</div>
<div className="product-info">
<h3>{product.name}</h3>
<p className="product-price">${product.price}</p>
<Link to={`/productdetail`} className="view-product">View Details</Link>
</div>
</div>
))}
</div>
<div className="view-all">
<Link to="/shop" className="view-all-button">View All Products</Link>
</div>
</section>
{/* About Section */}
<section className="about-preview">
<div className="about-content">
<h2>Why Choose Wraffle?</h2>
<div className="features">
<div className="feature">
<h3>Premium Quality</h3>
<p>High-quality fabrics and craftsmanship in every hoodie</p>
</div>
<div className="feature">
<h3>Unique Designs</h3>
<p>Exclusive designs that stand out from the crowd</p>
</div>
<div className="feature">
<h3>Fast Shipping</h3>
<p>Quick delivery to get your hoodie as soon as possible</p>
</div>
</div>
<Link to="/about" className="learn-more">Learn More About Us</Link>
</div>
</section>
</div>
);
};
export default Home;

View File

@@ -0,0 +1,482 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
/* Container */
.product-details-container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
padding: 60px 40px 120px;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
position: relative;
}
.product-details-wrapper {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1.5fr 1fr;
gap: 60px;
align-items: start;
}
/* Loading & Error States */
.product-details-loading,
.product-details-error {
min-height: 60vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
}
.product-details-error button {
padding: 12px 32px;
background: #d4a574;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.product-details-error button:hover {
background: #c59563;
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(212, 165, 116, 0.3);
}
/* Left Panel - Details */
.details-panel {
background: white;
padding: 40px 30px;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
position: sticky;
top: 20px;
transition: all 0.3s ease;
}
.details-panel:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
}
.details-title {
font-size: 28px;
font-weight: 700;
margin: 0 0 30px 0;
color: #1a1a1a;
letter-spacing: -0.5px;
}
.details-tabs {
display: flex;
gap: 12px;
margin-bottom: 30px;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 0;
}
.tab-button {
background: none;
border: none;
padding: 12px 0;
font-size: 15px;
font-weight: 500;
color: #666;
cursor: pointer;
position: relative;
transition: all 0.3s ease;
margin-right: 20px;
}
.tab-button:hover {
color: #1a1a1a;
}
.tab-button.active {
color: #1a1a1a;
font-weight: 600;
}
.tab-button.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: #d4a574;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
.details-content {
animation: fadeIn 0.4s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tab-content {
padding: 0;
}
.details-description {
font-size: 15px;
line-height: 1.8;
color: #444;
margin: 0;
}
.details-description strong {
color: #1a1a1a;
display: block;
margin-top: 12px;
}
/* Center Panel - Product Image */
.product-image-panel {
display: flex;
align-items: center;
justify-content: center;
}
.product-image-wrapper {
width: 100%;
max-width: 600px;
aspect-ratio: 1;
background: white;
border-radius: 24px;
padding: 40px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.4s ease;
position: relative;
overflow: hidden;
}
.product-image-wrapper::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(212, 165, 116, 0.05) 0%, rgba(212, 165, 116, 0) 50%);
opacity: 0;
transition: opacity 0.3s ease;
}
.product-image-wrapper:hover::before {
opacity: 1;
}
.product-image-wrapper:hover {
transform: scale(1.02);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
}
.product-image {
width: 100%;
height: 100%;
object-fit: contain;
transition: transform 0.6s ease;
animation: productSlideIn 0.6s ease;
}
@keyframes productSlideIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Right Panel - Product Info */
.product-info-panel {
background: white;
padding: 40px 35px;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
position: sticky;
top: 20px;
transition: all 0.3s ease;
}
.product-info-panel:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
}
.product-name {
font-size: 32px;
font-weight: 700;
margin: 0 0 20px 0;
color: #1a1a1a;
letter-spacing: -0.5px;
line-height: 1.2;
}
.product-tagline {
font-size: 14px;
line-height: 1.7;
color: #555;
margin: 0 0 30px 0;
}
.product-price {
font-size: 36px;
font-weight: 700;
color: #1a1a1a;
margin: 0 0 35px 0;
letter-spacing: -1px;
}
.size-selector {
margin-bottom: 30px;
}
.size-selector label {
display: block;
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.size-dropdown {
width: 100%;
padding: 14px 18px;
font-size: 15px;
font-weight: 500;
color: #1a1a1a;
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%231a1a1a' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 18px center;
padding-right: 45px;
}
.size-dropdown:hover {
border-color: #d4a574;
background: white;
}
.size-dropdown:focus {
outline: none;
border-color: #d4a574;
background: white;
box-shadow: 0 0 0 4px rgba(212, 165, 116, 0.1);
}
.add-to-cart-btn {
width: 100%;
padding: 18px 32px;
background: #1a1a1a;
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
position: relative;
overflow: hidden;
}
.add-to-cart-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.add-to-cart-btn:hover::before {
left: 100%;
}
.add-to-cart-btn:hover {
background: #d4a574;
transform: translateY(-2px);
box-shadow: 0 12px 24px rgba(212, 165, 116, 0.4);
}
.add-to-cart-btn:active {
transform: translateY(0);
}
/* Navigation Arrows */
.navigation-arrows {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px;
z-index: 10;
}
.arrow-button {
width: 56px;
height: 56px;
background: white;
border: 2px solid #e9ecef;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
color: #666;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.arrow-button:hover {
background: #1a1a1a;
border-color: #1a1a1a;
color: white;
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
.arrow-button:active {
transform: translateY(-2px);
}
.arrow-button svg {
width: 24px;
height: 24px;
}
/* Responsive Design */
@media (max-width: 1200px) {
.product-details-wrapper {
grid-template-columns: 1fr;
gap: 40px;
}
.details-panel,
.product-info-panel {
position: static;
}
.product-image-wrapper {
max-width: 500px;
margin: 0 auto;
}
}
@media (max-width: 768px) {
.product-details-container {
padding: 30px 20px 100px;
}
.product-details-wrapper {
gap: 30px;
}
.details-panel,
.product-info-panel {
padding: 30px 20px;
}
.details-title {
font-size: 24px;
}
.product-name {
font-size: 26px;
}
.product-price {
font-size: 28px;
}
.product-image-wrapper {
padding: 20px;
max-width: 100%;
}
.navigation-arrows {
bottom: 30px;
}
.arrow-button {
width: 48px;
height: 48px;
}
.details-tabs {
flex-wrap: wrap;
}
.tab-button {
margin-right: 15px;
font-size: 14px;
}
}
@media (max-width: 480px) {
.product-details-container {
padding: 20px 15px 80px;
}
.details-panel,
.product-info-panel {
padding: 25px 18px;
border-radius: 12px;
}
.product-name {
font-size: 22px;
}
.product-price {
font-size: 24px;
}
.details-description {
font-size: 14px;
}
.add-to-cart-btn {
padding: 16px 28px;
font-size: 15px;
}
.arrow-button {
width: 44px;
height: 44px;
}
}

View File

@@ -0,0 +1,228 @@
import React, { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import productService from '../services/productService';
import cartService from '../services/cartService';
import './ProductDetails.css';
const ProductDetails = () => {
const location = useLocation();
const selectedProduct = location.state?.selectedProduct; // Product passed from Shop page
const [products, setProducts] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState('details');
const [selectedSize, setSelectedSize] = useState('Large');
useEffect(() => {
fetchProducts();
}, []);
useEffect(() => {
// If a product was selected from Shop page, find it and set as current
if (selectedProduct && products.length > 0) {
const productIndex = products.findIndex(p => p.id === selectedProduct.id);
if (productIndex !== -1) {
setCurrentIndex(productIndex);
}
}
}, [selectedProduct, products]);
const fetchProducts = async () => {
try {
setLoading(true);
const data = await productService.getProducts();
setProducts(data.products || []);
setLoading(false);
} catch (err) {
console.error('Error fetching products:', err);
setError('Failed to load products. Please try again later.');
setLoading(false);
}
};
const currentProduct = products[currentIndex];
const handlePrevious = () => {
setCurrentIndex((prev) => (prev > 0 ? prev - 1 : products.length - 1));
};
const handleNext = () => {
setCurrentIndex((prev) => (prev < products.length - 1 ? prev + 1 : 0));
};
const navigate = useNavigate();
const handleAddToCart = async () => {
if (!currentProduct) return;
try {
await cartService.addToCart(currentProduct.id, 1);
alert(`Added ${currentProduct.name} to cart.`);
} catch (err) {
// If unauthorized, redirect to login
if (err.response && (err.response.status === 401 || err.response.status === 403)) {
alert('Please login to add items to your cart.');
navigate('/auth');
} else if (err.response && err.response.data && err.response.data.message) {
alert(err.response.data.message);
} else {
console.error('Add to cart error', err);
alert('Failed to add to cart. Please try again later.');
}
}
};
if (loading) {
return (
<div className="product-details-loading">
<p>Loading products...</p>
</div>
);
}
if (error) {
return (
<div className="product-details-error">
<p>{error}</p>
<button onClick={fetchProducts}>Retry</button>
</div>
);
}
if (!currentProduct) {
return (
<div className="product-details-error">
<p>No products available</p>
</div>
);
}
return (
<div className="product-details-container">
<div className="product-details-wrapper">
{/* Left Panel - Details */}
<div className="details-panel">
<h2 className="details-title">Details</h2>
<div className="details-tabs">
<button
className={`tab-button ${activeTab === 'details' ? 'active' : ''}`}
onClick={() => setActiveTab('details')}
>
{currentProduct.category || 'Product'}
</button>
<button
className={`tab-button ${activeTab === 'sale' ? 'active' : ''}`}
onClick={() => setActiveTab('sale')}
>
Sale
</button>
<button
className={`tab-button ${activeTab === 'faq' ? 'active' : ''}`}
onClick={() => setActiveTab('faq')}
>
FAQ
</button>
</div>
<div className="details-content">
{activeTab === 'details' && (
<div className="tab-content">
<p className="details-description">
{currentProduct.description || `Meet the ${currentProduct.name} where clean style meets unmatched comfort. Built from ultra-premium fabric, this product delivers a soft, relaxed feel that keeps you cozy while looking effortlessly sharp. Its crisp finish gives it a refined edge, and the upcoming fresh design details make it a true standout piece.`}
</p>
</div>
)}
{activeTab === 'sale' && (
<div className="tab-content">
<p className="details-description">
Special offer! Get exclusive discounts on this amazing product. Limited time only. Don't miss out on this incredible deal!
</p>
</div>
)}
{activeTab === 'faq' && (
<div className="tab-content">
<p className="details-description">
<strong>Q: What materials is this made from?</strong><br/>
A: Premium quality materials for maximum comfort and durability.<br/><br/>
<strong>Q: How do I care for this product?</strong><br/>
A: Machine wash cold, tumble dry low. Iron on low heat if needed.
</p>
</div>
)}
</div>
</div>
{/* Center Panel - Product Image */}
<div className="product-image-panel">
<div className="product-image-wrapper">
<img
src={currentProduct.image || currentProduct.image_url || currentProduct.imageUrl || 'https://via.placeholder.com/500x600?text=Product+Image'}
alt={currentProduct.name}
className="product-image"
/>
</div>
</div>
{/* Right Panel - Product Info */}
<div className="product-info-panel">
<h1 className="product-name">{currentProduct.name}</h1>
<p className="product-tagline">
{currentProduct.tagline || `Whether you're dressing it up, keeping it casual, or aiming for that street-ready vibe, the ${currentProduct.name} is your new go-to. Elevate your wardrobe with a product that blends style effortlessly.`}
</p>
<div className="product-price">
{currentProduct.price ? currentProduct.price.toLocaleString() : 'XXX'}
</div>
<div className="size-selector">
<label htmlFor="size">Size</label>
<select
id="size"
value={selectedSize}
onChange={(e) => setSelectedSize(e.target.value)}
className="size-dropdown"
>
<option value="Small">Small</option>
<option value="Medium">Medium</option>
<option value="Large">Large</option>
<option value="X-Large">X-Large</option>
<option value="XX-Large">XX-Large</option>
</select>
</div>
<button className="add-to-cart-btn" onClick={handleAddToCart}>
Add to Cart
</button>
</div>
</div>
{/* Navigation Arrows */}
<div className="navigation-arrows">
<button
className="arrow-button arrow-left"
onClick={handlePrevious}
aria-label="Previous product"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M15 18L9 12L15 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
<button
className="arrow-button arrow-right"
onClick={handleNext}
aria-label="Next product"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M9 18L15 12L9 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
</div>
</div>
);
};
export default ProductDetails;

100
src/pages/ResetPassword.jsx Normal file
View File

@@ -0,0 +1,100 @@
import React, { useState } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import authService from '../services/authService';
import './Auth.css';
const ResetPassword = () => {
const [formData, setFormData] = useState({
password: '',
confirmPassword: ''
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const token = searchParams.get('token');
const handleInputChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
setLoading(false);
return;
}
if (!token) {
setError('Invalid or missing reset token.');
setLoading(false);
return;
}
try {
await authService.resetPassword({ token, password: formData.password });
setError('Password has been reset successfully. You can now login with your new password.');
setTimeout(() => {
navigate('/auth');
}, 3000);
} catch (err) {
setError(err.response?.data?.message || 'An error occurred.');
} finally {
setLoading(false);
}
};
return (
<div className="auth-container">
<div className="auth-card">
<div className="auth-header">
<div className="auth-logo">
<img src="/wraffle_logo.png" alt="Wraffle" className="auth-logo-img" />
</div>
<h2>Reset Password</h2>
</div>
<form onSubmit={handleSubmit} className="auth-form">
<div className="form-group">
<label htmlFor="password">New Password</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm New Password</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
required
/>
</div>
{error && <div className="error-message">{error}</div>}
<button type="submit" className="auth-button" disabled={loading}>
{loading ? 'Resetting...' : 'Reset Password'}
</button>
</form>
</div>
</div>
);
};
export default ResetPassword;

240
src/pages/Shop.css Normal file
View File

@@ -0,0 +1,240 @@
.shop {
padding: 2rem 1rem;
min-height: calc(100vh - 160px);
}
.shop-header {
text-align: center;
margin-bottom: 3rem;
}
.shop-header h1 {
color: #000;
font-size: 2.5rem;
margin-bottom: 1rem;
}
.shop-header p {
color: #666;
font-size: 1.1rem;
}
.shop-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
.search-bar {
flex: 1;
max-width: 400px;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: #FFB400;
}
.category-filter {
min-width: 200px;
}
.category-select {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 1rem;
background-color: white;
cursor: pointer;
transition: border-color 0.3s ease;
}
.category-select:focus {
outline: none;
border-color: #FFB400;
}
.products-section {
max-width: 1200px;
margin: 0 auto;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
.product-card {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
display: flex;
flex-direction: column;
max-height: fit-content;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
.product-image {
height: 200px;
overflow: hidden;
position: relative;
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.out-of-stock {
position: absolute;
top: 10px;
right: 10px;
background-color: #ff4444;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 3px;
font-size: 0.8rem;
font-weight: bold;
}
.product-info {
padding: 1.5rem;
}
.product-info h3 {
color: #000;
margin-bottom: 0.5rem;
font-size: 1.2rem;
}
.product-description {
color: #666;
font-size: 0.9rem;
margin-bottom: 1rem;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.product-price {
color: #FFB400;
font-size: 1.3rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.product-stock {
color: #888;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.view-product {
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
color: white;
padding: 0.875rem 1.75rem;
text-decoration: none;
border-radius: 12px;
display: inline-block;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: 600;
font-size: 0.95rem;
text-transform: uppercase;
letter-spacing: 0.5px;
position: relative;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.view-product::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
transition: left 0.6s ease;
}
.view-product:hover:not(.disabled)::before {
left: 100%;
}
.view-product:hover:not(.disabled) {
background: linear-gradient(135deg, #d4a574 0%, #e6b684 100%);
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(212, 165, 116, 0.4);
}
.view-product:active:not(.disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(212, 165, 116, 0.3);
}
.view-product.disabled {
background: linear-gradient(135deg, #e0e0e0 0%, #cccccc 100%);
color: #888;
cursor: not-allowed;
box-shadow: none;
}
.no-products {
text-align: center;
padding: 4rem 2rem;
}
.no-products h3 {
color: #666;
font-size: 1.5rem;
margin-bottom: 1rem;
}
.no-products p {
color: #999;
font-size: 1.1rem;
}
@media (max-width: 768px) {
.shop-controls {
flex-direction: column;
align-items: stretch;
}
.search-bar,
.category-filter {
max-width: none;
}
.products-grid {
grid-template-columns: 1fr;
}
.shop-header h1 {
font-size: 2rem;
}
}

120
src/pages/Shop.jsx Normal file
View File

@@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import productService from '../services/productService';
import LoadingSpinner from '../components/LoadingSpinner';
import './Shop.css';
const Shop = () => {
const [products, setProducts] = useState([]);
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedCategory, setSelectedCategory] = useState('');
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
const fetchProductsAndCategories = async () => {
try {
const [productsResponse, categoriesResponse] = await Promise.all([
productService.getProducts(),
productService.getCategories()
]);
setProducts(productsResponse.products || []);
setCategories(categoriesResponse.categories || []);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};
fetchProductsAndCategories();
}, []);
const filteredProducts = products.filter(product => {
const matchesCategory = !selectedCategory || product.category === selectedCategory;
const matchesSearch = !searchTerm ||
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description.toLowerCase().includes(searchTerm.toLowerCase());
return matchesCategory && matchesSearch;
});
if (loading) {
return <LoadingSpinner />;
}
return (
<div className="shop">
<div className="shop-header">
<h1>Shop Hoodies</h1>
<p>Find your perfect hoodie from our collection</p>
</div>
<div className="shop-controls">
<div className="search-bar">
<input
type="text"
placeholder="Search hoodies..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
<div className="category-filter">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="category-select"
>
<option value="">All Categories</option>
{categories.map(category => (
<option key={category} value={category}>{category}</option>
))}
</select>
</div>
</div>
<div className="products-section">
{filteredProducts.length === 0 ? (
<div className="no-products">
<h3>No products found</h3>
<p>Try adjusting your search or filter criteria</p>
</div>
) : (
<div className="products-grid">
{filteredProducts.map((product) => (
<div key={product.id} className="product-card">
<div className="product-image">
<img src={product.image_url || '/placeholder.jpg'} alt={product.name} />
{product.stock_quantity === 0 && (
<div className="out-of-stock">Out of Stock</div>
)}
</div>
<div className="product-info">
<h3>{product.name}</h3>
<p className="product-description">{product.description}</p>
<p className="product-price">${product.price}</p>
<p className="product-stock">
{product.stock_quantity > 0
? `${product.stock_quantity} in stock`
: 'Out of stock'
}
</p>
<Link
to="/productdetail"
state={{ selectedProduct: product }}
className={`view-product ${product.stock_quantity === 0 ? 'disabled' : ''}`}
>
View Product
</Link>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default Shop;

146
src/pages/VerifyEmail.css Normal file
View File

@@ -0,0 +1,146 @@
.verify-email-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #000000 0%, #333333 100%);
padding: 20px;
}
.verify-email-card {
background: #000000;
border: 2px solid #FFB400;
border-radius: 15px;
padding: 40px;
text-align: center;
max-width: 500px;
width: 100%;
box-shadow: 0 10px 30px rgba(255, 215, 0, 0.3);
}
.verify-email-card h2 {
color: #FFB400;
margin-bottom: 20px;
font-size: 2rem;
font-weight: bold;
}
.verify-email-card p {
color: #ffffff;
margin-bottom: 20px;
font-size: 1.1rem;
line-height: 1.5;
}
.redirect-message {
color: #FFB400 !important;
font-weight: bold;
margin-bottom: 30px !important;
}
/* Spinner */
.spinner {
width: 50px;
height: 50px;
border: 4px solid #333333;
border-top: 4px solid #FFB400;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Success styles */
.success-icon {
width: 80px;
height: 80px;
background: #FFB400;
color: #000000;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: bold;
margin: 0 auto 20px;
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.4);
}
.success h2 {
color: #FFB400 !important;
}
/* Error styles */
.error-icon {
width: 80px;
height: 80px;
background: #ff4444;
color: #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: bold;
margin: 0 auto 20px;
box-shadow: 0 5px 15px rgba(255, 68, 68, 0.4);
}
.error h2 {
color: #ff4444 !important;
}
/* Buttons */
.close-btn, .retry-btn {
background: #FFB400;
color: #000000;
border: none;
padding: 12px 30px;
border-radius: 8px;
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.close-btn:hover, .retry-btn:hover {
background: #ffed4e;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.4);
}
.retry-btn {
background: #ff4444;
color: #ffffff;
}
.retry-btn:hover {
background: #ff6666;
box-shadow: 0 5px 15px rgba(255, 102, 102, 0.4);
}
/* Responsive design */
@media (max-width: 768px) {
.verify-email-card {
padding: 30px 20px;
margin: 20px;
}
.verify-email-card h2 {
font-size: 1.5rem;
}
.verify-email-card p {
font-size: 1rem;
}
.close-btn, .retry-btn {
padding: 10px 25px;
font-size: 1rem;
}
}

89
src/pages/VerifyEmail.jsx Normal file
View File

@@ -0,0 +1,89 @@
import React, { useEffect, useState } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import axios from 'axios';
import API_BASE_URL from '../config/api';
import './VerifyEmail.css';
const VerifyEmail = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [status, setStatus] = useState('verifying');
const [message, setMessage] = useState('');
useEffect(() => {
// Auto-close after 10 seconds from page open
const closeTimer = setTimeout(() => {
navigate('/');
}, 10000);
const verifyEmail = async () => {
const token = searchParams.get('token');
if (!token) {
setStatus('error');
setMessage('No verification token provided');
return;
}
try {
const response = await axios.get(`${API_BASE_URL}/auth/verify-email?token=${token}`);
setStatus('success');
setMessage('Email verified successfully!');
// Redirect to success page after verification
navigate('/verify-email-success');
} catch (error) {
setStatus('error');
setMessage(error.response?.data?.message || 'Verification failed');
}
};
verifyEmail();
// Cleanup timer on unmount
return () => clearTimeout(closeTimer);
}, [searchParams, navigate]);
return (
<div className="verify-email-container">
<div className="verify-email-card">
{status === 'verifying' && (
<div className="verifying">
<div className="spinner"></div>
<h2>Verifying your email...</h2>
</div>
)}
{status === 'success' && (
<div className="success">
<div className="success-icon"></div>
<h2>Email Verified!</h2>
<p>{message}</p>
<p className="redirect-message">Redirecting to home page in 10 seconds...</p>
<button
className="close-btn"
onClick={() => navigate('/')}
>
Go to Home Now
</button>
</div>
)}
{status === 'error' && (
<div className="error">
<div className="error-icon"></div>
<h2>Verification Failed</h2>
<p>{message}</p>
<button
className="retry-btn"
onClick={() => navigate('/login')}
>
Back to Login
</button>
</div>
)}
</div>
</div>
);
};
export default VerifyEmail;

View File

@@ -0,0 +1,96 @@
.verify-success-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #000000 0%, #333333 100%);
padding: 20px;
}
.verify-success-card {
background: #000000;
border: 2px solid #FFB400;
border-radius: 15px;
padding: 40px;
text-align: center;
max-width: 500px;
width: 100%;
box-shadow: 0 10px 30px rgba(255, 215, 0, 0.3);
}
.verify-success-card h2 {
color: #FFB400;
margin-bottom: 20px;
font-size: 2rem;
font-weight: bold;
}
.verify-success-card p {
color: #ffffff;
margin-bottom: 20px;
font-size: 1.1rem;
line-height: 1.5;
}
.redirect-message {
color: #FFB400 !important;
font-weight: bold;
margin-bottom: 30px !important;
}
/* Success icon */
.success-icon {
width: 80px;
height: 80px;
background: #FFB400;
color: #000000;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: bold;
margin: 0 auto 20px;
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.4);
}
/* Button */
.home-btn {
background: #FFB400;
color: #000000;
border: none;
padding: 12px 30px;
border-radius: 8px;
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.home-btn:hover {
background: #ffed4e;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.4);
}
/* Responsive design */
@media (max-width: 768px) {
.verify-success-card {
padding: 30px 20px;
margin: 20px;
}
.verify-success-card h2 {
font-size: 1.5rem;
}
.verify-success-card p {
font-size: 1rem;
}
.home-btn {
padding: 10px 25px;
font-size: 1rem;
}
}

View File

@@ -0,0 +1,36 @@
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import './VerifyEmailSuccess.css';
const VerifyEmailSuccess = () => {
const navigate = useNavigate();
useEffect(() => {
// Auto-close after 10 seconds
const timer = setTimeout(() => {
navigate('/');
}, 10000);
// Cleanup timer on unmount
return () => clearTimeout(timer);
}, [navigate]);
return (
<div className="verify-success-container">
<div className="verify-success-card">
<div className="success-icon"></div>
<h2>Email Verified Successfully!</h2>
<p>Your email has been verified. You can now access all features of your account.</p>
<p className="redirect-message">Redirecting to home page in 10 seconds...</p>
<button
className="home-btn"
onClick={() => navigate('/')}
>
Go to Home Now
</button>
</div>
</div>
);
};
export default VerifyEmailSuccess;

View File

@@ -0,0 +1,105 @@
import axios from 'axios';
import API_BASE_URL from '../config/api';
class AuthService {
constructor() {
this.api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json'
},
withCredentials: true // Enable sending cookies with requests
});
// Handle 403 responses (invalid token) by logging out
this.api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 403) {
this.logout();
window.location.href = '/auth'; // Assuming there's an auth page
}
return Promise.reject(error);
}
);
}
async register(userData) {
const response = await this.api.post('/auth/register', userData);
return response.data;
}
async login(credentials) {
const response = await this.api.post('/auth/login', credentials);
// Token is now stored in httpOnly cookie by backend
// We rely on httpOnly cookie for authentication. Return response data for UI.
return response.data;
}
// Set a token in memory (Authorization header) for the current session only.
// This does NOT persist the token to localStorage and is safe for short-lived
// development convenience. The server-side httpOnly cookie remains primary.
setToken(token) {
if (token) {
this.api.defaults.headers['Authorization'] = `Bearer ${token}`;
}
}
clearToken() {
delete this.api.defaults.headers['Authorization'];
}
async verifyEmail(token) {
const response = await this.api.get(`/auth/verify-email?token=${token}`);
return response.data;
}
async getProfile() {
const response = await this.api.get('/auth/profile');
return response.data;
}
async logout() {
try {
await this.api.post('/auth/logout');
// Cookies are cleared by backend; no local storage to clear since we avoid storing tokens in dev.
} catch (error) {
console.error('Logout API call failed:', error);
}
}
async resendVerificationEmail(email) {
const response = await this.api.post('/auth/resend-verification', email);
return response.data;
}
async forgotPassword(email) {
const response = await this.api.post('/auth/forgot-password', { email });
return response.data;
}
async resetPassword(data) {
const response = await this.api.post('/auth/reset-password', data);
return response.data;
}
getToken() {
// Token is now stored in httpOnly cookie, not accessible from JavaScript
// We'll check authentication status via a server call or maintain a flag
return null; // Cookies are not accessible from client-side JavaScript
}
async isAuthenticated() {
try {
// Check authentication by making a request to a protected endpoint
await this.api.get('/auth/profile');
return true;
} catch (error) {
return false;
}
}
}
export default new AuthService();

View File

@@ -0,0 +1,34 @@
import axios from 'axios';
import API_BASE_URL from '../config/api';
class CartService {
constructor() {
this.api = axios.create({
baseURL: API_BASE_URL,
headers: { 'Content-Type': 'application/json' },
withCredentials: true
});
}
async getCart() {
const res = await this.api.get('/cart');
return res.data;
}
async addToCart(productId, quantity = 1) {
const res = await this.api.post('/cart/add', { productId, quantity });
return res.data;
}
async updateItem(itemId, quantity) {
const res = await this.api.put(`/cart/item/${itemId}`, { quantity });
return res.data;
}
async removeItem(itemId) {
const res = await this.api.put(`/cart/item/${itemId}`, { quantity: 0 });
return res.data;
}
}
export default new CartService();

View File

@@ -0,0 +1,43 @@
import axios from 'axios';
import API_BASE_URL from '../config/api';
class OrderService {
constructor() {
this.api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json'
}
});
// Add withCredentials to send cookies with requests
this.api.defaults.withCredentials = true;
}
async getOrders() {
const response = await this.api.get('/orders');
return response.data;
}
async getOrder(id) {
const response = await this.api.get(`/orders/${id}`);
return response.data;
}
async createOrder(orderData) {
const response = await this.api.post('/orders', orderData);
return response.data;
}
async updateOrderStatus(id, statusData) {
const response = await this.api.put(`/orders/${id}/status`, statusData);
return response.data;
}
async getAllOrders(params = {}) {
const response = await this.api.get('/orders/admin/all', { params });
return response.data;
}
}
export default new OrderService();

View File

@@ -0,0 +1,89 @@
// src/services/productService.jsx
import axios from 'axios';
import API_BASE_URL from '../config/api';
class ProductService {
constructor() {
this.token = null; // in-memory token
this.api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
// withCredentials: true // keep commented unless you use cookie auth
});
// Interceptor uses this.token (in-memory)
this.api.interceptors.request.use((config) => {
const t = this.token;
if (t) {
// ensure token is clean string
const token = String(t).replace(/(^"|"$)/g, '').trim().replace(/[\r\n\t]+/g, '');
config.headers = { ...config.headers, Authorization: `Bearer ${token}` };
}
return config;
}, (err) => Promise.reject(err));
}
// call this after login or when token becomes available
setToken(token) {
this.token = token;
}
// optional: allow clearing token (logout)
clearToken() {
this.token = null;
}
async getProducts(params = {}) {
const response = await this.api.get('/products', { params });
return response.data;
}
async getProduct(id) {
const response = await this.api.get(`/products/${id}`);
return response.data;
}
async getCategories() {
const response = await this.api.get('/products/categories/all');
return response.data;
}
// Note: backend column names use snake_case (see DB). Map fields accordingly.
async addProduct(productData) {
// map camelCase keys to DB expected snake_case if needed
const payload = this._mapClientToServer(productData);
const response = await this.api.post('/products', payload);
return response.data;
}
async updateProduct(id, productData) {
const payload = this._mapClientToServer(productData);
const response = await this.api.put(`/products/${id}`, payload);
return response.data;
}
async deleteProduct(id) {
const response = await this.api.delete(`/products/${id}`);
return response.data;
}
// helper to map client camelCase -> server snake_case (adjust as necessary)
_mapClientToServer(data = {}) {
const mapped = {};
if (data.name !== undefined) mapped.name = data.name;
if (data.description !== undefined) mapped.description = data.description;
if (data.price !== undefined) mapped.price = data.price;
if (data.imageUrl !== undefined) mapped.image_url = data.imageUrl;
if (data.imageUrl1 !== undefined) mapped.image_url1 = data.imageUrl1;
if (data.stockQuantity !== undefined) mapped.stock_quantity = data.stockQuantity;
if (data.category !== undefined) mapped.category = data.category;
if (data.details !== undefined) mapped.details = data.details;
if (data.sizeAvailable !== undefined) mapped.size_available = data.sizeAvailable;
return mapped;
}
}
export default new ProductService();