Files
ToBePlayed-Frontend/src/components/AppearanceCard.jsx
T
lobo ed5df8c40d
Build, Test & Push Frontend / quality-check (push) Successful in 32s
Build, Test & Push Frontend / docker (push) Successful in 26s
feat(effects): polish 8 effects and add 7 new engines with parallax
Synthwave, fireflies, snow, sakura, nebula: CSS/SVG. Matrix and
constellation: canvas. Existing engines gain depth, color, glow, and
parallax (mouse and device tilt, opt-in on iOS)
2026-10-03 17:31:19 +02:00

121 lines
6.5 KiB
React

import {useState} from 'react';
import {useTranslation} from 'react-i18next';
import {ImageOff, Palette, RotateCcw} from 'lucide-react';
import {Button} from '@/components/ui/button';
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';
import {disableTilt, requestTilt, tiltAllowed, tiltNeedsPermission} from '../utils/parallax.js';
export default function AppearanceCard() {
const {t} = useTranslation();
const {appearance, equipped} = usePreferences();
const saveAppearance = useSaveAppearance();
const [tilt, setTilt] = useState(tiltAllowed);
const [tiltRefused, setTiltRefused] = useState(false);
// requestTilt() starts inside the tap, as iOS demands.
const changeTilt = (on) => {
if (!on) {
disableTilt();
setTilt(false);
return;
}
requestTilt().then((granted) => {
setTilt(granted);
setTiltRefused(!granted);
});
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2"><Palette className="size-4 text-primary"/>{t('SETTINGS.APPEARANCE')}</CardTitle>
</CardHeader>
<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>
<p className="text-xs text-muted-foreground">{t('SETTINGS.WIDE_LAYOUT_HINT')}</p>
</div>
<Switch id="wideLayout" checked={appearance.wideLayout}
onCheckedChange={(wideLayout) => saveAppearance({wideLayout})}/>
</div>
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<Label htmlFor="reduceMotion">{t('SETTINGS.REDUCE_MOTION')}</Label>
<p className="text-xs text-muted-foreground">{t('SETTINGS.REDUCE_MOTION_HINT')}</p>
</div>
<Switch id="reduceMotion" checked={appearance.reduceMotion}
onCheckedChange={(reduceMotion) => saveAppearance({reduceMotion})}/>
</div>
{tiltNeedsPermission() && (
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<Label htmlFor="tilt">{t('SETTINGS.TILT')}</Label>
<p className="text-xs text-muted-foreground">
{t(tiltRefused ? 'SETTINGS.TILT_DENIED' : 'SETTINGS.TILT_HINT')}
</p>
</div>
<Switch id="tilt" checked={tilt} disabled={appearance.reduceMotion}
onCheckedChange={changeTilt}/>
</div>
)}
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<Label id="background-label">{t('SETTINGS.BACKGROUND')}</Label>
{!appearance.backgroundAppid && (
<p className="text-xs text-muted-foreground">{t('SETTINGS.BACKGROUND_HINT')}</p>
)}
</div>
{appearance.backgroundAppid && (
<div className="flex items-center gap-2">
<img src={coverUrl(appearance.backgroundAppid)} alt=""
onError={(event) => { event.currentTarget.hidden = true; }}
className="h-10 w-20 rounded-md object-cover"/>
<Button type="button" variant="outline" size="sm"
aria-describedby="background-label"
onClick={() => saveAppearance({backgroundAppid: null})}>
<ImageOff/>{t('SETTINGS.BACKGROUND_REMOVE')}
</Button>
</div>
)}
</div>
<div className="space-y-3">
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<Label id="banner-label" htmlFor="showBanner">{t('SETTINGS.BANNER')}</Label>
{appearance.showBanner && !appearance.bannerAppid && (
<p className="text-xs text-muted-foreground">{t('SETTINGS.BANNER_HINT')}</p>
)}
</div>
<Switch id="showBanner" checked={appearance.showBanner}
onCheckedChange={(showBanner) => saveAppearance({showBanner})}/>
</div>
{appearance.showBanner && appearance.bannerAppid && (
<div className="flex items-center justify-end gap-2">
<img src={coverUrl(appearance.bannerAppid)} alt=""
onError={(event) => { event.currentTarget.hidden = true; }}
className="h-10 w-20 rounded-md object-cover"/>
<Button type="button" variant="outline" size="sm"
aria-describedby="banner-label"
onClick={() => saveAppearance({bannerAppid: null})}>
<RotateCcw/>{t('SETTINGS.BANNER_AUTO')}
</Button>
</div>
)}
</div>
</CardContent>
</Card>
);
}