diff --git a/pony-frontend/src/App.js b/pony-frontend/src/App.js
index 68ae3f1..12d28a0 100644
--- a/pony-frontend/src/App.js
+++ b/pony-frontend/src/App.js
@@ -1,11 +1,15 @@
-import React, { useState, useEffect, useRef, useCallback } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
-import ReactDice from './dice/ReactDice';
-import SceneMusic, { playClick, playRoll, playSuccess, playFail, playSelect, playVictory, setMasterVolume, resumeAudioContext } from './SceneMusic';
+import SceneMusic, { playClick, playRoll, playSuccess, playFail, playSelect, playVictory, resumeAudioContext } from './SceneMusic';
+import TutorialOverlay from './components/TutorialOverlay';
+import HomePage from './pages/HomePage';
+import ThemeSelectPage from './pages/ThemeSelectPage';
+import PonySelectPage from './pages/PonySelectPage';
+import GameScenePage from './pages/GameScenePage';
+import GameEndPage from './pages/GameEndPage';
+import * as api from './services/api';
import './App.css';
-const API = window.location.origin.replace('3001', '8082');
-
// Fallback pony data if API fails to load
const DEFAULT_PONIES = [
{ navn: 'Jordpony', emoji: '🐴', img: 'jordpony.png', bonus: 'Stærk 💪', color: '#8B4513', diceBonus: 1 },
@@ -14,19 +18,7 @@ const DEFAULT_PONIES = [
{ 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
+// Achievement hook
function useAchievements() {
const [stats, setStats] = useState(() => {
try {
@@ -57,7 +49,7 @@ function useAchievements() {
return { stats, recordGame, resetStats };
}
-// Tutorial system
+// Tutorial hook
function useTutorial() {
const [shown, setShown] = useState(() => {
return localStorage.getItem('pony_tutorial_seen') === 'true';
@@ -71,223 +63,6 @@ function useTutorial() {
return { shown, markSeen };
}
-// Tutorial overlay component
-function TutorialOverlay({ onClose }) {
- return (
-
- e.stopPropagation()}
- >
- 🦄 Sådan spiller du!
-
- - Vælg en pony — hver har sine egne fordele ✨
- - Læs scenen — find ud af hvad du skal gøre 📖
- - Kast terningerne — held og lykke! 🎲
- - Overlev 5 scener — for at vinde! 🏆
-
-
-
-
- );
-}
-
-// Sparkle component
-function Sparkles() {
- const sparkles = Array.from({ length: 20 }, (_, i) => ({
- id: i,
- x: Math.random() * 100,
- y: Math.random() * 100,
- size: 8 + Math.random() * 16,
- delay: Math.random() * 3,
- duration: 2 + Math.random() * 2,
- emoji: ['✨', '⭐', '💫', '🌟', '🦋'][Math.floor(Math.random() * 5)]
- }));
- return (
-
- {sparkles.map((s) => (
-
- {s.emoji}
-
- ))}
-
- );
-}
-
-// Confetti component for victory
-function Confetti() {
- const pieces = Array.from({ length: 40 }, (_, i) => ({
- id: i,
- x: Math.random() * 100,
- delay: Math.random() * 2,
- duration: 2 + Math.random() * 2,
- color: ['#FF69B4', '#FFD700', '#00CED1', '#FF6347', '#7B68EE', '#32CD32'][Math.floor(Math.random() * 6)],
- size: 6 + Math.random() * 10,
- shape: Math.random() > 0.5 ? 'circle' : 'square'
- }));
- return (
-
- {pieces.map((p) => (
-
- ))}
-
- );
-}
-
-// Floating background elements
-function FloatingBg() {
- const items = [
- { emoji: '🌈', x: 10, y: 20, d: 4 },
- { emoji: '🦋', x: 80, y: 15, d: 3 },
- { emoji: '🌸', x: 20, y: 70, d: 5 },
- { emoji: '🎈', x: 70, y: 60, d: 3.5 },
- { emoji: '🍭', x: 50, y: 80, d: 4.5 },
- { emoji: '🌙', x: 90, y: 40, d: 5.5 },
- { emoji: '🎀', x: 5, y: 50, d: 3 },
- { emoji: '🌻', x: 60, y: 30, d: 4 },
- ];
- return (
-
- {items.map((item, i) => (
-
- {item.emoji}
-
- ))}
-
- );
-}
-
-// Dice component using react-dice-complete
-function DiceRoll({ dice }) {
- const reactDice = useRef(null);
- const hasRolled = useRef(false);
-
- useEffect(() => {
- if (dice && dice.length > 0 && !hasRolled.current) {
- hasRolled.current = true;
- playRoll();
- setTimeout(() => {
- reactDice.current?.rollAll(dice);
- }, 100);
- }
- }, [dice]);
-
- return (
-
-
-
- );
-}
-
-// Volume slider component
-function VolumeControl({ volume, onChange }) {
- return (
-
-
- {volume === 0 ? '🔇' : volume < 0.5 ? '🔉' : '🔊'}
-
- {
- const v = parseFloat(e.target.value);
- onChange(v);
- setMasterVolume(v);
- }}
- className="volume-slider"
- aria-label="Justér lydstyrke"
- title={`Lydstyrke: ${Math.round(volume * 100)}%`}
- />
-
- );
-}
-
-// Achievements display
-function Achievements({ stats }) {
- return (
-
-
🏆 Statistikk
-
-
- 🎮
- {stats.games}
- Spil
-
-
- 🌟
- {stats.wins}
- Sejre
-
-
- 💪
- {stats.losses}
- Nederlag
-
-
- 🏆
- {stats.bestScore}
- Bedste score
-
-
- {stats.games >= 1 && (
-
- {stats.wins / stats.games >= 0.5 ? '⭐ Halv vejen til Pony Mester!' : '🐣 Keep trying, little pony!'}
-
- )}
-
- );
-}
-
-// Page transition wrapper
const pageVariants = {
initial: { opacity: 0, y: 30 },
animate: { opacity: 1, y: 0 },
@@ -305,22 +80,16 @@ function App() {
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);
+ api.loadContent().then(result => {
+ setContent(result || { ponies: DEFAULT_PONIES, themes: [] });
+ });
}, []);
- // Auto-scroll history feed
- useEffect(() => {
- if (scrollRef.current && data && data.history && data.history.length > 0) {
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
- }
- }, [data]);
-
// Record game end achievements
const isGameEnd = page === 'game' && data && data.finished;
useEffect(() => {
@@ -331,19 +100,12 @@ function App() {
}
}, [isGameEnd, data]);
- const startGame = async (typeIdx) => {
+ const handleStartGame = async (typeIdx) => {
playSelect();
setLoading(true);
setError(null);
try {
- const r = await fetch(`${API}/api/start`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ type: typeIdx, tema: selectedTheme })
- });
- if (!r.ok) throw new Error('Server fejl');
- const json = await r.json();
+ const json = await api.startGame(typeIdx, selectedTheme);
setData(json);
setPage('game');
} catch (e) {
@@ -353,19 +115,13 @@ function App() {
}
};
- const rollDice = async () => {
+ const handleRollDice = async () => {
playRoll();
setDiceRolling(true);
setLoading(true);
try {
- const r = await fetch(`${API}/api/kast`, {
- method: 'POST',
- credentials: 'include'
- });
- if (!r.ok) throw new Error('Server fejl');
- const json = await r.json();
+ const json = await api.rollDice();
setData(json);
- // Play SFX based on result
const lastResult = json.history && json.history[json.history.length - 1];
if (lastResult) {
if (lastResult.success) playSuccess();
@@ -379,18 +135,12 @@ function App() {
}
};
- const retryRoll = async () => {
- // Retry the last roll without losing game state
+ const handleRetryRoll = async () => {
setError(null);
setDiceRolling(true);
setLoading(true);
try {
- const r = await fetch(`${API}/api/kast`, {
- method: 'POST',
- credentials: 'include'
- });
- if (!r.ok) throw new Error('Server fejl');
- const json = await r.json();
+ const json = await api.rollDice();
setData(json);
const lastResult = json.history && json.history[json.history.length - 1];
if (lastResult) {
@@ -412,13 +162,18 @@ function App() {
setError(null);
};
- // Enable sound on first user interaction
const enableSound = () => {
resumeAudioContext();
setSoundEnabled(true);
};
- // === LOADING (always shows first) ===
+ const handleTutorialClose = () => {
+ markSeen();
+ setSelectedTheme(0);
+ navigateTo('theme');
+ };
+
+ // === LOADING ===
if (loading) {
return (
@@ -437,7 +192,7 @@ function App() {
);
}
- // === ERROR (always shows first) ===
+ // === ERROR ===
if (error) {
const isMidGame = page === 'game' && data;
return (
@@ -450,7 +205,7 @@ function App() {
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="btn-start"
- onClick={retryRoll}
+ onClick={handleRetryRoll}
style={{ marginBottom: '0.5rem' }}
aria-label="Prøv igen"
>
@@ -464,10 +219,10 @@ function App() {
);
}
- // === RENDER PAGES WITH TRANSITIONS ===
+ // === RENDER PAGES ===
return (
- {/* Sound enable prompt */}
+ {/* Sound prompt */}
{!soundEnabled && (
- { markSeen(); setSelectedTheme(0); navigateTo('theme'); }} />
+
)}
- {/* HOME */}
- {page === 'home' && soundEnabled && (
-
- 0 ? 'home' : 'none'} />
-
-
-
-
- {showAchievements && (
- setShowAchievements(false)}
+ {/* Pages */}
+ {soundEnabled && (
+ <>
+ {page === 'home' && (
+
)}
- {showAchievements && (
-
-
-
-
- )}
-
-
-
-
- 🦄🌈✨
-
-
- My Little Pony
-
-
- Tails of Equestria
-
-
- Vælg din pony og gå på eventyr! 🎮
-
- { setSelectedTheme(0); navigateTo('theme'); }}
- aria-label="Start nyt spil"
- >
- 🎮 Start Nyt Spil!
-
-
-
- )}
- {/* CHOOSE THEME */}
- {page === 'theme' && soundEnabled && (
-
- 0 ? 'home' : 'none'} />
-
-
-
-
-
- Vælg et eventyr! 📖
-
-
- Hvilken historie vil du opleve?
-
-
- {content.themes.map((t, i) => (
-
{ 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'); } }}
- >
- {t.emoji}
- {t.titel}
- {t.sceneCount || 5} scener
-
- ))}
-
- navigateTo('home')} aria-label="Tilbage til forsiden">
- Tilbage
-
-
- )}
-
- {/* CHOOSE PONY */}
- {page === 'start' && soundEnabled && (
-
- 0 ? 'home' : 'none'} />
-
-
-
-
-
- Vælg din Pony! 🐴
-
-
- Hver pony har sine egne superkræfter!
-
-
- {content.ponies.map((p, i) => (
-
startGame(i)}
- role="button"
- tabIndex={0}
- aria-label={`Vælg ${p.navn} - ${p.bonus}`}
- onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') startGame(i); }}
- >
-
-
-
- {p.emoji}
- {p.navn}
- {p.bonus}
- {p.diceBonus > 0 && (
- +{p.diceBonus} på første terning 🎲
- )}
-
- ))}
-
- navigateTo('home')} aria-label="Tilbage til forsiden">
- Tilbage
-
-
- )}
-
- {/* GAME END */}
- {isGameEnd && soundEnabled && (
-
- 0 ? (data.victory ? 'victory' : data.mixed ? 'mixed' : 'defeat') : 'none'} />
-
-
-
- {data.victory && }
-
- {data.victory ? '🌟 SEJR! 🌟' : data.mixed ? '⚖️ Blant resultat! ⚖️' : '💪 Prøv igen! 💪'}
-
-
- {data.endText}
-
-
- {data.score}
-
-
- 📖 Hvad skete der? 📖
- {data.history.map((item, i) => (
-
- {item.action}
-
- {item.result}
- {item.story && {item.story}
}
-
- ))}
-
-
- { setData(null); setPage('start'); }}
- aria-label="Spil igen"
- >
- 🎲 Spil Igen!
-
- { setData(null); setPage('home'); }}
- aria-label="Tilbage til forsiden"
- >
- 🏠 Forside
-
-
-
- )}
-
- {/* GAME SCENE */}
- {page === 'game' && data && !data.finished && soundEnabled && (
-
- 0 ? 'game' : 'none'} />
-
-
-
-
-
-
- {Array.from({ length: 5 }, (_, i) => (
-
- ))}
-
- {data.tema}
-
-
-
-
-
- {data.ponyName}
- {data.ponyType}
-
-
+ )}
- {/* History feed scrolls up */}
-
-
- {(data.history || []).map((item, i) => (
-
-
- {item.success ? '✅' : '❌'}
- {item.action}
-
-
- {item.result}
- {item.story && {item.story}
}
-
- ))}
-
-
+ {page === 'start' && (
+
+ )}
- {/* Current scene */}
-
- {data.sceneNum}
- {data.sceneText}
-
- Du skal:
- {data.actionText}
-
- {data.difficulty}
-
+ {page === 'game' && data && !data.finished && (
+
+ )}
-
- 🎲 KAST TERNINGERNE! 🎲
-
-
+ {isGameEnd && (
+ {
+ if (p === 'start') {
+ setData(null);
+ setPage('start');
+ } else {
+ navigateTo(p);
+ }
+ }}
+ />
+ )}
+ >
)}
);
}
-export default App;
+export default App;
\ No newline at end of file
diff --git a/pony-frontend/src/components/Achievements.js b/pony-frontend/src/components/Achievements.js
new file mode 100644
index 0000000..3e1cdb0
--- /dev/null
+++ b/pony-frontend/src/components/Achievements.js
@@ -0,0 +1,36 @@
+import React from 'react';
+
+export default function Achievements({ stats }) {
+ return (
+
+
🏆 Statistikk
+
+
+ 🎮
+ {stats.games}
+ Spil
+
+
+ 🌟
+ {stats.wins}
+ Sejre
+
+
+ 💪
+ {stats.losses}
+ Nederlag
+
+
+ 🏆
+ {stats.bestScore}
+ Bedste score
+
+
+ {stats.games >= 1 && (
+
+ {stats.wins / stats.games >= 0.5 ? '⭐ Halv vejen til Pony Mester!' : '🐣 Keep trying, little pony!'}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/pony-frontend/src/components/Confetti.js b/pony-frontend/src/components/Confetti.js
new file mode 100644
index 0000000..5498032
--- /dev/null
+++ b/pony-frontend/src/components/Confetti.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+
+export default function Confetti() {
+ const pieces = Array.from({ length: 40 }, (_, i) => ({
+ id: i,
+ x: Math.random() * 100,
+ delay: Math.random() * 2,
+ duration: 2 + Math.random() * 2,
+ color: ['#FF69B4', '#FFD700', '#00CED1', '#FF6347', '#7B68EE', '#32CD32'][Math.floor(Math.random() * 6)],
+ size: 6 + Math.random() * 10,
+ shape: Math.random() > 0.5 ? 'circle' : 'square',
+ }));
+ return (
+
+ {pieces.map((p) => (
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/pony-frontend/src/components/DiceRoll.js b/pony-frontend/src/components/DiceRoll.js
new file mode 100644
index 0000000..715c835
--- /dev/null
+++ b/pony-frontend/src/components/DiceRoll.js
@@ -0,0 +1,39 @@
+import React, { useRef, useEffect } from 'react';
+import ReactDice from '../dice/ReactDice';
+import { playRoll } from '../SceneMusic';
+
+export default function DiceRoll({ dice }) {
+ const reactDice = useRef(null);
+ const hasRolled = useRef(false);
+
+ useEffect(() => {
+ if (dice && dice.length > 0 && !hasRolled.current) {
+ hasRolled.current = true;
+ playRoll();
+ setTimeout(() => {
+ reactDice.current?.rollAll(dice);
+ }, 100);
+ }
+ }, [dice]);
+
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/pony-frontend/src/components/FloatingBg.js b/pony-frontend/src/components/FloatingBg.js
new file mode 100644
index 0000000..c20dfe6
--- /dev/null
+++ b/pony-frontend/src/components/FloatingBg.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+
+const items = [
+ { emoji: '🌈', x: 10, y: 20, d: 4 },
+ { emoji: '🦋', x: 80, y: 15, d: 3 },
+ { emoji: '🌸', x: 20, y: 70, d: 5 },
+ { emoji: '🎈', x: 70, y: 60, d: 3.5 },
+ { emoji: '🍭', x: 50, y: 80, d: 4.5 },
+ { emoji: '🌙', x: 90, y: 40, d: 5.5 },
+ { emoji: '🎀', x: 5, y: 50, d: 3 },
+ { emoji: '🌻', x: 60, y: 30, d: 4 },
+];
+
+export default function FloatingBg() {
+ return (
+
+ {items.map((item, i) => (
+
+ {item.emoji}
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/pony-frontend/src/components/Sparkles.js b/pony-frontend/src/components/Sparkles.js
new file mode 100644
index 0000000..4a9b73f
--- /dev/null
+++ b/pony-frontend/src/components/Sparkles.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+
+export default function Sparkles() {
+ const sparkles = Array.from({ length: 20 }, (_, i) => ({
+ id: i,
+ x: Math.random() * 100,
+ y: Math.random() * 100,
+ size: 8 + Math.random() * 16,
+ delay: Math.random() * 3,
+ duration: 2 + Math.random() * 2,
+ emoji: ['✨', '⭐', '💫', '🌟', '🦋'][Math.floor(Math.random() * 5)],
+ }));
+ return (
+
+ {sparkles.map((s) => (
+
+ {s.emoji}
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/pony-frontend/src/components/TutorialOverlay.js b/pony-frontend/src/components/TutorialOverlay.js
new file mode 100644
index 0000000..58f3a0a
--- /dev/null
+++ b/pony-frontend/src/components/TutorialOverlay.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+
+export default function TutorialOverlay({ onClose }) {
+ return (
+
+ e.stopPropagation()}
+ >
+ 🦄 Sådan spiller du!
+
+ - Vælg en pony — hver har sine egne fordele ✨
+ - Læs scenen — find ud af hvad du skal gøre 📖
+ - Kast terningerne — held og lykke! 🎲
+ - Overlev 5 scener — for at vinde! 🏆
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/pony-frontend/src/components/VolumeControl.js b/pony-frontend/src/components/VolumeControl.js
new file mode 100644
index 0000000..96f536b
--- /dev/null
+++ b/pony-frontend/src/components/VolumeControl.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { setMasterVolume } from '../SceneMusic';
+
+export default function VolumeControl({ volume, onChange }) {
+ return (
+
+
+ {volume === 0 ? '🔇' : volume < 0.5 ? '🔉' : '🔊'}
+
+ {
+ const v = parseFloat(e.target.value);
+ onChange(v);
+ setMasterVolume(v);
+ }}
+ className="volume-slider"
+ aria-label="Justér lydstyrke"
+ title={`Lydstyrke: ${Math.round(volume * 100)}%`}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/pony-frontend/src/pages/GameEndPage.js b/pony-frontend/src/pages/GameEndPage.js
new file mode 100644
index 0000000..048c3ae
--- /dev/null
+++ b/pony-frontend/src/pages/GameEndPage.js
@@ -0,0 +1,103 @@
+/**
+ * Game end page — victory/defeat summary with history and replay buttons.
+ */
+
+import React from 'react';
+import { motion } from 'framer-motion';
+import SceneMusic from '../SceneMusic';
+import VolumeControl from '../components/VolumeControl';
+import Confetti from '../components/Confetti';
+import DiceRoll from '../components/DiceRoll';
+
+export default function GameEndPage({ data, onNavigate }) {
+ const sceneType = data.victory ? 'victory' : data.mixed ? 'mixed' : 'defeat';
+
+ return (
+
+
+
+ {}} />
+
+ {data.victory && }
+
+ {data.victory ? '🌟 SEJR! 🌟' : data.mixed ? '⚖️ Blant resultat! ⚖️' : '💪 Prøv igen! 💪'}
+
+
+ {data.endText}
+
+
+ {data.score}
+
+
+ 📖 Hvad skete der? 📖
+ {data.history.map((item, i) => (
+
+ {item.action}
+
+ {item.result}
+ {item.story && {item.story}
}
+
+ ))}
+
+
+ onNavigate('start')}
+ aria-label="Spil igen"
+ >
+ 🎲 Spil Igen!
+
+ onNavigate('home')}
+ aria-label="Tilbage til forsiden"
+ >
+ 🏠 Forside
+
+
+
+ );
+}
+
+const pageVariants = {
+ initial: { opacity: 0, y: 30 },
+ animate: { opacity: 1, y: 0 },
+ exit: { opacity: 0, y: -30 },
+};
\ No newline at end of file
diff --git a/pony-frontend/src/pages/GameScenePage.js b/pony-frontend/src/pages/GameScenePage.js
new file mode 100644
index 0000000..2fd973c
--- /dev/null
+++ b/pony-frontend/src/pages/GameScenePage.js
@@ -0,0 +1,123 @@
+/**
+ * Game scene — active gameplay with scene text, dice roll, and history feed.
+ */
+
+import React, { useRef, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import SceneMusic from '../SceneMusic';
+import VolumeControl from '../components/VolumeControl';
+import FloatingBg from '../components/FloatingBg';
+import DiceRoll from '../components/DiceRoll';
+
+const API = window.location.origin.replace('3001', '8082');
+
+export default function GameScenePage({ data, onRollDice, volume, setVolume }) {
+ const scrollRef = useRef(null);
+
+ useEffect(() => {
+ if (scrollRef.current && data && data.history && data.history.length > 0) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ }, [data]);
+
+ return (
+
+ 0 ? 'game' : 'none'} />
+
+
+
+
+
+
+ {Array.from({ length: 5 }, (_, i) => (
+
+ ))}
+
+ {data.tema}
+
+
+
+
+
+ {data.ponyName}
+ {data.ponyType}
+
+
+
+
+
+ {(data.history || []).map((item, i) => (
+
+
+ {item.success ? '✅' : '❌'}
+ {item.action}
+
+
+ {item.result}
+ {item.story && {item.story}
}
+
+ ))}
+
+
+
+
+ {data.sceneNum}
+ {data.sceneText}
+
+ Du skal:
+ {data.actionText}
+
+ {data.difficulty}
+
+
+
+ 🎲 KAST TERNINGERNE! 🎲
+
+
+ );
+}
+
+const pageVariants = {
+ initial: { opacity: 0, y: 30 },
+ animate: { opacity: 1, y: 0 },
+ exit: { opacity: 0, y: -30 },
+};
\ No newline at end of file
diff --git a/pony-frontend/src/pages/HomePage.js b/pony-frontend/src/pages/HomePage.js
new file mode 100644
index 0000000..0a91af3
--- /dev/null
+++ b/pony-frontend/src/pages/HomePage.js
@@ -0,0 +1,104 @@
+/**
+ * Home page — title, subtitle, start button.
+ */
+
+import React from 'react';
+import { motion } from 'framer-motion';
+import SceneMusic from '../SceneMusic';
+import VolumeControl from '../components/VolumeControl';
+import FloatingBg from '../components/FloatingBg';
+import Sparkles from '../components/Sparkles';
+import Achievements from '../components/Achievements';
+
+export default function HomePage({ volume, setVolume, stats, showAchievements, setShowAchievements, onNavigate }) {
+ return (
+
+ 0 ? 'home' : 'none'} />
+
+
+
+
+ {showAchievements && (
+ setShowAchievements(false)}
+ />
+ )}
+ {showAchievements && (
+
+
+
+
+ )}
+
+
+
+
+ 🦄🌈✨
+
+
+ My Little Pony
+
+
+ Tails of Equestria
+
+
+ Vælg din pony og gå på eventyr! 🎮
+
+ onNavigate('theme')}
+ aria-label="Start nyt spil"
+ >
+ 🎮 Start Nyt Spil!
+
+
+
+ );
+}
+
+const pageVariants = {
+ initial: { opacity: 0, y: 30 },
+ animate: { opacity: 1, y: 0 },
+ exit: { opacity: 0, y: -30 },
+};
\ No newline at end of file
diff --git a/pony-frontend/src/pages/PonySelectPage.js b/pony-frontend/src/pages/PonySelectPage.js
new file mode 100644
index 0000000..4f3ed61
--- /dev/null
+++ b/pony-frontend/src/pages/PonySelectPage.js
@@ -0,0 +1,78 @@
+/**
+ * Pony selection page — choose your character.
+ */
+
+import React from 'react';
+import { motion } from 'framer-motion';
+import SceneMusic from '../SceneMusic';
+import VolumeControl from '../components/VolumeControl';
+import FloatingBg from '../components/FloatingBg';
+
+const API = window.location.origin.replace('3001', '8082');
+
+export default function PonySelectPage({ ponies, onStartGame, volume, setVolume, onNavigate }) {
+ return (
+
+ 0 ? 'home' : 'none'} />
+
+
+
+
+
+ Vælg din Pony! 🐴
+
+
+ Hver pony har sine egne superkræfter!
+
+
+ {ponies.map((p, i) => (
+
onStartGame(i)}
+ role="button"
+ tabIndex={0}
+ aria-label={`Vælg ${p.navn} - ${p.bonus}`}
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onStartGame(i); }}
+ >
+
+
+
+ {p.emoji}
+ {p.navn}
+ {p.bonus}
+ {p.diceBonus > 0 && (
+ +{p.diceBonus} på første terning 🎲
+ )}
+
+ ))}
+
+ onNavigate('home')} aria-label="Tilbage til forsiden">
+ Tilbage
+
+
+ );
+}
+
+const pageVariants = {
+ initial: { opacity: 0, y: 30 },
+ animate: { opacity: 1, y: 0 },
+ exit: { opacity: 0, y: -30 },
+};
\ No newline at end of file
diff --git a/pony-frontend/src/pages/ThemeSelectPage.js b/pony-frontend/src/pages/ThemeSelectPage.js
new file mode 100644
index 0000000..3c15bad
--- /dev/null
+++ b/pony-frontend/src/pages/ThemeSelectPage.js
@@ -0,0 +1,67 @@
+/**
+ * Theme selection page — choose which adventure to play.
+ */
+
+import React from 'react';
+import { motion } from 'framer-motion';
+import SceneMusic from '../SceneMusic';
+import VolumeControl from '../components/VolumeControl';
+import FloatingBg from '../components/FloatingBg';
+
+export default function ThemeSelectPage({ themes, selectedTheme, setSelectedTheme, volume, setVolume, onNavigate }) {
+ return (
+
+ 0 ? 'home' : 'none'} />
+
+
+
+
+
+ Vælg et eventyr! 📖
+
+
+ Hvilken historie vil du opleve?
+
+
+ {themes.map((t, i) => (
+
{ setSelectedTheme(i); onNavigate('start'); }}
+ role="button"
+ tabIndex={0}
+ aria-label={`Vælg ${t.titel}`}
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { setSelectedTheme(i); onNavigate('start'); } }}
+ >
+ {t.emoji}
+ {t.titel}
+ {t.sceneCount || 5} scener
+
+ ))}
+
+ onNavigate('home')} aria-label="Tilbage til forsiden">
+ Tilbage
+
+
+ );
+}
+
+const pageVariants = {
+ initial: { opacity: 0, y: 30 },
+ animate: { opacity: 1, y: 0 },
+ exit: { opacity: 0, y: -30 },
+};
\ No newline at end of file
diff --git a/pony-frontend/src/services/api.js b/pony-frontend/src/services/api.js
new file mode 100644
index 0000000..252791b
--- /dev/null
+++ b/pony-frontend/src/services/api.js
@@ -0,0 +1,84 @@
+/**
+ * API service layer — all backend communication.
+ *
+ * Centralizes fetch calls so pages don't know about URLs or credentials.
+ */
+
+const API = window.location.origin.replace('3001', '8082');
+
+/**
+ * Fetch and parse JSON, throwing on non-OK.
+ */
+async function apiFetch(path, options = {}) {
+ const resp = await fetch(`${API}${path}`, {
+ credentials: 'include',
+ ...options,
+ });
+ if (!resp.ok) {
+ throw new Error(`Server fejl (${resp.status})`);
+ }
+ return resp.json();
+}
+
+/**
+ * Start a new game.
+ * @param {number} typeIdx - pony type index
+ * @param {number} temaIdx - theme index
+ * @returns {Promise