feat: theme selection page, dynamic content from API, pony data from backend

This commit is contained in:
2026-08-09 07:47:49 +00:00
parent 466dbc2214
commit cfb0a95df3
2 changed files with 111 additions and 7 deletions

View File

@@ -6,13 +6,26 @@ import './App.css';
const API = window.location.origin.replace('3001', '8082');
const PONIES = [
// Fallback pony data if API fails to load
const DEFAULT_PONIES = [
{ navn: 'Jordpony', emoji: '🐴', img: 'jordpony.png', bonus: 'Stærk 💪', color: '#8B4513', diceBonus: 1 },
{ navn: 'Pegasus', emoji: '🦅', img: 'pegasus.png', bonus: 'Flyver 🪽', color: '#87CEEB', diceBonus: 1 },
{ navn: 'Enhjørning', emoji: '🦄', img: 'enhjorning.png', bonus: 'Magisk horn ✨', color: '#9370DB', diceBonus: 2 },
{ navn: 'Alicorn', emoji: '👑', img: 'alicorn.png', bonus: 'Magi + vinger 🌟', color: '#FFD700', diceBonus: 2 },
{ name: 'Alicorn', emoji: '👑', img: 'alicorn.png', bonus: 'Magi + vinger 🌟', color: '#FFD700', diceBonus: 2 },
];
// Fetch content (ponies + themes) from backend
async function loadContent() {
try {
const r = await fetch(`${API}/api/content`);
if (r.ok) {
const data = await r.json();
return { ponies: data.ponies, themes: data.themes };
}
} catch (e) { /* fall through */ }
return { ponies: DEFAULT_PONIES, themes: [] };
}
// Achievement system
function useAchievements() {
const [stats, setStats] = useState(() => {
@@ -290,10 +303,17 @@ function App() {
const [volume, setVolume] = useState(0.5);
const [showAchievements, setShowAchievements] = useState(false);
const [soundEnabled, setSoundEnabled] = useState(false);
const [selectedTheme, setSelectedTheme] = useState(0);
const [content, setContent] = useState({ ponies: DEFAULT_PONIES, themes: [] });
const scrollRef = useRef(null);
const { stats, recordGame } = useAchievements();
const { shown: tutorialShown, markSeen } = useTutorial();
// Load content from backend on mount
useEffect(() => {
loadContent().then(setContent);
}, []);
// Auto-scroll history feed
useEffect(() => {
if (scrollRef.current && data && data.history && data.history.length > 0) {
@@ -320,7 +340,7 @@ function App() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ type: typeIdx, tema: 0 })
body: JSON.stringify({ type: typeIdx, tema: selectedTheme })
});
if (!r.ok) throw new Error('Server fejl');
const json = await r.json();
@@ -475,7 +495,7 @@ function App() {
{/* Tutorial overlay */}
{!tutorialShown && soundEnabled && !showAchievements && (
<AnimatePresence>
<TutorialOverlay onClose={() => { markSeen(); navigateTo('start'); }} />
<TutorialOverlay onClose={() => { markSeen(); setSelectedTheme(0); navigateTo('theme'); }} />
</AnimatePresence>
)}
@@ -556,7 +576,7 @@ function App() {
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="btn-start"
onClick={() => navigateTo('start')}
onClick={() => { setSelectedTheme(0); navigateTo('theme'); }}
aria-label="Start nyt spil"
>
🎮 Start Nyt Spil!
@@ -565,6 +585,57 @@ function App() {
</motion.div>
)}
{/* CHOOSE THEME */}
{page === 'theme' && soundEnabled && (
<motion.div
key="theme"
variants={pageVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.4 }}
className="start-page"
>
<SceneMusic sceneType={volume > 0 ? 'home' : 'none'} />
<div className="top-bar">
<VolumeControl volume={volume} onChange={setVolume} />
</div>
<FloatingBg />
<motion.h1 initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="title">
Vælg et eventyr! 📖
</motion.h1>
<motion.p className="page-desc" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.3 }}>
Hvilken historie vil du opleve?
</motion.p>
<div className="pony-choices">
{content.themes.map((t, i) => (
<motion.div
key={t.id || i}
whileHover={{ scale: 1.08, y: -8 }}
whileTap={{ scale: 0.95 }}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.15 }}
className="pony-card"
style={{ borderColor: selectedTheme === i ? '#f093fb' : 'transparent' }}
onClick={() => { setSelectedTheme(i); navigateTo('start'); }}
role="button"
tabIndex={0}
aria-label={`Vælg ${t.titel}`}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { setSelectedTheme(i); navigateTo('start'); } }}
>
<div className="pony-emoji">{t.emoji}</div>
<h3>{t.titel}</h3>
<p className="pony-bonus">{t.sceneCount || 5} scener</p>
</motion.div>
))}
</div>
<motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }} className="btn-back" onClick={() => navigateTo('home')} aria-label="Tilbage til forsiden">
Tilbage
</motion.button>
</motion.div>
)}
{/* CHOOSE PONY */}
{page === 'start' && soundEnabled && (
<motion.div
@@ -588,7 +659,7 @@ function App() {
Hver pony har sine egne superkræfter!
</motion.p>
<div className="pony-choices">
{PONIES.map((p, i) => (
{content.ponies.map((p, i) => (
<motion.div
key={p.navn}
whileHover={{ scale: 1.08, y: -8, rotate: [0, -2, 2, 0] }}

View File

@@ -7,6 +7,8 @@ Uses game_id cookies for session persistence.
from flask import Blueprint, request, jsonify, make_response
from app.services.game_service import start_game, roll_scene, format_scene_data
from app.services.persistence import create_game, get_game, update_game, delete_game
from app.data.themes import THEMAER
from app.game.pony import PONITYPER
api_bp = Blueprint("api", __name__)
@@ -14,7 +16,7 @@ GAME_COOKIE = "pony_game_id"
def _get_game_from_cookie():
"""Get game from cookie, return (response_modifier, game) tuple."""
"""Get game from cookie, return (game_id, game) tuple."""
game_id = request.cookies.get(GAME_COOKIE)
if not game_id:
return None, None
@@ -92,6 +94,37 @@ def api_reset():
return resp
@api_bp.route("/api/content", methods=["GET"])
def api_content():
"""Get all game metadata: pony types and themes.
Used by frontend to render selection pages dynamically.
"""
return jsonify({
"ponies": [
{
"id": p["id"],
"navn": p["navn"],
"emoji": p["emoji"],
"bonus": p["bonus"],
"tekst": p["tekst"],
"img": p["img"],
}
for p in PONITYPER
],
"themes": [
{
"id": t["id"],
"titel": t["titel"],
"emoji": t.get("emoji", ""),
"intro": t.get("intro", ""),
"sceneCount": len(t.get("scener", [])),
}
for t in THEMAER
],
})
@api_bp.route("/api/health", methods=["GET"])
def api_health():
"""Health check endpoint."""