diff --git a/pony-frontend/src/App.css b/pony-frontend/src/App.css index a6c2983..759b4d7 100644 --- a/pony-frontend/src/App.css +++ b/pony-frontend/src/App.css @@ -84,7 +84,8 @@ body { .home, .start-page, .game, -.game-end { +.game-end, +.pixel-configurator { min-height: 100vh; padding: var(--space-xl); position: relative; @@ -103,7 +104,8 @@ body { } .start-page, -.game-end { +.game-end, +.pixel-configurator { text-align: center; } @@ -162,6 +164,24 @@ body { transform: scale(1.05); } +.btn-secondary { + background: rgba(255, 255, 255, 0.85); + color: var(--color-accent); + border: none; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-pill); + font-size: 1rem; + cursor: pointer; + box-shadow: var(--shadow-sm); + margin-top: var(--space-md); + transition: transform 0.2s; +} + +.btn-secondary:hover { + transform: scale(1.05); + background: var(--color-white); +} + .btn-back { background: rgba(255, 255, 255, 0.2); color: var(--color-white); @@ -375,6 +395,99 @@ body { margin-top: var(--space-xs); } +/* --- Pixel pony configurator --- */ +.pixel-configurator-preview { + display: flex; + justify-content: center; + margin: var(--space-lg) auto; + padding: var(--space-lg); + background: rgba(255, 255, 255, 0.15); + border-radius: var(--radius-lg); + max-width: 320px; +} + +.pixel-configurator-section { + max-width: 600px; + margin: 0 auto var(--space-lg); +} + +.pixel-configurator-section h2 { + color: var(--color-white); + font-size: 1.1rem; + margin-bottom: var(--space-sm); +} + +.pixel-mane-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + gap: var(--space-sm); +} + +.pixel-mane-swatch { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-xs); + background: var(--color-white); + border: 3px solid transparent; + border-radius: var(--radius-md); + padding: var(--space-sm); + cursor: pointer; + font-size: var(--font-small); + color: var(--color-muted); + transition: transform 0.15s; +} + +.pixel-mane-swatch:hover { + transform: scale(1.05); +} + +.pixel-mane-swatch.is-selected { + border-color: var(--color-accent); +} + +.pixel-color-row, +.pixel-toggle-row { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); + justify-content: center; +} + +.pixel-color-swatch { + width: 40px; + height: 40px; + border-radius: var(--radius-round); + border: 3px solid rgba(255, 255, 255, 0.6); + cursor: pointer; + transition: transform 0.15s; +} + +.pixel-color-swatch:hover { + transform: scale(1.1); +} + +.pixel-color-swatch.is-selected { + border-color: var(--focus-color); + box-shadow: var(--shadow-md); +} + +.pixel-toggle { + background: var(--color-white); + color: var(--color-muted); + border: 3px solid transparent; + border-radius: var(--radius-pill); + padding: var(--space-sm) var(--space-lg); + font-size: 1rem; + cursor: pointer; +} + +.pixel-toggle.is-selected { + border-color: var(--color-accent); + color: var(--color-accent); + font-weight: bold; +} + /* --- Pony mini card (in-game) --- */ .pony-card-mini { display: flex; @@ -1060,10 +1173,15 @@ input:focus-visible, .home-content, .start-page, .game, - .game-end { + .game-end, + .pixel-configurator { padding: var(--space-md) !important; } + .pixel-mane-grid { + grid-template-columns: repeat(3, 1fr) !important; + } + .pony-choices { grid-template-columns: repeat(2, 1fr) !important; } @@ -1095,6 +1213,10 @@ input:focus-visible, grid-template-columns: 1fr !important; } + .pixel-mane-grid { + grid-template-columns: repeat(2, 1fr) !important; + } + .title { font-size: 1.5rem !important; } diff --git a/pony-frontend/src/App.js b/pony-frontend/src/App.js index 15ff4cc..e817b88 100644 --- a/pony-frontend/src/App.js +++ b/pony-frontend/src/App.js @@ -314,6 +314,14 @@ function App() { /> )} + {page === 'pixelConfigurator' && ( + + )} + {page === 'start' && ( 🎮 Start Nyt Spil! + onNavigate('pixelConfigurator')} + aria-label="Design din pixel pony" + > + 🎨 Design din Pony + ); diff --git a/pony-frontend/src/pages/PixelPonyConfiguratorPage.test.js b/pony-frontend/src/pages/PixelPonyConfiguratorPage.test.js new file mode 100644 index 0000000..66d5370 --- /dev/null +++ b/pony-frontend/src/pages/PixelPonyConfiguratorPage.test.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import PixelPonyConfiguratorPage from './PixelPonyConfiguratorPage'; + +jest.mock('framer-motion', () => { + const React = require('react'); + const motion = {}; + ['div', 'button', 'h1', 'h2', 'span'].forEach(tag => { + motion[tag] = ({ children, ...props }) => + React.createElement(tag, { 'data-motion': 'true', ...props }, children); + }); + return { motion, AnimatePresence: ({ children }) => children }; +}); + +jest.mock('../SceneMusic', () => function SceneMusic() { return null; }); +jest.mock('../services/tts', () => ({ speakDanish: jest.fn(), cancelSpeech: jest.fn() })); + +beforeEach(() => { + window.localStorage.clear(); +}); + +test('renders mane options, color swatches, and toggles', () => { + render( {}} onNavigate={() => {}} />); + expect(screen.getByRole('button', { name: 'Vælg manke: Boglig' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '🦄 Horn' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '🪽 Vinger' })).toBeInTheDocument(); +}); + +test('selecting a mane style marks it as pressed', async () => { + render( {}} onNavigate={() => {}} />); + const bubbly = screen.getByRole('button', { name: 'Vælg manke: Boblende' }); + expect(bubbly).toHaveAttribute('aria-pressed', 'false'); + await userEvent.click(bubbly); + expect(bubbly).toHaveAttribute('aria-pressed', 'true'); +}); + +test('toggling wings flips its pressed state', async () => { + render( {}} onNavigate={() => {}} />); + const wings = screen.getByRole('button', { name: '🪽 Vinger' }); + expect(wings).toHaveAttribute('aria-pressed', 'false'); + await userEvent.click(wings); + expect(wings).toHaveAttribute('aria-pressed', 'true'); +}); + +test('saving persists the appearance to localStorage', async () => { + render( {}} onNavigate={() => {}} />); + await userEvent.click(screen.getByRole('button', { name: 'Vælg manke: Boblende' })); + const saveButton = screen.getByRole('button', { name: 'Gem min pony' }); + await userEvent.click(saveButton); + const saved = JSON.parse(window.localStorage.getItem('pony_appearance')); + expect(saved.mane).toBe('bubbly'); + expect(saveButton).toHaveTextContent('✅ Gemt!'); +}); + +test('back button navigates home', async () => { + const onNavigate = jest.fn(); + render( {}} onNavigate={onNavigate} />); + await userEvent.click(screen.getByRole('button', { name: 'Tilbage til forsiden' })); + expect(onNavigate).toHaveBeenCalledWith('home'); +});