Skip to content
Merged
29 changes: 17 additions & 12 deletions frontend/e2e/mobile.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { test, expect } from '@playwright/test';
import { test, expect, type Page } from '@playwright/test';
import { randomUUID } from 'crypto';

// Below the 860px breakpoint, section links live inside the collapsed
// nav-menu panel (#12) — open it via the toggle before interacting with them.
async function openMobileNav(page: Page) {
await page.getByRole('button', { name: /menu|navigation/i }).click();
}

test.describe('Mobile Navigation', () => {
test.use({ viewport: { width: 375, height: 667 } });

Expand All @@ -13,10 +19,12 @@ test.describe('Mobile Navigation', () => {

test('should navigate between sections on mobile', async ({ page }) => {
await page.goto('/');


await openMobileNav(page);
await page.getByRole('link', { name: /features/i }).click();
await expect(page.locator('#features')).toBeInViewport();


await openMobileNav(page);
await page.getByRole('link', { name: /about/i }).click();
await expect(page.locator('#about')).toBeInViewport();
});
Expand All @@ -32,10 +40,11 @@ test.describe('Mobile Navigation', () => {

test('should handle touch interactions', async ({ page }) => {
await page.goto('/');


await page.getByRole('button', { name: /menu|navigation/i }).tap();
const featuresLink = page.getByRole('link', { name: /features/i });
await featuresLink.tap();

await expect(page.locator('#features')).toBeInViewport();
});
});
Expand Down Expand Up @@ -139,17 +148,10 @@ test.describe('Touch Target Sizes', () => {
test.describe('Mobile Nav Toggle', () => {
test.use({ viewport: { width: 375, height: 667 } });

// The mobile nav toggle (#12) is feature-detected: this suite should keep
// passing before it ships and start exercising it the moment it does.
test('should open and close the mobile nav menu via the toggle', async ({ page }) => {
await page.goto('/');

const navToggle = page.getByRole('button', { name: /menu|navigation/i });
if ((await navToggle.count()) === 0) {
test.skip(true, 'Mobile nav toggle (#12) not yet implemented');
return;
}

await expect(navToggle).toHaveAttribute('aria-expanded', 'false');
await navToggle.tap();
await expect(navToggle).toHaveAttribute('aria-expanded', 'true');
Expand Down Expand Up @@ -191,6 +193,7 @@ test.describe('Mobile Core Journeys', () => {

// Browse: real UI, mobile viewport
await page.goto('/');
await page.getByRole('button', { name: /menu|navigation/i }).tap();
await page.getByRole('link', { name: /how it works/i }).tap();
await expect(page.locator('#how-it-works')).toBeInViewport();
await expect(page.getByRole('heading', { name: /place bets/i })).toBeVisible();
Expand Down Expand Up @@ -351,6 +354,7 @@ test.describe('Touch Gesture – Tap to Select Outcome', () => {
test('tap on navigation link should navigate to the target section', async ({ page }) => {
await page.goto('/');

await page.getByRole('button', { name: /menu|navigation/i }).tap();
const featuresLink = page.getByRole('link', { name: /features/i });
await featuresLink.tap();

Expand Down Expand Up @@ -394,6 +398,7 @@ test.describe('Touch Gesture – Long Press', () => {
test('long press on navigation link should not break navigation', async ({ page }) => {
await page.goto('/');

await page.getByRole('button', { name: /menu|navigation/i }).tap();
const featuresLink = page.getByRole('link', { name: /features/i });
const box = await featuresLink.boundingBox();

Expand Down
131 changes: 131 additions & 0 deletions frontend/src/app/account/bets/bets.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
.bet-history-page {
max-width: var(--container);
margin: 0 auto;
padding: clamp(2rem, 4vw, 3rem) 1.5rem;
}

.bet-history-page__heading {
font-family: var(--font-display);
font-size: var(--text-xl);
margin: 0 0 1.5rem;
color: var(--fg);
}

.bet-history-page__hint {
color: var(--fg-muted);
font-size: var(--text-base);
}

.bet-history-page__status {
color: var(--fg-muted);
font-size: var(--text-sm);
}

.bet-history-page__error {
color: var(--destructive);
font-size: var(--text-sm);
padding: 0.85rem 1.1rem;
background: rgba(248, 113, 113, 0.12);
border: 1px solid var(--destructive);
border-radius: var(--radius-sm);
}

.bet-history-page__table-wrapper {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow-x: auto;
margin-bottom: 1.5rem;
}

.bet-history-page__table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-sm);
}

.bet-history-page__table th,
.bet-history-page__table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}

.bet-history-page__table th {
background: var(--surface-2);
color: var(--fg-muted);
font-weight: 600;
text-transform: uppercase;
font-size: var(--text-xs);
letter-spacing: 0.05em;
}

.bet-history-page__table td {
color: var(--fg);
}

.bet-history-page__table tbody tr:last-child td {
border-bottom: none;
}

.bet-history-page__table tbody tr:hover td {
background: var(--surface-2);
}

.bet-history-page__pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}

.bet-history-page__pagination-btn {
min-height: 40px;
padding: 0 1.25rem;
font-family: var(--font-body);
font-size: var(--text-sm);
font-weight: 500;
color: var(--fg);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
cursor: pointer;
transition: border-color var(--dur-fast), background-color var(--dur-fast), transform var(--dur-fast) var(--ease-expo);
}

.bet-history-page__pagination-btn:hover:not(:disabled) {
border-color: var(--border-strong);
background: var(--surface-2);
transform: translateY(-1px);
}

.bet-history-page__pagination-btn:focus-visible {
outline: 2px solid var(--ring-strong);
outline-offset: 2px;
}

.bet-history-page__pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

@media (max-width: 480px) {
.bet-history-page__pagination {
flex-direction: column-reverse;
align-items: stretch;
}

.bet-history-page__pagination-btn {
width: 100%;
}
}

@media (prefers-reduced-motion: reduce) {
.bet-history-page__pagination-btn {
transition: none;
}
.bet-history-page__pagination-btn:hover:not(:disabled) {
transform: none;
}
}
69 changes: 44 additions & 25 deletions frontend/src/app/account/bets/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import React, { useEffect, useState, useCallback } from 'react';
import './bets.css';

// TODO: replace with the shared wallet hook from #77 once merged. This local
// stub exposes the same shape (connected address + change subscription) so
Expand Down Expand Up @@ -92,47 +93,65 @@ export default function BetHistoryPage() {
if (!address) {
return (
<div className="bet-history-page">
<p>Connect your wallet to view your bet history.</p>
<p className="bet-history-page__hint">Connect your wallet to view your bet history.</p>
</div>
);
}

return (
<div className="bet-history-page">
<h1>Bet History</h1>
{loading && <p role="status">Loading bets…</p>}
{error && <p role="alert">{error}</p>}
{!loading && !error && bets.length === 0 && <p>No bets found for this address.</p>}
<h1 className="bet-history-page__heading">Bet History</h1>
{loading && (
<p className="bet-history-page__status" role="status">
Loading bets…
</p>
)}
{error && (
<p className="bet-history-page__error" role="alert">
{error}
</p>
)}
{!loading && !error && bets.length === 0 && (
<p className="bet-history-page__hint">No bets found for this address.</p>
)}

{bets.length > 0 && (
<table>
<thead>
<tr>
<th>Market</th>
<th>Outcome</th>
<th>Amount</th>
<th>Placed</th>
</tr>
</thead>
<tbody>
{bets.map((bet) => (
<tr key={bet.id}>
<td>{bet.market_id}</td>
<td>{bet.outcome}</td>
<td>{bet.amount}</td>
<td>{new Date(bet.placed_at).toLocaleString()}</td>
<div className="bet-history-page__table-wrapper">
<table className="bet-history-page__table">
<thead>
<tr>
<th>Market</th>
<th>Outcome</th>
<th>Amount</th>
<th>Placed</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{bets.map((bet) => (
<tr key={bet.id}>
<td>{bet.market_id}</td>
<td>{bet.outcome}</td>
<td>{bet.amount}</td>
<td>{new Date(bet.placed_at).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
)}

<div className="bet-history-page__pagination">
<button type="button" disabled={page === 0 || loading} onClick={() => setPage((p) => Math.max(0, p - 1))}>
<button
type="button"
className="bet-history-page__pagination-btn"
disabled={page === 0 || loading}
onClick={() => setPage((p) => Math.max(0, p - 1))}
>
Previous
</button>
<button
type="button"
className="bet-history-page__pagination-btn"
disabled={loading || bets.length < PAGE_SIZE}
onClick={() => setPage((p) => p + 1)}
>
Expand Down
Loading
Loading