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.logging_config import setup_logging
|
||||
from app.routes.health import router as health_router
|
||||
from app.routes.pages import router as pages_router
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -47,6 +48,7 @@ def create_app() -> FastAPI:
|
||||
|
||||
# Register route modules
|
||||
app.include_router(health_router)
|
||||
app.include_router(pages_router)
|
||||
|
||||
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