feat(points): unlock items, progress bars and skins
Build, Test & Push Frontend / quality-check (push) Successful in 28s
Build, Test & Push Frontend / docker (push) Successful in 25s

Locked items show a progress bar until the goal is reached. Four
CSS-only skins (Pastel, Retro CRT, Cyberpunk, Steam) in skins.css,
light and dark. A skin overrides accent and corners; badges, borders,
frames, effects and game art stay on top. New Platinum shine and
Equalizer effects.
This commit is contained in:
lobo committed 2026-10-02 15:40:46 +02:00
1 parent 8598024bce
commit b6f3d123fb
22 files changed
+622 -38

No files matched your search

+3 -1
View File
@@ -6,13 +6,14 @@ import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
import {Label} from '@/components/ui/label';
import {Switch} from '@/components/ui/switch';
import {AccentPicker, CornersPicker} from './StylePickers.jsx';
import {equippedSkin} from '../utils/rewards.js';
import {usePreferences} from '../context/PreferencesContext.jsx';
import useSaveAppearance from '../hooks/useSaveAppearance.js';
import {coverUrl} from '../utils/games.js';
export default function AppearanceCard() {
const {t} = useTranslation();
const {appearance} = usePreferences();
const {appearance, equipped} = usePreferences();
const saveAppearance = useSaveAppearance();
return (
@@ -23,6 +24,7 @@ export default function AppearanceCard() {
<CardContent className="space-y-6">
<AccentPicker id="accent" value={appearance.accent} onChange={(accent) => saveAppearance({accent})}/>
<CornersPicker id="corners" value={appearance.corners} onChange={(corners) => saveAppearance({corners})}/>
{equippedSkin(equipped) && <p className="text-xs text-muted-foreground">{t('SETTINGS.SKIN_OVERRIDES')}</p>}
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<Label htmlFor="wideLayout">{t('SETTINGS.WIDE_LAYOUT')}</Label>
+31
View File
@@ -0,0 +1,31 @@
// @vitest-environment jsdom
import {afterEach, describe, expect, it, vi} from 'vitest';
import {cleanup, render, screen} from '@testing-library/react';
import AppearanceCard from './AppearanceCard.jsx';
const preferences = vi.hoisted(() => ({value: null}));
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
vi.mock('../context/PreferencesContext.jsx', () => ({usePreferences: () => preferences.value}));
vi.mock('../hooks/useSaveAppearance.js', () => ({default: () => vi.fn()}));
const show = (equipped) => {
preferences.value = {
appearance: {accent: 'violet', corners: 'default', wideLayout: false, reduceMotion: false, showBanner: true},
equipped,
};
render(<AppearanceCard/>);
};
afterEach(cleanup);
describe('AppearanceCard', () => {
it('tells that the skin sets colors and corners only while a skin is worn', () => {
show({SKIN: 'SKIN_STEAM'});
expect(screen.getByText('SETTINGS.SKIN_OVERRIDES')).toBeTruthy();
});
it('shows no skin hint without a skin', () => {
show({});
expect(screen.queryByText('SETTINGS.SKIN_OVERRIDES')).toBeNull();
});
});
+3 -1
View File
@@ -1,6 +1,6 @@
import {cn} from '@/lib/utils';
export const EFFECTS = ['triangles', 'starfield', 'pixels', 'aurora', 'mesh', 'grid', 'waves', 'bokeh'];
export const EFFECTS = ['triangles', 'starfield', 'pixels', 'aurora', 'mesh', 'grid', 'waves', 'bokeh', 'equalizer'];
const range = (count) => Array.from({length: count}, (_, i) => i);
@@ -73,6 +73,8 @@ function shapes(effect) {
<path d={WAVE} fill="currentColor"/>
</svg>
));
case 'equalizer':
return range(16).map((i) => <span key={i}/>);
case 'bokeh':
return range(10).map((i) => (
<span key={i} style={{
+1 -1
View File
@@ -21,7 +21,7 @@ import {coverEffect, detailEffect} from '../utils/rewards.js';
const SPEEDRUN_PREFIX = 'https://www.speedrun.com/';
// The spans of each completed effect decoration; effects.css places them.
const DETAIL_ELEMENTS = {'fx-detail-gold-shine': 8, 'fx-detail-neon-pulse': 1, 'fx-detail-holo': 1, 'fx-detail-ember': 6};
const DETAIL_ELEMENTS = {'fx-detail-gold-shine': 8, 'fx-detail-neon-pulse': 1, 'fx-detail-holo': 1, 'fx-detail-ember': 6, 'fx-detail-platinum': 6};
export default function GameHeroCard({game, auditor, profileStyle, onCorrect, onMerge, onHiddenChanged, onProfileStyleSaved}) {
const {t} = useTranslation();
+1 -1
View File
@@ -8,7 +8,7 @@ function Card({
<div
data-slot="card"
className={cn(
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-(--card-shadow)",
className
)}
{...props}