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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -314,6 +314,14 @@ function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{page === 'pixelConfigurator' && (
|
||||
<PixelPonyConfiguratorPage
|
||||
volume={volume}
|
||||
setVolume={setVolume}
|
||||
onNavigate={navigateTo}
|
||||
/>
|
||||
)}
|
||||
|
||||
{page === 'start' && (
|
||||
<PonySelectPage
|
||||
ponies={content.ponies}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
61
pony-frontend/src/pages/PixelPonyConfiguratorPage.test.js
Normal file
61
pony-frontend/src/pages/PixelPonyConfiguratorPage.test.js
Normal 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');
|
||||
});
|
||||
Reference in New Issue
Block a user