diff --git a/QUICKSTART.md b/QUICKSTART.md index 0f563e6..bdaa403 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -163,7 +163,7 @@ FRONTEND_URL=http://localhost:5173 ``` VITE_API_URL=http://localhost:8000/api -VITE_APP_NAME=StyleHub +VITE_APP_NAME=Brand Master ``` ## Common Commands diff --git a/README.md b/README.md index 2ebcacf..f349625 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ cp .env.example .env Edit `.env`: ``` VITE_API_URL=http://localhost:8000/api -VITE_APP_NAME=StyleHub +VITE_APP_NAME=Brand Master ``` ### 3. Run Development Server diff --git a/backend/app/routers/__pycache__/auth.cpython-314.pyc b/backend/app/routers/__pycache__/auth.cpython-314.pyc index 4fe2a58..e693939 100644 Binary files a/backend/app/routers/__pycache__/auth.cpython-314.pyc and b/backend/app/routers/__pycache__/auth.cpython-314.pyc differ diff --git a/backend/app/routers/__pycache__/products.cpython-314.pyc b/backend/app/routers/__pycache__/products.cpython-314.pyc index ad066b6..6fcd827 100644 Binary files a/backend/app/routers/__pycache__/products.cpython-314.pyc and b/backend/app/routers/__pycache__/products.cpython-314.pyc differ diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index a70ab97..8d44ee3 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session -from datetime import timedelta +from datetime import timedelta, datetime +from pydantic import BaseModel, EmailStr from app.database.database import get_db from app.models import User from app.schemas.user import UserCreate, UserResponse @@ -15,6 +16,15 @@ from app.config import settings router = APIRouter(prefix="/api/auth", tags=["auth"]) +class ForgotPasswordRequest(BaseModel): + email: EmailStr + + +class ResetPasswordRequest(BaseModel): + token: str + new_password: str + + @router.post("/register", response_model=UserResponse) def register(user: UserCreate, db: Session = Depends(get_db)): db_user = db.query(User).filter(User.email == user.email).first() @@ -66,3 +76,57 @@ def verify_token_endpoint(token: str): detail="Invalid token", ) return {"user_id": user_id, "valid": True} + + +@router.post("/forgot-password") +def forgot_password(request: ForgotPasswordRequest, db: Session = Depends(get_db)): + """ + Initiate password reset process. + In a production app, this would send an email with a reset link. + For now, we'll generate a reset token that can be used. + """ + user = db.query(User).filter(User.email == request.email).first() + if not user: + # Don't reveal if email exists or not for security + return {"message": "If the email exists, a reset link has been sent"} + + # Create a password reset token (valid for 1 hour) + reset_token = create_access_token( + data={"sub": str(user.id), "type": "password_reset"}, + expires_delta=timedelta(hours=1) + ) + + # In production, send this token via email + # For development, we'll just return it + print(f"Password reset token for {request.email}: {reset_token}") + + return { + "message": "If the email exists, a reset link has been sent", + "reset_token": reset_token # Remove this in production + } + + +@router.post("/reset-password") +def reset_password(request: ResetPasswordRequest, db: Session = Depends(get_db)): + """ + Reset password using the token from forgot-password endpoint. + """ + user_id = verify_token(request.token) + if user_id is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid or expired reset token", + ) + + user = db.query(User).filter(User.id == int(user_id)).first() + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found", + ) + + # Update password + user.hashed_password = get_password_hash(request.new_password) + db.commit() + + return {"message": "Password reset successful"} diff --git a/backend/app/routers/products.py b/backend/app/routers/products.py index db359bc..b5ef5a4 100644 --- a/backend/app/routers/products.py +++ b/backend/app/routers/products.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File from fastapi.responses import FileResponse -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, joinedload from typing import List, Optional from pathlib import Path from app.database.database import get_db @@ -38,7 +38,7 @@ def list_products( limit: int = 20, db: Session = Depends(get_db), ): - query = db.query(Product) + query = db.query(Product).options(joinedload(Product.model)) if category_id: query = query.filter(Product.category_id == category_id) @@ -57,12 +57,26 @@ def list_products( if featured is not None: query = query.filter(Product.is_featured == featured) - return query.offset(skip).limit(limit).all() + products = query.offset(skip).limit(limit).all() + + # Inherit sizes from model if product doesn't have sizes + for product in products: + if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model: + product.sizes = product.model.sizes or [] + + return products @router.get("/search", response_model=List[ProductResponse]) def search(q: str, skip: int = 0, limit: int = 20, db: Session = Depends(get_db)): - return search_products(db, q, skip=skip, limit=limit) + products = search_products(db, q, skip=skip, limit=limit) + + # Inherit sizes from model if product doesn't have sizes + for product in products: + if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model: + product.sizes = product.model.sizes or [] + + return products @router.get("/{product_id}", response_model=ProductResponse) @@ -70,6 +84,11 @@ def get_product(product_id: int, db: Session = Depends(get_db)): product = get_product_by_id(db, product_id) if not product: raise HTTPException(status_code=404, detail="Product not found") + + # If product doesn't have sizes but has a model, inherit sizes from model + if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model: + product.sizes = product.model.sizes or [] + return product diff --git a/backend/app/services/__pycache__/product.cpython-314.pyc b/backend/app/services/__pycache__/product.cpython-314.pyc index 52fc964..2b29653 100644 Binary files a/backend/app/services/__pycache__/product.cpython-314.pyc and b/backend/app/services/__pycache__/product.cpython-314.pyc differ diff --git a/backend/app/services/product.py b/backend/app/services/product.py index 731aeed..6581b5f 100644 --- a/backend/app/services/product.py +++ b/backend/app/services/product.py @@ -1,4 +1,4 @@ -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, joinedload from app.models import Product, Category from app.schemas.product import ProductCreate, ProductUpdate from typing import List, Optional @@ -13,7 +13,7 @@ def get_products( skip: int = 0, limit: int = 10, ) -> List[Product]: - query = db.query(Product) + query = db.query(Product).options(joinedload(Product.model)) if category_id: query = query.filter(Product.category_id == category_id) @@ -28,7 +28,7 @@ def get_products( def get_product_by_id(db: Session, product_id: int) -> Optional[Product]: - return db.query(Product).filter(Product.id == product_id).first() + return db.query(Product).options(joinedload(Product.model)).filter(Product.id == product_id).first() def create_product(db: Session, product: ProductCreate) -> Product: @@ -67,6 +67,7 @@ def delete_product(db: Session, product_id: int) -> bool: def search_products(db: Session, query: str, skip: int = 0, limit: int = 10) -> List[Product]: return ( db.query(Product) + .options(joinedload(Product.model)) .filter( Product.name.ilike(f"%{query}%") | Product.brand.ilike(f"%{query}%") ) diff --git a/frontend/.env.example b/frontend/.env.example index 63f5530..bed298e 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,2 +1,2 @@ VITE_API_URL=http://localhost:8000/api -VITE_APP_NAME=StyleHub +VITE_APP_NAME=Brand Master diff --git a/frontend/index.html b/frontend/index.html index 3b7d53e..ee35a87 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - StyleHub - Fashion & Shoe Store + Brand Master - Fashion & Shoe Store
diff --git a/frontend/src/components/AddToCartModal.jsx b/frontend/src/components/AddToCartModal.jsx new file mode 100644 index 0000000..4b43c51 --- /dev/null +++ b/frontend/src/components/AddToCartModal.jsx @@ -0,0 +1,36 @@ +import React from 'react' +import { useNavigate } from 'react-router-dom' +import '../styles/global.css' + +export default function AddToCartModal({ isOpen, onClose, productName }) { + const navigate = useNavigate() + + if (!isOpen) return null + + const handleGoToCart = () => { + navigate('/cart') + onClose() + } + + const handleContinueShopping = () => { + onClose() + } + + return ( +
+
e.stopPropagation()}> +
+

Added to Cart!

+

{productName} has been added to your cart

+
+ + +
+
+
+ ) +} diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx index 6037da6..5581b59 100644 --- a/frontend/src/components/Footer.jsx +++ b/frontend/src/components/Footer.jsx @@ -7,13 +7,14 @@ export default function Footer() { ) diff --git a/frontend/src/components/Modal.jsx b/frontend/src/components/Modal.jsx new file mode 100644 index 0000000..890351f --- /dev/null +++ b/frontend/src/components/Modal.jsx @@ -0,0 +1,22 @@ +import React from 'react' +import '../styles/global.css' + +export default function Modal({ isOpen, onClose, children, title }) { + if (!isOpen) return null + + return ( +
+
e.stopPropagation()}> + {title && ( +
+

{title}

+ +
+ )} +
{children}
+
+
+ ) +} diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index cf91dc3..a225ff5 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -1,5 +1,5 @@ import React, { useContext } from 'react' -import { Link, useNavigate } from 'react-router-dom' +import { Link, useNavigate, useLocation } from 'react-router-dom' import { AuthContext } from '../context/AuthContext' import { CartContext } from '../context/CartContext' import SearchBar from './SearchBar' @@ -9,17 +9,22 @@ export default function Navbar() { const { user, token, logout } = useContext(AuthContext) const { cart } = useContext(CartContext) const navigate = useNavigate() + const location = useLocation() const handleLogout = () => { logout() navigate('/') } + const isActive = (path) => { + return location.pathname === path ? 'active' : '' + } + return (