Public Access
feat(points): balance, history and shop page
Add /points page with wallet balance, shop grouped by slot and ledger history with game names. The wallet loads once in PreferencesContext after login, so the header shows a balance chip and the link. Premium accents gold and teal override the free accent while equipped. German and English.
This commit is contained in:
1 parent
22633a9e45
commit
ca3d34bad0
16 files changed
+556
-9
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import {Link, NavLink, useLocation, useNavigate} from 'react-router-dom';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {ChartColumn, Dices, Eye, Languages, LayoutDashboard, Library, LogOut, Megaphone, Moon, Newspaper, Settings, Shield, Sun} from 'lucide-react';
|
||||
import {ChartColumn, Coins, Dices, Eye, Languages, LayoutDashboard, Library, LogOut, Megaphone, Moon, Newspaper, Settings, Shield, Sun} from 'lucide-react';
|
||||
import {toast} from 'sonner';
|
||||
|
||||
import {Badge} from '@/components/ui/badge';
|
||||
@@ -43,7 +43,7 @@ const navLinkClass = (isActive) => cn(
|
||||
|
||||
export default function AppHeader({isAuthenticated, onLogout}) {
|
||||
const {t, i18n} = useTranslation();
|
||||
const {isDarkMode, setDarkMode, setLanguage} = usePreferences();
|
||||
const {isDarkMode, setDarkMode, setLanguage, wallet} = usePreferences();
|
||||
const {pathname} = useLocation();
|
||||
const steamProfile = useSteamProfile(isAuthenticated);
|
||||
const currentUser = useCurrentUser(isAuthenticated);
|
||||
@@ -126,6 +126,11 @@ export default function AppHeader({isAuthenticated, onLogout}) {
|
||||
{isAuthenticated && viewingAs && (
|
||||
<Badge variant="secondary" className="mr-1">{t('NAV.VIEW_AS.BADGE', {role: t(`ROLES.${viewingAs}`)})}</Badge>
|
||||
)}
|
||||
{isAuthenticated && wallet && (
|
||||
<Button asChild variant="ghost" size="sm" title={t('POINTS.TITLE')}>
|
||||
<Link to="/points"><Coins className="text-primary"/><span className="tabular-nums">{wallet.balance}</span></Link>
|
||||
</Button>
|
||||
)}
|
||||
{isAuthenticated ? (
|
||||
<DropdownMenu onOpenChange={(open) => open && setMenuOpenings((count) => count + 1)}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -144,6 +149,9 @@ export default function AppHeader({isAuthenticated, onLogout}) {
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings"><Settings/>{t('NAV.SETTINGS')}</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/points"><Coins/>{t('NAV.POINTS')}</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/whats-new"><Newspaper/>{t('NAV.WHATS_NEW')}</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, fireEvent, screen} from '@testing-library/react';
|
||||
import AppHeader from './AppHeader.jsx';
|
||||
import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
import {renderWithProviders} from '../test/renderWithProviders.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
function Load() {
|
||||
const {reloadWallet} = usePreferences();
|
||||
return <button onClick={reloadWallet}>load</button>;
|
||||
}
|
||||
|
||||
describe('AppHeader points chip', () => {
|
||||
it('shows the balance as a link to the points page once the wallet is loaded', async () => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
fetchRoutes({
|
||||
'GET /api/users/me': json(200, {role: 'USER', activeRole: 'USER'}),
|
||||
'GET /api/library/steam-profile': json(200, {}),
|
||||
'GET /api/rewards/wallet': json(200, {balance: 135, catalog: [], owned: [], equipped: {}, history: []}),
|
||||
});
|
||||
renderWithProviders(<><Load/><AppHeader isAuthenticated onLogout={() => {}}/></>);
|
||||
expect(screen.queryByTitle('POINTS.TITLE')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByText('load'));
|
||||
|
||||
const chip = await screen.findByTitle('POINTS.TITLE');
|
||||
expect(chip.textContent).toBe('135');
|
||||
expect(chip.getAttribute('href')).toBe('/points');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user