Skip to content
Merged
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
3 changes: 0 additions & 3 deletions frontend/babel.config.js

This file was deleted.

34 changes: 34 additions & 0 deletions frontend/docs/WALLET_BUTTON.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# WalletConnectButton Component

The `WalletButton` component in `src/components/layout/WalletButton.tsx` implements the wallet connection functionality.

## Features

- **Disconnected State**: Shows "Connect Wallet" button
- **Connection Flow**: Calls `connectFreighter()` via `useWallet()` hook
- **Connected State**: Displays truncated address and XLM balance
- **Dropdown Menu**:
- Copy Address (with feedback)
- View on Stellar Explorer
- Disconnect
- **Loading State**: Shows spinner while connecting
- **Error Handling**: Gracefully handles Freighter not installed
- **Keyboard Support**: Escape key closes dropdown

## Usage

```tsx
import { WalletButton } from '@/components/layout/WalletButton';

export function Header() {
return <WalletButton />;
}
```

## Integration

The component is integrated in the Header and uses:
- `useWallet()` hook for state management
- Zustand store for wallet state persistence
- sessionStorage for address persistence (security)
- Freighter wallet extension for signing
5 changes: 5 additions & 0 deletions frontend/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
626 changes: 487 additions & 139 deletions frontend/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@
"format": "prettier --write 'src/**/*.{ts,tsx}'"
},
"dependencies": {
"@stellar/stellar-sdk": "^15.1.0",
"@tanstack/react-query": "^5.45.0",
"next": "14.2.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"zustand": "^4.5.2"
},
"devDependencies": {
"@babel/preset-env": "^7.29.2",
"@babel/preset-typescript": "^7.29.7",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^14.3.1",
"@testing-library/user-event": "^14.6.1",
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { Header } from '../components/layout/Header';
import { QueryProvider } from '../providers/QueryProvider';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });
Expand All @@ -19,8 +20,10 @@ export default function RootLayout({ children }: { children: React.ReactNode }):
return (
<html lang="en" className={inter.className}>
<body className="bg-gray-950 text-white min-h-screen">
<Header />
{children}
<QueryProvider>
<Header />
{children}
</QueryProvider>
</body>
</html>
);
Expand Down
83 changes: 15 additions & 68 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,9 @@ import { useRouter, useSearchParams } from 'next/navigation';
import { useMarkets } from '../hooks/useMarkets';
import { MarketCard } from '../components/market/MarketCard';
import { MarketCardSkeleton } from '../components/market/MarketCardSkeleton';
import { StatsBanner } from '../components/ui/StatsBanner';
import { MarketFilters } from '../components/market/MarketFilters';

const WEIGHT_CLASSES = [
'All Weight Classes',
'Heavyweight',
'Light Heavyweight',
'Super Middleweight',
'Middleweight',
'Super Welterweight',
'Welterweight',
'Super Lightweight',
'Lightweight',
'Super Featherweight',
'Featherweight',
'Super Bantamweight',
'Bantamweight',
'Super Flyweight',
'Flyweight',
'Minimumweight',
];
const STATUSES = ['All', 'Open', 'Resolved'];
const SORT_OPTIONS = [
{ value: 'date_asc', label: 'Date ↑' },
{ value: 'date_desc', label: 'Date ↓' },
{ value: 'pool_desc', label: 'Pool ↓' },
];
const LIMIT = 12;

