Public Access
feat(welcome): add welcome page for new users after first login
Shows six feature cards and two next steps (Steam, import). The dashboard redirects to it while Auth settings.welcomeSeenAt is null; "Let's go" and "Skip" set the stamp. Settings links to it to reopen later.
This commit is contained in:
1 parent
b811255c16
commit
b129f6a6e4
11 files changed
+348
-2
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import {Link} from 'react-router-dom';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {UserRound} from 'lucide-react';
|
||||
import {Rocket, UserRound} from 'lucide-react';
|
||||
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
|
||||
import {useCurrentUserContext} from '../context/CurrentUserContext.jsx';
|
||||
import {formatDateTime} from '../utils/format.js';
|
||||
@@ -38,6 +40,9 @@ export default function AccountInfoCard() {
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<Button asChild variant="link" className="mt-3 h-auto p-0">
|
||||
<Link to="/welcome"><Rocket/>{t('SETTINGS.OPEN_WELCOME')}</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import {Navigate} from 'react-router-dom';
|
||||
import {useCurrentUserContext} from '../context/CurrentUserContext.jsx';
|
||||
|
||||
/**
|
||||
* Opens the welcome page for a user who has not seen it. Only null counts: undefined is "still loading".
|
||||
* Relies on Auth sending `welcomeSeenAt: null` explicitly (no NON_NULL Jackson config).
|
||||
*/
|
||||
export default function WelcomeGate({children}) {
|
||||
const {user} = useCurrentUserContext();
|
||||
return user?.settings?.welcomeSeenAt === null ? <Navigate to="/welcome" replace/> : children;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, screen, waitFor} from '@testing-library/react';
|
||||
import WelcomeGate from './WelcomeGate.jsx';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
import {renderWithProviders} from '../test/renderWithProviders.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
|
||||
const me = (settings) => ({id: 1, email: 'a@b.de', role: 'USER', settings});
|
||||
|
||||
const renderGate = (settings) => {
|
||||
fetchRoutes({'GET /api/users/me': json(200, me(settings))});
|
||||
return renderWithProviders(<WelcomeGate><p>dashboard</p></WelcomeGate>, {
|
||||
path: '/dashboard', entries: ['/dashboard'], routes: {'/welcome': <p>welcome</p>},
|
||||
});
|
||||
};
|
||||
|
||||
describe('WelcomeGate', () => {
|
||||
beforeEach(() => localStorage.setItem('tbp_session', '1'));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('opens the welcome page when the user has not seen it', async () => {
|
||||
const {router} = renderGate({welcomeSeenAt: null});
|
||||
|
||||
await screen.findByText('welcome');
|
||||
expect(router.state.location.pathname).toBe('/welcome');
|
||||
});
|
||||
|
||||
it('shows the children once the welcome page was seen', async () => {
|
||||
renderGate({welcomeSeenAt: '2026-10-01T10:00:00Z'});
|
||||
|
||||
expect(await screen.findByText('dashboard')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows the children when /me has no such field', async () => {
|
||||
const {router} = renderGate({});
|
||||
|
||||
expect(await screen.findByText('dashboard')).toBeTruthy();
|
||||
await waitFor(() => expect(router.state.location.pathname).toBe('/dashboard'));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user