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/.pytest_cache/
|
||||||
backend/.vscode/
|
backend/.vscode/
|
||||||
backend/instance/
|
backend/instance/
|
||||||
|
uploads/
|
||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
frontend/node_modules/
|
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_SECRET_KEY=your-secret-key-here-change-this-in-production
|
||||||
JWT_ALGORITHM=HS256
|
JWT_ALGORITHM=HS256
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
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 pydantic_settings import BaseSettings
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
@ -9,7 +10,7 @@ class Settings(BaseSettings):
|
|||||||
frontend_url: str = "http://localhost:5173"
|
frontend_url: str = "http://localhost:5173"
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = str(Path(__file__).parent.parent / ".env")
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
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 import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
import uvicorn
|
||||||
from app.database.database import engine, Base
|
from app.database.database import engine, Base
|
||||||
from app.config import settings
|
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
|
# Create tables
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
@ -27,11 +41,16 @@ app.include_router(auth.router)
|
|||||||
app.include_router(users.router)
|
app.include_router(users.router)
|
||||||
app.include_router(products.router)
|
app.include_router(products.router)
|
||||||
app.include_router(categories.router)
|
app.include_router(categories.router)
|
||||||
|
app.include_router(models.router)
|
||||||
|
app.include_router(brands.router)
|
||||||
app.include_router(cart.router)
|
app.include_router(cart.router)
|
||||||
app.include_router(orders.router)
|
app.include_router(orders.router)
|
||||||
app.include_router(wishlist.router)
|
app.include_router(wishlist.router)
|
||||||
app.include_router(contact.router)
|
app.include_router(contact.router)
|
||||||
|
|
||||||
|
# Mount static files for uploads
|
||||||
|
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def read_root():
|
def read_root():
|
||||||
@ -45,3 +64,7 @@ def read_root():
|
|||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health_check():
|
def health_check():
|
||||||
return {"status": "healthy"}
|
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 .user import User
|
||||||
from .product import Product
|
from .product import Product
|
||||||
from .category import Category
|
from .category import Category
|
||||||
|
from .model import Model
|
||||||
from .cart import Cart, CartItem
|
from .cart import Cart, CartItem
|
||||||
from .order import Order, OrderItem
|
from .order import Order, OrderItem
|
||||||
from .wishlist import Wishlist
|
from .wishlist import Wishlist
|
||||||
@ -10,6 +11,7 @@ __all__ = [
|
|||||||
"User",
|
"User",
|
||||||
"Product",
|
"Product",
|
||||||
"Category",
|
"Category",
|
||||||
|
"Model",
|
||||||
"Cart",
|
"Cart",
|
||||||
"CartItem",
|
"CartItem",
|
||||||
"Order",
|
"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)
|
name = Column(String, unique=True, index=True)
|
||||||
slug = Column(String, unique=True, index=True)
|
slug = Column(String, unique=True, index=True)
|
||||||
description = Column(String, nullable=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 sqlalchemy.orm import relationship
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.database.database import Base
|
from app.database.database import Base
|
||||||
@ -9,10 +9,12 @@ class Product(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
name = Column(String, index=True)
|
name = Column(String, index=True)
|
||||||
|
slug = Column(String, unique=True, index=True, nullable=True)
|
||||||
description = Column(Text)
|
description = Column(Text)
|
||||||
price = Column(Float)
|
price = Column(Float)
|
||||||
discount_price = Column(Float, nullable=True)
|
discount_price = Column(Float, nullable=True)
|
||||||
category_id = Column(Integer, ForeignKey("category.id"))
|
category_id = Column(Integer, ForeignKey("category.id"))
|
||||||
|
model_id = Column(Integer, ForeignKey("model.id", ondelete="SET NULL"), nullable=True)
|
||||||
gender = Column(String) # men, women
|
gender = Column(String) # men, women
|
||||||
brand = Column(String)
|
brand = Column(String)
|
||||||
sizes = Column(JSON) # ["S", "M", "L", "XL", ...]
|
sizes = Column(JSON) # ["S", "M", "L", "XL", ...]
|
||||||
@ -21,8 +23,11 @@ class Product(Base):
|
|||||||
images = Column(JSON) # Array of image URLs
|
images = Column(JSON) # Array of image URLs
|
||||||
is_featured = Column(Boolean, default=False)
|
is_featured = Column(Boolean, default=False)
|
||||||
is_on_sale = 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)
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
category = relationship("Category")
|
category = relationship("Category")
|
||||||
|
model = relationship("Model", back_populates="products")
|
||||||
cart_items = relationship("CartItem", back_populates="product")
|
cart_items = relationship("CartItem", back_populates="product")
|
||||||
order_items = relationship("OrderItem", back_populates="product")
|
order_items = relationship("OrderItem", back_populates="product")
|
||||||
|
|||||||
@ -24,6 +24,7 @@ class User(Base):
|
|||||||
postal_code = Column(String, nullable=True)
|
postal_code = Column(String, nullable=True)
|
||||||
country = Column(String, nullable=True)
|
country = Column(String, nullable=True)
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
|
is_admin = Column(Boolean, default=False)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow)
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
cart = relationship("Cart", back_populates="user", uselist=False)
|
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_expires = timedelta(minutes=settings.access_token_expire_minutes)
|
||||||
access_token = create_access_token(
|
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 {
|
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,
|
remove_from_cart,
|
||||||
clear_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"])
|
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)
|
@router.get("", response_model=CartResponse)
|
||||||
def get_user_cart(token: str, db: Session = Depends(get_db)):
|
def get_user_cart(
|
||||||
user_id = get_user_id_from_token(token)
|
current_user: User = Depends(get_current_user),
|
||||||
cart = get_cart(db, user_id)
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
cart = get_cart(db, current_user.id)
|
||||||
if not cart:
|
if not cart:
|
||||||
raise HTTPException(status_code=404, detail="Cart not found")
|
raise HTTPException(status_code=404, detail="Cart not found")
|
||||||
return cart
|
return cart
|
||||||
|
|
||||||
|
|
||||||
@router.post("/add", response_model=dict)
|
@router.post("/add", response_model=dict)
|
||||||
def add_item_to_cart(token: str, item: CartItemCreate, db: Session = Depends(get_db)):
|
def add_item_to_cart(
|
||||||
user_id = get_user_id_from_token(token)
|
item: CartItemCreate,
|
||||||
cart_item = add_to_cart(db, user_id, item)
|
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}
|
return {"message": "Item added to cart", "item_id": cart_item.id}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{cart_item_id}", response_model=dict)
|
@router.put("/{cart_item_id}", response_model=dict)
|
||||||
def update_item(
|
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)
|
cart_item = update_cart_item(db, cart_item_id, update)
|
||||||
if not cart_item:
|
if not cart_item:
|
||||||
raise HTTPException(status_code=404, detail="Cart item not found")
|
raise HTTPException(status_code=404, detail="Cart item not found")
|
||||||
@ -52,16 +50,21 @@ def update_item(
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/{cart_item_id}")
|
@router.delete("/{cart_item_id}")
|
||||||
def remove_item(cart_item_id: int, token: str, db: Session = Depends(get_db)):
|
def remove_item(
|
||||||
user_id = get_user_id_from_token(token)
|
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):
|
if not remove_from_cart(db, cart_item_id):
|
||||||
raise HTTPException(status_code=404, detail="Cart item not found")
|
raise HTTPException(status_code=404, detail="Cart item not found")
|
||||||
return {"message": "Item removed from cart"}
|
return {"message": "Item removed from cart"}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("")
|
@router.delete("")
|
||||||
def clear_user_cart(token: str, db: Session = Depends(get_db)):
|
def clear_user_cart(
|
||||||
user_id = get_user_id_from_token(token)
|
current_user: User = Depends(get_current_user),
|
||||||
if not clear_cart(db, user_id):
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
if not clear_cart(db, current_user.id):
|
||||||
raise HTTPException(status_code=404, detail="Cart not found")
|
raise HTTPException(status_code=404, detail="Cart not found")
|
||||||
return {"message": "Cart cleared"}
|
return {"message": "Cart cleared"}
|
||||||
|
|||||||
@ -2,8 +2,9 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List
|
from typing import List
|
||||||
from app.database.database import get_db
|
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.schemas.category import CategoryCreate, CategoryResponse, CategoryUpdate
|
||||||
|
from app.services.auth import get_current_admin_user
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/categories", tags=["categories"])
|
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)
|
@router.post("", response_model=CategoryResponse)
|
||||||
def create_category(category: CategoryCreate, db: Session = Depends(get_db)):
|
def create_category(
|
||||||
# TODO: Add admin check
|
category: CategoryCreate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
admin: User = Depends(get_current_admin_user)
|
||||||
|
):
|
||||||
db_category = Category(**category.dict())
|
db_category = Category(**category.dict())
|
||||||
db.add(db_category)
|
db.add(db_category)
|
||||||
db.commit()
|
db.commit()
|
||||||
@ -33,9 +37,11 @@ def create_category(category: CategoryCreate, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
@router.put("/{category_id}", response_model=CategoryResponse)
|
@router.put("/{category_id}", response_model=CategoryResponse)
|
||||||
def update_category(
|
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()
|
category = db.query(Category).filter(Category.id == category_id).first()
|
||||||
if not category:
|
if not category:
|
||||||
raise HTTPException(status_code=404, detail="Category not found")
|
raise HTTPException(status_code=404, detail="Category not found")
|
||||||
@ -49,8 +55,11 @@ def update_category(
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/{category_id}")
|
@router.delete("/{category_id}")
|
||||||
def delete_category(category_id: int, db: Session = Depends(get_db)):
|
def delete_category(
|
||||||
# TODO: Add admin check
|
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()
|
category = db.query(Category).filter(Category.id == category_id).first()
|
||||||
if not category:
|
if not category:
|
||||||
raise HTTPException(status_code=404, detail="Category not found")
|
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,
|
get_user_orders,
|
||||||
update_order_status,
|
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"])
|
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)
|
@router.post("", response_model=OrderResponse)
|
||||||
def create_new_order(token: str, order_data: OrderCreate, db: Session = Depends(get_db)):
|
def create_new_order(
|
||||||
user_id = get_user_id_from_token(token)
|
order_data: OrderCreate,
|
||||||
order = create_order(db, user_id, order_data)
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
order = create_order(db, current_user.id, order_data)
|
||||||
if not order:
|
if not order:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
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
|
return order
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user/orders", response_model=List[OrderResponse])
|
@router.get("", response_model=List[OrderResponse])
|
||||||
def get_user_order_history(token: str, db: Session = Depends(get_db)):
|
def get_user_order_history(
|
||||||
user_id = get_user_id_from_token(token)
|
current_user: User = Depends(get_current_user),
|
||||||
return get_user_orders(db, user_id)
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
return get_user_orders(db, current_user.id)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{order_id}", response_model=OrderResponse)
|
@router.get("/{order_id}", response_model=OrderResponse)
|
||||||
def get_order(order_id: int, token: str, db: Session = Depends(get_db)):
|
def get_order(
|
||||||
user_id = get_user_id_from_token(token)
|
order_id: int,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
order = get_order_by_id(db, order_id)
|
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")
|
raise HTTPException(status_code=404, detail="Order not found")
|
||||||
return order
|
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 sqlalchemy.orm import Session
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
from pathlib import Path
|
||||||
from app.database.database import get_db
|
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 (
|
from app.schemas.product import (
|
||||||
ProductCreate,
|
ProductCreate,
|
||||||
ProductResponse,
|
ProductResponse,
|
||||||
@ -16,6 +18,8 @@ from app.services.product import (
|
|||||||
delete_product,
|
delete_product,
|
||||||
search_products,
|
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"])
|
router = APIRouter(prefix="/api/products", tags=["products"])
|
||||||
|
|
||||||
@ -23,22 +27,37 @@ router = APIRouter(prefix="/api/products", tags=["products"])
|
|||||||
@router.get("", response_model=List[ProductResponse])
|
@router.get("", response_model=List[ProductResponse])
|
||||||
def list_products(
|
def list_products(
|
||||||
category_id: Optional[int] = None,
|
category_id: Optional[int] = None,
|
||||||
|
model_id: Optional[int] = None,
|
||||||
|
brand: Optional[str] = None,
|
||||||
gender: Optional[str] = None,
|
gender: Optional[str] = None,
|
||||||
|
min_price: Optional[float] = None,
|
||||||
|
max_price: Optional[float] = None,
|
||||||
on_sale: Optional[bool] = None,
|
on_sale: Optional[bool] = None,
|
||||||
featured: Optional[bool] = None,
|
featured: Optional[bool] = None,
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
return get_products(
|
query = db.query(Product)
|
||||||
db,
|
|
||||||
category_id=category_id,
|
if category_id:
|
||||||
gender=gender,
|
query = query.filter(Product.category_id == category_id)
|
||||||
on_sale=on_sale,
|
if model_id:
|
||||||
featured=featured,
|
query = query.filter(Product.model_id == model_id)
|
||||||
skip=skip,
|
if brand:
|
||||||
limit=limit,
|
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])
|
@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)
|
@router.post("", response_model=ProductResponse)
|
||||||
def create_new_product(product: ProductCreate, db: Session = Depends(get_db)):
|
def create_new_product(
|
||||||
# TODO: Add admin check
|
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)
|
return create_product(db, product)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{product_id}", response_model=ProductResponse)
|
@router.put("/{product_id}", response_model=ProductResponse)
|
||||||
def update_existing_product(
|
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)
|
product = update_product(db, product_id, product_update)
|
||||||
if not product:
|
if not product:
|
||||||
raise HTTPException(status_code=404, detail="Product not found")
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
@ -72,8 +107,82 @@ def update_existing_product(
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/{product_id}")
|
@router.delete("/{product_id}")
|
||||||
def delete_existing_product(product_id: int, db: Session = Depends(get_db)):
|
def delete_existing_product(
|
||||||
# TODO: Add admin check
|
product_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
admin: User = Depends(get_current_admin_user),
|
||||||
|
):
|
||||||
if not delete_product(db, product_id):
|
if not delete_product(db, product_id):
|
||||||
raise HTTPException(status_code=404, detail="Product not found")
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
return {"message": "Product deleted successfully"}
|
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.database.database import get_db
|
||||||
from app.models import User
|
from app.models import User
|
||||||
from app.schemas.user import UserResponse, UserUpdate
|
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"])
|
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)
|
@router.get("/me", response_model=UserResponse)
|
||||||
def get_current_user_profile(token: str, db: Session = Depends(get_db)):
|
def get_current_user_profile(
|
||||||
user = get_current_user(token, db)
|
current_user: User = Depends(get_current_user),
|
||||||
return user
|
):
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
@router.put("/me", response_model=UserResponse)
|
@router.put("/me", response_model=UserResponse)
|
||||||
def update_user_profile(token: str, user_update: UserUpdate, db: Session = Depends(get_db)):
|
def update_user_profile(
|
||||||
user = get_current_user(token, db)
|
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():
|
for field, value in user_update.dict(exclude_unset=True).items():
|
||||||
setattr(user, field, value)
|
setattr(user, field, value)
|
||||||
|
|||||||
@ -2,28 +2,20 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List
|
from typing import List
|
||||||
from app.database.database import get_db
|
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.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"])
|
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])
|
@router.get("", response_model=List[ProductResponse])
|
||||||
def get_wishlist(token: str, db: Session = Depends(get_db)):
|
def get_wishlist(
|
||||||
user_id = get_user_id_from_token(token)
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
wishlist_items = (
|
wishlist_items = (
|
||||||
db.query(Wishlist).filter(Wishlist.user_id == user_id).all()
|
db.query(Wishlist).filter(Wishlist.user_id == current_user.id).all()
|
||||||
)
|
)
|
||||||
products = [
|
products = [
|
||||||
db.query(Product).filter(Product.id == item.product_id).first()
|
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}")
|
@router.post("/{product_id}")
|
||||||
def add_to_wishlist(product_id: int, token: str, db: Session = Depends(get_db)):
|
def add_to_wishlist(
|
||||||
user_id = get_user_id_from_token(token)
|
product_id: int,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
existing = (
|
existing = (
|
||||||
db.query(Wishlist)
|
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()
|
.first()
|
||||||
)
|
)
|
||||||
if existing:
|
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",
|
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.add(wishlist_item)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Product added to wishlist"}
|
return {"message": "Product added to wishlist"}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{product_id}")
|
@router.delete("/{product_id}")
|
||||||
def remove_from_wishlist(product_id: int, token: str, db: Session = Depends(get_db)):
|
def remove_from_wishlist(
|
||||||
user_id = get_user_id_from_token(token)
|
product_id: int,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
wishlist_item = (
|
wishlist_item = (
|
||||||
db.query(Wishlist)
|
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()
|
.first()
|
||||||
)
|
)
|
||||||
if not wishlist_item:
|
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
|
name: str
|
||||||
slug: str
|
slug: str
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
image: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class CategoryUpdate(BaseModel):
|
class CategoryUpdate(BaseModel):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
image: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class CategoryResponse(BaseModel):
|
class CategoryResponse(BaseModel):
|
||||||
@ -19,6 +21,7 @@ class CategoryResponse(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
slug: str
|
slug: str
|
||||||
description: Optional[str]
|
description: Optional[str]
|
||||||
|
image: Optional[str]
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
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 pydantic import BaseModel
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
class ProductCreate(BaseModel):
|
class ProductCreate(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
slug: Optional[str] = None
|
||||||
description: str
|
description: str
|
||||||
price: float
|
price: float
|
||||||
discount_price: Optional[float] = None
|
discount_price: Optional[float] = None
|
||||||
category_id: int
|
category_id: int
|
||||||
|
model_id: Optional[int] = None
|
||||||
gender: str # men, women
|
gender: str # men, women
|
||||||
brand: str
|
brand: str
|
||||||
sizes: List[str]
|
sizes: List[str]
|
||||||
colors: List[str]
|
colors: Optional[List[str]] = []
|
||||||
stock: int
|
stock: int
|
||||||
images: List[str]
|
images: List[str]
|
||||||
is_featured: bool = False
|
is_featured: bool = False
|
||||||
is_on_sale: bool = False
|
is_on_sale: bool = False
|
||||||
|
override_price: Optional[Decimal] = None
|
||||||
|
override_sizes: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
class ProductUpdate(BaseModel):
|
class ProductUpdate(BaseModel):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
|
slug: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
price: Optional[float] = None
|
price: Optional[float] = None
|
||||||
discount_price: Optional[float] = None
|
discount_price: Optional[float] = None
|
||||||
category_id: Optional[int] = None
|
category_id: Optional[int] = None
|
||||||
|
model_id: Optional[int] = None
|
||||||
gender: Optional[str] = None
|
gender: Optional[str] = None
|
||||||
brand: Optional[str] = None
|
brand: Optional[str] = None
|
||||||
sizes: Optional[List[str]] = None
|
sizes: Optional[List[str]] = None
|
||||||
@ -33,23 +40,29 @@ class ProductUpdate(BaseModel):
|
|||||||
images: Optional[List[str]] = None
|
images: Optional[List[str]] = None
|
||||||
is_featured: Optional[bool] = None
|
is_featured: Optional[bool] = None
|
||||||
is_on_sale: Optional[bool] = None
|
is_on_sale: Optional[bool] = None
|
||||||
|
override_price: Optional[Decimal] = None
|
||||||
|
override_sizes: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
class ProductResponse(BaseModel):
|
class ProductResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
|
slug: Optional[str]
|
||||||
description: str
|
description: str
|
||||||
price: float
|
price: float
|
||||||
discount_price: Optional[float]
|
discount_price: Optional[float]
|
||||||
category_id: int
|
category_id: int
|
||||||
|
model_id: Optional[int]
|
||||||
gender: str
|
gender: str
|
||||||
brand: str
|
brand: str
|
||||||
sizes: List[str]
|
sizes: List[str]
|
||||||
colors: List[str]
|
colors: Optional[List[str]]
|
||||||
stock: int
|
stock: int
|
||||||
images: List[str]
|
images: List[str]
|
||||||
is_featured: bool
|
is_featured: bool
|
||||||
is_on_sale: bool
|
is_on_sale: bool
|
||||||
|
override_price: Optional[Decimal]
|
||||||
|
override_sizes: Optional[List[str]]
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
@ -29,6 +29,7 @@ class UserResponse(UserBase):
|
|||||||
postal_code: Optional[str]
|
postal_code: Optional[str]
|
||||||
country: Optional[str]
|
country: Optional[str]
|
||||||
is_active: bool
|
is_active: bool
|
||||||
|
is_admin: bool
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
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 typing import Optional
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.models import User
|
from app.models import User
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from app.database.database import get_db
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
|
||||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
@ -37,11 +41,17 @@ def verify_token(token: str) -> Optional[int]:
|
|||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm]
|
token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm]
|
||||||
)
|
)
|
||||||
user_id: int = payload.get("sub")
|
user_id_str: str = payload.get("sub")
|
||||||
if user_id is None:
|
if user_id_str is None:
|
||||||
return None
|
return None
|
||||||
|
# Convert string to integer
|
||||||
|
user_id = int(user_id_str)
|
||||||
return user_id
|
return user_id
|
||||||
except JWTError:
|
except JWTError as e:
|
||||||
|
return None
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
return None
|
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):
|
if not user or not verify_password(password, user.hashed_password):
|
||||||
return None
|
return None
|
||||||
return user
|
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
|
fastapi>=0.104.1
|
||||||
uvicorn==0.24.0
|
uvicorn>=0.24.0
|
||||||
sqlalchemy==2.0.23
|
sqlalchemy>=2.0.23
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary>=2.9.11
|
||||||
python-dotenv==1.0.0
|
python-dotenv>=1.0.0
|
||||||
pydantic==2.5.0
|
pydantic>=2.5.0
|
||||||
pydantic-settings==2.1.0
|
pydantic-settings>=2.1.0
|
||||||
python-multipart==0.0.6
|
python-multipart>=0.0.6
|
||||||
python-jose[cryptography]==3.3.0
|
python-jose[cryptography]>=3.3.0
|
||||||
passlib[bcrypt]==1.7.4
|
passlib[bcrypt]>=1.7.4
|
||||||
email-validator==2.1.0
|
email-validator>=2.1.0
|
||||||
|
|||||||
@ -1,6 +1,48 @@
|
|||||||
-- E-Commerce Database Schema
|
-- E-Commerce Database Schema
|
||||||
-- Run this file to create tables and populate initial data
|
-- Run this file to create database, user, tables and populate initial data
|
||||||
-- psql -U ecommerce_user -d ecommerce_db -h localhost -f schema.sql
|
-- 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
|
-- CREATE TABLES
|
||||||
@ -11,9 +53,27 @@ CREATE TABLE category (
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(100) UNIQUE NOT NULL,
|
name VARCHAR(100) UNIQUE NOT NULL,
|
||||||
slug 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
|
-- User Table
|
||||||
CREATE TABLE "user" (
|
CREATE TABLE "user" (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
@ -26,6 +86,7 @@ CREATE TABLE "user" (
|
|||||||
postal_code VARCHAR(20),
|
postal_code VARCHAR(20),
|
||||||
country VARCHAR(100),
|
country VARCHAR(100),
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
is_admin BOOLEAN DEFAULT FALSE,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -33,10 +94,12 @@ CREATE TABLE "user" (
|
|||||||
CREATE TABLE product (
|
CREATE TABLE product (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
|
slug VARCHAR(255) UNIQUE,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
price DECIMAL(10, 2) NOT NULL,
|
price DECIMAL(10, 2) NOT NULL,
|
||||||
discount_price DECIMAL(10, 2),
|
discount_price DECIMAL(10, 2),
|
||||||
category_id INTEGER NOT NULL,
|
category_id INTEGER NOT NULL,
|
||||||
|
model_id INTEGER,
|
||||||
gender VARCHAR(50),
|
gender VARCHAR(50),
|
||||||
brand VARCHAR(100),
|
brand VARCHAR(100),
|
||||||
sizes JSONB,
|
sizes JSONB,
|
||||||
@ -45,10 +108,16 @@ CREATE TABLE product (
|
|||||||
images JSONB,
|
images JSONB,
|
||||||
is_featured BOOLEAN DEFAULT FALSE,
|
is_featured BOOLEAN DEFAULT FALSE,
|
||||||
is_on_sale BOOLEAN DEFAULT FALSE,
|
is_on_sale BOOLEAN DEFAULT FALSE,
|
||||||
|
override_price DECIMAL(10, 2),
|
||||||
|
override_sizes JSONB,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
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
|
-- Cart Table
|
||||||
CREATE TABLE cart (
|
CREATE TABLE cart (
|
||||||
id SERIAL PRIMARY KEY,
|
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 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',
|
'user@example.com',
|
||||||
'$2b$12$KIXxPfROLqWHYIgC5FCOO.7yqKU8RvOmOhP7kJZnYLh6pJn2FSfKy', -- password: password123
|
'$2b$12$jNWZdZNMbKEkOjA8Gq3ZUOTml23zfqhFDPJ8AlZQ51WhUyi04AH7C', -- password: password123
|
||||||
'John Doe',
|
'John Doe',
|
||||||
'1234567890',
|
'1234567890',
|
||||||
'123 Main Street',
|
'123 Main Street',
|
||||||
'New York',
|
'New York',
|
||||||
'10001',
|
'10001',
|
||||||
'USA',
|
'USA',
|
||||||
TRUE
|
TRUE,
|
||||||
|
FALSE
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'jane@example.com',
|
'jane@example.com',
|
||||||
'$2b$12$KIXxPfROLqWHYIgC5FCOO.7yqKU8RvOmOhP7kJZnYLh6pJn2FSfKy', -- password: password123
|
'$2b$12$jNWZdZNMbKEkOjA8Gq3ZUOTml23zfqhFDPJ8AlZQ51WhUyi04AH7C', -- password: password123
|
||||||
'Jane Smith',
|
'Jane Smith',
|
||||||
'9876543210',
|
'9876543210',
|
||||||
'456 Oak Avenue',
|
'456 Oak Avenue',
|
||||||
'Los Angeles',
|
'Los Angeles',
|
||||||
'90001',
|
'90001',
|
||||||
'USA',
|
'USA',
|
||||||
TRUE
|
TRUE,
|
||||||
|
FALSE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create carts for users
|
-- Create carts for users
|
||||||
INSERT INTO cart (user_id) VALUES
|
INSERT INTO cart (user_id) VALUES
|
||||||
(1),
|
(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 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;
|
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
|
-- COMPLETE
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- Schema created successfully!
|
-- Schema created successfully!
|
||||||
--
|
--
|
||||||
-- User: ecommerce_user
|
|
||||||
-- Database: ecommerce_db
|
-- Database: ecommerce_db
|
||||||
|
-- User: ecommerce_user
|
||||||
|
-- Password: password
|
||||||
-- Host: localhost:5432
|
-- Host: localhost:5432
|
||||||
--
|
--
|
||||||
|
-- Connection string for .env file:
|
||||||
|
-- DATABASE_URL=postgresql://ecommerce_user:password@localhost:5432/ecommerce_db
|
||||||
|
--
|
||||||
-- Tables created: 9
|
-- Tables created: 9
|
||||||
-- - category, user, product, cart, cart_item
|
-- - category, user, product, cart, cart_item
|
||||||
-- - order, order_item, user_wishlist, contact_message
|
-- - order, order_item, user_wishlist, contact_message
|
||||||
--
|
--
|
||||||
-- Demo accounts:
|
-- Demo accounts:
|
||||||
-- - user@example.com / password123
|
-- - ADMIN: admin@example.com / password123 (is_admin: true)
|
||||||
-- - jane@example.com / password123
|
-- - 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:
|
-- Next steps:
|
||||||
-- 1. Update backend/.env with your credentials
|
-- 1. Update backend/.env with: DATABASE_URL=postgresql://ecommerce_user:password@localhost:5432/ecommerce_db
|
||||||
-- 2. Run: uvicorn app.main:app --reload --port 8000
|
-- 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 About from './pages/About'
|
||||||
import Contact from './pages/Contact'
|
import Contact from './pages/Contact'
|
||||||
import Sales from './pages/Sales'
|
import Sales from './pages/Sales'
|
||||||
|
import Admin from './pages/Admin'
|
||||||
|
import Models from './pages/Models'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -42,6 +44,8 @@ function App() {
|
|||||||
<Route path="/about" element={<About />} />
|
<Route path="/about" element={<About />} />
|
||||||
<Route path="/contact" element={<Contact />} />
|
<Route path="/contact" element={<Contact />} />
|
||||||
<Route path="/sales" element={<Sales />} />
|
<Route path="/sales" element={<Sales />} />
|
||||||
|
<Route path="/admin" element={<Admin />} />
|
||||||
|
<Route path="/admin/models" element={<Models />} />
|
||||||
<Route path="*" element={<Navigate to="/" />} />
|
<Route path="*" element={<Navigate to="/" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -6,12 +6,11 @@ const api = axios.create({
|
|||||||
baseURL: API_URL,
|
baseURL: API_URL,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add token to requests
|
// Add token to requests as Bearer token in Authorization header
|
||||||
api.interceptors.request.use((config) => {
|
api.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
if (token) {
|
if (token) {
|
||||||
config.params = config.params || {}
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
config.params.token = token
|
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
export default function CategoryCard({ category }) {
|
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 (
|
return (
|
||||||
<Link to={`/products?category=${category.slug}`} className="category-card">
|
<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="/sales">Sales</Link></li>
|
||||||
<li><Link to="/about">About</Link></li>
|
<li><Link to="/about">About</Link></li>
|
||||||
<li><Link to="/contact">Contact</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>
|
</ul>
|
||||||
|
|
||||||
<div className="navbar-icons">
|
<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'
|
import '../styles/global.css'
|
||||||
|
|
||||||
export default function ProductFilters({ onFilter }) {
|
export default function ProductFilters({ onFilter }) {
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
gender: '',
|
gender: '',
|
||||||
priceRange: 'all',
|
brand: '',
|
||||||
|
model_id: '',
|
||||||
|
size: '',
|
||||||
|
min_price: '',
|
||||||
|
max_price: '',
|
||||||
onSale: false,
|
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 handleFilterChange = (key, value) => {
|
||||||
const newFilters = { ...filters, [key]: value }
|
const newFilters = { ...filters, [key]: value }
|
||||||
setFilters(newFilters)
|
setFilters(newFilters)
|
||||||
onFilter(newFilters)
|
onFilter(newFilters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
const resetFilters = {
|
||||||
|
gender: '',
|
||||||
|
brand: '',
|
||||||
|
model_id: '',
|
||||||
|
size: '',
|
||||||
|
min_price: '',
|
||||||
|
max_price: '',
|
||||||
|
onSale: false,
|
||||||
|
}
|
||||||
|
setFilters(resetFilters)
|
||||||
|
onFilter(resetFilters)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="filters-sidebar">
|
<div className="filters-sidebar">
|
||||||
<h3>Filters</h3>
|
<h3>Filters</h3>
|
||||||
@ -31,19 +95,73 @@ export default function ProductFilters({ onFilter }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="filter-group">
|
<div className="filter-group">
|
||||||
<label>Price Range</label>
|
<label>Brand</label>
|
||||||
<select
|
<select
|
||||||
value={filters.priceRange}
|
value={filters.brand}
|
||||||
onChange={(e) => handleFilterChange('priceRange', e.target.value)}
|
onChange={(e) => handleFilterChange('brand', e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="all">All Prices</option>
|
<option value="">All Brands</option>
|
||||||
<option value="0-50">$0 - $50</option>
|
{brands.map((brand, index) => (
|
||||||
<option value="50-100">$50 - $100</option>
|
<option key={index} value={brand}>
|
||||||
<option value="100-200">$100 - $200</option>
|
{brand}
|
||||||
<option value="200+">$200+</option>
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div className="filter-group">
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
@ -55,7 +173,7 @@ export default function ProductFilters({ onFilter }) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="btn btn-full" onClick={() => window.location.reload()}>
|
<button className="btn btn-full" onClick={resetFilters}>
|
||||||
Reset Filters
|
Reset Filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,20 +1,37 @@
|
|||||||
import React, { createContext, useState, useEffect } from 'react'
|
import React, { createContext, useState, useEffect } from 'react'
|
||||||
|
import api from '../api'
|
||||||
|
|
||||||
export const AuthContext = createContext()
|
export const AuthContext = createContext()
|
||||||
|
|
||||||
export const AuthProvider = ({ children }) => {
|
export const AuthProvider = ({ children }) => {
|
||||||
const [user, setUser] = useState(null)
|
const [user, setUser] = useState(null)
|
||||||
const [token, setToken] = useState(localStorage.getItem('token'))
|
const [token, setToken] = useState(localStorage.getItem('token'))
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) {
|
if (token) {
|
||||||
localStorage.setItem('token', token)
|
localStorage.setItem('token', token)
|
||||||
|
fetchUserData()
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [token])
|
}, [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 = () => {
|
const logout = () => {
|
||||||
setUser(null)
|
setUser(null)
|
||||||
setToken(null)
|
setToken(null)
|
||||||
@ -23,7 +40,7 @@ export const AuthProvider = ({ children }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ user, setUser, token, setToken, loading, setLoading, logout }}>
|
<AuthContext.Provider value={{ user, setUser, token, setToken, loading, setLoading, logout }}>
|
||||||
{children}
|
{!loading && children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,12 +18,11 @@ export const CartProvider = ({ children }) => {
|
|||||||
setTotal(newTotal)
|
setTotal(newTotal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const addToCart = (product, quantity = 1, size = null, color = null) => {
|
const addToCart = (product, quantity = 1, size = null) => {
|
||||||
const existingItem = cart.find(
|
const existingItem = cart.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.product.id === product.id &&
|
item.product.id === product.id &&
|
||||||
item.size === size &&
|
item.size === size
|
||||||
item.color === color
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
@ -35,7 +34,7 @@ export const CartProvider = ({ children }) => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} 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>
|
<div>
|
||||||
<p className="product-name">{item.product.name}</p>
|
<p className="product-name">{item.product.name}</p>
|
||||||
{item.size && <p>Size: {item.size}</p>}
|
{item.size && <p>Size: {item.size}</p>}
|
||||||
{item.color && <p>Color: {item.color}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -33,9 +33,7 @@ export default function Checkout() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const response = await api.post('/orders', formData, {
|
const response = await api.post('/orders', formData)
|
||||||
params: { token },
|
|
||||||
})
|
|
||||||
|
|
||||||
alert('Order placed successfully!')
|
alert('Order placed successfully!')
|
||||||
clearCart()
|
clearCart()
|
||||||
|
|||||||
@ -85,9 +85,11 @@ export default function Login() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="demo-account">
|
<div className="demo-account">
|
||||||
<p><strong>Demo Account:</strong></p>
|
<p><strong>Demo Accounts:</strong></p>
|
||||||
<p>Email: user@example.com</p>
|
<p style={{ color: '#ff6b6b', fontWeight: 'bold' }}>
|
||||||
<p>Password: password123</p>
|
Admin: admin@example.com / password123
|
||||||
|
</p>
|
||||||
|
<p>User: user@example.com / password123</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 () => {
|
const fetchOrders = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/orders/user/orders', {
|
const response = await api.get('/orders')
|
||||||
params: { token },
|
|
||||||
})
|
|
||||||
setOrders(response.data)
|
setOrders(response.data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching orders:', error)
|
console.error('Error fetching orders:', error)
|
||||||
|
|||||||
@ -11,9 +11,9 @@ export default function ProductDetail() {
|
|||||||
const [product, setProduct] = useState(null)
|
const [product, setProduct] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [selectedSize, setSelectedSize] = useState('')
|
const [selectedSize, setSelectedSize] = useState('')
|
||||||
const [selectedColor, setSelectedColor] = useState('')
|
|
||||||
const [quantity, setQuantity] = useState(1)
|
const [quantity, setQuantity] = useState(1)
|
||||||
const [inWishlist, setInWishlist] = useState(false)
|
const [inWishlist, setInWishlist] = useState(false)
|
||||||
|
const [selectedImageIndex, setSelectedImageIndex] = useState(0)
|
||||||
const { addToCart } = useContext(CartContext)
|
const { addToCart } = useContext(CartContext)
|
||||||
const { token } = useContext(AuthContext)
|
const { token } = useContext(AuthContext)
|
||||||
|
|
||||||
@ -25,7 +25,6 @@ export default function ProductDetail() {
|
|||||||
try {
|
try {
|
||||||
const response = await api.get(`/products/${id}`)
|
const response = await api.get(`/products/${id}`)
|
||||||
setProduct(response.data)
|
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])
|
if (response.data.sizes.length > 0) setSelectedSize(response.data.sizes[0])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching product:', error)
|
console.error('Error fetching product:', error)
|
||||||
@ -45,11 +44,10 @@ export default function ProductDetail() {
|
|||||||
product_id: product.id,
|
product_id: product.id,
|
||||||
quantity,
|
quantity,
|
||||||
size: selectedSize,
|
size: selectedSize,
|
||||||
color: selectedColor,
|
|
||||||
token,
|
token,
|
||||||
})
|
})
|
||||||
|
|
||||||
addToCart(product, quantity, selectedSize, selectedColor)
|
addToCart(product, quantity, selectedSize)
|
||||||
alert('Product added to cart!')
|
alert('Product added to cart!')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding to cart:', error)
|
console.error('Error adding to cart:', error)
|
||||||
@ -64,9 +62,9 @@ export default function ProductDetail() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (inWishlist) {
|
if (inWishlist) {
|
||||||
await api.delete(`/wishlist/${product.id}`, { params: { token } })
|
await api.delete(`/wishlist/${product.id}`)
|
||||||
} else {
|
} else {
|
||||||
await api.post(`/wishlist/${product.id}`, null, { params: { token } })
|
await api.post(`/wishlist/${product.id}`)
|
||||||
}
|
}
|
||||||
setInWishlist(!inWishlist)
|
setInWishlist(!inWishlist)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -83,11 +81,54 @@ export default function ProductDetail() {
|
|||||||
<div className="product-detail">
|
<div className="product-detail">
|
||||||
<div className="detail-container">
|
<div className="detail-container">
|
||||||
<div className="detail-images">
|
<div className="detail-images">
|
||||||
<img src={product.images[0]} alt={product.name} />
|
{/* Main Image */}
|
||||||
{product.images.slice(1).map((img, idx) => (
|
<div className="main-image-container">
|
||||||
<img key={idx} src={img} alt={`View ${idx + 2}`} />
|
<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>
|
||||||
|
|
||||||
<div className="detail-info">
|
<div className="detail-info">
|
||||||
<h1>{product.name}</h1>
|
<h1>{product.name}</h1>
|
||||||
@ -110,24 +151,6 @@ export default function ProductDetail() {
|
|||||||
|
|
||||||
<p className="description">{product.description}</p>
|
<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 && (
|
{product.sizes.length > 0 && (
|
||||||
<div className="option-group">
|
<div className="option-group">
|
||||||
<label>Size:</label>
|
<label>Size:</label>
|
||||||
|
|||||||
@ -10,16 +10,17 @@ export default function Products() {
|
|||||||
const [products, setProducts] = useState([])
|
const [products, setProducts] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [sortBy, setSortBy] = useState('latest')
|
const [sortBy, setSortBy] = useState('latest')
|
||||||
|
const [activeFilters, setActiveFilters] = useState({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProducts()
|
fetchProducts()
|
||||||
}, [searchParams])
|
}, [searchParams, activeFilters, sortBy])
|
||||||
|
|
||||||
const fetchProducts = async () => {
|
const fetchProducts = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const categorySlug = searchParams.get('category')
|
const categorySlug = searchParams.get('category')
|
||||||
const params = { limit: 50 }
|
const params = { limit: 50, ...activeFilters }
|
||||||
|
|
||||||
if (categorySlug) {
|
if (categorySlug) {
|
||||||
// Get category by slug
|
// Get category by slug
|
||||||
@ -28,9 +29,16 @@ export default function Products() {
|
|||||||
if (category) params.category_id = category.id
|
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 })
|
const response = await api.get('/products', { params })
|
||||||
let sorted = [...response.data]
|
let sorted = [...response.data]
|
||||||
|
|
||||||
|
// Apply client-side sorting
|
||||||
if (sortBy === 'price-low') {
|
if (sortBy === 'price-low') {
|
||||||
sorted.sort((a, b) => (a.discount_price || a.price) - (b.discount_price || b.price))
|
sorted.sort((a, b) => (a.discount_price || a.price) - (b.discount_price || b.price))
|
||||||
} else if (sortBy === 'price-high') {
|
} else if (sortBy === 'price-high') {
|
||||||
@ -48,8 +56,7 @@ export default function Products() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFilter = (filters) => {
|
const handleFilter = (filters) => {
|
||||||
// Apply filters locally for now
|
setActiveFilters(filters)
|
||||||
console.log('Filters:', filters)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -28,9 +28,7 @@ export default function Profile() {
|
|||||||
|
|
||||||
const fetchProfile = async () => {
|
const fetchProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/users/me', {
|
const response = await api.get('/users/me')
|
||||||
params: { token },
|
|
||||||
})
|
|
||||||
setFormData(response.data)
|
setFormData(response.data)
|
||||||
setUser(response.data)
|
setUser(response.data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -69,7 +67,21 @@ export default function Profile() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profile-page">
|
<div className="profile-page">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1rem' }}>
|
||||||
<h1>My Profile</h1>
|
<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">
|
<div className="profile-container">
|
||||||
<form onSubmit={handleSubmit} className="profile-form">
|
<form onSubmit={handleSubmit} className="profile-form">
|
||||||
|
|||||||
@ -21,9 +21,7 @@ export default function Wishlist() {
|
|||||||
|
|
||||||
const fetchWishlist = async () => {
|
const fetchWishlist = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/wishlist', {
|
const response = await api.get('/wishlist')
|
||||||
params: { token },
|
|
||||||
})
|
|
||||||
setWishlistItems(response.data)
|
setWishlistItems(response.data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching wishlist:', error)
|
console.error('Error fetching wishlist:', error)
|
||||||
@ -34,9 +32,7 @@ export default function Wishlist() {
|
|||||||
|
|
||||||
const handleRemoveFromWishlist = async (productId) => {
|
const handleRemoveFromWishlist = async (productId) => {
|
||||||
try {
|
try {
|
||||||
await api.delete(`/wishlist/${productId}`, {
|
await api.delete(`/wishlist/${productId}`)
|
||||||
params: { token },
|
|
||||||
})
|
|
||||||
setWishlistItems(
|
setWishlistItems(
|
||||||
wishlistItems.filter((item) => item.id !== productId)
|
wishlistItems.filter((item) => item.id !== productId)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -542,6 +542,83 @@ button {
|
|||||||
gap: 1rem;
|
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 {
|
.detail-images img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user