Add brands management system with backend persistence
This commit is contained in:
parent
d182f76201
commit
fbb3e7d850
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,6 +8,7 @@ backend/*.pyc
|
||||
backend/.pytest_cache/
|
||||
backend/.vscode/
|
||||
backend/instance/
|
||||
uploads/
|
||||
|
||||
# Frontend
|
||||
frontend/node_modules/
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/ecommerce_db
|
||||
DATABASE_URL=postgresql://ecommerce_user:password@localhost:5432/ecommerce_db
|
||||
JWT_SECRET_KEY=your-secret-key-here-change-this-in-production
|
||||
JWT_ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
|
||||
BIN
backend/app/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/app/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/__pycache__/config.cpython-314.pyc
Normal file
BIN
backend/app/__pycache__/config.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/__pycache__/main.cpython-314.pyc
Normal file
BIN
backend/app/__pycache__/main.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/__pycache__/utils.cpython-314.pyc
Normal file
BIN
backend/app/__pycache__/utils.cpython-314.pyc
Normal file
Binary file not shown.
@ -1,4 +1,5 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
@ -9,7 +10,7 @@ class Settings(BaseSettings):
|
||||
frontend_url: str = "http://localhost:5173"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file = str(Path(__file__).parent.parent / ".env")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
BIN
backend/app/database/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/app/database/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/database/__pycache__/database.cpython-314.pyc
Normal file
BIN
backend/app/database/__pycache__/database.cpython-314.pyc
Normal file
Binary file not shown.
@ -1,8 +1,22 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add backend directory to Python path
|
||||
backend_dir = Path(__file__).resolve().parent.parent
|
||||
if str(backend_dir) not in sys.path:
|
||||
sys.path.insert(0, str(backend_dir))
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import uvicorn
|
||||
from app.database.database import engine, Base
|
||||
from app.config import settings
|
||||
from app.routers import auth, users, products, categories, cart, orders, wishlist, contact
|
||||
from app.routers import auth, users, products, categories, cart, orders, wishlist, contact, models, brands
|
||||
|
||||
# Create uploads directory if it doesn't exist
|
||||
uploads_dir = Path("uploads")
|
||||
uploads_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Create tables
|
||||
Base.metadata.create_all(bind=engine)
|
||||
@ -27,11 +41,16 @@ app.include_router(auth.router)
|
||||
app.include_router(users.router)
|
||||
app.include_router(products.router)
|
||||
app.include_router(categories.router)
|
||||
app.include_router(models.router)
|
||||
app.include_router(brands.router)
|
||||
app.include_router(cart.router)
|
||||
app.include_router(orders.router)
|
||||
app.include_router(wishlist.router)
|
||||
app.include_router(contact.router)
|
||||
|
||||
# Mount static files for uploads
|
||||
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
@ -45,3 +64,7 @@ def read_root():
|
||||
@app.get("/health")
|
||||
def health_check():
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
@ -1,6 +1,7 @@
|
||||
from .user import User
|
||||
from .product import Product
|
||||
from .category import Category
|
||||
from .model import Model
|
||||
from .cart import Cart, CartItem
|
||||
from .order import Order, OrderItem
|
||||
from .wishlist import Wishlist
|
||||
@ -10,6 +11,7 @@ __all__ = [
|
||||
"User",
|
||||
"Product",
|
||||
"Category",
|
||||
"Model",
|
||||
"Cart",
|
||||
"CartItem",
|
||||
"Order",
|
||||
|
||||
BIN
backend/app/models/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/app/models/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/brand.cpython-314.pyc
Normal file
BIN
backend/app/models/__pycache__/brand.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/cart.cpython-314.pyc
Normal file
BIN
backend/app/models/__pycache__/cart.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/category.cpython-314.pyc
Normal file
BIN
backend/app/models/__pycache__/category.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/contact_message.cpython-314.pyc
Normal file
BIN
backend/app/models/__pycache__/contact_message.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/model.cpython-314.pyc
Normal file
BIN
backend/app/models/__pycache__/model.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/order.cpython-314.pyc
Normal file
BIN
backend/app/models/__pycache__/order.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/product.cpython-314.pyc
Normal file
BIN
backend/app/models/__pycache__/product.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/user.cpython-314.pyc
Normal file
BIN
backend/app/models/__pycache__/user.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/wishlist.cpython-314.pyc
Normal file
BIN
backend/app/models/__pycache__/wishlist.cpython-314.pyc
Normal file
Binary file not shown.
8
backend/app/models/brand.py
Normal file
8
backend/app/models/brand.py
Normal file
@ -0,0 +1,8 @@
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from ..database.database import Base
|
||||
|
||||
class Brand(Base):
|
||||
__tablename__ = "brand"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), unique=True, nullable=False, index=True)
|
||||
@ -9,3 +9,4 @@ class Category(Base):
|
||||
name = Column(String, unique=True, index=True)
|
||||
slug = Column(String, unique=True, index=True)
|
||||
description = Column(String, nullable=True)
|
||||
image = Column(String, nullable=True)
|
||||
|
||||
21
backend/app/models/model.py
Normal file
21
backend/app/models/model.py
Normal file
@ -0,0 +1,21 @@
|
||||
from sqlalchemy import Column, Integer, String, DECIMAL, ForeignKey, TIMESTAMP, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from app.database.database import Base
|
||||
|
||||
|
||||
class Model(Base):
|
||||
__tablename__ = "model"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
category_id = Column(Integer, ForeignKey("category.id", ondelete="CASCADE"), nullable=False)
|
||||
brand = Column(String(100), nullable=False)
|
||||
base_price = Column(DECIMAL(10, 2), nullable=True)
|
||||
sizes = Column(JSON, nullable=True)
|
||||
description = Column(String, nullable=True)
|
||||
created_at = Column(TIMESTAMP, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
category = relationship("Category", backref="models")
|
||||
products = relationship("Product", back_populates="model")
|
||||
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, Text, ForeignKey, JSON
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, Text, ForeignKey, JSON, DECIMAL
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from app.database.database import Base
|
||||
@ -9,10 +9,12 @@ class Product(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True)
|
||||
slug = Column(String, unique=True, index=True, nullable=True)
|
||||
description = Column(Text)
|
||||
price = Column(Float)
|
||||
discount_price = Column(Float, nullable=True)
|
||||
category_id = Column(Integer, ForeignKey("category.id"))
|
||||
model_id = Column(Integer, ForeignKey("model.id", ondelete="SET NULL"), nullable=True)
|
||||
gender = Column(String) # men, women
|
||||
brand = Column(String)
|
||||
sizes = Column(JSON) # ["S", "M", "L", "XL", ...]
|
||||
@ -21,8 +23,11 @@ class Product(Base):
|
||||
images = Column(JSON) # Array of image URLs
|
||||
is_featured = Column(Boolean, default=False)
|
||||
is_on_sale = Column(Boolean, default=False)
|
||||
override_price = Column(DECIMAL(10, 2), nullable=True)
|
||||
override_sizes = Column(JSON, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
category = relationship("Category")
|
||||
model = relationship("Model", back_populates="products")
|
||||
cart_items = relationship("CartItem", back_populates="product")
|
||||
order_items = relationship("OrderItem", back_populates="product")
|
||||
|
||||
@ -24,6 +24,7 @@ class User(Base):
|
||||
postal_code = Column(String, nullable=True)
|
||||
country = Column(String, nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
cart = relationship("Cart", back_populates="user", uselist=False)
|
||||
|
||||
BIN
backend/app/routers/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/app/routers/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/auth.cpython-314.pyc
Normal file
BIN
backend/app/routers/__pycache__/auth.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/brands.cpython-314.pyc
Normal file
BIN
backend/app/routers/__pycache__/brands.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/cart.cpython-314.pyc
Normal file
BIN
backend/app/routers/__pycache__/cart.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/categories.cpython-314.pyc
Normal file
BIN
backend/app/routers/__pycache__/categories.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/contact.cpython-314.pyc
Normal file
BIN
backend/app/routers/__pycache__/contact.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/models.cpython-314.pyc
Normal file
BIN
backend/app/routers/__pycache__/models.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/orders.cpython-314.pyc
Normal file
BIN
backend/app/routers/__pycache__/orders.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/products.cpython-314.pyc
Normal file
BIN
backend/app/routers/__pycache__/products.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/users.cpython-314.pyc
Normal file
BIN
backend/app/routers/__pycache__/users.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/wishlist.cpython-314.pyc
Normal file
BIN
backend/app/routers/__pycache__/wishlist.cpython-314.pyc
Normal file
Binary file not shown.
@ -47,7 +47,7 @@ def login(email: str, password: str, db: Session = Depends(get_db)):
|
||||
|
||||
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.id}, expires_delta=access_token_expires
|
||||
data={"sub": str(user.id)}, expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
89
backend/app/routers/brands.py
Normal file
89
backend/app/routers/brands.py
Normal file
@ -0,0 +1,89 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from ..database.database import get_db
|
||||
from ..models.brand import Brand
|
||||
from ..models.user import User
|
||||
from ..schemas.brand import BrandCreate, BrandUpdate, BrandResponse
|
||||
from ..services.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/brands", tags=["brands"])
|
||||
|
||||
@router.get("", response_model=List[BrandResponse])
|
||||
def list_brands(db: Session = Depends(get_db)):
|
||||
"""Get all brands"""
|
||||
brands = db.query(Brand).order_by(Brand.name).all()
|
||||
return brands
|
||||
|
||||
@router.get("/{brand_id}", response_model=BrandResponse)
|
||||
def get_brand(brand_id: int, db: Session = Depends(get_db)):
|
||||
"""Get a specific brand"""
|
||||
brand = db.query(Brand).filter(Brand.id == brand_id).first()
|
||||
if not brand:
|
||||
raise HTTPException(status_code=404, detail="Brand not found")
|
||||
return brand
|
||||
|
||||
@router.post("", response_model=BrandResponse)
|
||||
def create_brand(
|
||||
brand_data: BrandCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Create a new brand (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
# Check if brand already exists
|
||||
existing = db.query(Brand).filter(Brand.name == brand_data.name).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Brand already exists")
|
||||
|
||||
brand = Brand(name=brand_data.name)
|
||||
db.add(brand)
|
||||
db.commit()
|
||||
db.refresh(brand)
|
||||
return brand
|
||||
|
||||
@router.put("/{brand_id}", response_model=BrandResponse)
|
||||
def update_brand(
|
||||
brand_id: int,
|
||||
brand_data: BrandUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Update a brand (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
brand = db.query(Brand).filter(Brand.id == brand_id).first()
|
||||
if not brand:
|
||||
raise HTTPException(status_code=404, detail="Brand not found")
|
||||
|
||||
# Check if new name conflicts with existing brand
|
||||
if brand_data.name != brand.name:
|
||||
existing = db.query(Brand).filter(Brand.name == brand_data.name).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Brand name already exists")
|
||||
|
||||
brand.name = brand_data.name
|
||||
db.commit()
|
||||
db.refresh(brand)
|
||||
return brand
|
||||
|
||||
@router.delete("/{brand_id}")
|
||||
def delete_brand(
|
||||
brand_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Delete a brand (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
brand = db.query(Brand).filter(Brand.id == brand_id).first()
|
||||
if not brand:
|
||||
raise HTTPException(status_code=404, detail="Brand not found")
|
||||
|
||||
db.delete(brand)
|
||||
db.commit()
|
||||
return {"message": "Brand deleted successfully"}
|
||||
@ -9,42 +9,40 @@ from app.services.cart import (
|
||||
remove_from_cart,
|
||||
clear_cart,
|
||||
)
|
||||
from app.services.auth import verify_token
|
||||
from app.services.auth import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/api/cart", tags=["cart"])
|
||||
|
||||
|
||||
def get_user_id_from_token(token: str) -> int:
|
||||
user_id = verify_token(token)
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
)
|
||||
return user_id
|
||||
|
||||
|
||||
@router.get("", response_model=CartResponse)
|
||||
def get_user_cart(token: str, db: Session = Depends(get_db)):
|
||||
user_id = get_user_id_from_token(token)
|
||||
cart = get_cart(db, user_id)
|
||||
def get_user_cart(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
cart = get_cart(db, current_user.id)
|
||||
if not cart:
|
||||
raise HTTPException(status_code=404, detail="Cart not found")
|
||||
return cart
|
||||
|
||||
|
||||
@router.post("/add", response_model=dict)
|
||||
def add_item_to_cart(token: str, item: CartItemCreate, db: Session = Depends(get_db)):
|
||||
user_id = get_user_id_from_token(token)
|
||||
cart_item = add_to_cart(db, user_id, item)
|
||||
def add_item_to_cart(
|
||||
item: CartItemCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
cart_item = add_to_cart(db, current_user.id, item)
|
||||
return {"message": "Item added to cart", "item_id": cart_item.id}
|
||||
|
||||
|
||||
@router.put("/{cart_item_id}", response_model=dict)
|
||||
def update_item(
|
||||
cart_item_id: int, token: str, update: CartItemUpdate, db: Session = Depends(get_db)
|
||||
cart_item_id: int,
|
||||
update: CartItemUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user_id = get_user_id_from_token(token)
|
||||
cart_item = update_cart_item(db, cart_item_id, update)
|
||||
if not cart_item:
|
||||
raise HTTPException(status_code=404, detail="Cart item not found")
|
||||
@ -52,16 +50,21 @@ def update_item(
|
||||
|
||||
|
||||
@router.delete("/{cart_item_id}")
|
||||
def remove_item(cart_item_id: int, token: str, db: Session = Depends(get_db)):
|
||||
user_id = get_user_id_from_token(token)
|
||||
def remove_item(
|
||||
cart_item_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
if not remove_from_cart(db, cart_item_id):
|
||||
raise HTTPException(status_code=404, detail="Cart item not found")
|
||||
return {"message": "Item removed from cart"}
|
||||
|
||||
|
||||
@router.delete("")
|
||||
def clear_user_cart(token: str, db: Session = Depends(get_db)):
|
||||
user_id = get_user_id_from_token(token)
|
||||
if not clear_cart(db, user_id):
|
||||
def clear_user_cart(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
if not clear_cart(db, current_user.id):
|
||||
raise HTTPException(status_code=404, detail="Cart not found")
|
||||
return {"message": "Cart cleared"}
|
||||
|
||||
@ -2,8 +2,9 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.database.database import get_db
|
||||
from app.models import Category
|
||||
from app.models import Category, User
|
||||
from app.schemas.category import CategoryCreate, CategoryResponse, CategoryUpdate
|
||||
from app.services.auth import get_current_admin_user
|
||||
|
||||
router = APIRouter(prefix="/api/categories", tags=["categories"])
|
||||
|
||||
@ -22,8 +23,11 @@ def get_category(category_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("", response_model=CategoryResponse)
|
||||
def create_category(category: CategoryCreate, db: Session = Depends(get_db)):
|
||||
# TODO: Add admin check
|
||||
def create_category(
|
||||
category: CategoryCreate,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_current_admin_user)
|
||||
):
|
||||
db_category = Category(**category.dict())
|
||||
db.add(db_category)
|
||||
db.commit()
|
||||
@ -33,9 +37,11 @@ def create_category(category: CategoryCreate, db: Session = Depends(get_db)):
|
||||
|
||||
@router.put("/{category_id}", response_model=CategoryResponse)
|
||||
def update_category(
|
||||
category_id: int, category_update: CategoryUpdate, db: Session = Depends(get_db)
|
||||
category_id: int,
|
||||
category_update: CategoryUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_current_admin_user)
|
||||
):
|
||||
# TODO: Add admin check
|
||||
category = db.query(Category).filter(Category.id == category_id).first()
|
||||
if not category:
|
||||
raise HTTPException(status_code=404, detail="Category not found")
|
||||
@ -49,8 +55,11 @@ def update_category(
|
||||
|
||||
|
||||
@router.delete("/{category_id}")
|
||||
def delete_category(category_id: int, db: Session = Depends(get_db)):
|
||||
# TODO: Add admin check
|
||||
def delete_category(
|
||||
category_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_current_admin_user)
|
||||
):
|
||||
category = db.query(Category).filter(Category.id == category_id).first()
|
||||
if not category:
|
||||
raise HTTPException(status_code=404, detail="Category not found")
|
||||
|
||||
105
backend/app/routers/models.py
Normal file
105
backend/app/routers/models.py
Normal file
@ -0,0 +1,105 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from app.database.database import get_db
|
||||
from app.models import Model, User
|
||||
from app.schemas.model import ModelCreate, ModelUpdate, ModelResponse
|
||||
from app.services.auth import get_current_admin_user
|
||||
|
||||
router = APIRouter(prefix="/api/models", tags=["models"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[ModelResponse])
|
||||
def get_models(
|
||||
category_id: Optional[int] = None,
|
||||
brand: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all models with optional filtering by category and brand"""
|
||||
query = db.query(Model)
|
||||
|
||||
if category_id:
|
||||
query = query.filter(Model.category_id == category_id)
|
||||
if brand:
|
||||
query = query.filter(Model.brand.ilike(f"%{brand}%"))
|
||||
|
||||
return query.all()
|
||||
|
||||
|
||||
@router.get("/{model_id}", response_model=ModelResponse)
|
||||
def get_model(model_id: int, db: Session = Depends(get_db)):
|
||||
"""Get a specific model by ID"""
|
||||
model = db.query(Model).filter(Model.id == model_id).first()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="Model not found")
|
||||
return model
|
||||
|
||||
|
||||
@router.post("", response_model=ModelResponse)
|
||||
def create_model(
|
||||
model_data: ModelCreate,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""Create a new model (admin only)"""
|
||||
# Check if model with same name, category, and brand already exists
|
||||
existing = db.query(Model).filter(
|
||||
Model.name == model_data.name,
|
||||
Model.category_id == model_data.category_id,
|
||||
Model.brand == model_data.brand
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Model '{model_data.name}' already exists for brand '{model_data.brand}' in this category"
|
||||
)
|
||||
|
||||
db_model = Model(**model_data.dict())
|
||||
db.add(db_model)
|
||||
db.commit()
|
||||
db.refresh(db_model)
|
||||
return db_model
|
||||
|
||||
|
||||
@router.put("/{model_id}", response_model=ModelResponse)
|
||||
def update_model(
|
||||
model_id: int,
|
||||
model_update: ModelUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""Update a model (admin only)"""
|
||||
model = db.query(Model).filter(Model.id == model_id).first()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="Model not found")
|
||||
|
||||
for field, value in model_update.dict(exclude_unset=True).items():
|
||||
setattr(model, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(model)
|
||||
return model
|
||||
|
||||
|
||||
@router.delete("/{model_id}")
|
||||
def delete_model(
|
||||
model_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""Delete a model (admin only)"""
|
||||
model = db.query(Model).filter(Model.id == model_id).first()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="Model not found")
|
||||
|
||||
db.delete(model)
|
||||
db.commit()
|
||||
return {"message": "Model deleted successfully"}
|
||||
|
||||
|
||||
@router.get("/brands/list")
|
||||
def get_brands(db: Session = Depends(get_db)):
|
||||
"""Get list of unique brands"""
|
||||
brands = db.query(Model.brand).distinct().all()
|
||||
return [brand[0] for brand in brands if brand[0]]
|
||||
@ -9,25 +9,19 @@ from app.services.order import (
|
||||
get_user_orders,
|
||||
update_order_status,
|
||||
)
|
||||
from app.services.auth import verify_token
|
||||
from app.services.auth import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/api/orders", tags=["orders"])
|
||||
|
||||
|
||||
def get_user_id_from_token(token: str) -> int:
|
||||
user_id = verify_token(token)
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
)
|
||||
return user_id
|
||||
|
||||
|
||||
@router.post("", response_model=OrderResponse)
|
||||
def create_new_order(token: str, order_data: OrderCreate, db: Session = Depends(get_db)):
|
||||
user_id = get_user_id_from_token(token)
|
||||
order = create_order(db, user_id, order_data)
|
||||
def create_new_order(
|
||||
order_data: OrderCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
order = create_order(db, current_user.id, order_data)
|
||||
if not order:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@ -36,16 +30,21 @@ def create_new_order(token: str, order_data: OrderCreate, db: Session = Depends(
|
||||
return order
|
||||
|
||||
|
||||
@router.get("/user/orders", response_model=List[OrderResponse])
|
||||
def get_user_order_history(token: str, db: Session = Depends(get_db)):
|
||||
user_id = get_user_id_from_token(token)
|
||||
return get_user_orders(db, user_id)
|
||||
@router.get("", response_model=List[OrderResponse])
|
||||
def get_user_order_history(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return get_user_orders(db, current_user.id)
|
||||
|
||||
|
||||
@router.get("/{order_id}", response_model=OrderResponse)
|
||||
def get_order(order_id: int, token: str, db: Session = Depends(get_db)):
|
||||
user_id = get_user_id_from_token(token)
|
||||
def get_order(
|
||||
order_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
order = get_order_by_id(db, order_id)
|
||||
if not order or order.user_id != user_id:
|
||||
if not order or order.user_id != current_user.id:
|
||||
raise HTTPException(status_code=404, detail="Order not found")
|
||||
return order
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
from app.database.database import get_db
|
||||
from app.models import Product, Category
|
||||
from app.models import Product, Category, User
|
||||
from app.schemas.product import (
|
||||
ProductCreate,
|
||||
ProductResponse,
|
||||
@ -16,6 +18,8 @@ from app.services.product import (
|
||||
delete_product,
|
||||
search_products,
|
||||
)
|
||||
from app.services.auth import get_current_admin_user
|
||||
from app.utils import save_upload_file, generate_slug
|
||||
|
||||
router = APIRouter(prefix="/api/products", tags=["products"])
|
||||
|
||||
@ -23,22 +27,37 @@ router = APIRouter(prefix="/api/products", tags=["products"])
|
||||
@router.get("", response_model=List[ProductResponse])
|
||||
def list_products(
|
||||
category_id: Optional[int] = None,
|
||||
model_id: Optional[int] = None,
|
||||
brand: Optional[str] = None,
|
||||
gender: Optional[str] = None,
|
||||
min_price: Optional[float] = None,
|
||||
max_price: Optional[float] = None,
|
||||
on_sale: Optional[bool] = None,
|
||||
featured: Optional[bool] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return get_products(
|
||||
db,
|
||||
category_id=category_id,
|
||||
gender=gender,
|
||||
on_sale=on_sale,
|
||||
featured=featured,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
query = db.query(Product)
|
||||
|
||||
if category_id:
|
||||
query = query.filter(Product.category_id == category_id)
|
||||
if model_id:
|
||||
query = query.filter(Product.model_id == model_id)
|
||||
if brand:
|
||||
query = query.filter(Product.brand.ilike(f"%{brand}%"))
|
||||
if gender:
|
||||
query = query.filter(Product.gender == gender)
|
||||
if min_price is not None:
|
||||
query = query.filter(Product.price >= min_price)
|
||||
if max_price is not None:
|
||||
query = query.filter(Product.price <= max_price)
|
||||
if on_sale is not None:
|
||||
query = query.filter(Product.is_on_sale == on_sale)
|
||||
if featured is not None:
|
||||
query = query.filter(Product.is_featured == featured)
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
@router.get("/search", response_model=List[ProductResponse])
|
||||
@ -55,16 +74,32 @@ def get_product(product_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("", response_model=ProductResponse)
|
||||
def create_new_product(product: ProductCreate, db: Session = Depends(get_db)):
|
||||
# TODO: Add admin check
|
||||
def create_new_product(
|
||||
product: ProductCreate,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
# Auto-generate slug if not provided
|
||||
if not product.slug:
|
||||
base_slug = generate_slug(f"{product.brand} {product.name}")
|
||||
# Check if slug exists and make it unique
|
||||
slug = base_slug
|
||||
counter = 1
|
||||
while db.query(Product).filter(Product.slug == slug).first():
|
||||
slug = f"{base_slug}-{counter}"
|
||||
counter += 1
|
||||
product.slug = slug
|
||||
|
||||
return create_product(db, product)
|
||||
|
||||
|
||||
@router.put("/{product_id}", response_model=ProductResponse)
|
||||
def update_existing_product(
|
||||
product_id: int, product_update: ProductUpdate, db: Session = Depends(get_db)
|
||||
product_id: int,
|
||||
product_update: ProductUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
# TODO: Add admin check
|
||||
product = update_product(db, product_id, product_update)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
@ -72,8 +107,82 @@ def update_existing_product(
|
||||
|
||||
|
||||
@router.delete("/{product_id}")
|
||||
def delete_existing_product(product_id: int, db: Session = Depends(get_db)):
|
||||
# TODO: Add admin check
|
||||
def delete_existing_product(
|
||||
product_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
if not delete_product(db, product_id):
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
return {"message": "Product deleted successfully"}
|
||||
|
||||
|
||||
@router.post("/upload-image")
|
||||
async def upload_product_image(
|
||||
file: UploadFile = File(...),
|
||||
admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Upload a product image and return the URL"""
|
||||
# Validate file type
|
||||
allowed_types = ["image/jpeg", "image/png", "image/jpg", "image/webp", "image/gif"]
|
||||
if file.content_type not in allowed_types:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"File type {file.content_type} not allowed. Allowed types: {', '.join(allowed_types)}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Save file and get path
|
||||
file_path = save_upload_file(file, folder="products")
|
||||
# Return full URL
|
||||
return {"url": file_path, "message": "Image uploaded successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error uploading file: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/upload-images")
|
||||
async def upload_multiple_images(
|
||||
files: List[UploadFile] = File(...),
|
||||
admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Upload multiple product images and return the URLs"""
|
||||
allowed_types = ["image/jpeg", "image/png", "image/jpg", "image/webp", "image/gif"]
|
||||
uploaded_urls = []
|
||||
errors = []
|
||||
|
||||
for file in files:
|
||||
if file.content_type not in allowed_types:
|
||||
errors.append(f"File {file.filename}: type {file.content_type} not allowed")
|
||||
continue
|
||||
|
||||
try:
|
||||
file_path = save_upload_file(file, folder="products")
|
||||
uploaded_urls.append(file_path)
|
||||
except Exception as e:
|
||||
errors.append(f"File {file.filename}: {str(e)}")
|
||||
|
||||
return {
|
||||
"urls": uploaded_urls,
|
||||
"uploaded_count": len(uploaded_urls),
|
||||
"errors": errors if errors else None,
|
||||
"message": f"Successfully uploaded {len(uploaded_urls)} of {len(files)} images"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/filters/brands")
|
||||
def get_brands(db: Session = Depends(get_db)):
|
||||
"""Get list of unique brands from products"""
|
||||
brands = db.query(Product.brand).distinct().filter(Product.brand.isnot(None)).all()
|
||||
return [brand[0] for brand in brands if brand[0]]
|
||||
|
||||
|
||||
@router.get("/filters/sizes")
|
||||
def get_sizes(db: Session = Depends(get_db)):
|
||||
"""Get list of unique sizes from products"""
|
||||
products = db.query(Product.sizes).filter(Product.sizes.isnot(None)).all()
|
||||
sizes_set = set()
|
||||
for product in products:
|
||||
if product.sizes:
|
||||
for size in product.sizes:
|
||||
sizes_set.add(size)
|
||||
return sorted(list(sizes_set))
|
||||
|
||||
@ -3,33 +3,30 @@ from sqlalchemy.orm import Session
|
||||
from app.database.database import get_db
|
||||
from app.models import User
|
||||
from app.schemas.user import UserResponse, UserUpdate
|
||||
from app.services.auth import verify_token
|
||||
from app.services.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["users"])
|
||||
|
||||
|
||||
def get_current_user(token: str, db: Session = Depends(get_db)) -> User:
|
||||
user_id = verify_token(token)
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
)
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
def get_current_user_profile(token: str, db: Session = Depends(get_db)):
|
||||
user = get_current_user(token, db)
|
||||
return user
|
||||
def get_current_user_profile(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
return current_user
|
||||
|
||||
|
||||
@router.put("/me", response_model=UserResponse)
|
||||
def update_user_profile(token: str, user_update: UserUpdate, db: Session = Depends(get_db)):
|
||||
user = get_current_user(token, db)
|
||||
def update_user_profile(
|
||||
user_update: UserUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
for field, value in user_update.dict(exclude_unset=True).items():
|
||||
setattr(current_user, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(current_user)
|
||||
return current_user
|
||||
|
||||
for field, value in user_update.dict(exclude_unset=True).items():
|
||||
setattr(user, field, value)
|
||||
|
||||
@ -2,28 +2,20 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.database.database import get_db
|
||||
from app.models import Wishlist, Product
|
||||
from app.models import Wishlist, Product, User
|
||||
from app.schemas.product import ProductResponse
|
||||
from app.services.auth import verify_token
|
||||
from app.services.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/wishlist", tags=["wishlist"])
|
||||
|
||||
|
||||
def get_user_id_from_token(token: str) -> int:
|
||||
user_id = verify_token(token)
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
)
|
||||
return user_id
|
||||
|
||||
|
||||
@router.get("", response_model=List[ProductResponse])
|
||||
def get_wishlist(token: str, db: Session = Depends(get_db)):
|
||||
user_id = get_user_id_from_token(token)
|
||||
def get_wishlist(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
wishlist_items = (
|
||||
db.query(Wishlist).filter(Wishlist.user_id == user_id).all()
|
||||
db.query(Wishlist).filter(Wishlist.user_id == current_user.id).all()
|
||||
)
|
||||
products = [
|
||||
db.query(Product).filter(Product.id == item.product_id).first()
|
||||
@ -33,12 +25,14 @@ def get_wishlist(token: str, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/{product_id}")
|
||||
def add_to_wishlist(product_id: int, token: str, db: Session = Depends(get_db)):
|
||||
user_id = get_user_id_from_token(token)
|
||||
|
||||
def add_to_wishlist(
|
||||
product_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
existing = (
|
||||
db.query(Wishlist)
|
||||
.filter(Wishlist.user_id == user_id, Wishlist.product_id == product_id)
|
||||
.filter(Wishlist.user_id == current_user.id, Wishlist.product_id == product_id)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
@ -47,19 +41,21 @@ def add_to_wishlist(product_id: int, token: str, db: Session = Depends(get_db)):
|
||||
detail="Product already in wishlist",
|
||||
)
|
||||
|
||||
wishlist_item = Wishlist(user_id=user_id, product_id=product_id)
|
||||
wishlist_item = Wishlist(user_id=current_user.id, product_id=product_id)
|
||||
db.add(wishlist_item)
|
||||
db.commit()
|
||||
return {"message": "Product added to wishlist"}
|
||||
|
||||
|
||||
@router.delete("/{product_id}")
|
||||
def remove_from_wishlist(product_id: int, token: str, db: Session = Depends(get_db)):
|
||||
user_id = get_user_id_from_token(token)
|
||||
|
||||
def remove_from_wishlist(
|
||||
product_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
wishlist_item = (
|
||||
db.query(Wishlist)
|
||||
.filter(Wishlist.user_id == user_id, Wishlist.product_id == product_id)
|
||||
.filter(Wishlist.user_id == current_user.id, Wishlist.product_id == product_id)
|
||||
.first()
|
||||
)
|
||||
if not wishlist_item:
|
||||
|
||||
BIN
backend/app/schemas/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/app/schemas/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/brand.cpython-314.pyc
Normal file
BIN
backend/app/schemas/__pycache__/brand.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/cart.cpython-314.pyc
Normal file
BIN
backend/app/schemas/__pycache__/cart.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/category.cpython-314.pyc
Normal file
BIN
backend/app/schemas/__pycache__/category.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/contact.cpython-314.pyc
Normal file
BIN
backend/app/schemas/__pycache__/contact.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/model.cpython-314.pyc
Normal file
BIN
backend/app/schemas/__pycache__/model.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/order.cpython-314.pyc
Normal file
BIN
backend/app/schemas/__pycache__/order.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/product.cpython-314.pyc
Normal file
BIN
backend/app/schemas/__pycache__/product.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/user.cpython-314.pyc
Normal file
BIN
backend/app/schemas/__pycache__/user.cpython-314.pyc
Normal file
Binary file not shown.
16
backend/app/schemas/brand.py
Normal file
16
backend/app/schemas/brand.py
Normal file
@ -0,0 +1,16 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class BrandBase(BaseModel):
|
||||
name: str
|
||||
|
||||
class BrandCreate(BrandBase):
|
||||
pass
|
||||
|
||||
class BrandUpdate(BrandBase):
|
||||
pass
|
||||
|
||||
class BrandResponse(BrandBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@ -7,11 +7,13 @@ class CategoryCreate(BaseModel):
|
||||
name: str
|
||||
slug: str
|
||||
description: Optional[str] = None
|
||||
image: Optional[str] = None
|
||||
|
||||
|
||||
class CategoryUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
image: Optional[str] = None
|
||||
|
||||
|
||||
class CategoryResponse(BaseModel):
|
||||
@ -19,6 +21,7 @@ class CategoryResponse(BaseModel):
|
||||
name: str
|
||||
slug: str
|
||||
description: Optional[str]
|
||||
image: Optional[str]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
36
backend/app/schemas/model.py
Normal file
36
backend/app/schemas/model.py
Normal file
@ -0,0 +1,36 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class ModelCreate(BaseModel):
|
||||
name: str
|
||||
category_id: int
|
||||
brand: str
|
||||
base_price: Optional[Decimal] = None
|
||||
sizes: Optional[List[str]] = []
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class ModelUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
category_id: Optional[int] = None
|
||||
brand: Optional[str] = None
|
||||
base_price: Optional[Decimal] = None
|
||||
sizes: Optional[List[str]] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class ModelResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
category_id: int
|
||||
brand: str
|
||||
base_price: Optional[Decimal]
|
||||
sizes: Optional[List[str]]
|
||||
description: Optional[str]
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@ -1,30 +1,37 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
name: str
|
||||
slug: Optional[str] = None
|
||||
description: str
|
||||
price: float
|
||||
discount_price: Optional[float] = None
|
||||
category_id: int
|
||||
model_id: Optional[int] = None
|
||||
gender: str # men, women
|
||||
brand: str
|
||||
sizes: List[str]
|
||||
colors: List[str]
|
||||
colors: Optional[List[str]] = []
|
||||
stock: int
|
||||
images: List[str]
|
||||
is_featured: bool = False
|
||||
is_on_sale: bool = False
|
||||
override_price: Optional[Decimal] = None
|
||||
override_sizes: Optional[List[str]] = None
|
||||
|
||||
|
||||
class ProductUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
slug: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
price: Optional[float] = None
|
||||
discount_price: Optional[float] = None
|
||||
category_id: Optional[int] = None
|
||||
model_id: Optional[int] = None
|
||||
gender: Optional[str] = None
|
||||
brand: Optional[str] = None
|
||||
sizes: Optional[List[str]] = None
|
||||
@ -33,23 +40,29 @@ class ProductUpdate(BaseModel):
|
||||
images: Optional[List[str]] = None
|
||||
is_featured: Optional[bool] = None
|
||||
is_on_sale: Optional[bool] = None
|
||||
override_price: Optional[Decimal] = None
|
||||
override_sizes: Optional[List[str]] = None
|
||||
|
||||
|
||||
class ProductResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
slug: Optional[str]
|
||||
description: str
|
||||
price: float
|
||||
discount_price: Optional[float]
|
||||
category_id: int
|
||||
model_id: Optional[int]
|
||||
gender: str
|
||||
brand: str
|
||||
sizes: List[str]
|
||||
colors: List[str]
|
||||
colors: Optional[List[str]]
|
||||
stock: int
|
||||
images: List[str]
|
||||
is_featured: bool
|
||||
is_on_sale: bool
|
||||
override_price: Optional[Decimal]
|
||||
override_sizes: Optional[List[str]]
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
|
||||
@ -29,6 +29,7 @@ class UserResponse(UserBase):
|
||||
postal_code: Optional[str]
|
||||
country: Optional[str]
|
||||
is_active: bool
|
||||
is_admin: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
|
||||
BIN
backend/app/services/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/app/services/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/services/__pycache__/auth.cpython-314.pyc
Normal file
BIN
backend/app/services/__pycache__/auth.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/services/__pycache__/cart.cpython-314.pyc
Normal file
BIN
backend/app/services/__pycache__/cart.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/services/__pycache__/order.cpython-314.pyc
Normal file
BIN
backend/app/services/__pycache__/order.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/services/__pycache__/product.cpython-314.pyc
Normal file
BIN
backend/app/services/__pycache__/product.cpython-314.pyc
Normal file
Binary file not shown.
@ -2,11 +2,15 @@ from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from passlib.context import CryptContext
|
||||
from jose import JWTError, jwt
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from app.config import settings
|
||||
from app.models import User
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database.database import get_db
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
@ -37,11 +41,17 @@ def verify_token(token: str) -> Optional[int]:
|
||||
payload = jwt.decode(
|
||||
token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm]
|
||||
)
|
||||
user_id: int = payload.get("sub")
|
||||
if user_id is None:
|
||||
user_id_str: str = payload.get("sub")
|
||||
if user_id_str is None:
|
||||
return None
|
||||
# Convert string to integer
|
||||
user_id = int(user_id_str)
|
||||
return user_id
|
||||
except JWTError:
|
||||
except JWTError as e:
|
||||
return None
|
||||
except (ValueError, TypeError) as e:
|
||||
return None
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
@ -50,3 +60,32 @@ def authenticate_user(db: Session, email: str, password: str) -> Optional[User]:
|
||||
if not user or not verify_password(password, user.hashed_password):
|
||||
return None
|
||||
return user
|
||||
|
||||
|
||||
def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
token = credentials.credentials
|
||||
user_id = verify_token(token)
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
)
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found",
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
def get_current_admin_user(current_user: User = Depends(get_current_user)) -> User:
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin access required",
|
||||
)
|
||||
return current_user
|
||||
|
||||
32
backend/app/utils.py
Normal file
32
backend/app/utils.py
Normal file
@ -0,0 +1,32 @@
|
||||
import re
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
def generate_slug(text: str) -> str:
|
||||
"""Generate a URL-friendly slug from text"""
|
||||
# Convert to lowercase
|
||||
slug = text.lower()
|
||||
# Replace spaces and special characters with hyphens
|
||||
slug = re.sub(r'[^\w\s-]', '', slug)
|
||||
slug = re.sub(r'[\s_-]+', '-', slug)
|
||||
slug = re.sub(r'^-+|-+$', '', slug)
|
||||
return slug
|
||||
|
||||
def save_upload_file(upload_file, folder: str = "products") -> str:
|
||||
"""Save uploaded file and return the file path"""
|
||||
# Create uploads directory if it doesn't exist
|
||||
upload_dir = Path("uploads") / folder
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate unique filename
|
||||
file_extension = os.path.splitext(upload_file.filename)[1]
|
||||
unique_filename = f"{uuid.uuid4()}{file_extension}"
|
||||
file_path = upload_dir / unique_filename
|
||||
|
||||
# Save file
|
||||
with open(file_path, "wb") as buffer:
|
||||
buffer.write(upload_file.file.read())
|
||||
|
||||
# Return relative path for URL
|
||||
return f"/uploads/{folder}/{unique_filename}"
|
||||
24
backend/migrations/001_add_model_table.sql
Normal file
24
backend/migrations/001_add_model_table.sql
Normal file
@ -0,0 +1,24 @@
|
||||
-- Migration: Add model table and update product table
|
||||
-- Create model table
|
||||
CREATE TABLE IF NOT EXISTS model (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
brand VARCHAR(100) NOT NULL,
|
||||
base_price DECIMAL(10, 2),
|
||||
sizes JSONB,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
|
||||
UNIQUE (name, category_id, brand)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_model_category ON model(category_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_brand ON model(brand);
|
||||
|
||||
-- Add new columns to product table
|
||||
ALTER TABLE product ADD COLUMN IF NOT EXISTS model_id INTEGER REFERENCES model(id) ON DELETE SET NULL;
|
||||
ALTER TABLE product ADD COLUMN IF NOT EXISTS override_price DECIMAL(10, 2);
|
||||
ALTER TABLE product ADD COLUMN IF NOT EXISTS override_sizes JSONB;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_product_model ON product(model_id);
|
||||
24
backend/migrations/002_add_brands_table.sql
Normal file
24
backend/migrations/002_add_brands_table.sql
Normal file
@ -0,0 +1,24 @@
|
||||
-- Migration: Add brands table
|
||||
-- Date: 2026-05-01
|
||||
-- Description: Create brands table for storing brand names
|
||||
|
||||
CREATE TABLE IF NOT EXISTS brand (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_brand_name ON brand(name);
|
||||
|
||||
-- Insert existing brands from products
|
||||
INSERT INTO brand (name)
|
||||
SELECT DISTINCT brand
|
||||
FROM product
|
||||
WHERE brand IS NOT NULL AND brand != ''
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Insert existing brands from model table
|
||||
INSERT INTO brand (name)
|
||||
SELECT DISTINCT brand
|
||||
FROM model
|
||||
WHERE brand IS NOT NULL AND brand != ''
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
@ -1,11 +1,11 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
sqlalchemy==2.0.23
|
||||
psycopg2-binary==2.9.9
|
||||
python-dotenv==1.0.0
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
python-multipart==0.0.6
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
email-validator==2.1.0
|
||||
fastapi>=0.104.1
|
||||
uvicorn>=0.24.0
|
||||
sqlalchemy>=2.0.23
|
||||
psycopg2-binary>=2.9.11
|
||||
python-dotenv>=1.0.0
|
||||
pydantic>=2.5.0
|
||||
pydantic-settings>=2.1.0
|
||||
python-multipart>=0.0.6
|
||||
python-jose[cryptography]>=3.3.0
|
||||
passlib[bcrypt]>=1.7.4
|
||||
email-validator>=2.1.0
|
||||
|
||||
@ -1,6 +1,48 @@
|
||||
-- E-Commerce Database Schema
|
||||
-- Run this file to create tables and populate initial data
|
||||
-- psql -U ecommerce_user -d ecommerce_db -h localhost -f schema.sql
|
||||
-- Run this file to create database, user, tables and populate initial data
|
||||
-- Usage: psql -U postgres -h localhost -f schema.sql
|
||||
|
||||
-- ============================================
|
||||
-- CREATE DATABASE AND USER
|
||||
-- ============================================
|
||||
|
||||
-- Drop existing database and user if they exist (for clean setup)
|
||||
DROP DATABASE IF EXISTS ecommerce_db;
|
||||
DROP USER IF EXISTS ecommerce_user;
|
||||
|
||||
-- Create database (Windows compatible - uses system defaults)
|
||||
CREATE DATABASE ecommerce_db
|
||||
WITH
|
||||
OWNER = postgres
|
||||
ENCODING = 'UTF8'
|
||||
TABLESPACE = pg_default
|
||||
CONNECTION LIMIT = -1;
|
||||
|
||||
-- Create user with password
|
||||
CREATE USER ecommerce_user WITH PASSWORD 'password';
|
||||
|
||||
-- Grant all privileges on database to user
|
||||
GRANT ALL PRIVILEGES ON DATABASE ecommerce_db TO ecommerce_user;
|
||||
|
||||
-- Connect to the ecommerce_db database
|
||||
\c ecommerce_db
|
||||
|
||||
-- Grant schema privileges including CREATE permission
|
||||
GRANT ALL ON SCHEMA public TO ecommerce_user;
|
||||
GRANT CREATE ON SCHEMA public TO ecommerce_user;
|
||||
|
||||
-- Make ecommerce_user owner of public schema to avoid permission issues
|
||||
ALTER SCHEMA public OWNER TO ecommerce_user;
|
||||
|
||||
-- Grant privileges on all current and future tables
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ecommerce_user;
|
||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ecommerce_user;
|
||||
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ecommerce_user;
|
||||
|
||||
-- Grant privileges on future objects
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ecommerce_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ecommerce_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO ecommerce_user;
|
||||
|
||||
-- ============================================
|
||||
-- CREATE TABLES
|
||||
@ -11,9 +53,27 @@ CREATE TABLE category (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
slug VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT
|
||||
description TEXT,
|
||||
image VARCHAR(500)
|
||||
);
|
||||
|
||||
-- Model Table (e.g., New Balance 9060)
|
||||
CREATE TABLE model (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
brand VARCHAR(100) NOT NULL,
|
||||
base_price DECIMAL(10, 2),
|
||||
sizes JSONB,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
|
||||
UNIQUE (name, category_id, brand)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_model_category ON model(category_id);
|
||||
CREATE INDEX idx_model_brand ON model(brand);
|
||||
|
||||
-- User Table
|
||||
CREATE TABLE "user" (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@ -26,6 +86,7 @@ CREATE TABLE "user" (
|
||||
postal_code VARCHAR(20),
|
||||
country VARCHAR(100),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_admin BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
@ -33,10 +94,12 @@ CREATE TABLE "user" (
|
||||
CREATE TABLE product (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) UNIQUE,
|
||||
description TEXT,
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
discount_price DECIMAL(10, 2),
|
||||
category_id INTEGER NOT NULL,
|
||||
model_id INTEGER,
|
||||
gender VARCHAR(50),
|
||||
brand VARCHAR(100),
|
||||
sizes JSONB,
|
||||
@ -45,10 +108,16 @@ CREATE TABLE product (
|
||||
images JSONB,
|
||||
is_featured BOOLEAN DEFAULT FALSE,
|
||||
is_on_sale BOOLEAN DEFAULT FALSE,
|
||||
override_price DECIMAL(10, 2),
|
||||
override_sizes JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE
|
||||
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (model_id) REFERENCES model(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_product_slug ON product(slug);
|
||||
CREATE INDEX idx_product_model ON product(model_id);
|
||||
|
||||
-- Cart Table
|
||||
CREATE TABLE cart (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@ -284,61 +353,88 @@ INSERT INTO product (name, description, price, discount_price, category_id, gend
|
||||
);
|
||||
|
||||
-- Insert Sample Users (for testing)
|
||||
INSERT INTO "user" (email, hashed_password, full_name, phone, address, city, postal_code, country, is_active) VALUES
|
||||
INSERT INTO "user" (email, hashed_password, full_name, phone, address, city, postal_code, country, is_active, is_admin) VALUES
|
||||
(
|
||||
'admin@example.com',
|
||||
'$2b$12$jNWZdZNMbKEkOjA8Gq3ZUOTml23zfqhFDPJ8AlZQ51WhUyi04AH7C', -- password: password123
|
||||
'Admin User',
|
||||
'1234567890',
|
||||
'123 Admin Street',
|
||||
'New York',
|
||||
'10001',
|
||||
'USA',
|
||||
TRUE,
|
||||
TRUE
|
||||
),
|
||||
(
|
||||
'user@example.com',
|
||||
'$2b$12$KIXxPfROLqWHYIgC5FCOO.7yqKU8RvOmOhP7kJZnYLh6pJn2FSfKy', -- password: password123
|
||||
'$2b$12$jNWZdZNMbKEkOjA8Gq3ZUOTml23zfqhFDPJ8AlZQ51WhUyi04AH7C', -- password: password123
|
||||
'John Doe',
|
||||
'1234567890',
|
||||
'123 Main Street',
|
||||
'New York',
|
||||
'10001',
|
||||
'USA',
|
||||
TRUE
|
||||
TRUE,
|
||||
FALSE
|
||||
),
|
||||
(
|
||||
'jane@example.com',
|
||||
'$2b$12$KIXxPfROLqWHYIgC5FCOO.7yqKU8RvOmOhP7kJZnYLh6pJn2FSfKy', -- password: password123
|
||||
'$2b$12$jNWZdZNMbKEkOjA8Gq3ZUOTml23zfqhFDPJ8AlZQ51WhUyi04AH7C', -- password: password123
|
||||
'Jane Smith',
|
||||
'9876543210',
|
||||
'456 Oak Avenue',
|
||||
'Los Angeles',
|
||||
'90001',
|
||||
'USA',
|
||||
TRUE
|
||||
TRUE,
|
||||
FALSE
|
||||
);
|
||||
|
||||
-- Create carts for users
|
||||
INSERT INTO cart (user_id) VALUES
|
||||
(1),
|
||||
(2);
|
||||
(2),
|
||||
(3);
|
||||
|
||||
-- ============================================
|
||||
-- SET PERMISSIONS
|
||||
-- SET PERMISSIONS (After all tables are created)
|
||||
-- ============================================
|
||||
-- Grant all privileges on all tables, sequences, and functions to the user
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ecommerce_user;
|
||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ecommerce_user;
|
||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ecommerce_user;
|
||||
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ecommerce_user;
|
||||
|
||||
-- Ensure future objects also get permissions
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ecommerce_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ecommerce_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO ecommerce_user;
|
||||
|
||||
-- ============================================
|
||||
-- COMPLETE
|
||||
-- ============================================
|
||||
-- Schema created successfully!
|
||||
--
|
||||
-- User: ecommerce_user
|
||||
-- Database: ecommerce_db
|
||||
-- User: ecommerce_user
|
||||
-- Password: password
|
||||
-- Host: localhost:5432
|
||||
--
|
||||
-- Connection string for .env file:
|
||||
-- DATABASE_URL=postgresql://ecommerce_user:password@localhost:5432/ecommerce_db
|
||||
--
|
||||
-- Tables created: 9
|
||||
-- - category, user, product, cart, cart_item
|
||||
-- - order, order_item, user_wishlist, contact_message
|
||||
--
|
||||
-- Demo accounts:
|
||||
-- - user@example.com / password123
|
||||
-- - jane@example.com / password123
|
||||
-- - ADMIN: admin@example.com / password123 (is_admin: true)
|
||||
-- - USER: user@example.com / password123
|
||||
-- - USER: jane@example.com / password123
|
||||
--
|
||||
-- Sample data: 5 categories, 9 products, 2 users
|
||||
-- Sample data: 5 categories, 9 products, 3 users
|
||||
--
|
||||
-- Next steps:
|
||||
-- 1. Update backend/.env with your credentials
|
||||
-- 2. Run: uvicorn app.main:app --reload --port 8000
|
||||
-- 1. Update backend/.env with: DATABASE_URL=postgresql://ecommerce_user:password@localhost:5432/ecommerce_db
|
||||
-- 2. Run: python backend/app/main.py
|
||||
-- 3. Login as admin to create/edit/delete products
|
||||
|
||||
BIN
ecommerce.db
Normal file
BIN
ecommerce.db
Normal file
Binary file not shown.
@ -19,6 +19,8 @@ import Wishlist from './pages/Wishlist'
|
||||
import About from './pages/About'
|
||||
import Contact from './pages/Contact'
|
||||
import Sales from './pages/Sales'
|
||||
import Admin from './pages/Admin'
|
||||
import Models from './pages/Models'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@ -42,6 +44,8 @@ function App() {
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
<Route path="/sales" element={<Sales />} />
|
||||
<Route path="/admin" element={<Admin />} />
|
||||
<Route path="/admin/models" element={<Models />} />
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@ -6,12 +6,11 @@ const api = axios.create({
|
||||
baseURL: API_URL,
|
||||
})
|
||||
|
||||
// Add token to requests
|
||||
// Add token to requests as Bearer token in Authorization header
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.params = config.params || {}
|
||||
config.params.token = token
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default function CategoryCard({ category }) {
|
||||
const categoryImage = `https://via.placeholder.com/300x300?text=${category.name}`
|
||||
const categoryImage = category.image || `https://via.placeholder.com/300x300?text=${category.name}`
|
||||
|
||||
return (
|
||||
<Link to={`/products?category=${category.slug}`} className="category-card">
|
||||
|
||||
@ -32,6 +32,9 @@ export default function Navbar() {
|
||||
<li><Link to="/sales">Sales</Link></li>
|
||||
<li><Link to="/about">About</Link></li>
|
||||
<li><Link to="/contact">Contact</Link></li>
|
||||
{user?.is_admin && (
|
||||
<li><Link to="/admin" style={{ color: '#ff6b6b', fontWeight: 'bold' }}>Admin</Link></li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<div className="navbar-icons">
|
||||
|
||||
@ -1,19 +1,83 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import api from '../api'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function ProductFilters({ onFilter }) {
|
||||
const [filters, setFilters] = useState({
|
||||
gender: '',
|
||||
priceRange: 'all',
|
||||
brand: '',
|
||||
model_id: '',
|
||||
size: '',
|
||||
min_price: '',
|
||||
max_price: '',
|
||||
onSale: false,
|
||||
})
|
||||
|
||||
const [brands, setBrands] = useState([])
|
||||
const [models, setModels] = useState([])
|
||||
const [sizes, setSizes] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
fetchBrands()
|
||||
fetchSizes()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (filters.brand) {
|
||||
fetchModels(filters.brand)
|
||||
} else {
|
||||
setModels([])
|
||||
setFilters(prev => ({ ...prev, model_id: '' }))
|
||||
}
|
||||
}, [filters.brand])
|
||||
|
||||
const fetchBrands = async () => {
|
||||
try {
|
||||
const response = await api.get('/products/filters/brands')
|
||||
setBrands(response.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching brands:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchModels = async (brand) => {
|
||||
try {
|
||||
const response = await api.get(`/models?brand=${brand}`)
|
||||
setModels(response.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching models:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSizes = async () => {
|
||||
try {
|
||||
const response = await api.get('/products/filters/sizes')
|
||||
setSizes(response.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching sizes:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFilterChange = (key, value) => {
|
||||
const newFilters = { ...filters, [key]: value }
|
||||
setFilters(newFilters)
|
||||
onFilter(newFilters)
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
const resetFilters = {
|
||||
gender: '',
|
||||
brand: '',
|
||||
model_id: '',
|
||||
size: '',
|
||||
min_price: '',
|
||||
max_price: '',
|
||||
onSale: false,
|
||||
}
|
||||
setFilters(resetFilters)
|
||||
onFilter(resetFilters)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="filters-sidebar">
|
||||
<h3>Filters</h3>
|
||||
@ -31,19 +95,73 @@ export default function ProductFilters({ onFilter }) {
|
||||
</div>
|
||||
|
||||
<div className="filter-group">
|
||||
<label>Price Range</label>
|
||||
<label>Brand</label>
|
||||
<select
|
||||
value={filters.priceRange}
|
||||
onChange={(e) => handleFilterChange('priceRange', e.target.value)}
|
||||
value={filters.brand}
|
||||
onChange={(e) => handleFilterChange('brand', e.target.value)}
|
||||
>
|
||||
<option value="all">All Prices</option>
|
||||
<option value="0-50">$0 - $50</option>
|
||||
<option value="50-100">$50 - $100</option>
|
||||
<option value="100-200">$100 - $200</option>
|
||||
<option value="200+">$200+</option>
|
||||
<option value="">All Brands</option>
|
||||
{brands.map((brand, index) => (
|
||||
<option key={index} value={brand}>
|
||||
{brand}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{filters.brand && models.length > 0 && (
|
||||
<div className="filter-group">
|
||||
<label>Model</label>
|
||||
<select
|
||||
value={filters.model_id}
|
||||
onChange={(e) => handleFilterChange('model_id', e.target.value)}
|
||||
>
|
||||
<option value="">All Models</option>
|
||||
{models.map((model) => (
|
||||
<option key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="filter-group">
|
||||
<label>Size</label>
|
||||
<select
|
||||
value={filters.size}
|
||||
onChange={(e) => handleFilterChange('size', e.target.value)}
|
||||
>
|
||||
<option value="">All Sizes</option>
|
||||
{sizes.map((size, index) => (
|
||||
<option key={index} value={size}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="filter-group">
|
||||
<label>Price Range</label>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Min"
|
||||
value={filters.min_price}
|
||||
onChange={(e) => handleFilterChange('min_price', e.target.value)}
|
||||
style={{ width: '80px', padding: '0.5rem' }}
|
||||
/>
|
||||
<span>-</span>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Max"
|
||||
value={filters.max_price}
|
||||
onChange={(e) => handleFilterChange('max_price', e.target.value)}
|
||||
style={{ width: '80px', padding: '0.5rem' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="filter-group">
|
||||
<label>
|
||||
<input
|
||||
@ -55,7 +173,7 @@ export default function ProductFilters({ onFilter }) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button className="btn btn-full" onClick={() => window.location.reload()}>
|
||||
<button className="btn btn-full" onClick={resetFilters}>
|
||||
Reset Filters
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -1,20 +1,37 @@
|
||||
import React, { createContext, useState, useEffect } from 'react'
|
||||
import api from '../api'
|
||||
|
||||
export const AuthContext = createContext()
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null)
|
||||
const [token, setToken] = useState(localStorage.getItem('token'))
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
localStorage.setItem('token', token)
|
||||
fetchUserData()
|
||||
} else {
|
||||
localStorage.removeItem('token')
|
||||
setLoading(false)
|
||||
}
|
||||
}, [token])
|
||||
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
const response = await api.get('/users/me')
|
||||
setUser(response.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching user data:', error)
|
||||
// If token is invalid, clear it
|
||||
setToken(null)
|
||||
setUser(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
setUser(null)
|
||||
setToken(null)
|
||||
@ -23,7 +40,7 @@ export const AuthProvider = ({ children }) => {
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, setUser, token, setToken, loading, setLoading, logout }}>
|
||||
{children}
|
||||
{!loading && children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@ -18,12 +18,11 @@ export const CartProvider = ({ children }) => {
|
||||
setTotal(newTotal)
|
||||
}
|
||||
|
||||
const addToCart = (product, quantity = 1, size = null, color = null) => {
|
||||
const addToCart = (product, quantity = 1, size = null) => {
|
||||
const existingItem = cart.find(
|
||||
(item) =>
|
||||
item.product.id === product.id &&
|
||||
item.size === size &&
|
||||
item.color === color
|
||||
item.size === size
|
||||
)
|
||||
|
||||
if (existingItem) {
|
||||
@ -35,7 +34,7 @@ export const CartProvider = ({ children }) => {
|
||||
)
|
||||
)
|
||||
} else {
|
||||
setCart([...cart, { product, quantity, size, color }])
|
||||
setCart([...cart, { product, quantity, size }])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1224
frontend/src/pages/Admin.jsx
Normal file
1224
frontend/src/pages/Admin.jsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -52,7 +52,6 @@ export default function Cart() {
|
||||
<div>
|
||||
<p className="product-name">{item.product.name}</p>
|
||||
{item.size && <p>Size: {item.size}</p>}
|
||||
{item.color && <p>Color: {item.color}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -33,9 +33,7 @@ export default function Checkout() {
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await api.post('/orders', formData, {
|
||||
params: { token },
|
||||
})
|
||||
const response = await api.post('/orders', formData)
|
||||
|
||||
alert('Order placed successfully!')
|
||||
clearCart()
|
||||
|
||||
@ -85,9 +85,11 @@ export default function Login() {
|
||||
</p>
|
||||
|
||||
<div className="demo-account">
|
||||
<p><strong>Demo Account:</strong></p>
|
||||
<p>Email: user@example.com</p>
|
||||
<p>Password: password123</p>
|
||||
<p><strong>Demo Accounts:</strong></p>
|
||||
<p style={{ color: '#ff6b6b', fontWeight: 'bold' }}>
|
||||
Admin: admin@example.com / password123
|
||||
</p>
|
||||
<p>User: user@example.com / password123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
331
frontend/src/pages/Models.jsx
Normal file
331
frontend/src/pages/Models.jsx
Normal file
@ -0,0 +1,331 @@
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import api from '../api'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function Models() {
|
||||
const navigate = useNavigate()
|
||||
const { user } = useContext(AuthContext)
|
||||
const [models, setModels] = useState([])
|
||||
const [categories, setCategories] = useState([])
|
||||
const [brands, setBrands] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [editingModel, setEditingModel] = useState(null)
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
category_id: '',
|
||||
brand: '',
|
||||
base_price: '',
|
||||
sizes: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
// Redirect if not admin
|
||||
useEffect(() => {
|
||||
if (!user?.is_admin) {
|
||||
navigate('/')
|
||||
}
|
||||
}, [user, navigate])
|
||||
|
||||
useEffect(() => {
|
||||
fetchModels()
|
||||
fetchCategories()
|
||||
fetchBrands()
|
||||
}, [])
|
||||
|
||||
const fetchModels = async () => {
|
||||
try {
|
||||
const response = await api.get('/models')
|
||||
setModels(response.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching models:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const response = await api.get('/categories')
|
||||
setCategories(response.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchBrands = async () => {
|
||||
try {
|
||||
const response = await api.get('/brands')
|
||||
setBrands(response.data.map(b => b.name).sort())
|
||||
} catch (error) {
|
||||
console.error('Error fetching brands:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target
|
||||
setFormData({ ...formData, [name]: value })
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const modelData = {
|
||||
...formData,
|
||||
category_id: parseInt(formData.category_id),
|
||||
base_price: formData.base_price ? parseFloat(formData.base_price) : null,
|
||||
sizes: formData.sizes ? formData.sizes.split(',').map(s => s.trim()) : [],
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingModel) {
|
||||
await api.put(`/models/${editingModel.id}`, modelData)
|
||||
alert('Model updated successfully!')
|
||||
} else {
|
||||
await api.post('/models', modelData)
|
||||
alert('Model created successfully!')
|
||||
}
|
||||
setShowForm(false)
|
||||
setEditingModel(null)
|
||||
resetForm()
|
||||
fetchModels()
|
||||
} catch (error) {
|
||||
console.error('Error saving model:', error)
|
||||
alert('Error saving model: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (model) => {
|
||||
setEditingModel(model)
|
||||
setFormData({
|
||||
name: model.name || '',
|
||||
category_id: model.category_id || '',
|
||||
brand: model.brand || '',
|
||||
base_price: model.base_price || '',
|
||||
sizes: Array.isArray(model.sizes) ? model.sizes.join(', ') : '',
|
||||
description: model.description || '',
|
||||
})
|
||||
setShowForm(true)
|
||||
setTimeout(() => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm('Are you sure you want to delete this model? This will unlink all associated products.')) return
|
||||
|
||||
try {
|
||||
await api.delete(`/models/${id}`)
|
||||
alert('Model deleted successfully!')
|
||||
fetchModels()
|
||||
} catch (error) {
|
||||
console.error('Error deleting model:', error)
|
||||
alert('Error deleting model: ' + (error.response?.data?.detail || 'Unknown error'))
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
name: '',
|
||||
category_id: '',
|
||||
brand: '',
|
||||
base_price: '',
|
||||
sizes: '',
|
||||
description: '',
|
||||
})
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setShowForm(false)
|
||||
setEditingModel(null)
|
||||
resetForm()
|
||||
}
|
||||
|
||||
if (!user?.is_admin) {
|
||||
return null
|
||||
}
|
||||
|
||||
const getCategoryName = (categoryId) => {
|
||||
const category = categories.find(c => c.id === categoryId)
|
||||
return category ? category.name : 'Unknown'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="models-page" style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||||
<h1>Manage Models</h1>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setShowForm(!showForm)
|
||||
setEditingModel(null)
|
||||
resetForm()
|
||||
}}
|
||||
>
|
||||
{showForm ? 'Cancel' : '+ Add New Model'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<div style={{ backgroundColor: '#f5f5f5', padding: '2rem', borderRadius: '8px', marginBottom: '2rem' }}>
|
||||
<h2>{editingModel ? 'Edit Model' : 'Create New Model'}</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||
<div className="form-group">
|
||||
<label>Model Name * (e.g., 9060, Air Max 90)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="9060"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Brand *</label>
|
||||
<select
|
||||
name="brand"
|
||||
value={formData.brand}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Select Brand</option>
|
||||
{brands.map(brand => (
|
||||
<option key={brand} value={brand}>{brand}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Category *</label>
|
||||
<select name="category_id" value={formData.category_id} onChange={handleChange} required>
|
||||
<option value="">Select Category</option>
|
||||
{categories.map(category => (
|
||||
<option key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Base Price (Default for all products)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="base_price"
|
||||
value={formData.base_price}
|
||||
onChange={handleChange}
|
||||
placeholder="129.99"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||||
<label>Default Sizes (comma separated)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="sizes"
|
||||
value={formData.sizes}
|
||||
onChange={handleChange}
|
||||
placeholder="7, 7.5, 8, 8.5, 9, 9.5, 10, 10.5, 11, 11.5, 12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||||
<label>Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows="4"
|
||||
placeholder="Model description..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '1.5rem' }}>
|
||||
<button type="submit" className="btn btn-primary" style={{ marginRight: '1rem' }}>
|
||||
{editingModel ? 'Update Model' : 'Create Model'}
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h2>Models ({models.length})</h2>
|
||||
{loading ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '1rem' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#f0f0f0', textAlign: 'left' }}>
|
||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>ID</th>
|
||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Model Name</th>
|
||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Brand</th>
|
||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Category</th>
|
||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Base Price</th>
|
||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Sizes</th>
|
||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{models.map(model => (
|
||||
<tr key={model.id}>
|
||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{model.id}</td>
|
||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{model.name}</td>
|
||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{model.brand}</td>
|
||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{getCategoryName(model.category_id)}</td>
|
||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
||||
{model.base_price ? `$${parseFloat(model.base_price).toFixed(2)}` : '-'}
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em' }}>
|
||||
{model.sizes ? model.sizes.join(', ') : '-'}
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
||||
<button
|
||||
onClick={() => handleEdit(model)}
|
||||
style={{
|
||||
marginRight: '0.5rem',
|
||||
padding: '0.25rem 0.75rem',
|
||||
backgroundColor: '#4CAF50',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(model.id)}
|
||||
style={{
|
||||
padding: '0.25rem 0.75rem',
|
||||
backgroundColor: '#f44336',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -20,9 +20,7 @@ export default function Orders() {
|
||||
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
const response = await api.get('/orders/user/orders', {
|
||||
params: { token },
|
||||
})
|
||||
const response = await api.get('/orders')
|
||||
setOrders(response.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching orders:', error)
|
||||
|
||||
@ -11,9 +11,9 @@ export default function ProductDetail() {
|
||||
const [product, setProduct] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [selectedSize, setSelectedSize] = useState('')
|
||||
const [selectedColor, setSelectedColor] = useState('')
|
||||
const [quantity, setQuantity] = useState(1)
|
||||
const [inWishlist, setInWishlist] = useState(false)
|
||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0)
|
||||
const { addToCart } = useContext(CartContext)
|
||||
const { token } = useContext(AuthContext)
|
||||
|
||||
@ -25,7 +25,6 @@ export default function ProductDetail() {
|
||||
try {
|
||||
const response = await api.get(`/products/${id}`)
|
||||
setProduct(response.data)
|
||||
if (response.data.colors.length > 0) setSelectedColor(response.data.colors[0])
|
||||
if (response.data.sizes.length > 0) setSelectedSize(response.data.sizes[0])
|
||||
} catch (error) {
|
||||
console.error('Error fetching product:', error)
|
||||
@ -45,11 +44,10 @@ export default function ProductDetail() {
|
||||
product_id: product.id,
|
||||
quantity,
|
||||
size: selectedSize,
|
||||
color: selectedColor,
|
||||
token,
|
||||
})
|
||||
|
||||
addToCart(product, quantity, selectedSize, selectedColor)
|
||||
addToCart(product, quantity, selectedSize)
|
||||
alert('Product added to cart!')
|
||||
} catch (error) {
|
||||
console.error('Error adding to cart:', error)
|
||||
@ -64,9 +62,9 @@ export default function ProductDetail() {
|
||||
|
||||
try {
|
||||
if (inWishlist) {
|
||||
await api.delete(`/wishlist/${product.id}`, { params: { token } })
|
||||
await api.delete(`/wishlist/${product.id}`)
|
||||
} else {
|
||||
await api.post(`/wishlist/${product.id}`, null, { params: { token } })
|
||||
await api.post(`/wishlist/${product.id}`)
|
||||
}
|
||||
setInWishlist(!inWishlist)
|
||||
} catch (error) {
|
||||
@ -83,11 +81,54 @@ export default function ProductDetail() {
|
||||
<div className="product-detail">
|
||||
<div className="detail-container">
|
||||
<div className="detail-images">
|
||||
<img src={product.images[0]} alt={product.name} />
|
||||
{product.images.slice(1).map((img, idx) => (
|
||||
<img key={idx} src={img} alt={`View ${idx + 2}`} />
|
||||
{/* Main Image */}
|
||||
<div className="main-image-container">
|
||||
<img
|
||||
src={product.images[selectedImageIndex] || product.images[0]}
|
||||
alt={product.name}
|
||||
className="main-image"
|
||||
/>
|
||||
|
||||
{/* Navigation Arrows */}
|
||||
{product.images.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
className="nav-arrow prev"
|
||||
onClick={() => setSelectedImageIndex((prev) =>
|
||||
prev === 0 ? product.images.length - 1 : prev - 1
|
||||
)}
|
||||
aria-label="Previous image"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
className="nav-arrow next"
|
||||
onClick={() => setSelectedImageIndex((prev) =>
|
||||
prev === product.images.length - 1 ? 0 : prev + 1
|
||||
)}
|
||||
aria-label="Next image"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Thumbnail Gallery */}
|
||||
{product.images.length > 1 && (
|
||||
<div className="thumbnail-gallery">
|
||||
{product.images.map((img, idx) => (
|
||||
<img
|
||||
key={idx}
|
||||
src={img}
|
||||
alt={`View ${idx + 1}`}
|
||||
className={`thumbnail ${selectedImageIndex === idx ? 'active' : ''}`}
|
||||
onClick={() => setSelectedImageIndex(idx)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="detail-info">
|
||||
<h1>{product.name}</h1>
|
||||
@ -110,24 +151,6 @@ export default function ProductDetail() {
|
||||
|
||||
<p className="description">{product.description}</p>
|
||||
|
||||
{product.colors.length > 0 && (
|
||||
<div className="option-group">
|
||||
<label>Color:</label>
|
||||
<div className="color-options">
|
||||
{product.colors.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
className={`color-btn ${selectedColor === color ? 'active' : ''}`}
|
||||
onClick={() => setSelectedColor(color)}
|
||||
title={color}
|
||||
>
|
||||
{color}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{product.sizes.length > 0 && (
|
||||
<div className="option-group">
|
||||
<label>Size:</label>
|
||||
|
||||
@ -10,16 +10,17 @@ export default function Products() {
|
||||
const [products, setProducts] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [sortBy, setSortBy] = useState('latest')
|
||||
const [activeFilters, setActiveFilters] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts()
|
||||
}, [searchParams])
|
||||
}, [searchParams, activeFilters, sortBy])
|
||||
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const categorySlug = searchParams.get('category')
|
||||
const params = { limit: 50 }
|
||||
const params = { limit: 50, ...activeFilters }
|
||||
|
||||
if (categorySlug) {
|
||||
// Get category by slug
|
||||
@ -28,9 +29,16 @@ export default function Products() {
|
||||
if (category) params.category_id = category.id
|
||||
}
|
||||
|
||||
// Convert filter values to API parameters
|
||||
if (activeFilters.min_price) params.min_price = parseFloat(activeFilters.min_price)
|
||||
if (activeFilters.max_price) params.max_price = parseFloat(activeFilters.max_price)
|
||||
if (activeFilters.onSale) params.on_sale = true
|
||||
if (activeFilters.model_id) params.model_id = parseInt(activeFilters.model_id)
|
||||
|
||||
const response = await api.get('/products', { params })
|
||||
let sorted = [...response.data]
|
||||
|
||||
// Apply client-side sorting
|
||||
if (sortBy === 'price-low') {
|
||||
sorted.sort((a, b) => (a.discount_price || a.price) - (b.discount_price || b.price))
|
||||
} else if (sortBy === 'price-high') {
|
||||
@ -48,8 +56,7 @@ export default function Products() {
|
||||
}
|
||||
|
||||
const handleFilter = (filters) => {
|
||||
// Apply filters locally for now
|
||||
console.log('Filters:', filters)
|
||||
setActiveFilters(filters)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -28,9 +28,7 @@ export default function Profile() {
|
||||
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const response = await api.get('/users/me', {
|
||||
params: { token },
|
||||
})
|
||||
const response = await api.get('/users/me')
|
||||
setFormData(response.data)
|
||||
setUser(response.data)
|
||||
} catch (error) {
|
||||
@ -69,7 +67,21 @@ export default function Profile() {
|
||||
|
||||
return (
|
||||
<div className="profile-page">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1rem' }}>
|
||||
<h1>My Profile</h1>
|
||||
{user?.is_admin && (
|
||||
<span style={{
|
||||
backgroundColor: '#ff6b6b',
|
||||
color: 'white',
|
||||
padding: '0.25rem 0.75rem',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
ADMIN
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="profile-container">
|
||||
<form onSubmit={handleSubmit} className="profile-form">
|
||||
|
||||
@ -21,9 +21,7 @@ export default function Wishlist() {
|
||||
|
||||
const fetchWishlist = async () => {
|
||||
try {
|
||||
const response = await api.get('/wishlist', {
|
||||
params: { token },
|
||||
})
|
||||
const response = await api.get('/wishlist')
|
||||
setWishlistItems(response.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching wishlist:', error)
|
||||
@ -34,9 +32,7 @@ export default function Wishlist() {
|
||||
|
||||
const handleRemoveFromWishlist = async (productId) => {
|
||||
try {
|
||||
await api.delete(`/wishlist/${productId}`, {
|
||||
params: { token },
|
||||
})
|
||||
await api.delete(`/wishlist/${productId}`)
|
||||
setWishlistItems(
|
||||
wishlistItems.filter((item) => item.id !== productId)
|
||||
)
|
||||
|
||||
@ -542,6 +542,83 @@ button {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.main-image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.main-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav-arrow:hover {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
.nav-arrow.prev {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.nav-arrow.next {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.thumbnail-gallery {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.3s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.thumbnail:hover {
|
||||
border-color: var(--primary-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.thumbnail.active {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.detail-images img {
|
||||
width: 100%;
|
||||
max-height: 500px;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user