Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions e2e/wallet-connect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

/** Inject a fake Freighter wallet object before any app code runs. */
function mockFreighter(page: import('@playwright/test').Page) {
return page.addInitScript(() => {
return page.addInitScript((addr) => {

Check failure on line 7 in e2e/wallet-connect.spec.ts

View workflow job for this annotation

GitHub Actions / Lint and Test

'addr' is defined but never used
(window as unknown as Record<string, unknown>).freighter = {
isConnected: () => Promise.resolve({ isConnected: true }),
requestAccess: () =>
Expand All @@ -15,7 +15,7 @@
signMessage: (message: string) =>
Promise.resolve({ signedMessage: `mocked_signature_${message}`, error: null }),
};
});
}, MOCK_ADDRESS);
}

test.describe('Wallet Connect – Freighter Mocked', () => {
Expand Down
96 changes: 88 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 43 additions & 21 deletions src/components/RankProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,64 @@ interface RankProgressBarProps {
xp: number;
}

const TIER_COLORS: Record<string, string> = {
Rookie: '#9CA3AF',
Trader: '#22C55E',
Analyst: '#2C4BFD',
Strategist: '#A855F7',
Master: '#F59E0B',
Legend: '#EC4899',
};

export default function RankProgressBar({ xp }: RankProgressBarProps) {
const { current, next, progress } = getRankTiers(xp);
const color = TIER_COLORS[current.name] ?? '#2C4BFD';
const nextColor = next ? (TIER_COLORS[next.name] ?? '#2C4BFD') : color;

return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-400 font-medium">Rank</span>
<span className="rounded bg-cyan-500/10 px-2 py-0.5 text-xs font-bold text-cyan-300">
{current.name}
</span>
</div>
<div className="flex items-center gap-1 text-sm text-gray-400">
<span className="font-medium">Experience</span>
<span className="font-bold text-white">{xp} XP</span>
</div>
<span className="text-sm text-gray-400">Rank</span>
<span
className="rounded-full px-3 py-1 text-sm font-bold"
style={{ backgroundColor: `${color}26`, color }}
>
{current.name}
</span>
</div>

<div className="space-y-1">
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-400">Experience</span>
<span className="font-mono text-sm text-gray-300">{xp} XP</span>
</div>

<div
role="progressbar"
aria-valuenow={xp}
aria-valuemin={current.minXp}
aria-valuemax={next ? next.minXp : xp}
aria-label="XP progress to next rank"
className="h-2 w-full overflow-hidden rounded-full bg-white/5"
aria-valuenow={xp - current.minXp}
aria-valuemin={0}
aria-valuemax={next ? next.minXp - current.minXp : xp - current.minXp}
aria-label={
next
? `XP progress toward ${next.name}: ${Math.round(progress)}%`
: 'Maximum rank reached'
}
className="h-2 overflow-hidden rounded-full bg-gray-800"
>
<div
className="h-full bg-cyan-500 transition-all duration-300"
style={{ width: `${progress}%` }}
className="h-full rounded-full transition-all duration-500"
style={{ width: `${progress}%`, backgroundColor: next ? color : nextColor }}
/>
</div>
{next && (
<p className="text-right text-xs text-gray-500">
{next.minXp - xp} XP to {next.name}

{next ? (
<p className="text-xs text-gray-500">
<span className="font-mono">{xp}</span> /{' '}
<span className="font-mono">{next.minXp}</span> XP to{' '}
<span style={{ color: nextColor }}>{next.name}</span>
</p>
) : (
<p className="text-xs text-amber-400">Maximum rank reached!</p>
)}
</div>
</div>
Expand Down
53 changes: 52 additions & 1 deletion src/components/RecentActivity.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import RecentActivity from './RecentActivity';
import type { RecentActivityItem } from '../types';
Expand Down Expand Up @@ -88,6 +88,57 @@ describe('RecentActivity', () => {
});
});

describe('filter chips', () => {
it('renders all three filter options', () => {
render(<RecentActivity items={mockItems} />);
const tabs = screen.getAllByRole('tab');
expect(tabs).toHaveLength(3);
expect(tabs[0]).toHaveTextContent('all');
expect(tabs[1]).toHaveTextContent('correct');
expect(tabs[2]).toHaveTextContent('incorrect');
});

it('defaults to "all" filter showing all items', () => {
render(<RecentActivity items={mockItems} />);
expect(screen.getAllByRole('listitem')).toHaveLength(3);
});

it('filters to show only correct (Won) items when "correct" is selected', () => {
render(<RecentActivity items={mockItems} />);
fireEvent.click(screen.getByRole('tab', { name: 'correct' }));
expect(screen.getAllByRole('listitem')).toHaveLength(2);
expect(screen.getByText('BTC')).toBeInTheDocument();
expect(screen.getByText('XLM')).toBeInTheDocument();
expect(screen.queryByText('ETH')).not.toBeInTheDocument();
});

it('filters to show only incorrect (Lost) items when "incorrect" is selected', () => {
render(<RecentActivity items={mockItems} />);
fireEvent.click(screen.getByRole('tab', { name: 'incorrect' }));
expect(screen.getAllByRole('listitem')).toHaveLength(1);
expect(screen.getByText('ETH')).toBeInTheDocument();
expect(screen.queryByText('BTC')).not.toBeInTheDocument();
expect(screen.queryByText('XLM')).not.toBeInTheDocument();
});

it('shows filter-specific empty message when no items match', () => {
const allWon: RecentActivityItem[] = [
{ id: '1', asset: 'BTC', result: 'Won', amount: 10, mode: 'updown' },
];
render(<RecentActivity items={allWon} />);
fireEvent.click(screen.getByRole('tab', { name: 'incorrect' }));
expect(screen.getByText(/no incorrect predictions yet/i)).toBeInTheDocument();
expect(screen.queryByRole('list')).not.toBeInTheDocument();
});

it('shows default empty message when items array is empty regardless of filter', () => {
render(<RecentActivity items={[]} />);
fireEvent.click(screen.getByRole('tab', { name: 'correct' }));
expect(screen.getByText(/no predictions yet/i)).toBeInTheDocument();
expect(screen.queryByRole('list')).not.toBeInTheDocument();
});
});

describe('loading and error states', () => {
it('renders a loading state with skeletons', () => {
render(<RecentActivity items={[]} isLoading={true} />);
Expand Down
Loading
Loading