Add brands management system with backend persistence

This commit is contained in:
dvirlabs 2026-05-01 19:14:39 +03:00
parent d182f76201
commit fbb3e7d850
90 changed files with 2686 additions and 225 deletions

1
.gitignore vendored
View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

View File

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

View 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")

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View 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"}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View File

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

View 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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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
View 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}"

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

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View 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>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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