Work
This commit is contained in:
parent
01cdb9a8c6
commit
6630045f67
@ -163,7 +163,7 @@ FRONTEND_URL=http://localhost:5173
|
||||
|
||||
```
|
||||
VITE_API_URL=http://localhost:8000/api
|
||||
VITE_APP_NAME=StyleHub
|
||||
VITE_APP_NAME=Brand Master
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
|
||||
@ -129,7 +129,7 @@ cp .env.example .env
|
||||
Edit `.env`:
|
||||
```
|
||||
VITE_API_URL=http://localhost:8000/api
|
||||
VITE_APP_NAME=StyleHub
|
||||
VITE_APP_NAME=Brand Master
|
||||
```
|
||||
|
||||
### 3. Run Development Server
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -1,6 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from app.database.database import get_db
|
||||
from app.models import User
|
||||
from app.schemas.user import UserCreate, UserResponse
|
||||
@ -15,6 +16,15 @@ from app.config import settings
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
|
||||
class ForgotPasswordRequest(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
|
||||
class ResetPasswordRequest(BaseModel):
|
||||
token: str
|
||||
new_password: str
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
def register(user: UserCreate, db: Session = Depends(get_db)):
|
||||
db_user = db.query(User).filter(User.email == user.email).first()
|
||||
@ -66,3 +76,57 @@ def verify_token_endpoint(token: str):
|
||||
detail="Invalid token",
|
||||
)
|
||||
return {"user_id": user_id, "valid": True}
|
||||
|
||||
|
||||
@router.post("/forgot-password")
|
||||
def forgot_password(request: ForgotPasswordRequest, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Initiate password reset process.
|
||||
In a production app, this would send an email with a reset link.
|
||||
For now, we'll generate a reset token that can be used.
|
||||
"""
|
||||
user = db.query(User).filter(User.email == request.email).first()
|
||||
if not user:
|
||||
# Don't reveal if email exists or not for security
|
||||
return {"message": "If the email exists, a reset link has been sent"}
|
||||
|
||||
# Create a password reset token (valid for 1 hour)
|
||||
reset_token = create_access_token(
|
||||
data={"sub": str(user.id), "type": "password_reset"},
|
||||
expires_delta=timedelta(hours=1)
|
||||
)
|
||||
|
||||
# In production, send this token via email
|
||||
# For development, we'll just return it
|
||||
print(f"Password reset token for {request.email}: {reset_token}")
|
||||
|
||||
return {
|
||||
"message": "If the email exists, a reset link has been sent",
|
||||
"reset_token": reset_token # Remove this in production
|
||||
}
|
||||
|
||||
|
||||
@router.post("/reset-password")
|
||||
def reset_password(request: ResetPasswordRequest, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Reset password using the token from forgot-password endpoint.
|
||||
"""
|
||||
user_id = verify_token(request.token)
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid or expired reset token",
|
||||
)
|
||||
|
||||
user = db.query(User).filter(User.id == int(user_id)).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
# Update password
|
||||
user.hashed_password = get_password_hash(request.new_password)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Password reset successful"}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
from app.database.database import get_db
|
||||
@ -38,7 +38,7 @@ def list_products(
|
||||
limit: int = 20,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
query = db.query(Product)
|
||||
query = db.query(Product).options(joinedload(Product.model))
|
||||
|
||||
if category_id:
|
||||
query = query.filter(Product.category_id == category_id)
|
||||
@ -57,12 +57,26 @@ def list_products(
|
||||
if featured is not None:
|
||||
query = query.filter(Product.is_featured == featured)
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
products = query.offset(skip).limit(limit).all()
|
||||
|
||||
# Inherit sizes from model if product doesn't have sizes
|
||||
for product in products:
|
||||
if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model:
|
||||
product.sizes = product.model.sizes or []
|
||||
|
||||
return products
|
||||
|
||||
|
||||
@router.get("/search", response_model=List[ProductResponse])
|
||||
def search(q: str, skip: int = 0, limit: int = 20, db: Session = Depends(get_db)):
|
||||
return search_products(db, q, skip=skip, limit=limit)
|
||||
products = search_products(db, q, skip=skip, limit=limit)
|
||||
|
||||
# Inherit sizes from model if product doesn't have sizes
|
||||
for product in products:
|
||||
if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model:
|
||||
product.sizes = product.model.sizes or []
|
||||
|
||||
return products
|
||||
|
||||
|
||||
@router.get("/{product_id}", response_model=ProductResponse)
|
||||
@ -70,6 +84,11 @@ def get_product(product_id: int, db: Session = Depends(get_db)):
|
||||
product = get_product_by_id(db, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
# If product doesn't have sizes but has a model, inherit sizes from model
|
||||
if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model:
|
||||
product.sizes = product.model.sizes or []
|
||||
|
||||
return product
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from app.models import Product, Category
|
||||
from app.schemas.product import ProductCreate, ProductUpdate
|
||||
from typing import List, Optional
|
||||
@ -13,7 +13,7 @@ def get_products(
|
||||
skip: int = 0,
|
||||
limit: int = 10,
|
||||
) -> List[Product]:
|
||||
query = db.query(Product)
|
||||
query = db.query(Product).options(joinedload(Product.model))
|
||||
|
||||
if category_id:
|
||||
query = query.filter(Product.category_id == category_id)
|
||||
@ -28,7 +28,7 @@ def get_products(
|
||||
|
||||
|
||||
def get_product_by_id(db: Session, product_id: int) -> Optional[Product]:
|
||||
return db.query(Product).filter(Product.id == product_id).first()
|
||||
return db.query(Product).options(joinedload(Product.model)).filter(Product.id == product_id).first()
|
||||
|
||||
|
||||
def create_product(db: Session, product: ProductCreate) -> Product:
|
||||
@ -67,6 +67,7 @@ def delete_product(db: Session, product_id: int) -> bool:
|
||||
def search_products(db: Session, query: str, skip: int = 0, limit: int = 10) -> List[Product]:
|
||||
return (
|
||||
db.query(Product)
|
||||
.options(joinedload(Product.model))
|
||||
.filter(
|
||||
Product.name.ilike(f"%{query}%") | Product.brand.ilike(f"%{query}%")
|
||||
)
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
VITE_API_URL=http://localhost:8000/api
|
||||
VITE_APP_NAME=StyleHub
|
||||
VITE_APP_NAME=Brand Master
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>StyleHub - Fashion & Shoe Store</title>
|
||||
<title>Brand Master - Fashion & Shoe Store</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
36
frontend/src/components/AddToCartModal.jsx
Normal file
36
frontend/src/components/AddToCartModal.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function AddToCartModal({ isOpen, onClose, productName }) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const handleGoToCart = () => {
|
||||
navigate('/cart')
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleContinueShopping = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-content cart-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="cart-modal-icon">✓</div>
|
||||
<h2>Added to Cart!</h2>
|
||||
<p className="cart-modal-message">{productName} has been added to your cart</p>
|
||||
<div className="cart-modal-actions">
|
||||
<button className="btn btn-secondary" onClick={handleContinueShopping}>
|
||||
Continue Shopping
|
||||
</button>
|
||||
<button className="btn" onClick={handleGoToCart}>
|
||||
Go to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -7,13 +7,14 @@ export default function Footer() {
|
||||
<footer className="footer">
|
||||
<div className="footer-container">
|
||||
<div className="footer-section">
|
||||
<h3>StyleHub</h3>
|
||||
<div className="footer-brand">
|
||||
<span className="footer-logo">👟</span>
|
||||
<h3>Brand Master</h3>
|
||||
</div>
|
||||
<p>Your ultimate destination for fashion and footwear.</p>
|
||||
<div className="social-links">
|
||||
<a href="#" title="Facebook">f</a>
|
||||
<a href="#" title="Twitter">𝕏</a>
|
||||
<a href="#" title="Instagram">📷</a>
|
||||
<a href="#" title="LinkedIn">in</a>
|
||||
<a href="https://instagram.com" target="_blank" rel="noopener noreferrer" title="Instagram">📷</a>
|
||||
<a href="https://wa.me/972532441361" target="_blank" rel="noopener noreferrer" title="WhatsApp">💬</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -40,14 +41,13 @@ export default function Footer() {
|
||||
|
||||
<div className="footer-section">
|
||||
<h4>Contact</h4>
|
||||
<p>Email: info@stylehub.com</p>
|
||||
<p>Phone: +1 (555) 123-4567</p>
|
||||
<p>Address: 123 Fashion St, NY 10001</p>
|
||||
<p>Email: info@brandmaster.com</p>
|
||||
<p>Phone: +972 53-244-1361</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="footer-bottom">
|
||||
<p>© 2024 StyleHub. All rights reserved.</p>
|
||||
<p>© 2026 Brand Master. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
|
||||
22
frontend/src/components/Modal.jsx
Normal file
22
frontend/src/components/Modal.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function Modal({ isOpen, onClose, children, title }) {
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
{title && (
|
||||
<div className="modal-header">
|
||||
<h2>{title}</h2>
|
||||
<button className="modal-close" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="modal-body">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import { CartContext } from '../context/CartContext'
|
||||
import SearchBar from './SearchBar'
|
||||
@ -9,17 +9,22 @@ export default function Navbar() {
|
||||
const { user, token, logout } = useContext(AuthContext)
|
||||
const { cart } = useContext(CartContext)
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
const isActive = (path) => {
|
||||
return location.pathname === path ? 'active' : ''
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="navbar">
|
||||
<div className="navbar-container">
|
||||
<Link to="/" className="navbar-logo">
|
||||
<span className="logo-icon">👟</span> StyleHub
|
||||
<span className="logo-icon">👟</span> Brand Master
|
||||
</Link>
|
||||
|
||||
<div className="navbar-center">
|
||||
@ -27,13 +32,13 @@ export default function Navbar() {
|
||||
</div>
|
||||
|
||||
<ul className="navbar-menu">
|
||||
<li><Link to="/">Home</Link></li>
|
||||
<li><Link to="/products">Shop</Link></li>
|
||||
<li><Link to="/sales">Sales</Link></li>
|
||||
<li><Link to="/about">About</Link></li>
|
||||
<li><Link to="/contact">Contact</Link></li>
|
||||
<li><Link to="/" className={isActive('/')}>Home</Link></li>
|
||||
<li><Link to="/products" className={isActive('/products')}>Shop</Link></li>
|
||||
<li><Link to="/sales" className={isActive('/sales')}>Sales</Link></li>
|
||||
<li><Link to="/about" className={isActive('/about')}>About</Link></li>
|
||||
<li><Link to="/contact" className={isActive('/contact')}>Contact</Link></li>
|
||||
{user?.is_admin && (
|
||||
<li><Link to="/admin" style={{ color: '#ff6b6b', fontWeight: 'bold' }}>Admin</Link></li>
|
||||
<li><Link to="/admin" className={`admin-link ${isActive('/admin')}`}>Admin</Link></li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
|
||||
25
frontend/src/components/Toast.jsx
Normal file
25
frontend/src/components/Toast.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function Toast({ message, type = 'success', onClose, duration = 3000 }) {
|
||||
useEffect(() => {
|
||||
if (duration > 0) {
|
||||
const timer = setTimeout(onClose, duration)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [duration, onClose])
|
||||
|
||||
return (
|
||||
<div className={`toast toast-${type}`}>
|
||||
<div className="toast-icon">
|
||||
{type === 'success' && '✓'}
|
||||
{type === 'error' && '✕'}
|
||||
{type === 'info' && 'ℹ'}
|
||||
</div>
|
||||
<div className="toast-message">{message}</div>
|
||||
<button className="toast-close" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -4,12 +4,12 @@ import '../styles/global.css'
|
||||
export default function About() {
|
||||
return (
|
||||
<div className="about-page">
|
||||
<h1>About StyleHub</h1>
|
||||
<h1>About Brand Master</h1>
|
||||
|
||||
<section className="about-section">
|
||||
<h2>Our Story</h2>
|
||||
<p>
|
||||
Founded in 2020, StyleHub started as a passion project to bring
|
||||
Founded in 2020, Brand Master started as a passion project to bring
|
||||
quality fashion and footwear to customers worldwide. What began as
|
||||
a small collection has grown into a comprehensive online store
|
||||
featuring over 1000 products from leading brands.
|
||||
|
||||
@ -2,11 +2,13 @@ import React, { useState, useEffect, useContext } from 'react'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import api from '../api'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import Toast from '../components/Toast'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function Admin() {
|
||||
const navigate = useNavigate()
|
||||
const { user, token } = useContext(AuthContext)
|
||||
const [toast, setToast] = useState(null)
|
||||
const [products, setProducts] = useState([])
|
||||
const [categories, setCategories] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@ -187,10 +189,10 @@ export default function Admin() {
|
||||
const allImages = [...currentImages, ...newImages.map(url => url)]
|
||||
setFormData({ ...formData, images: allImages.join(', ') })
|
||||
|
||||
alert('Images uploaded successfully!')
|
||||
setToast({ type: 'success', message: 'Images uploaded successfully!' })
|
||||
} catch (error) {
|
||||
console.error('Error uploading image:', error)
|
||||
alert('Error uploading image: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
setToast({ type: 'error', message: 'Error uploading image: ' + (error.response?.data?.detail || 'Unknown error') })
|
||||
} finally {
|
||||
setUploadingImage(false)
|
||||
}
|
||||
@ -222,10 +224,10 @@ export default function Admin() {
|
||||
try {
|
||||
if (editingProduct) {
|
||||
await api.put(`/products/${editingProduct.id}`, productData)
|
||||
alert('Product updated successfully!')
|
||||
setToast({ type: 'success', message: 'Product updated successfully!' })
|
||||
} else {
|
||||
await api.post('/products', productData)
|
||||
alert('Product created successfully!')
|
||||
setToast({ type: 'success', message: 'Product created successfully!' })
|
||||
}
|
||||
|
||||
setShowForm(false)
|
||||
@ -234,7 +236,7 @@ export default function Admin() {
|
||||
fetchProducts()
|
||||
} catch (error) {
|
||||
console.error('Error saving product:', error)
|
||||
alert('Error saving product: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
setToast({ type: 'error', message: 'Error saving product: ' + (error.response?.data?.detail || 'Unknown error') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,7 +275,7 @@ export default function Admin() {
|
||||
}, 100)
|
||||
} catch (error) {
|
||||
console.error('Error in handleEdit:', error)
|
||||
alert('Error loading product: ' + error.message)
|
||||
setToast({ type: 'error', message: 'Error loading product: ' + error.message })
|
||||
}
|
||||
}
|
||||
|
||||
@ -282,11 +284,11 @@ export default function Admin() {
|
||||
|
||||
try {
|
||||
await api.delete(`/products/${id}`)
|
||||
alert('Product deleted successfully!')
|
||||
setToast({ type: 'success', message: 'Product deleted successfully!' })
|
||||
fetchProducts()
|
||||
} catch (error) {
|
||||
console.error('Error deleting product:', error)
|
||||
alert('Error deleting product')
|
||||
setToast({ type: 'error', message: 'Error deleting product' })
|
||||
}
|
||||
}
|
||||
|
||||
@ -341,10 +343,10 @@ export default function Admin() {
|
||||
? response.data.url
|
||||
: `http://localhost:8000${response.data.url}`
|
||||
setCategoryFormData({ ...categoryFormData, image: imageUrl })
|
||||
alert('Image uploaded successfully!')
|
||||
setToast({ type: 'success', message: 'Image uploaded successfully!' })
|
||||
} catch (error) {
|
||||
console.error('Error uploading image:', error)
|
||||
alert('Error uploading image')
|
||||
setToast({ type: 'error', message: 'Error uploading image' })
|
||||
} finally {
|
||||
setUploadingImage(false)
|
||||
}
|
||||
@ -356,10 +358,10 @@ export default function Admin() {
|
||||
try {
|
||||
if (editingCategory) {
|
||||
await api.put(`/categories/${editingCategory.id}`, categoryFormData)
|
||||
alert('Category updated successfully!')
|
||||
setToast({ type: 'success', message: 'Category updated successfully!' })
|
||||
} else {
|
||||
await api.post('/categories', categoryFormData)
|
||||
alert('Category created successfully!')
|
||||
setToast({ type: 'success', message: 'Category created successfully!' })
|
||||
}
|
||||
setShowCategoryForm(false)
|
||||
setEditingCategory(null)
|
||||
@ -367,7 +369,7 @@ export default function Admin() {
|
||||
fetchCategories()
|
||||
} catch (error) {
|
||||
console.error('Error saving category:', error)
|
||||
alert('Error saving category: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
setToast({ type: 'error', message: 'Error saving category: ' + (error.response?.data?.detail || 'Unknown error') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,11 +392,11 @@ export default function Admin() {
|
||||
|
||||
try {
|
||||
await api.delete(`/categories/${id}`)
|
||||
alert('Category deleted successfully!')
|
||||
setToast({ type: 'success', message: 'Category deleted successfully!' })
|
||||
fetchCategories()
|
||||
} catch (error) {
|
||||
console.error('Error deleting category:', error)
|
||||
alert('Error deleting category: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
setToast({ type: 'error', message: 'Error deleting category: ' + (error.response?.data?.detail || 'Unknown error') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -412,7 +414,7 @@ export default function Admin() {
|
||||
const newBrand = brandFormData.name.trim()
|
||||
|
||||
if (!newBrand) {
|
||||
alert('Please enter a brand name')
|
||||
setToast({ type: 'error', message: 'Please enter a brand name' })
|
||||
return
|
||||
}
|
||||
|
||||
@ -421,10 +423,10 @@ export default function Admin() {
|
||||
setShowBrandForm(false)
|
||||
setBrandFormData({ name: '' })
|
||||
fetchBrands()
|
||||
alert('Brand added successfully!')
|
||||
setToast({ type: 'success', message: 'Brand added successfully!' })
|
||||
} catch (error) {
|
||||
console.error('Error adding brand:', error)
|
||||
alert('Error adding brand: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
setToast({ type: 'error', message: 'Error adding brand: ' + (error.response?.data?.detail || 'Unknown error') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -433,7 +435,7 @@ export default function Admin() {
|
||||
const newBrand = brandFormData.name.trim()
|
||||
|
||||
if (!newBrand) {
|
||||
alert('Please enter a brand name')
|
||||
setToast({ type: 'error', message: 'Please enter a brand name' })
|
||||
return
|
||||
}
|
||||
|
||||
@ -451,7 +453,7 @@ export default function Admin() {
|
||||
// Find the brand ID
|
||||
const brandToUpdate = brandsList.find(b => b.name === oldBrand)
|
||||
if (!brandToUpdate) {
|
||||
alert('Brand not found')
|
||||
setToast({ type: 'error', message: 'Brand not found' })
|
||||
return
|
||||
}
|
||||
|
||||
@ -479,10 +481,10 @@ export default function Admin() {
|
||||
fetchProducts()
|
||||
fetchModels()
|
||||
|
||||
alert('Brand updated successfully!')
|
||||
setToast({ type: 'success', message: 'Brand updated successfully!' })
|
||||
} catch (error) {
|
||||
console.error('Error updating brand:', error)
|
||||
alert('Error updating brand: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
setToast({ type: 'error', message: 'Error updating brand: ' + (error.response?.data?.detail || 'Unknown error') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -491,7 +493,7 @@ export default function Admin() {
|
||||
const modelCount = models.filter(m => m.brand === brand).length
|
||||
|
||||
if (productCount > 0 || modelCount > 0) {
|
||||
alert(`Cannot delete "${brand}". It is used in ${productCount} product(s) and ${modelCount} model(s).`)
|
||||
setToast({ type: 'error', message: `Cannot delete "${brand}". It is used in ${productCount} product(s) and ${modelCount} model(s).` })
|
||||
return
|
||||
}
|
||||
|
||||
@ -503,16 +505,16 @@ export default function Admin() {
|
||||
// Find the brand ID
|
||||
const brandToDelete = brandsList.find(b => b.name === brand)
|
||||
if (!brandToDelete) {
|
||||
alert('Brand not found')
|
||||
setToast({ type: 'error', message: 'Brand not found' })
|
||||
return
|
||||
}
|
||||
|
||||
await api.delete(`/brands/${brandToDelete.id}`)
|
||||
fetchBrands()
|
||||
alert('Brand deleted successfully!')
|
||||
setToast({ type: 'success', message: 'Brand deleted successfully!' })
|
||||
} catch (error) {
|
||||
console.error('Error deleting brand:', error)
|
||||
alert('Error deleting brand: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
setToast({ type: 'error', message: 'Error deleting brand: ' + (error.response?.data?.detail || 'Unknown error') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -536,10 +538,10 @@ export default function Admin() {
|
||||
try {
|
||||
if (editingModel) {
|
||||
await api.put(`/models/${editingModel.id}`, modelData)
|
||||
alert('Model updated successfully!')
|
||||
setToast({ type: 'success', message: 'Model updated successfully!' })
|
||||
} else {
|
||||
await api.post('/models', modelData)
|
||||
alert('Model created successfully!')
|
||||
setToast({ type: 'success', message: 'Model created successfully!' })
|
||||
}
|
||||
setShowModelForm(false)
|
||||
setEditingModel(null)
|
||||
@ -547,7 +549,7 @@ export default function Admin() {
|
||||
fetchModels()
|
||||
} catch (error) {
|
||||
console.error('Error saving model:', error)
|
||||
alert('Error saving model: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
setToast({ type: 'error', message: 'Error saving model: ' + (error.response?.data?.detail || 'Unknown error') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -573,11 +575,11 @@ export default function Admin() {
|
||||
|
||||
try {
|
||||
await api.delete(`/models/${id}`)
|
||||
alert('Model deleted successfully!')
|
||||
setToast({ type: 'success', message: 'Model deleted successfully!' })
|
||||
fetchModels()
|
||||
} catch (error) {
|
||||
console.error('Error deleting model:', error)
|
||||
alert('Error deleting model: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
setToast({ type: 'error', message: 'Error deleting model: ' + (error.response?.data?.detail || 'Unknown error') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -1514,6 +1516,14 @@ export default function Admin() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
import React, { useContext } from 'react'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { CartContext } from '../context/CartContext'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import Toast from '../components/Toast'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function Cart() {
|
||||
const { cart, removeFromCart, updateQuantity, total, clearCart } = useContext(CartContext)
|
||||
const { token } = useContext(AuthContext)
|
||||
const navigate = useNavigate()
|
||||
const [toast, setToast] = useState(null)
|
||||
|
||||
const handleCheckout = () => {
|
||||
if (!token) {
|
||||
navigate('/login')
|
||||
} else if (cart.length === 0) {
|
||||
alert('Your cart is empty')
|
||||
setToast({ type: 'info', message: 'Your cart is empty' })
|
||||
} else {
|
||||
navigate('/checkout')
|
||||
}
|
||||
@ -113,6 +115,14 @@ export default function Cart() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import api from '../api'
|
||||
import { CartContext } from '../context/CartContext'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import Toast from '../components/Toast'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function Checkout() {
|
||||
@ -10,37 +11,133 @@ export default function Checkout() {
|
||||
const { cart, total, clearCart } = useContext(CartContext)
|
||||
const { token, user } = useContext(AuthContext)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [toast, setToast] = useState(null)
|
||||
const [formData, setFormData] = useState({
|
||||
full_name: user?.username || '',
|
||||
email: user?.email || '',
|
||||
phone: '',
|
||||
shipping_address: user?.address || '',
|
||||
shipping_city: user?.city || '',
|
||||
shipping_postal_code: user?.postal_code || '',
|
||||
shipping_country: user?.country || '',
|
||||
shipping_country: user?.country || 'Israel',
|
||||
})
|
||||
const [errors, setErrors] = useState({})
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
})
|
||||
// Clear error when user starts typing
|
||||
if (errors[e.target.name]) {
|
||||
setErrors({
|
||||
...errors,
|
||||
[e.target.name]: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {}
|
||||
|
||||
if (!formData.full_name.trim()) {
|
||||
newErrors.full_name = 'Full name is required'
|
||||
}
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = 'Email is required'
|
||||
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
||||
newErrors.email = 'Please enter a valid email'
|
||||
}
|
||||
|
||||
if (!formData.phone.trim()) {
|
||||
newErrors.phone = 'Phone number is required'
|
||||
}
|
||||
|
||||
if (!formData.shipping_address.trim()) {
|
||||
newErrors.shipping_address = 'Address is required'
|
||||
}
|
||||
|
||||
if (!formData.shipping_city.trim()) {
|
||||
newErrors.shipping_city = 'City is required'
|
||||
}
|
||||
|
||||
if (!formData.shipping_country.trim()) {
|
||||
newErrors.shipping_country = 'Country is required'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const generateWhatsAppMessage = () => {
|
||||
const orderTotal = total + 10 + total * 0.1
|
||||
|
||||
let message = `🛍️ *New Order from Brand Master*\n\n`
|
||||
message += `📋 *Customer Information:*\n`
|
||||
message += `Name: ${formData.full_name}\n`
|
||||
message += `Email: ${formData.email}\n`
|
||||
message += `Phone: ${formData.phone}\n\n`
|
||||
|
||||
message += `📍 *Shipping Address:*\n`
|
||||
message += `${formData.shipping_address}\n`
|
||||
message += `${formData.shipping_city}, ${formData.shipping_postal_code}\n`
|
||||
message += `${formData.shipping_country}\n\n`
|
||||
|
||||
message += `🛒 *Order Items:*\n`
|
||||
cart.forEach((item, index) => {
|
||||
const itemPrice = item.product.discount_price || item.product.price
|
||||
message += `${index + 1}. ${item.product.name}\n`
|
||||
message += ` Quantity: ${item.quantity}\n`
|
||||
message += ` Price: ₪${itemPrice.toFixed(2)} x ${item.quantity} = ₪${(itemPrice * item.quantity).toFixed(2)}\n\n`
|
||||
})
|
||||
|
||||
message += `💰 *Order Summary:*\n`
|
||||
message += `Subtotal: ₪${total.toFixed(2)}\n`
|
||||
message += `Shipping: ₪10.00\n`
|
||||
message += `Tax: ₪${(total * 0.1).toFixed(2)}\n`
|
||||
message += `*Total: ₪${orderTotal.toFixed(2)}*\n`
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!token) {
|
||||
navigate('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (!validateForm()) {
|
||||
setToast({ type: 'error', message: 'Please fill in all required fields' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// Generate WhatsApp message
|
||||
const whatsappMessage = generateWhatsAppMessage()
|
||||
const whatsappNumber = '972532441361'
|
||||
const whatsappUrl = `https://wa.me/${whatsappNumber}?text=${encodeURIComponent(whatsappMessage)}`
|
||||
|
||||
// Save order to backend
|
||||
const response = await api.post('/orders', formData)
|
||||
|
||||
alert('Order placed successfully!')
|
||||
// Clear cart and show success
|
||||
clearCart()
|
||||
navigate(`/orders/${response.data.id}`)
|
||||
setToast({ type: 'success', message: 'Order placed successfully!' })
|
||||
|
||||
// Open WhatsApp after a short delay
|
||||
setTimeout(() => {
|
||||
window.open(whatsappUrl, '_blank')
|
||||
navigate('/orders')
|
||||
}, 1500)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error placing order:', error)
|
||||
alert('Error placing order')
|
||||
setToast({ type: 'error', message: 'Error placing order. Please try again.' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@ -52,30 +149,89 @@ export default function Checkout() {
|
||||
|
||||
<div className="checkout-container">
|
||||
<form onSubmit={handleSubmit} className="checkout-form">
|
||||
<div className="form-section">
|
||||
<h2>Contact Information</h2>
|
||||
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Full Name <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="full_name"
|
||||
value={formData.full_name}
|
||||
onChange={handleChange}
|
||||
className={errors.full_name ? 'error' : ''}
|
||||
placeholder="Enter your full name"
|
||||
/>
|
||||
{errors.full_name && <span className="error-message">{errors.full_name}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Email <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className={errors.email ? 'error' : ''}
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
{errors.email && <span className="error-message">{errors.email}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Phone <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
className={errors.phone ? 'error' : ''}
|
||||
placeholder="05X-XXX-XXXX"
|
||||
/>
|
||||
{errors.phone && <span className="error-message">{errors.phone}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h2>Shipping Address</h2>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Address</label>
|
||||
<label>
|
||||
Street Address <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="shipping_address"
|
||||
value={formData.shipping_address}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className={errors.shipping_address ? 'error' : ''}
|
||||
placeholder="Street address, apartment, suite, etc."
|
||||
/>
|
||||
{errors.shipping_address && <span className="error-message">{errors.shipping_address}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>City</label>
|
||||
<label>
|
||||
City <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="shipping_city"
|
||||
value={formData.shipping_city}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className={errors.shipping_city ? 'error' : ''}
|
||||
placeholder="City"
|
||||
/>
|
||||
{errors.shipping_city && <span className="error-message">{errors.shipping_city}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
@ -85,30 +241,34 @@ export default function Checkout() {
|
||||
name="shipping_postal_code"
|
||||
value={formData.shipping_postal_code}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Postal code"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Country</label>
|
||||
<label>
|
||||
Country <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="shipping_country"
|
||||
value={formData.shipping_country}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className={errors.shipping_country ? 'error' : ''}
|
||||
placeholder="Country"
|
||||
/>
|
||||
{errors.shipping_country && <span className="error-message">{errors.shipping_country}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h2>Payment Method</h2>
|
||||
<p>💳 Credit/Debit Card</p>
|
||||
<p className="text-muted">Payment processing simulated</p>
|
||||
<p className="payment-info">💳 Payment will be processed via WhatsApp</p>
|
||||
<p className="text-muted">After placing your order, you'll be redirected to WhatsApp to confirm payment details.</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn btn-full" disabled={loading}>
|
||||
<button type="submit" className="btn btn-full" disabled={loading || cart.length === 0}>
|
||||
{loading ? 'Processing...' : 'Place Order'}
|
||||
</button>
|
||||
</form>
|
||||
@ -128,6 +288,14 @@ export default function Checkout() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import api from '../api'
|
||||
import Toast from '../components/Toast'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function Contact() {
|
||||
@ -11,6 +12,7 @@ export default function Contact() {
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const [toast, setToast] = useState(null)
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
@ -32,13 +34,14 @@ export default function Contact() {
|
||||
subject: '',
|
||||
message: '',
|
||||
})
|
||||
setToast({ type: 'success', message: 'Message sent successfully! We\'ll get back to you soon.' })
|
||||
|
||||
setTimeout(() => {
|
||||
setSubmitted(false)
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error)
|
||||
alert('Error sending message')
|
||||
setToast({ type: 'error', message: 'Error sending message. Please try again.' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@ -54,28 +57,43 @@ export default function Contact() {
|
||||
|
||||
<div className="info-item">
|
||||
<h3>📞 Phone</h3>
|
||||
<p>+1 (555) 123-4567</p>
|
||||
<p>+972 53-244-1361</p>
|
||||
</div>
|
||||
|
||||
<div className="info-item">
|
||||
<h3>✉️ Email</h3>
|
||||
<p>info@stylehub.com</p>
|
||||
</div>
|
||||
|
||||
<div className="info-item">
|
||||
<h3>📍 Address</h3>
|
||||
<p>123 Fashion Street</p>
|
||||
<p>New York, NY 10001</p>
|
||||
<p>United States</p>
|
||||
<p>info@brandmaster.com</p>
|
||||
</div>
|
||||
|
||||
<div className="info-item">
|
||||
<h3>Follow Us</h3>
|
||||
<div className="social-links">
|
||||
<a href="#">Facebook</a>
|
||||
<a href="#">Twitter</a>
|
||||
<a href="#">Instagram</a>
|
||||
<a href="#">LinkedIn</a>
|
||||
<div className="social-icons">
|
||||
<a
|
||||
href="https://instagram.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="social-icon instagram"
|
||||
title="Follow us on Instagram"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
|
||||
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
|
||||
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
|
||||
</svg>
|
||||
<span>Instagram</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://wa.me/972532441361"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="social-icon whatsapp"
|
||||
title="Contact us on WhatsApp"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||
</svg>
|
||||
<span>WhatsApp</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -138,6 +156,14 @@ export default function Contact() {
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ export default function Home() {
|
||||
{/* Hero Section */}
|
||||
<section className="hero">
|
||||
<div className="hero-content">
|
||||
<h1>Welcome to StyleHub</h1>
|
||||
<h1>Welcome to Brand Master</h1>
|
||||
<p>Discover the latest in fashion and footwear</p>
|
||||
<Link to="/products" className="btn btn-large">
|
||||
Shop Now
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import React, { useState, useContext } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import api from '../api'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import Toast from '../components/Toast'
|
||||
import Modal from '../components/Modal'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function Login() {
|
||||
@ -12,7 +14,10 @@ export default function Login() {
|
||||
password: '',
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [toast, setToast] = useState(null)
|
||||
const [showForgotPassword, setShowForgotPassword] = useState(false)
|
||||
const [resetEmail, setResetEmail] = useState('')
|
||||
const [resetLoading, setResetLoading] = useState(false)
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
@ -23,7 +28,6 @@ export default function Login() {
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
@ -36,22 +40,38 @@ export default function Login() {
|
||||
|
||||
setToken(response.data.access_token)
|
||||
setUser(response.data.user)
|
||||
navigate('/')
|
||||
setToast({ type: 'success', message: 'Login successful!' })
|
||||
setTimeout(() => navigate('/'), 1000)
|
||||
} catch (error) {
|
||||
setError('Invalid email or password')
|
||||
setToast({ type: 'error', message: 'Invalid email or password' })
|
||||
console.error('Login error:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleForgotPassword = async (e) => {
|
||||
e.preventDefault()
|
||||
setResetLoading(true)
|
||||
|
||||
try {
|
||||
await api.post('/auth/forgot-password', { email: resetEmail })
|
||||
setToast({ type: 'success', message: 'Password reset link sent to your email!' })
|
||||
setShowForgotPassword(false)
|
||||
setResetEmail('')
|
||||
} catch (error) {
|
||||
setToast({ type: 'error', message: 'Error sending reset link. Please try again.' })
|
||||
console.error('Forgot password error:', error)
|
||||
} finally {
|
||||
setResetLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="auth-container">
|
||||
<h1>Login</h1>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label>Email</label>
|
||||
@ -75,23 +95,66 @@ export default function Login() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="forgot-password-link">
|
||||
<button
|
||||
type="button"
|
||||
className="link-button"
|
||||
onClick={() => setShowForgotPassword(true)}
|
||||
>
|
||||
Forgot Password?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn btn-full" disabled={loading}>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p>
|
||||
Don't have an account? <a href="/register">Sign up here</a>
|
||||
<p className="auth-switch">
|
||||
Don't have an account? <Link to="/register">Sign up here</Link>
|
||||
</p>
|
||||
|
||||
<div className="demo-account">
|
||||
<p><strong>Demo Accounts:</strong></p>
|
||||
<p style={{ color: '#ff6b6b', fontWeight: 'bold' }}>
|
||||
<p className="admin-demo">
|
||||
Admin: admin@example.com / password123
|
||||
</p>
|
||||
<p>User: user@example.com / password123</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isOpen={showForgotPassword}
|
||||
onClose={() => setShowForgotPassword(false)}
|
||||
title="Reset Password"
|
||||
>
|
||||
<form onSubmit={handleForgotPassword}>
|
||||
<p className="modal-description">
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label>Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={resetEmail}
|
||||
onChange={(e) => setResetEmail(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-full" disabled={resetLoading}>
|
||||
{resetLoading ? 'Sending...' : 'Send Reset Link'}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,11 +2,13 @@ import React, { useState, useEffect, useContext } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import api from '../api'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import Toast from '../components/Toast'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function Models() {
|
||||
const navigate = useNavigate()
|
||||
const { user } = useContext(AuthContext)
|
||||
const [toast, setToast] = useState(null)
|
||||
const [models, setModels] = useState([])
|
||||
const [categories, setCategories] = useState([])
|
||||
const [brands, setBrands] = useState([])
|
||||
@ -82,10 +84,10 @@ export default function Models() {
|
||||
try {
|
||||
if (editingModel) {
|
||||
await api.put(`/models/${editingModel.id}`, modelData)
|
||||
alert('Model updated successfully!')
|
||||
setToast({ type: 'success', message: 'Model updated successfully!' })
|
||||
} else {
|
||||
await api.post('/models', modelData)
|
||||
alert('Model created successfully!')
|
||||
setToast({ type: 'success', message: 'Model created successfully!' })
|
||||
}
|
||||
setShowForm(false)
|
||||
setEditingModel(null)
|
||||
@ -93,7 +95,7 @@ export default function Models() {
|
||||
fetchModels()
|
||||
} catch (error) {
|
||||
console.error('Error saving model:', error)
|
||||
alert('Error saving model: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
setToast({ type: 'error', message: 'Error saving model: ' + (error.response?.data?.detail || 'Unknown error') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,11 +120,11 @@ export default function Models() {
|
||||
|
||||
try {
|
||||
await api.delete(`/models/${id}`)
|
||||
alert('Model deleted successfully!')
|
||||
setToast({ type: 'success', message: 'Model deleted successfully!' })
|
||||
fetchModels()
|
||||
} catch (error) {
|
||||
console.error('Error deleting model:', error)
|
||||
alert('Error deleting model: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
setToast({ type: 'error', message: 'Error deleting model: ' + (error.response?.data?.detail || 'Unknown error') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,6 +328,14 @@ export default function Models() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
|
||||
import api from '../api'
|
||||
import { CartContext } from '../context/CartContext'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import AddToCartModal from '../components/AddToCartModal'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function ProductDetail() {
|
||||
@ -14,6 +15,7 @@ export default function ProductDetail() {
|
||||
const [quantity, setQuantity] = useState(1)
|
||||
const [inWishlist, setInWishlist] = useState(false)
|
||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0)
|
||||
const [showCartModal, setShowCartModal] = useState(false)
|
||||
const { addToCart } = useContext(CartContext)
|
||||
const { token } = useContext(AuthContext)
|
||||
|
||||
@ -48,7 +50,7 @@ export default function ProductDetail() {
|
||||
})
|
||||
|
||||
addToCart(product, quantity, selectedSize)
|
||||
alert('Product added to cart!')
|
||||
setShowCartModal(true)
|
||||
} catch (error) {
|
||||
console.error('Error adding to cart:', error)
|
||||
}
|
||||
@ -214,6 +216,12 @@ export default function ProductDetail() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddToCartModal
|
||||
isOpen={showCartModal}
|
||||
onClose={() => setShowCartModal(false)}
|
||||
productName={product.name}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { useState, useEffect, useContext } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import api from '../api'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import Toast from '../components/Toast'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function Profile() {
|
||||
@ -17,6 +18,7 @@ export default function Profile() {
|
||||
})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [toast, setToast] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
@ -54,10 +56,10 @@ export default function Profile() {
|
||||
params: { token },
|
||||
})
|
||||
setUser(response.data)
|
||||
alert('Profile updated successfully!')
|
||||
setToast({ type: 'success', message: 'Profile updated successfully!' })
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error)
|
||||
alert('Error updating profile')
|
||||
setToast({ type: 'error', message: 'Error updating profile. Please try again.' })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@ -161,6 +163,14 @@ export default function Profile() {
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React, { useState, useContext } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import api from '../api'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import Toast from '../components/Toast'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function Register() {
|
||||
@ -13,7 +14,7 @@ export default function Register() {
|
||||
full_name: '',
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [toast, setToast] = useState(null)
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
@ -24,7 +25,6 @@ export default function Register() {
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
@ -34,7 +34,7 @@ export default function Register() {
|
||||
full_name: formData.full_name,
|
||||
})
|
||||
|
||||
alert('Account created successfully! Logging you in...')
|
||||
setToast({ type: 'success', message: 'Account created successfully! Logging you in...' })
|
||||
|
||||
const loginResponse = await api.post('/auth/login', null, {
|
||||
params: {
|
||||
@ -45,9 +45,10 @@ export default function Register() {
|
||||
|
||||
setToken(loginResponse.data.access_token)
|
||||
setUser(loginResponse.data.user)
|
||||
navigate('/')
|
||||
|
||||
setTimeout(() => navigate('/'), 1500)
|
||||
} catch (error) {
|
||||
setError(error.response?.data?.detail || 'Registration failed')
|
||||
setToast({ type: 'error', message: error.response?.data?.detail || 'Registration failed' })
|
||||
console.error('Registration error:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@ -59,8 +60,6 @@ export default function Register() {
|
||||
<div className="auth-container">
|
||||
<h1>Create Account</h1>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label>Full Name</label>
|
||||
@ -101,10 +100,18 @@ export default function Register() {
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p>
|
||||
Already have an account? <a href="/login">Login here</a>
|
||||
<p className="auth-switch">
|
||||
Already have an account? <Link to="/login">Login here</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -99,12 +99,24 @@ button {
|
||||
color: var(--secondary);
|
||||
font-weight: 500;
|
||||
transition: color 0.3s;
|
||||
padding-bottom: 0.25rem;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.navbar-menu a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.navbar-menu a.active {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
.navbar-menu a.admin-link {
|
||||
color: var(--primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -907,6 +919,43 @@ button {
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 0.95rem;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.form-group input.error,
|
||||
.form-group select.error,
|
||||
.form-group textarea.error {
|
||||
border-color: #f44336;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #f44336;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: block;
|
||||
color: #f44336;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.payment-info {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--gray);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
@ -977,6 +1026,42 @@ button {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.forgot-password-link {
|
||||
text-align: right;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.link-button:hover {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.auth-switch {
|
||||
margin-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
color: var(--gray);
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.admin-demo {
|
||||
color: #ff6b6b !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
@ -1266,6 +1351,49 @@ button {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.social-icons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--light);
|
||||
color: var(--secondary);
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.social-icon:hover {
|
||||
transform: translateX(8px);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.social-icon svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.social-icon.instagram:hover {
|
||||
background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-icon.whatsapp:hover {
|
||||
background: #25D366;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-icon span {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.contact-form {
|
||||
background: var(--light);
|
||||
padding: 2rem;
|
||||
@ -1359,6 +1487,216 @@ button {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 2rem;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--gray-light);
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
color: var(--gray);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Cart Modal Specific */
|
||||
.cart-modal {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.cart-modal-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
.cart-modal h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
.cart-modal-message {
|
||||
color: var(--gray);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.cart-modal-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cart-modal-actions .btn {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
/* Toast Notifications */
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
z-index: 1001;
|
||||
min-width: 300px;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
border-left: 4px solid #4CAF50;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
border-left: 4px solid #f44336;
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-left: 4px solid #2196F3;
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toast-success .toast-icon {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast-error .toast-icon {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast-info .toast-icon {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: var(--gray);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
background: var(--secondary);
|
||||
@ -1383,6 +1721,22 @@ button {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.footer-brand h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.footer-section p {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 0.9rem;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user