371 lines
10 KiB
Markdown
371 lines
10 KiB
Markdown
# 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**
|
||
|
||
```bash
|
||
.venv/bin/pip install -r requirements.txt
|
||
```
|
||
|
||
Expected: `jinja2`, `pytest`, `httpx` installed without error.
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
mkdir -p app/templates app/static
|
||
touch app/static/.gitkeep
|
||
```
|
||
|
||
- [ ] **Step 2: Write `app/templates/base.html`**
|
||
|
||
```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**
|
||
|
||
```bash
|
||
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`**
|
||
|
||
```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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
mkdir -p tests && touch tests/__init__.py
|
||
```
|
||
|
||
Schreibe `tests/test_landing.py`:
|
||
|
||
```python
|
||
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**
|
||
|
||
```bash
|
||
.venv/bin/pytest tests/test_landing.py -v
|
||
```
|
||
|
||
Expected: FAIL — `GET /` returns JSON, not HTML.
|
||
|
||
- [ ] **Step 3: Replace `app/main.py` with template-serving version**
|
||
|
||
```python
|
||
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**
|
||
|
||
```bash
|
||
.venv/bin/pytest tests/test_landing.py -v
|
||
```
|
||
|
||
Expected: All 5 tests PASS.
|
||
|
||
- [ ] **Step 5: Verify in browser**
|
||
|
||
```bash
|
||
.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**
|
||
|
||
```bash
|
||
git add app/main.py tests/
|
||
git commit -m "feat: serve landing page via Jinja2 with module grid"
|
||
```
|