Add Phase 1 MVP: D&D text-adventure server

FastAPI + Postgres backend with fastapi-users auth, games/characters
CRUD, and a WebSocket chat endpoint where an x.ai Grok DM narrates
play, rolls dice, and maintains character sheets via tool calls.
React + Tailwind frontend covers registration/login, game list and
creation, participation-code join flow, character viewer, and a
real-time chat session view. Docker Compose skeleton (Postgres +
backend) included; Caddy/frontend container wiring is deferred to
the deploy milestone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thorsten
2026-08-31 18:43:08 +02:00
commit f37dc9fa76
75 changed files with 106988 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
import { useEffect, useState } from "react";
import { Link, useParams, useSearchParams } from "react-router-dom";
import { Character, getCharacter } from "../api/characters";
export default function CharacterDetail() {
const { characterId } = useParams<{ characterId: string }>();
const [searchParams] = useSearchParams();
const backGameId = searchParams.get("back");
const [character, setCharacter] = useState<Character | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!characterId) return;
getCharacter(characterId)
.then(setCharacter)
.finally(() => setLoading(false));
}, [characterId]);
if (loading) return <p className="p-8 text-slate-400">Lädt…</p>;
if (!character) return <p className="p-8 text-slate-400">Charakter nicht gefunden.</p>;
return (
<div className="mx-auto max-w-2xl px-4 py-8">
{backGameId && (
<Link
to={`/games/${backGameId}`}
className="mb-4 inline-block rounded bg-slate-800 px-3 py-2 text-sm hover:bg-slate-700"
>
← Zurück zum Spiel
</Link>
)}
<h1 className="text-2xl font-bold text-amber-400">{character.name}</h1>
<p className="mt-1 text-slate-400">
{character.race ?? "?"} · {character.char_class ?? "?"} · Stufe {character.level}
</p>
{Object.keys(character.stats).length > 0 && (
<div className="mt-4 grid grid-cols-3 gap-3 sm:grid-cols-6">
{Object.entries(character.stats).map(([stat, value]) => (
<div key={stat} className="rounded bg-slate-800 p-2 text-center">
<div className="text-xs uppercase text-slate-500">{stat}</div>
<div className="text-lg font-semibold text-slate-100">{value}</div>
</div>
))}
</div>
)}
<div className="mt-6 whitespace-pre-wrap rounded-lg border border-slate-800 bg-slate-900 p-4 text-slate-300">
{character.description || "Noch keine Beschreibung."}
</div>
</div>
);
}