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!

-
    -
  1. Vælg en pony — hver har sine egne fordele ✨
  2. -
  3. Læs scenen — find ud af hvad du skal gøre 📖
  4. -
  5. Kast terningerne — held og lykke! 🎲
  6. -
  7. Overlev 5 scener — for at vinde! 🏆
  8. -
- -
-
- ); -} - -// 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.navn} - -
{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!

+
    +
  1. Vælg en pony — hver har sine egne fordele ✨
  2. +
  3. Læs scenen — find ud af hvad du skal gøre 📖
  4. +
  5. Kast terningerne — held og lykke! 🎲
  6. +
  7. Overlev 5 scener — for at vinde! 🏆
  8. +
+ +
+
+ ); +} \ 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.navn} + +
{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} game state JSON + */ +export async function startGame(typeIdx, temaIdx) { + return apiFetch('/api/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: typeIdx, tema: temaIdx }), + }); +} + +/** + * Roll dice for the current scene. + * @returns {Promise} updated game state JSON + */ +export async function rollDice() { + return apiFetch('/api/kast', { method: 'POST' }); +} + +/** + * Get current scene data. + * @returns {Promise} scene JSON + */ +export async function getScene() { + return apiFetch('/api/scene'); +} + +/** + * Reset the current game. + */ +export async function resetGame() { + return apiFetch('/api/reset', { method: 'POST' }); +} + +/** + * Load ponies + themes metadata. + * @returns {Promise<{ponies: any[], themes: any[]}>} + */ +export async function loadContent() { + try { + const data = await apiFetch('/api/content'); + return { ponies: data.ponies, themes: data.themes }; + } catch { + return null; + } +} + +/** + * Health check. + * @returns {Promise} + */ +export async function healthCheck() { + try { + const data = await apiFetch('/api/health'); + return !!data.ok; + } catch { + return false; + } +} \ No newline at end of file