feat: add base template with Pico CSS dark theme and home page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ from fastapi.templating import Jinja2Templates
|
|||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.logging_config import setup_logging
|
from app.logging_config import setup_logging
|
||||||
from app.routes.health import router as health_router
|
from app.routes.health import router as health_router
|
||||||
|
from app.routes.pages import router as pages_router
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ def create_app() -> FastAPI:
|
|||||||
|
|
||||||
# Register route modules
|
# Register route modules
|
||||||
app.include_router(health_router)
|
app.include_router(health_router)
|
||||||
|
app.include_router(pages_router)
|
||||||
|
|
||||||
logger.info("app_started", environment=settings.app_env)
|
logger.info("app_started", environment=settings.app_env)
|
||||||
|
|
||||||
|
|||||||
23
app/routes/pages.py
Normal file
23
app/routes/pages.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
"""Page routes for serving full HTML pages.
|
||||||
|
|
||||||
|
Renders Jinja2 templates for user-facing pages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
router = APIRouter(tags=["pages"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def home_page(request: Request) -> HTMLResponse:
|
||||||
|
"""Render the home page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The incoming HTTP request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Rendered home page HTML.
|
||||||
|
"""
|
||||||
|
templates = request.app.state.templates
|
||||||
|
return templates.TemplateResponse(request, "pages/home.html")
|
||||||
36
app/static/css/sneakyswole.css
Normal file
36
app/static/css/sneakyswole.css
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/* SneakySwole custom CSS overrides
|
||||||
|
Built on top of Pico CSS dark theme.
|
||||||
|
Keep Pico mostly untouched — only override what's necessary. */
|
||||||
|
|
||||||
|
/* Slightly bolder nav styling */
|
||||||
|
nav h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active nav link indicator */
|
||||||
|
nav a[aria-current="page"] {
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spacing for main content area */
|
||||||
|
main.container {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flash message styling */
|
||||||
|
.flash-success {
|
||||||
|
color: var(--pico-ins-color);
|
||||||
|
border-left: 4px solid var(--pico-ins-color);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-error {
|
||||||
|
color: var(--pico-del-color);
|
||||||
|
border-left: 4px solid var(--pico-del-color);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
47
app/templates/base.html
Normal file
47
app/templates/base.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}SneakySwole{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- Pico CSS (dark theme via data-theme="dark" on <html>) -->
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||||
|
|
||||||
|
<!-- Custom overrides -->
|
||||||
|
<link rel="stylesheet" href="/static/css/sneakyswole.css">
|
||||||
|
|
||||||
|
<!-- HTMX -->
|
||||||
|
<script src="https://unpkg.com/htmx.org@2.0.4"
|
||||||
|
integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
{% block head_extra %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="container">
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><strong><a href="/">SneakySwole</a></strong></li>
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
{% block nav_items %}
|
||||||
|
<!-- Phase 3 adds: profile switcher, login/logout -->
|
||||||
|
{% endblock %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
{% block flash %}{% endblock %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="container">
|
||||||
|
<small>SneakySwole — Open-source workout tracking</small>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
app/templates/pages/home.html
Normal file
12
app/templates/pages/home.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}SneakySwole — Home{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<hgroup>
|
||||||
|
<h1>SneakySwole</h1>
|
||||||
|
<p>Your open-source workout tracker</p>
|
||||||
|
</hgroup>
|
||||||
|
|
||||||
|
<p>Welcome to SneakySwole. Get started by logging in.</p>
|
||||||
|
{% endblock %}
|
||||||
22
tests/test_pages.py
Normal file
22
tests/test_pages.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"""Tests for HTML page routes."""
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
class TestHomePage:
|
||||||
|
"""Tests for GET /."""
|
||||||
|
|
||||||
|
def test_home_returns_200(self, client: TestClient) -> None:
|
||||||
|
"""GET / should return HTTP 200."""
|
||||||
|
response = client.get("/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_home_contains_app_name(self, client: TestClient) -> None:
|
||||||
|
"""GET / should render HTML containing the app name."""
|
||||||
|
response = client.get("/")
|
||||||
|
assert "SneakySwole" in response.text
|
||||||
|
|
||||||
|
def test_home_has_dark_theme(self, client: TestClient) -> None:
|
||||||
|
"""GET / should use the dark theme."""
|
||||||
|
response = client.get("/")
|
||||||
|
assert 'data-theme="dark"' in response.text
|
||||||
Reference in New Issue
Block a user