Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ __MACOSX/

# Node
node_modules/

/frontends/angular/app/environments/*
66 changes: 48 additions & 18 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 All @@ -54,14 +81,17 @@ async def get_product(id: str, session: SessionDep) -> Product:

@router.post("/api/users", status_code=status.HTTP_201_CREATED)
async def create_user(
user: NewUser, session: SessionDep, response: Response, _: AuthDep
user: NewUser, session: SessionDep, response: Response,
):
if session.get(User, user.username) is not None:
raise HTTPException(status_code=400, detail="User already exists")

user.password = hasher.hash(str(user.password))
pwd = user.password.get_secret_value()

session.add(User(**user.model_dump()))
user_data = user.model_dump()
user_data["password"] = hasher.hash(pwd)

session.add(User(**user_data))
session.commit()

response.headers["Location"] = f"/users/account"
Expand Down Expand Up @@ -144,11 +174,11 @@ async def delete_user_session(
@router.post("/api/carts", status_code=status.HTTP_201_CREATED)
async def create_cart(
id: UUID,
username: str | None,
response: Response,
user_session: UserSessionDep,
session: SessionDep,
_: AuthDep,
username: str | None = None,
):
cart = NewCart(id=id, username=username, session_id=user_session.id)

Expand Down Expand Up @@ -188,7 +218,7 @@ async def create_cart_item(
response: Response,
_: AuthDep,
):
cart = session.get(Cart, user_session.id)
cart = session.get(Cart, cart_id)

if cart is None or cart.session_id != user_session.id:
raise HTTPException(status_code=404, detail="Cart not found")
Expand All @@ -212,7 +242,7 @@ async def create_cart_item(
async def get_cart_items(
id: UUID, user_session: UserSessionDep, session: SessionDep, _: AuthDep
) -> Sequence[CartItem]:
cart = session.get(Cart, user_session.id)
cart = session.get(Cart, id)

if cart is None or cart.session_id != user_session.id:
raise HTTPException(status_code=404, detail="Cart not found")
Expand All @@ -230,7 +260,7 @@ async def get_cart_item(
session: SessionDep,
_: AuthDep,
) -> CartItem:
cart = session.get(Cart, user_session.id)
cart = session.get(Cart, id)

if cart is None or cart.session_id != user_session.id:
raise HTTPException(status_code=404, detail="Cart not found")
Expand All @@ -252,7 +282,7 @@ async def update_cart_item_quantity(
session: SessionDep,
_: AuthDep,
):
cart = session.get(Cart, user_session.id)
cart = session.get(Cart, id)

if cart is None or cart.session_id != user_session.id:
raise HTTPException(status_code=404, detail="Cart not found")
Expand All @@ -277,7 +307,7 @@ async def delete_cart_item(
session: SessionDep,
_: AuthDep,
):
cart = session.get(Cart, user_session.id)
cart = session.get(Cart, id)

if cart is None or cart.session_id != user_session.id:
raise HTTPException(status_code=404, detail="Cart not found")
Expand Down
2 changes: 1 addition & 1 deletion backend/app/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ def get_current_session(
return user_session


UserSessionDep = Annotated[UserSession, Depends(get_current_session)]
UserSessionDep = Annotated[UserSession, Depends(get_current_session)]
2 changes: 1 addition & 1 deletion backend/app/html/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,4 @@ async def create_account_post(
samesite="strict",
)

return redirect
return redirect
18 changes: 18 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware

from app.api.routes import router as json_router
from app.html.routes import router as html_router

app = FastAPI()

origins = [
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:4200",
"http://127.0.0.1:4200",
]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.mount("/static", StaticFiles(directory="app/static"), name="static")

app.include_router(json_router)
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
42 changes: 42 additions & 0 deletions backend/app/static/css/react/input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* Used only for building with tailwind */
@import "tailwindcss";

@layer base {
/* Pulled from lilluvdog style sheet */
:root {
--color-base-accent-1: 63, 68, 63;
--color-base-background-1: 255, 255, 255;
--color-base-background-2: 249, 246, 241;
--color-base-solid-button-labels: 255, 255, 255;
--color-base-text: 63, 68, 63;
--font-body-family: Assistant, sans-serif;
--font-body-style: normal;
--font-body-weight-bold: 700;
--font-body-weight: 400;
--font-heading-family: Assistant, sans-serif;
--font-heading-style: normal;
--font-heading-weight: 400;
}

:root {
--color-accent: rgb(var(--color-base-accent-1));
--color-bg-soft: rgb(var(--color-base-background-2));
--color-bg: rgb(var(--color-base-background-1));
--color-text: rgb(var(--color-base-text));
}

html {
font-family: var(--font-body-family);
}

body {
@apply bg-[color:var(--color-bg)] text-[color:var(--color-text)];
}

h1,
h2,
h3,
h4 {
font-family: var(--font-heading-family);
}
}
2 changes: 2 additions & 0 deletions backend/app/static/css/react/tailwind.css

Large diffs are not rendered by default.

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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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