feat: wire pixel pony configurator into home nav and app routing

Adds a "Design din Pony" entry point on the home page and a
pixelConfigurator route in App.js so the pixel-art configurator
(added in c19c8af) is actually reachable from the UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 11:18:57 +00:00
parent c19c8afc6d
commit 421c9259da
4 changed files with 203 additions and 3 deletions

View File

@@ -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;
}

View File

@@ -314,6 +314,14 @@ function App() {
/>
)}
{page === 'pixelConfigurator' && (
<PixelPonyConfiguratorPage
volume={volume}
setVolume={setVolume}
onNavigate={navigateTo}
/>
)}
{page === 'start' && (
<PonySelectPage
ponies={content.ponies}

View File

@@ -99,6 +99,15 @@ export default function HomePage({ volume, setVolume, stats, showAchievements, s
>
🎮 Start Nyt Spil!
</motion.button>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="btn-secondary"
onClick={() => onNavigate('pixelConfigurator')}
aria-label="Design din pixel pony"
>
🎨 Design din Pony
</motion.button>
</motion.div>
</motion.div>
);

View File

@@ -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(<PixelPonyConfiguratorPage volume={0.5} setVolume={() => {}} 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(<PixelPonyConfiguratorPage volume={0.5} setVolume={() => {}} 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(<PixelPonyConfiguratorPage volume={0.5} setVolume={() => {}} 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(<PixelPonyConfiguratorPage volume={0.5} setVolume={() => {}} 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(<PixelPonyConfiguratorPage volume={0.5} setVolume={() => {}} onNavigate={onNavigate} />);
await userEvent.click(screen.getByRole('button', { name: 'Tilbage til forsiden' }));
expect(onNavigate).toHaveBeenCalledWith('home');
});