export default function HomePage(): JSX.Element {
Expand Down Expand Up @@ -87,51 +65,20 @@ export default function HomePage(): JSX.Element {
<p className="text-gray-400 text-sm mt-1">Decentralized boxing prediction markets on Stellar</p>
</div>

{/* Filter bar */}
<div className="flex flex-wrap gap-3 items-center">
{/* Weight class dropdown */}
<select
value={weightClass}
onChange={(e) =>
setParam('weight_class', e.target.value === 'All Weight Classes' ? null : e.target.value)
}
className="min-h-[44px] bg-gray-800 text-white text-sm rounded-lg px-3 focus:outline-none focus:ring-2 focus:ring-amber-500"
>
{WEIGHT_CLASSES.map((w) => (
<option key={w}>{w}</option>
))}
</select>

{/* Status tabs */}
<div className="flex rounded-lg overflow-hidden border border-gray-700">
{STATUSES.map((s) => (
<button
key={s}
onClick={() => setParam('status', s === 'All' ? null : s.toLowerCase())}
className={`px-4 py-2 text-sm font-medium transition-colors ${
status === (s === 'All' ? 'All' : s)
? 'bg-amber-500 text-black'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
}`}
>
{s}
</button>
))}
</div>
{/* Stats Banner */}
<StatsBanner />

{/* Sort control */}
<select
value={sort}
onChange={(e) => setParam('sort', e.target.value)}
className="min-h-[44px] bg-gray-800 text-white text-sm rounded-lg px-3 focus:outline-none focus:ring-2 focus:ring-amber-500 ml-auto"
>
{SORT_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</div>
{/* Filter bar */}
<MarketFilters
weightClass={weightClass}
status={status}
sort={sort}
onWeightClassChange={(value) =>
setParam('weight_class', value === 'All Weight Classes' ? null : value)
}
onStatusChange={(value) => setParam('status', value === 'All' ? null : value.toLowerCase())}
onSortChange={(value) => setParam('sort', value)}
/>

{/* Error banner */}
{error && (
Expand Down
91 changes: 91 additions & 0 deletions frontend/src/components/market/MarketFilters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use client';

interface MarketFiltersProps {
weightClass: string;
status: string;
sort: string;
onWeightClassChange: (value: string) => void;
onStatusChange: (value: string) => void;
onSortChange: (value: string) => void;
}

const WEIGHT_CLASSES = [
'All Weight Classes',
'Heavyweight',
'Light Heavyweight',
'Super Middleweight',
'Middleweight',
'Super Welterweight',
'Welterweight',
'Super Lightweight',
'Lightweight',
'Super Featherweight',
'Featherweight',
'Super Bantamweight',
'Bantamweight',
'Super Flyweight',
'Flyweight',
'Minimumweight',
];

const STATUSES = ['All', 'Open', 'Resolved'];

const SORT_OPTIONS = [
{ value: 'date_asc', label: 'Date ↑' },
{ value: 'date_desc', label: 'Date ↓' },
{ value: 'pool_desc', label: 'Pool ↓' },
];

export function MarketFilters({
weightClass,
status,
sort,
onWeightClassChange,
onStatusChange,
onSortChange,
}: MarketFiltersProps): JSX.Element {
return (
<div className="flex flex-wrap gap-3 items-center">
{/* Weight class dropdown */}
<select
value={weightClass}
onChange={(e) => onWeightClassChange(e.target.value)}
className="min-h-[44px] bg-gray-800 text-white text-sm rounded-lg px-3 focus:outline-none focus:ring-2 focus:ring-amber-500"
>
{WEIGHT_CLASSES.map((w) => (
<option key={w}>{w}</option>
))}
</select>

{/* Status tabs */}
<div className="flex rounded-lg overflow-hidden border border-gray-700">
{STATUSES.map((s) => (
<button
key={s}
onClick={() => onStatusChange(s)}
className={`px-4 py-2 text-sm font-medium transition-colors ${
status === s
? 'bg-amber-500 text-black'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
}`}
>
{s}
</button>
))}
</div>

{/* Sort control */}
<select
value={sort}
onChange={(e) => onSortChange(e.target.value)}
className="min-h-[44px] bg-gray-800 text-white text-sm rounded-lg px-3 focus:outline-none focus:ring-2 focus:ring-amber-500 ml-auto"
>
{SORT_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</div>
);
}
2 changes: 2 additions & 0 deletions frontend/src/components/ui/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import { Component, type ErrorInfo, type ReactNode } from 'react';

interface Props {
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/components/ui/StatsBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use client';

import { useQuery } from '@tanstack/react-query';
import { api } from '../../services/api';

interface Stats {
totalMarkets: number;
totalVolume: number;
activeMarkets: number;
}

export function StatsBanner(): JSX.Element {
const { data: stats, isLoading } = useQuery<Stats>({
queryKey: ['stats'],
queryFn: async () => {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/stats`);
if (!res.ok) throw new Error('Failed to fetch stats');
return res.json();
},
staleTime: 1000 * 60 * 5,
});

const statItems = [
{ label: 'Total Markets', value: stats?.totalMarkets ?? 0 },
{ label: 'Total Volume', value: `$${(stats?.totalVolume ?? 0).toLocaleString()}` },
{ label: 'Active Markets', value: stats?.activeMarkets ?? 0 },
];

return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 bg-gradient-to-r from-amber-500/10 to-amber-600/10 rounded-lg p-6 border border-amber-500/20">
{statItems.map((item) => (
<div key={item.label} className="text-center">
{isLoading ? (
<div className="h-8 bg-gray-700 rounded animate-pulse mb-2" />
) : (
<p className="text-2xl font-bold text-amber-400">{item.value}</p>
)}
<p className="text-gray-400 text-sm mt-1">{item.label}</p>
</div>
))}
</div>
);
}
4 changes: 2 additions & 2 deletions frontend/src/hooks/useWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function useWallet(): UseWalletResult {
try {
const address = await connectWallet();
const balance = await getWalletBalance();
localStorage.setItem(STORAGE_KEY, address);
sessionStorage.setItem(STORAGE_KEY, address);
setWallet(address, balance);
} catch (e: any) {
setError(e?.message ?? 'Failed to connect wallet');
Expand All @@ -42,7 +42,7 @@ export function useWallet(): UseWalletResult {

const disconnect = useCallback(() => {
disconnectWallet();
localStorage.removeItem(STORAGE_KEY);
sessionStorage.removeItem(STORAGE_KEY);
clearWallet();
}, [clearWallet]);

Expand Down
17 changes: 17 additions & 0 deletions frontend/src/providers/QueryProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactNode } from 'react';

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 10, // 10 minutes
},
},
});

export function QueryProvider({ children }: { children: ReactNode }): JSX.Element {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}