Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions backend/app/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
from pwdlib import PasswordHash
from pydantic import EmailStr, NonNegativeInt, SecretStr
from sqlalchemy.dialects.postgresql import insert
from sqlmodel import select
from sqlmodel import Session, select

from app.dependencies import AuthDep, SessionDep, UserSessionDep
from app.dependencies import AuthDep, SessionDep, UserSessionDep, engine
from app.enums import Category
from app.models import (
Cart,
Expand All @@ -24,27 +24,54 @@
User,
UserSession,
)
from app.product_store import load_seed_data, load_seed_products

router = APIRouter()

hasher = PasswordHash.recommended()


@router.get("/api/storefront")
async def get_storefront():
seed = load_seed_data()
return {
"collection_hero": seed.collection_hero,
"products": seed.products,
}


@router.get("/api/products")
async def get_products(
session: SessionDep, categories: Annotated[list[Category] | None, Query()] = None
categories: Annotated[list[Category] | None, Query()] = None,
) -> Sequence[Product]:
query = select(Product)
if engine is None:
products = load_seed_products()
if categories is None:
return products

if categories is not None:
query = query.where(Product.categories.overlap(categories))
requested_categories = {str(category) for category in categories}
return [
product
for product in products
if requested_categories.intersection(product.categories)
]

return session.exec(query).all()
with Session(engine) as session:
query = select(Product)

if categories is not None:
query = query.where(Product.categories.overlap(categories))

return session.exec(query).all()


@router.get("/api/products/{id}")
async def get_product(id: str, session: SessionDep) -> Product:
product = session.get(Product, id)
async def get_product(id: str) -> Product:
if engine is None:
product = next((product for product in load_seed_products() if product.id == id), None)
else:
with Session(engine) as session:
product = session.get(Product, id)

if product is None:
raise HTTPException(status_code=404, detail="User not found")
Expand Down
19 changes: 10 additions & 9 deletions backend/app/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import datetime as dt
import os
import secrets
import sys
from typing import Annotated

from fastapi import Cookie, Depends, HTTPException, Response
Expand All @@ -10,16 +9,18 @@

from app.models import User, UserSession

if (POSTGRES_URL := os.getenv("POSTGRES_URL")) is None:
sys.exit("The POSTGRES_URL env variable must be set.")

if (API_KEY := os.getenv("API_KEY")) is None:
sys.exit("The API_KEY env variable must be set.")

engine = create_engine(POSTGRES_URL)
POSTGRES_URL = os.getenv("POSTGRES_URL")
API_KEY = os.getenv("API_KEY", "dev-api-key")
engine = create_engine(POSTGRES_URL) if POSTGRES_URL else None


def get_session():
if engine is None:
raise HTTPException(
status_code=503,
detail="Database is not configured. Set POSTGRES_URL to enable this endpoint.",
)

with Session(engine) as session:
yield session

Expand Down Expand Up @@ -62,7 +63,7 @@ def get_current_session(
if user_session is None:
raise HTTPException(status_code=401, detail="Invalid session")

if user_session.expires_at > dt.datetime.now():
if user_session.expires_at < dt.datetime.now():
session.delete(user_session)

raise HTTPException(status_code=401, detail="Session expired")
Expand Down
25 changes: 18 additions & 7 deletions backend/app/html/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
from jinja2 import Environment, FileSystemLoader
from pydantic import EmailStr, NonNegativeInt
from sqlalchemy.dialects.postgresql import insert
from sqlmodel import select
from sqlmodel import Session, select

from app.dependencies import AuthDep, SessionDep
from app.dependencies import AuthDep, SessionDep, engine
from app.enums import Category
from app.models import Cart, CartItem, NewCart, NewCartItem, NewUser, Product, User
from app.product_store import load_seed_products

router = APIRouter()

Expand All @@ -35,14 +36,24 @@ async def index():
@router.get("/html/products", response_class=HTMLResponse)
async def get_products(
request: Request,
session: SessionDep,
categories: Annotated[list[Category] | None, Query()] = None,
):
query = select(Product)
if categories:
query = query.where(Product.categories.overlap(categories))
if engine is None:
products = load_seed_products()
if categories:
requested_categories = {str(category) for category in categories}
products = [
product
for product in products
if requested_categories.intersection(product.categories)
]
else:
with Session(engine) as session:
query = select(Product)
if categories:
query = query.where(Product.categories.overlap(categories))

products = session.exec(query).all()
products = session.exec(query).all()

return templates.TemplateResponse(
request, "products.html", context={"products": products}
Expand Down
8 changes: 8 additions & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ class Product(SQLModel, table=True):
description: str
picture: str
price: Decimal = Field(decimal_places=2)
compare_at_price: Decimal | None = Field(default=None, decimal_places=2)
quantity: int = 0
handle: str | None = None
variant_label: str | None = None
badge_text: str | None = None
rating: Decimal = Field(default=Decimal("0.0"), decimal_places=1)
review_count: int = 0
hover_video: str
hover_picture: str
categories: list[str] = Field(sa_column=Column(ARRAY(Text)))


Expand Down
15 changes: 15 additions & 0 deletions backend/app/product_seed_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from sqlmodel import SQLModel

from app.models import Product


class CollectionHero(SQLModel):
title: str
subtitle: str
video: str
poster: str | None = None


class ProductSeed(SQLModel):
collection_hero: CollectionHero | None = None
products: list[Product]
13 changes: 13 additions & 0 deletions backend/app/product_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from pathlib import Path

from app.product_seed_models import ProductSeed

PRODUCTS_JSON_PATH = Path(__file__).resolve().parents[1] / "db" / "products.json"


def load_seed_data():
return ProductSeed.model_validate_json(PRODUCTS_JSON_PATH.read_text())


def load_seed_products():
return load_seed_data().products
Binary file removed backend/app/static/products/bamboo-glass-jar.jpg
Binary file not shown.
Binary file removed backend/app/static/products/baseball-cap.jpg
Binary file not shown.
Binary file removed backend/app/static/products/candle-holder.jpg
Binary file not shown.
Binary file removed backend/app/static/products/canvas-backpack.jpg
Binary file not shown.
Binary file removed backend/app/static/products/ceramic-vase.jpg
Binary file not shown.
Binary file removed backend/app/static/products/chef-knife.jpg
Binary file not shown.
Binary file removed backend/app/static/products/cutting-board.jpg
Binary file not shown.
Binary file removed backend/app/static/products/denim-jacket.jpg
Binary file not shown.
Binary file removed backend/app/static/products/desk-lamp.jpg
Binary file not shown.
Binary file removed backend/app/static/products/face-moisturizer.jpg
Binary file not shown.
Binary file removed backend/app/static/products/hair-brush.jpg
Binary file not shown.
Binary file removed backend/app/static/products/hairdryer.jpg
Binary file not shown.
Binary file added backend/app/static/products/lilluv-bandana.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added backend/app/static/products/liluv-gift-card.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed backend/app/static/products/loafers.jpg
Binary file not shown.
Binary file removed backend/app/static/products/mug.jpg
Diff not rendered.
Binary file removed backend/app/static/products/running-shoes.jpg
Diff not rendered.
Diff not rendered.
Binary file removed backend/app/static/products/sunglasses.jpg
Diff not rendered.
Binary file removed backend/app/static/products/tank-top.jpg
Diff not rendered.
Binary file removed backend/app/static/products/throw-blanket.jpg
Diff not rendered.
Binary file removed backend/app/static/products/watch.jpg
Diff not rendered.
Binary file not shown.
Binary file added backend/app/static/videos/liluv-bandana-video.mp4
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading