feat: theme selection page, dynamic content from API, pony data from backend
This commit is contained in:
@@ -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] }}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user