10 KiB
Landing Page & Shared Layout Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a shared Jinja2/Tailwind CSS layout and a module-card landing page for the University Process Hub.
Architecture: FastAPI serves HTML via Jinja2Templates. base.html defines the red navbar (#e2001a), a {% block content %} slot, and a footer. index.html extends it with a hero section, responsive 3-column module-card grid, and an info strip. Module metadata is a static list in main.py — no DB access on the landing page.
Tech Stack: FastAPI, Jinja2, Tailwind CSS Play CDN (no build step), pytest, httpx
Task 1: Add dependencies and update .gitignore
Files:
-
Modify:
requirements.txt -
Modify:
.gitignore -
Step 1: Update
requirements.txt
fastapi
uvicorn[standard]
pydantic-settings
sqlalchemy
jinja2
pytest
httpx
- Step 2: Add
.superpowers/to.gitignore
Append to the # Project specific section:
.superpowers/
- Step 3: Install new dependencies
.venv/bin/pip install -r requirements.txt
Expected: jinja2, pytest, httpx installed without error.
- Step 4: Commit
git add requirements.txt .gitignore
git commit -m "chore: add jinja2 and test dependencies"
Task 2: Create base layout template
Files:
-
Create:
app/templates/base.html -
Create:
app/static/.gitkeep -
Step 1: Create directories
mkdir -p app/templates app/static
touch app/static/.gitkeep
- Step 2: Write
app/templates/base.html
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}EFI Hub{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: { 'efi': '#e2001a' }
}
}
}
</script>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col">
<nav class="bg-efi h-14 flex items-center px-8 shrink-0">
<a href="/" class="text-white font-bold text-lg tracking-tight mr-8">
EFI<span class="opacity-70 font-normal">Hub</span>
</a>
{% for item in nav_items %}
<a href="{{ item.url }}"
class="text-white/85 hover:text-white text-sm px-4 h-14 flex items-center border-b-2
{% if item.active %}border-white text-white{% else %}border-transparent{% endif %}">
{{ item.label }}
</a>
{% endfor %}
<div class="ml-auto">
<button class="text-white text-sm px-4 py-1.5 rounded border border-white/40 bg-white/15 hover:bg-white/25">
Anmelden
</button>
</div>
</nav>
<main class="flex-1">
{% block content %}{% endblock %}
</main>
<footer class="bg-gray-100 border-t border-gray-300 px-8 py-3.5 flex items-center justify-between text-xs text-gray-400 shrink-0">
<span class="font-semibold text-gray-500">EFI Hub</span>
<span>Technische Hochschule Nürnberg Georg Simon Ohm · Fakultät EFI</span>
<span>v{{ app_version }}</span>
</footer>
</body>
</html>
- Step 3: Commit
git add app/templates/base.html app/static/.gitkeep
git commit -m "feat: add base layout template with red navbar and footer"
Task 3: Create landing page template
Files:
-
Create:
app/templates/index.html -
Step 1: Write
app/templates/index.html
{% extends "base.html" %}
{% block title %}University Process Hub{% endblock %}
{% block content %}
<div class="bg-white border-b border-gray-200 px-8 py-10">
<p class="text-xs font-semibold uppercase tracking-widest text-efi mb-2">
Fakultät EFI · TH Nürnberg
</p>
<h1 class="text-2xl font-bold text-gray-900 mb-2">University Process Hub</h1>
<p class="text-sm text-gray-500 max-w-xl leading-relaxed">
Modulares Informationssystem für Fakultätsprozesse.
Startpunkt für studentische Arbeiten und Schnittstelle für interne Dienste.
</p>
</div>
<div class="px-8 py-7">
<p class="text-xs font-semibold uppercase tracking-widest text-gray-400 mb-4">
Verfügbare Module
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{% for module in modules %}
<div class="bg-white rounded-md border border-gray-200 shadow-sm p-5 relative overflow-hidden
hover:shadow-md hover:border-efi transition-all duration-150 cursor-pointer">
<div class="absolute top-0 left-0 right-0 h-0.5 bg-efi"></div>
<div class="text-2xl mb-2">{{ module.icon }}</div>
<div class="font-semibold text-gray-900 text-sm mb-1">{{ module.name }}</div>
<div class="text-xs text-gray-500 leading-relaxed mb-3">{{ module.description }}</div>
{% if module.status == "active" %}
<span class="text-xs font-semibold px-2 py-0.5 rounded-full bg-green-50 text-green-700 border border-green-200">Aktiv</span>
{% else %}
<span class="text-xs font-semibold px-2 py-0.5 rounded-full bg-gray-100 text-gray-500 border border-gray-200">Geplant</span>
{% endif %}
</div>
{% endfor %}
<div class="bg-white rounded-md border border-dashed border-gray-300 p-5">
<div class="text-2xl mb-2 opacity-30">➕</div>
<div class="font-semibold text-gray-300 text-sm mb-1">Neues Modul</div>
<div class="text-xs text-gray-300 leading-relaxed">Erweiterbar durch studentische Arbeiten.</div>
</div>
</div>
</div>
<div class="bg-white border-t border-gray-200 px-8 py-4 flex flex-wrap items-center gap-8">
<span class="text-xs text-gray-500">API: <strong class="text-gray-800">FastAPI</strong></span>
<span class="text-xs text-gray-500">Datenbank: <strong class="text-gray-800">{{ db_mode }}</strong></span>
<span class="text-xs text-gray-500">WebSocket: <strong class="text-gray-800">aktiv</strong></span>
<div class="ml-auto text-xs font-medium text-efi">● System online</div>
</div>
{% endblock %}
- Step 2: Commit
git add app/templates/index.html
git commit -m "feat: add landing page template with module grid and info strip"
Task 4: Wire templates into FastAPI and write tests
Files:
-
Modify:
app/main.py -
Create:
tests/__init__.py -
Create:
tests/test_landing.py -
Step 1: Write the failing tests
mkdir -p tests && touch tests/__init__.py
Schreibe tests/test_landing.py:
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_landing_returns_html():
response = client.get("/")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
def test_landing_contains_title():
response = client.get("/")
assert "University Process Hub" in response.text
def test_landing_contains_rss_module():
response = client.get("/")
assert "RSS-Feed Server" in response.text
def test_landing_navbar_links_present():
response = client.get("/")
assert "Übersicht" in response.text
assert "RSS-Feeds" in response.text
def test_landing_info_strip_shows_db_mode():
response = client.get("/")
assert "SQLite" in response.text or "MariaDB" in response.text
- Step 2: Run tests to confirm they fail
.venv/bin/pytest tests/test_landing.py -v
Expected: FAIL — GET / returns JSON, not HTML.
- Step 3: Replace
app/main.pywith template-serving version
from fastapi import FastAPI, Request, WebSocket
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from app.core.config import get_settings
settings = get_settings()
app = FastAPI(
title="University Process Hub",
root_path=settings.APP_PREFIX,
)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
templates = Jinja2Templates(directory="app/templates")
MODULES = [
{
"icon": "📡",
"name": "RSS-Feed Server",
"description": "Aggregiert und verteilt Neuigkeiten der Fakultät als standardkonformen RSS 2.0 Feed.",
"status": "active",
},
{
"icon": "📅",
"name": "Kalender",
"description": "Veranstaltungen, Prüfungstermine und Fristen im iCal-Format.",
"status": "planned",
},
{
"icon": "🔔",
"name": "Benachrichtigungen",
"description": "Push-Nachrichten und WebSocket-basierte Echtzeit-Alerts für Studierende.",
"status": "planned",
},
{
"icon": "📊",
"name": "Auslastung",
"description": "Raum- und Ressourcenauslastung der Fakultät in Echtzeit.",
"status": "planned",
},
{
"icon": "📚",
"name": "Lehrveranstaltungen",
"description": "Stundenplan-API und Kursinformationen aus dem Campus-System.",
"status": "planned",
},
]
NAV_ITEMS = [
{"label": "Übersicht", "url": "/", "active": True},
{"label": "RSS-Feeds", "url": "/rss", "active": False},
{"label": "Kalender", "url": "/kalender", "active": False},
{"label": "Docs", "url": "/docs", "active": False},
]
def _db_mode() -> str:
url = settings.DATABASE_URL
return "SQLite (dev)" if url.startswith("sqlite") else "MariaDB (prod)"
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
return templates.TemplateResponse(
request,
"index.html",
{
"nav_items": NAV_ITEMS,
"modules": MODULES,
"db_mode": _db_mode(),
"app_version": "0.1.0",
},
)
@app.websocket("/ws/hello/{name}")
async def websocket_hello(websocket: WebSocket, name: str):
await websocket.accept()
await websocket.send_text(f"Hello, {name}!")
await websocket.close()
- Step 4: Run tests to confirm they pass
.venv/bin/pytest tests/test_landing.py -v
Expected: All 5 tests PASS.
- Step 5: Verify in browser
.venv/bin/uvicorn app.main:app --reload
Öffne http://localhost:8000 — Navbar rot, Hero-Bereich, 5 Modul-Karten + Placeholder-Karte, Info-Strip, Footer.
- Step 6: Commit
git add app/main.py tests/
git commit -m "feat: serve landing page via Jinja2 with module grid"