Public Access
feat(dashboard): pick the banner game or turn the banner off
This commit is contained in:
1 parent
23e502bf5b
commit
83494519f9
11 files changed
+136
-22
No files matched your search
+6
-2
@@ -85,7 +85,10 @@
|
|||||||
"WIDE_LAYOUT_HINT": "Nutzt mehr Platz auf breiten Bildschirmen.",
|
"WIDE_LAYOUT_HINT": "Nutzt mehr Platz auf breiten Bildschirmen.",
|
||||||
"BACKGROUND": "Hintergrund",
|
"BACKGROUND": "Hintergrund",
|
||||||
"BACKGROUND_HINT": "Öffne ein Spiel in deiner Bibliothek und wähle „Als Hintergrund“.",
|
"BACKGROUND_HINT": "Öffne ein Spiel in deiner Bibliothek und wähle „Als Hintergrund“.",
|
||||||
"BACKGROUND_REMOVE": "Hintergrund entfernen"
|
"BACKGROUND_REMOVE": "Hintergrund entfernen",
|
||||||
|
"BANNER": "Dashboard-Banner",
|
||||||
|
"BANNER_HINT": "Zeigt dein meistgespieltes Spiel der letzten zwei Wochen. Öffne ein Spiel und wähle „Als Banner“, um eins festzulegen.",
|
||||||
|
"BANNER_AUTO": "Wieder automatisch"
|
||||||
},
|
},
|
||||||
"VALIDATION": {
|
"VALIDATION": {
|
||||||
"EMAIL_REQUIRED": "Email ist erforderlich.",
|
"EMAIL_REQUIRED": "Email ist erforderlich.",
|
||||||
@@ -181,7 +184,8 @@
|
|||||||
"TTB_NORMALLY": "+ Extras",
|
"TTB_NORMALLY": "+ Extras",
|
||||||
"TTB_COMPLETELY": "100 %",
|
"TTB_COMPLETELY": "100 %",
|
||||||
"SET_BACKGROUND": "Als Hintergrund",
|
"SET_BACKGROUND": "Als Hintergrund",
|
||||||
"REMOVE_BACKGROUND": "Hintergrund entfernen"
|
"REMOVE_BACKGROUND": "Hintergrund entfernen",
|
||||||
|
"SET_BANNER": "Als Banner"
|
||||||
},
|
},
|
||||||
"LANDING": {
|
"LANDING": {
|
||||||
"STATS_TITLE": "Community-Statistiken",
|
"STATS_TITLE": "Community-Statistiken",
|
||||||
|
|||||||
+6
-2
@@ -85,7 +85,10 @@
|
|||||||
"WIDE_LAYOUT_HINT": "Uses more of a wide screen.",
|
"WIDE_LAYOUT_HINT": "Uses more of a wide screen.",
|
||||||
"BACKGROUND": "Background",
|
"BACKGROUND": "Background",
|
||||||
"BACKGROUND_HINT": "Open a game in your library and choose \"Use as background\".",
|
"BACKGROUND_HINT": "Open a game in your library and choose \"Use as background\".",
|
||||||
"BACKGROUND_REMOVE": "Remove background"
|
"BACKGROUND_REMOVE": "Remove background",
|
||||||
|
"BANNER": "Dashboard banner",
|
||||||
|
"BANNER_HINT": "Shows your most played game of the last two weeks. Open a game and choose \"Use as banner\" to pick one.",
|
||||||
|
"BANNER_AUTO": "Back to automatic"
|
||||||
},
|
},
|
||||||
"VALIDATION": {
|
"VALIDATION": {
|
||||||
"EMAIL_REQUIRED": "Email is required.",
|
"EMAIL_REQUIRED": "Email is required.",
|
||||||
@@ -181,7 +184,8 @@
|
|||||||
"TTB_NORMALLY": "+ Extras",
|
"TTB_NORMALLY": "+ Extras",
|
||||||
"TTB_COMPLETELY": "100%",
|
"TTB_COMPLETELY": "100%",
|
||||||
"SET_BACKGROUND": "Use as background",
|
"SET_BACKGROUND": "Use as background",
|
||||||
"REMOVE_BACKGROUND": "Remove background"
|
"REMOVE_BACKGROUND": "Remove background",
|
||||||
|
"SET_BANNER": "Use as banner"
|
||||||
},
|
},
|
||||||
"LANDING": {
|
"LANDING": {
|
||||||
"STATS_TITLE": "Community stats",
|
"STATS_TITLE": "Community stats",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe('useSaveAppearance', () => {
|
|||||||
const [url, request] = fetch.mock.calls[0];
|
const [url, request] = fetch.mock.calls[0];
|
||||||
expect(url).toContain('/api/users/settings/appearance');
|
expect(url).toContain('/api/users/settings/appearance');
|
||||||
expect(request.method).toBe('PUT');
|
expect(request.method).toBe('PUT');
|
||||||
expect(JSON.parse(request.body)).toEqual({accent: 'blue', corners: 'default', wideLayout: false, backgroundAppid: null});
|
expect(JSON.parse(request.body)).toEqual({accent: 'blue', corners: 'default', wideLayout: false, backgroundAppid: null, bannerAppid: null, showBanner: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('puts the old value back when Auth refuses', async () => {
|
it('puts the old value back when Auth refuses', async () => {
|
||||||
@@ -109,7 +109,7 @@ describe('useSaveAppearance', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('waits for a running load from Auth, then keeps its values for the fields not changed here', async () => {
|
it('waits for a running load from Auth, then keeps its values for the fields not changed here', async () => {
|
||||||
const fromServer = {accent: 'green', corners: 'default', wideLayout: false, backgroundAppid: null};
|
const fromServer = {accent: 'green', corners: 'default', wideLayout: false, backgroundAppid: null, bannerAppid: 620, showBanner: false};
|
||||||
let answerMe;
|
let answerMe;
|
||||||
globalThis.fetch = vi.fn((url, init) => (init?.method === 'PUT'
|
globalThis.fetch = vi.fn((url, init) => (init?.method === 'PUT'
|
||||||
? Promise.resolve({ok: true, status: 204})
|
? Promise.resolve({ok: true, status: 204})
|
||||||
@@ -196,7 +196,7 @@ describe('useSaveAppearance', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.preferences.appearance.corners).toBe('round');
|
expect(result.current.preferences.appearance.corners).toBe('round');
|
||||||
expect(puts()[2]).toEqual({accent: 'green', corners: 'round', wideLayout: true, backgroundAppid: null});
|
expect(puts()[2]).toEqual({accent: 'green', corners: 'round', wideLayout: true, backgroundAppid: null, bannerAppid: null, showBanner: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends nothing for a change that waited for the load when a reset came first', async () => {
|
it('sends nothing for a change that waited for the load when a reset came first', async () => {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import HeroArt from '../components/HeroArt.jsx';
|
|||||||
import SteamAvatar from '../components/SteamAvatar.jsx';
|
import SteamAvatar from '../components/SteamAvatar.jsx';
|
||||||
import useSteamProfile from '../hooks/useSteamProfile.js';
|
import useSteamProfile from '../hooks/useSteamProfile.js';
|
||||||
import {cn} from '@/lib/utils';
|
import {cn} from '@/lib/utils';
|
||||||
|
import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||||
|
|
||||||
const TOP_GAMES = 5;
|
const TOP_GAMES = 5;
|
||||||
// The games endpoint accepts only the page sizes 20, 40 and 80.
|
// The games endpoint accepts only the page sizes 20, 40 and 80.
|
||||||
@@ -189,8 +190,11 @@ const Dashboard = () => {
|
|||||||
}, [navigate, expireSession, t, fetchSteamCount, fetchTopGames, fetchPlaying, fetchSummary, fetchActivity]);
|
}, [navigate, expireSession, t, fetchSteamCount, fetchTopGames, fetchPlaying, fetchSummary, fetchActivity]);
|
||||||
|
|
||||||
const hasSteamId = !!userData?.steamId;
|
const hasSteamId = !!userData?.steamId;
|
||||||
|
const {appearance} = usePreferences();
|
||||||
const steamProfile = useSteamProfile(hasSteamId);
|
const steamProfile = useSteamProfile(hasSteamId);
|
||||||
const bannerAppid = activity?.recentGames?.[0]?.appid ?? null;
|
const bannerAppid = appearance.showBanner
|
||||||
|
? appearance.bannerAppid ?? activity?.recentGames?.[0]?.appid ?? null
|
||||||
|
: null;
|
||||||
const sync = useLibrarySync({
|
const sync = useLibrarySync({
|
||||||
enabled: hasSteamId,
|
enabled: hasSteamId,
|
||||||
expectInitialSync: hasSteamId && steamData.count === 0,
|
expectInitialSync: hasSteamId && steamData.count === 0,
|
||||||
@@ -217,7 +221,7 @@ const Dashboard = () => {
|
|||||||
return (
|
return (
|
||||||
<PageContainer className="p-4 md:p-8 space-y-6">
|
<PageContainer className="p-4 md:p-8 space-y-6">
|
||||||
<Card className="relative overflow-hidden p-6 md:p-8 flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
<Card className="relative overflow-hidden p-6 md:p-8 flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||||
{/* The banner is the game played most in the last two weeks; the gradient keeps the text readable. */}
|
{/* A picked game, else the game played most in the last two weeks; the gradient keeps the text readable. */}
|
||||||
<HeroArt key={bannerAppid} appid={bannerAppid}
|
<HeroArt key={bannerAppid} appid={bannerAppid}
|
||||||
className="pointer-events-none absolute inset-0 size-full object-cover opacity-60"/>
|
className="pointer-events-none absolute inset-0 size-full object-cover opacity-60"/>
|
||||||
<div className={cn('pointer-events-none absolute inset-0', bannerAppid
|
<div className={cn('pointer-events-none absolute inset-0', bannerAppid
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ describe('Dashboard', () => {
|
|||||||
expect(screen.queryByText('LIBRARY.EMPTY')).toBeNull();
|
expect(screen.queryByText('LIBRARY.EMPTY')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the Steam avatar and the most played game of the last two weeks as banner', async () => {
|
|
||||||
const avatar = 'https://avatars.steamstatic.com/abc_full.jpg';
|
const avatar = 'https://avatars.steamstatic.com/abc_full.jpg';
|
||||||
|
const mockDashboard = () => {
|
||||||
globalThis.fetch = vi.fn(async (url) => {
|
globalThis.fetch = vi.fn(async (url) => {
|
||||||
if (url.includes('/api/users/me')) return ok({email: 'a@b.de', displayName: 'Lobo', steamId: '76561198000000000'});
|
if (url.includes('/api/users/me')) return ok({email: 'a@b.de', displayName: 'Lobo', steamId: '76561198000000000'});
|
||||||
if (url.includes('/api/library/steam-profile')) return ok({personaName: 'Lobo', avatarUrl: avatar});
|
if (url.includes('/api/library/steam-profile')) return ok({personaName: 'Lobo', avatarUrl: avatar});
|
||||||
@@ -58,13 +58,34 @@ describe('Dashboard', () => {
|
|||||||
if (url.includes('/api/library/games-count')) return ok({count: 1, hasSteamId: true});
|
if (url.includes('/api/library/games-count')) return ok({count: 1, hasSteamId: true});
|
||||||
return ok({content: [], total: 0, byStatus: {}});
|
return ok({content: [], total: 0, byStatus: {}});
|
||||||
});
|
});
|
||||||
const {container} = render(
|
return render(
|
||||||
<MemoryRouter initialEntries={['/dashboard']}><PreferencesProvider>
|
<MemoryRouter initialEntries={['/dashboard']}><PreferencesProvider>
|
||||||
<Dashboard/>
|
<Dashboard/>
|
||||||
</PreferencesProvider></MemoryRouter>,
|
</PreferencesProvider></MemoryRouter>,
|
||||||
);
|
).container;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('shows the Steam avatar and the most played game of the last two weeks as banner', async () => {
|
||||||
|
const container = mockDashboard();
|
||||||
|
|
||||||
await waitFor(() => expect(container.querySelector(`img[src="${avatar}"]`)).not.toBeNull());
|
await waitFor(() => expect(container.querySelector(`img[src="${avatar}"]`)).not.toBeNull());
|
||||||
expect(container.querySelector('img[src$="/1086940/library_hero.jpg"]')).not.toBeNull();
|
expect(container.querySelector('img[src$="/1086940/library_hero.jpg"]')).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the picked banner game instead', async () => {
|
||||||
|
localStorage.setItem('tbp_appearance', JSON.stringify({bannerAppid: 620}));
|
||||||
|
const container = mockDashboard();
|
||||||
|
|
||||||
|
await waitFor(() => expect(container.querySelector(`img[src="${avatar}"]`)).not.toBeNull());
|
||||||
|
expect(container.querySelector('img[src$="/620/library_hero.jpg"]')).not.toBeNull();
|
||||||
|
expect(container.querySelector('img[src$="/1086940/library_hero.jpg"]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows no banner when it is turned off', async () => {
|
||||||
|
localStorage.setItem('tbp_appearance', JSON.stringify({bannerAppid: 620, showBanner: false}));
|
||||||
|
const container = mockDashboard();
|
||||||
|
|
||||||
|
await waitFor(() => expect(container.querySelector(`img[src="${avatar}"]`)).not.toBeNull());
|
||||||
|
expect(container.querySelector('img[src*="library_hero"]')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {useEffect, useMemo, useState} from 'react';
|
import {useEffect, useMemo, useState} from 'react';
|
||||||
import {Link, useNavigate, useParams} from 'react-router-dom';
|
import {Link, useNavigate, useParams} from 'react-router-dom';
|
||||||
import {useTranslation} from 'react-i18next';
|
import {useTranslation} from 'react-i18next';
|
||||||
import {ArrowLeft, Clock, ExternalLink, Image as ImageIcon, ImageOff, ShieldOff, Trophy} from 'lucide-react';
|
import {ArrowLeft, Clock, ExternalLink, Image as ImageIcon, ImageOff, PanelTop, RotateCcw, ShieldOff, Trophy} from 'lucide-react';
|
||||||
|
|
||||||
import {Badge} from '@/components/ui/badge';
|
import {Badge} from '@/components/ui/badge';
|
||||||
import {Button} from '@/components/ui/button';
|
import {Button} from '@/components/ui/button';
|
||||||
@@ -99,6 +99,7 @@ export default function GameDetail() {
|
|||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const isBackground = !!data && appearance.backgroundAppid === data.appid;
|
const isBackground = !!data && appearance.backgroundAppid === data.appid;
|
||||||
|
const isBanner = !!data && appearance.showBanner && appearance.bannerAppid === data.appid;
|
||||||
|
|
||||||
const stats = data ? [
|
const stats = data ? [
|
||||||
[t('GAME_DETAIL.UNLOCKED'), data.achievementsUnlocked],
|
[t('GAME_DETAIL.UNLOCKED'), data.achievementsUnlocked],
|
||||||
@@ -161,6 +162,16 @@ export default function GameDetail() {
|
|||||||
<ImageIcon/>{t('GAME_DETAIL.SET_BACKGROUND')}
|
<ImageIcon/>{t('GAME_DETAIL.SET_BACKGROUND')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{isBanner ? (
|
||||||
|
<Button variant="outline" size="sm" onClick={() => saveAppearance({bannerAppid: null})}>
|
||||||
|
<RotateCcw/>{t('SETTINGS.BANNER_AUTO')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="outline" size="sm"
|
||||||
|
onClick={() => saveAppearance({bannerAppid: data.appid, showBanner: true})}>
|
||||||
|
<PanelTop/>{t('GAME_DETAIL.SET_BANNER')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -56,4 +56,28 @@ describe('GameDetail', () => {
|
|||||||
fireEvent.click(remove);
|
fireEvent.click(remove);
|
||||||
expect(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_BACKGROUND'})).toBeTruthy();
|
expect(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_BACKGROUND'})).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the game as banner and goes back to the automatic banner', async () => {
|
||||||
|
renderDetail();
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_BANNER'}));
|
||||||
|
|
||||||
|
const auto = await screen.findByRole('button', {name: 'SETTINGS.BANNER_AUTO'});
|
||||||
|
const [, init] = fetch.mock.calls.find(([, call]) => call?.method === 'PUT');
|
||||||
|
expect(JSON.parse(init.body)).toMatchObject({bannerAppid: 220, showBanner: true});
|
||||||
|
|
||||||
|
fireEvent.click(auto);
|
||||||
|
expect(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_BANNER'})).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('offers "use as banner" for the picked game while the banner is off, and turns it on', async () => {
|
||||||
|
localStorage.setItem('tbp_appearance', JSON.stringify({bannerAppid: 220, showBanner: false}));
|
||||||
|
renderDetail();
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole('button', {name: 'GAME_DETAIL.SET_BANNER'}));
|
||||||
|
|
||||||
|
await waitFor(() => expect(fetch.mock.calls.some(([, call]) => call?.method === 'PUT')).toBe(true));
|
||||||
|
const [, init] = fetch.mock.calls.find(([, call]) => call?.method === 'PUT');
|
||||||
|
expect(JSON.parse(init.body)).toMatchObject({bannerAppid: 220, showBanner: true});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -4,7 +4,7 @@ import {useTranslation} from 'react-i18next';
|
|||||||
import {useForm} from 'react-hook-form';
|
import {useForm} from 'react-hook-form';
|
||||||
import {zodResolver} from '@hookform/resolvers/zod';
|
import {zodResolver} from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
import {Copy, Gamepad2, ImageOff, Loader2, Palette, Save, SlidersHorizontal, UserRound} from 'lucide-react';
|
import {Copy, Gamepad2, ImageOff, Loader2, Palette, RotateCcw, Save, SlidersHorizontal, UserRound} from 'lucide-react';
|
||||||
import {toast} from 'sonner';
|
import {toast} from 'sonner';
|
||||||
|
|
||||||
import {Button} from '@/components/ui/button';
|
import {Button} from '@/components/ui/button';
|
||||||
@@ -315,7 +315,7 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label>{t('SETTINGS.BACKGROUND')}</Label>
|
<Label id="background-label">{t('SETTINGS.BACKGROUND')}</Label>
|
||||||
{!appearance.backgroundAppid && (
|
{!appearance.backgroundAppid && (
|
||||||
<p className="text-xs text-muted-foreground">{t('SETTINGS.BACKGROUND_HINT')}</p>
|
<p className="text-xs text-muted-foreground">{t('SETTINGS.BACKGROUND_HINT')}</p>
|
||||||
)}
|
)}
|
||||||
@@ -326,12 +326,37 @@ export default function SettingsPage() {
|
|||||||
onError={(event) => { event.currentTarget.hidden = true; }}
|
onError={(event) => { event.currentTarget.hidden = true; }}
|
||||||
className="h-10 w-20 rounded-md object-cover"/>
|
className="h-10 w-20 rounded-md object-cover"/>
|
||||||
<Button type="button" variant="outline" size="sm"
|
<Button type="button" variant="outline" size="sm"
|
||||||
|
aria-describedby="background-label"
|
||||||
onClick={() => saveAppearance({backgroundAppid: null})}>
|
onClick={() => saveAppearance({backgroundAppid: null})}>
|
||||||
<ImageOff/>{t('SETTINGS.BACKGROUND_REMOVE')}
|
<ImageOff/>{t('SETTINGS.BACKGROUND_REMOVE')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ describe('SettingsPage appearance', () => {
|
|||||||
fireEvent.click(screen.getByRole('button', {name: 'SETTINGS.CORNER_OPTIONS.ROUND'}));
|
fireEvent.click(screen.getByRole('button', {name: 'SETTINGS.CORNER_OPTIONS.ROUND'}));
|
||||||
|
|
||||||
await waitFor(() => expect(appearancePuts()).toHaveLength(2));
|
await waitFor(() => expect(appearancePuts()).toHaveLength(2));
|
||||||
expect(appearancePuts()[1]).toEqual({accent: 'green', corners: 'round', wideLayout: false, backgroundAppid: null});
|
expect(appearancePuts()[1]).toEqual({accent: 'green', corners: 'round', wideLayout: false, backgroundAppid: null, bannerAppid: null, showBanner: true});
|
||||||
expect(screen.getByRole('button', {name: 'SETTINGS.ACCENT_OPTIONS.GREEN'}).getAttribute('aria-pressed')).toBe('true');
|
expect(screen.getByRole('button', {name: 'SETTINGS.ACCENT_OPTIONS.GREEN'}).getAttribute('aria-pressed')).toBe('true');
|
||||||
expect(screen.getByRole('button', {name: 'SETTINGS.ACCENT_OPTIONS.VIOLET'}).getAttribute('aria-pressed')).toBe('false');
|
expect(screen.getByRole('button', {name: 'SETTINGS.ACCENT_OPTIONS.VIOLET'}).getAttribute('aria-pressed')).toBe('false');
|
||||||
});
|
});
|
||||||
@@ -109,4 +109,18 @@ describe('SettingsPage appearance', () => {
|
|||||||
expect(appearancePuts()[0].backgroundAppid).toBeNull();
|
expect(appearancePuts()[0].backgroundAppid).toBeNull();
|
||||||
expect(screen.getByText('SETTINGS.BACKGROUND_HINT')).toBeTruthy();
|
expect(screen.getByText('SETTINGS.BACKGROUND_HINT')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('turns the banner off and goes back to the automatic banner', async () => {
|
||||||
|
localStorage.setItem('tbp_appearance', JSON.stringify({bannerAppid: 620}));
|
||||||
|
renderPage(profile());
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole('button', {name: 'SETTINGS.BANNER_AUTO'}));
|
||||||
|
await waitFor(() => expect(appearancePuts()).toHaveLength(1));
|
||||||
|
expect(appearancePuts()[0]).toMatchObject({bannerAppid: null, showBanner: true});
|
||||||
|
expect(screen.getByText('SETTINGS.BANNER_HINT')).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('switch', {name: 'SETTINGS.BANNER'}));
|
||||||
|
await waitFor(() => expect(appearancePuts()).toHaveLength(2));
|
||||||
|
expect(appearancePuts()[1].showBanner).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -2,7 +2,12 @@
|
|||||||
export const ACCENTS = ['violet', 'blue', 'green', 'orange', 'pink', 'red'];
|
export const ACCENTS = ['violet', 'blue', 'green', 'orange', 'pink', 'red'];
|
||||||
export const CORNERS = ['sharp', 'default', 'round'];
|
export const CORNERS = ['sharp', 'default', 'round'];
|
||||||
|
|
||||||
export const DEFAULT_APPEARANCE = {accent: 'violet', corners: 'default', wideLayout: false, backgroundAppid: null};
|
// bannerAppid null: the dashboard banner shows the game played most in the last two weeks.
|
||||||
|
export const DEFAULT_APPEARANCE = {
|
||||||
|
accent: 'violet', corners: 'default', wideLayout: false, backgroundAppid: null, bannerAppid: null, showBanner: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const appid = (value) => (Number.isInteger(value) && value > 0 ? value : null);
|
||||||
|
|
||||||
const STORAGE_KEY = 'tbp_appearance';
|
const STORAGE_KEY = 'tbp_appearance';
|
||||||
const REQUEST_TIMEOUT_MS = 10000;
|
const REQUEST_TIMEOUT_MS = 10000;
|
||||||
@@ -11,7 +16,9 @@ export const normalizeAppearance = (value) => ({
|
|||||||
accent: ACCENTS.includes(value?.accent) ? value.accent : DEFAULT_APPEARANCE.accent,
|
accent: ACCENTS.includes(value?.accent) ? value.accent : DEFAULT_APPEARANCE.accent,
|
||||||
corners: CORNERS.includes(value?.corners) ? value.corners : DEFAULT_APPEARANCE.corners,
|
corners: CORNERS.includes(value?.corners) ? value.corners : DEFAULT_APPEARANCE.corners,
|
||||||
wideLayout: value?.wideLayout === true,
|
wideLayout: value?.wideLayout === true,
|
||||||
backgroundAppid: Number.isInteger(value?.backgroundAppid) && value.backgroundAppid > 0 ? value.backgroundAppid : null,
|
backgroundAppid: appid(value?.backgroundAppid),
|
||||||
|
bannerAppid: appid(value?.bannerAppid),
|
||||||
|
showBanner: value?.showBanner !== false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import {
|
|||||||
|
|
||||||
describe('normalizeAppearance', () => {
|
describe('normalizeAppearance', () => {
|
||||||
it('keeps known values', () => {
|
it('keeps known values', () => {
|
||||||
const appearance = {accent: 'blue', corners: 'round', wideLayout: true, backgroundAppid: 620};
|
const appearance = {accent: 'blue', corners: 'round', wideLayout: true, backgroundAppid: 620, bannerAppid: 1086940, showBanner: false};
|
||||||
|
|
||||||
expect(normalizeAppearance(appearance)).toEqual(appearance);
|
expect(normalizeAppearance(appearance)).toEqual(appearance);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('replaces unknown or missing values with the defaults', () => {
|
it('replaces unknown or missing values with the defaults', () => {
|
||||||
expect(normalizeAppearance({accent: 'red;x', corners: 'pointy', wideLayout: 'yes', backgroundAppid: -1}))
|
expect(normalizeAppearance({accent: 'red;x', corners: 'pointy', wideLayout: 'yes', backgroundAppid: -1, bannerAppid: '620', showBanner: 'no'}))
|
||||||
.toEqual(DEFAULT_APPEARANCE);
|
.toEqual(DEFAULT_APPEARANCE);
|
||||||
expect(normalizeAppearance(null)).toEqual(DEFAULT_APPEARANCE);
|
expect(normalizeAppearance(null)).toEqual(DEFAULT_APPEARANCE);
|
||||||
expect(normalizeAppearance({backgroundAppid: 1.5})).toEqual(DEFAULT_APPEARANCE);
|
expect(normalizeAppearance({backgroundAppid: 1.5})).toEqual(DEFAULT_APPEARANCE);
|
||||||
@@ -27,9 +27,9 @@ describe('appearance cache', () => {
|
|||||||
afterEach(() => localStorage.clear());
|
afterEach(() => localStorage.clear());
|
||||||
|
|
||||||
it('reads back what was cached', () => {
|
it('reads back what was cached', () => {
|
||||||
cacheAppearance({accent: 'green', corners: 'sharp', wideLayout: false, backgroundAppid: null});
|
cacheAppearance({accent: 'green', corners: 'sharp', wideLayout: false, backgroundAppid: null, bannerAppid: 620, showBanner: true});
|
||||||
|
|
||||||
expect(readCachedAppearance()).toEqual({accent: 'green', corners: 'sharp', wideLayout: false, backgroundAppid: null});
|
expect(readCachedAppearance()).toEqual({accent: 'green', corners: 'sharp', wideLayout: false, backgroundAppid: null, bannerAppid: 620, showBanner: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to the defaults for a broken or empty cache', () => {
|
it('falls back to the defaults for a broken or empty cache', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user