Skip to content

Commit 9f2c09d

Browse files
OneStepAt4timeArgus
authored andcommitted
feat(dashboard): Getting Started card for new users (#3398)
- Shows when totalSessions < 3 and not dismissed - Dismissible via localStorage flag - CTA opens CreateSessionModal - Auto-hides after 3+ sessions exist - 7 Vitest tests (render, dismiss, CTA, edge cases) - Design token compatible (dark/light themes) Closes #3398
1 parent 46bd6e0 commit 9f2c09d

3 files changed

Lines changed: 143 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* __tests__/GettingStartedCard.test.tsx
3+
*/
4+
5+
import { describe, it, expect, beforeEach, vi } from 'vitest';
6+
import { render, screen, fireEvent } from '@testing-library/react';
7+
import GettingStartedCard from '../components/shared/GettingStartedCard';
8+
9+
describe('GettingStartedCard', () => {
10+
beforeEach(() => {
11+
localStorage.clear();
12+
});
13+
14+
it('renders when totalSessions < 3', () => {
15+
render(<GettingStartedCard totalSessions={0} onCreateSession={vi.fn()} />);
16+
expect(screen.getByText('Welcome to Aegis')).toBeTruthy();
17+
expect(screen.getByText('Create First Session')).toBeTruthy();
18+
});
19+
20+
it('does not render when totalSessions >= 3', () => {
21+
render(<GettingStartedCard totalSessions={3} onCreateSession={vi.fn()} />);
22+
expect(screen.queryByText('Welcome to Aegis')).toBeNull();
23+
});
24+
25+
it('does not render when totalSessions > 3', () => {
26+
render(<GettingStartedCard totalSessions={10} onCreateSession={vi.fn()} />);
27+
expect(screen.queryByText('Welcome to Aegis')).toBeNull();
28+
});
29+
30+
it('calls onCreateSession when CTA is clicked', () => {
31+
const onCreate = vi.fn();
32+
render(<GettingStartedCard totalSessions={0} onCreateSession={onCreate} />);
33+
fireEvent.click(screen.getByText('Create First Session'));
34+
expect(onCreate).toHaveBeenCalledOnce();
35+
});
36+
37+
it('dismisses and sets localStorage', () => {
38+
render(<GettingStartedCard totalSessions={0} onCreateSession={vi.fn()} />);
39+
fireEvent.click(screen.getByLabelText('Dismiss getting started card'));
40+
expect(screen.queryByText('Welcome to Aegis')).toBeNull();
41+
expect(localStorage.getItem('aegis-getting-started-dismissed')).toBe('true');
42+
});
43+
44+
it('does not render if previously dismissed', () => {
45+
localStorage.setItem('aegis-getting-started-dismissed', 'true');
46+
render(<GettingStartedCard totalSessions={0} onCreateSession={vi.fn()} />);
47+
expect(screen.queryByText('Welcome to Aegis')).toBeNull();
48+
});
49+
50+
it('renders for 1 and 2 sessions', () => {
51+
const { rerender } = render(
52+
<GettingStartedCard totalSessions={1} onCreateSession={vi.fn()} />
53+
);
54+
expect(screen.getByText('Welcome to Aegis')).toBeTruthy();
55+
56+
rerender(<GettingStartedCard totalSessions={2} onCreateSession={vi.fn()} />);
57+
expect(screen.getByText('Welcome to Aegis')).toBeTruthy();
58+
});
59+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* components/shared/GettingStartedCard.tsx — Welcome card for new users.
3+
*
4+
* Shows when total sessions < 3 and user hasn't dismissed it.
5+
* CTA opens the CreateSessionModal.
6+
* Uses design tokens for dark/light theme compatibility.
7+
*/
8+
9+
import { useState } from 'react';
10+
import { Rocket, X, ArrowRight } from 'lucide-react';
11+
12+
interface GettingStartedCardProps {
13+
totalSessions: number;
14+
onCreateSession: () => void;
15+
}
16+
17+
const DISMISS_KEY = 'aegis-getting-started-dismissed';
18+
19+
export default function GettingStartedCard({ totalSessions, onCreateSession }: GettingStartedCardProps) {
20+
const [dismissed, setDismissed] = useState(() => {
21+
return localStorage.getItem(DISMISS_KEY) === 'true';
22+
});
23+
24+
// Auto-hide: if user has 3+ sessions, never show
25+
if (totalSessions >= 3 || dismissed) return null;
26+
27+
const handleDismiss = () => {
28+
localStorage.setItem(DISMISS_KEY, 'true');
29+
setDismissed(true);
30+
};
31+
32+
const handleCreate = () => {
33+
onCreateSession();
34+
};
35+
36+
return (
37+
<div
38+
role="complementary"
39+
aria-label="Getting started"
40+
className="relative rounded-xl border border-[var(--color-accent-cyan)]/20 bg-gradient-to-br from-[var(--color-accent-cyan)]/5 to-[var(--color-surface-strong)] p-5 sm:p-6"
41+
>
42+
{/* Dismiss button */}
43+
<button
44+
type="button"
45+
onClick={handleDismiss}
46+
className="absolute right-3 top-3 rounded-md p-1 text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-primary)] hover:bg-[var(--color-void-lighter)]"
47+
aria-label="Dismiss getting started card"
48+
>
49+
<X className="h-4 w-4" />
50+
</button>
51+
52+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
53+
{/* Icon */}
54+
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-cyan)]/10 text-[var(--color-accent-cyan)]">
55+
<Rocket className="h-6 w-6" />
56+
</div>
57+
58+
{/* Content */}
59+
<div className="flex-1 min-w-0">
60+
<h2 className="text-base font-semibold text-[var(--color-text-primary)]">
61+
Welcome to Aegis
62+
</h2>
63+
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
64+
Create your first session to start managing Claude Code with audit logs, permissions, and real-time monitoring.
65+
</p>
66+
</div>
67+
68+
{/* CTA */}
69+
<button
70+
type="button"
71+
onClick={handleCreate}
72+
className="inline-flex min-h-[44px] shrink-0 items-center justify-center gap-2 rounded-lg border border-[var(--color-accent-cyan)]/30 bg-[var(--color-accent-cyan)]/10 px-5 py-2.5 text-sm font-semibold text-[var(--color-accent-cyan)] transition-all hover:bg-[var(--color-accent-cyan)]/20 hover:border-[var(--color-accent-cyan)]/50"
73+
>
74+
Create First Session
75+
<ArrowRight className="h-4 w-4" />
76+
</button>
77+
</div>
78+
</div>
79+
);
80+
}

dashboard/src/pages/OverviewPage.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { EfficiencyGauge } from '../components/analytics/EfficiencyGauge';
2323
import { useSessionRealtimeUpdates } from '../hooks/useSessionRealtimeUpdates';
2424
import { useT } from '../i18n/context';
2525
import { SessionHealthBanner } from '../components/shared/SessionHealthBanner';
26+
import GettingStartedCard from '../components/shared/GettingStartedCard';
2627
import { useStore } from '../store/useStore';
2728
import { getAnalyticsSummary } from '../api/client';
2829
import { formatCurrency, formatNumber } from '../utils/formatNumber';
@@ -193,6 +194,9 @@ export default function OverviewPage() {
193194
</div>
194195
</div>
195196

197+
{/* Getting Started — new users */}
198+
<GettingStartedCard totalSessions={totalSessions} onCreateSession={() => setModalOpen(true)} />
199+
196200
{/* Zone B: KPI Banner */}
197201
{!analyticsLoading && analytics && (
198202
<KPIBanner items={buildKPIItems(analytics)} />

0 commit comments

Comments
 (0)