Add content to shop page

This commit is contained in:
2025-10-29 09:39:42 +01:00
parent 8fd8b710fb
commit bd2f7b286e
14 changed files with 169 additions and 56 deletions

0
dev-server.sh Normal file → Executable file
View File

View File

@@ -0,0 +1,23 @@
from fastapi import APIRouter, Request
from allmende_payment_system.api.dependencies import SessionDep, UserDep
from allmende_payment_system.database import ensure_user
from allmende_payment_system.tools import get_jinja_renderer
root_router = APIRouter()
templates = get_jinja_renderer()
@root_router.get("/")
async def landing_page(request: Request, user_info: UserDep, session: SessionDep):
user = ensure_user(user_info, session)
print(f"User {user.username} ({user.display_name}) accessed landing page")
transactions = []
for account in user.accounts:
transactions += account.transactions
transactions = sorted(transactions, key=lambda t: t.timestamp)
return templates.TemplateResponse(
"index.html.jinja",
context={"request": request, "user": user, "transactions": transactions},
)

View File

@@ -0,0 +1,33 @@
import os
from typing import Annotated
from fastapi import Depends, HTTPException, Request
from sqlalchemy.orm import Session
from allmende_payment_system.database import SessionLocal
async def get_user(request: Request) -> dict:
if username := os.environ.get("APS_username", None):
return {
"username": username,
"display_name": os.environ.get("APS_display_name", "Missing Display Name"),
}
if "ynh_user" not in request.headers:
raise HTTPException(status_code=401, detail="Missing ynh_user header")
return {"username": request.headers["ynh_user"]}
UserDep = Annotated[dict, Depends(get_user)]
def get_session() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
SessionDep = Annotated[Session, Depends(get_session)]

View File

@@ -0,0 +1,20 @@
from fastapi import APIRouter, Request
from sqlalchemy import select
from allmende_payment_system.api import SessionDep
from allmende_payment_system.models import Area
from allmende_payment_system.tools import get_jinja_renderer
shop_router = APIRouter()
templates = get_jinja_renderer()
@shop_router.get("/shop")
async def get_shop(request: Request, session: SessionDep):
query = select(Area)
areas = session.scalars(query).all()
return templates.TemplateResponse(
"shop.html.jinja",
context={"request": request, "areas": areas},
)

View File

@@ -1,32 +1,13 @@
import os
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi import Depends, FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from allmende_payment_system.database import SessionLocal, ensure_user
async def get_user(request: Request) -> dict:
if username := os.environ.get("APS_username", None):
return {
"username": username,
"display_name": os.environ.get("APS_display_name", "Missing Display Name"),
}
if "ynh_user" not in request.headers:
raise HTTPException(status_code=401, detail="Missing ynh_user header")
return {"username": request.headers["ynh_user"]}
UserDep = Annotated[dict, Depends(get_user)]
from allmende_payment_system.api import root_router
from allmende_payment_system.api.dependencies import get_user
from allmende_payment_system.api.shop import shop_router
app = FastAPI(dependencies=[Depends(get_user)])
templates = Jinja2Templates(directory="src/allmende_payment_system/templates")
app.mount(
"/static",
StaticFiles(directory="src/allmende_payment_system/static"),
@@ -34,26 +15,5 @@ app.mount(
)
def get_session() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
SessionDep = Annotated[Session, Depends(get_session)]
@app.get("/")
async def landing_page(request: Request, user_info: UserDep, session: SessionDep):
user = ensure_user(user_info, session)
print(f"User {user.username} ({user.display_name}) accessed landing page")
transactions = []
for account in user.accounts:
transactions += account.transactions
transactions = sorted(transactions, key=lambda t: t.timestamp)
return templates.TemplateResponse(
"index.html.jinja",
context={"request": request, "user": user, "transactions": transactions},
)
app.include_router(root_router)
app.include_router(shop_router)

View File

@@ -52,6 +52,8 @@ class Area(Base):
__tablename__ = TABLE_PREFIX + "area"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(nullable=False, unique=True)
description: Mapped[str] = mapped_column(nullable=True)
image_path: Mapped[str] = mapped_column(nullable=True)
class Product(Base):

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -21,22 +21,22 @@
<ul class="nav nav-pills flex-column mb-auto">
<li class="nav-item">
<a href="#" class="nav-link active">
Dashboard
Übersicht
</a>
</li>
<li class="nav-item">
<a href="/shop" class="nav-link">
Einkaufen
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">
Projects
Lorem
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">
Reports
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">
Settings
Ipsum
</a>
</li>
</ul>

View File

@@ -45,7 +45,7 @@
{% for transaction in transactions[:10] %}
<div class="list-group-item d-flex justify-content-between align-items-start py-3">
<div class="flex-grow-1">
<div class="fw-semibold">{{ transaction.product.name }}</div>
<div class="fw-semibold">{{ transaction.product.name or transaction.type|transaction_type_de }}</div>
<small class="text-muted">
{{ transaction.timestamp }}
</small>

View File

@@ -0,0 +1,61 @@
{% extends "base.html.jinja" %}
{% block content %}
<!-- Shop Landing Page Header -->
<div class="mb-4">
<h2 class="h4 mb-3">Shop</h2>
<p class="text-muted">In welchem Bereich möchtest du einkaufen?</p>
</div>
<div class="row g-3">
{% for area in areas %}
<div class="col-md-6 col-lg-4">
<a href="#" class="text-decoration-none">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body d-flex align-items-center p-3">
<!-- Image on the left -->
<div class="me-3" style="width: 120px; flex-shrink: 0;">
<img
src="/static/img/{{ area.image_path }}"
alt="{{ area.name }}"
class="img-fluid rounded"
style="max-height: 100px; width: 100%; object-fit: contain;"
>
</div>
<!-- Title and description on the right -->
<div class="flex-grow-1">
<h5 class="card-title mb-1">{{ area.name }}</h5>
<p class="card-text text-muted small mb-0">{{ area.description or '' }}</p>
</div>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
<!-- Optional: Featured Products Section -->
{# <div class="mt-5">#}
{# <div class="d-flex justify-content-between align-items-center mb-3">#}
{# <h2 class="h4 mb-0">Featured Products</h2>#}
{# <a href="#" class="btn btn-outline-primary btn-sm">View All</a>#}
{# </div>#}
{# <div class="alert alert-info">#}
{# Featured products will appear here#}
{# </div>#}
{# </div>#}
{% endblock %}
{% block styles %}
<style>
/* Hover effect for shop tiles */
.hover-shadow:hover {
transform: translateY(-3px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
transition: all 0.3s ease;
}
.card {
border: none;
transition: all 0.3s ease;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,14 @@
from starlette.templating import Jinja2Templates
TRANSACTION_TYPE_DE = {
"deposit": "Einzahlung",
"withdrawal": "Auszahlung",
"expense": "Auslage",
"product": "Einkauf",
}
def get_jinja_renderer() -> Jinja2Templates:
renderer = Jinja2Templates(directory="src/allmende_payment_system/templates")
renderer.env.filters["transaction_type_de"] = lambda x: TRANSACTION_TYPE_DE[x]
return renderer