Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
04b362c
Add market category helper
Mosas2000 Apr 15, 2026
598637e
Pass categories to market scripts
Mosas2000 Apr 15, 2026
c5c16bb
Use deployer as market owner
Mosas2000 Apr 15, 2026
7e14029
Use active network in pages
Mosas2000 Apr 15, 2026
51b970b
Add frontend regression tests
Mosas2000 Apr 15, 2026
58d6709
Add trade page network test
Mosas2000 Apr 15, 2026
afad53a
Add portfolio page network test
Mosas2000 Apr 15, 2026
d4ad911
Add market loading network test
Mosas2000 Apr 15, 2026
2e5856a
network readonly fix (#65)
Mosas2000 Apr 16, 2026
259ae01
Issue 58 market category fix (#66)
Mosas2000 Apr 16, 2026
431b4ad
Make explorer links network aware
Mosas2000 Apr 16, 2026
3d97837
Add explorer link regressions
Mosas2000 Apr 16, 2026
b2b8b5a
Add watchlist storage helpers
Mosas2000 Apr 16, 2026
7044fb0
Add watchlist context
Mosas2000 Apr 16, 2026
7bf85f4
Add watchlist page routing
Mosas2000 Apr 16, 2026
9b6ca90
Add watchlist card toggle
Mosas2000 Apr 16, 2026
7d54073
Add watchlist navigation
Mosas2000 Apr 16, 2026
e369a48
Add watchlist storage tests
Mosas2000 Apr 16, 2026
2dbedcd
Add watchlist page tests
Mosas2000 Apr 16, 2026
5752998
Add watchlist nav tests
Mosas2000 Apr 16, 2026
43ddb98
Add explorer link tests
Mosas2000 Apr 16, 2026
d992982
Finalize feature cleanup
Mosas2000 Apr 16, 2026
33a7f9d
Refine component tests
Mosas2000 Apr 16, 2026
53d53e9
Stabilize page wiring
Mosas2000 Apr 16, 2026
38a0684
Polish footer and header links
Mosas2000 Apr 16, 2026
c64abda
Harden explorer links
Mosas2000 Apr 16, 2026
6e454c9
Improve regression coverage
Mosas2000 Apr 16, 2026
9f6124c
Stabilize interaction handling
Mosas2000 Apr 16, 2026
2a551ad
Tighten layout behavior
Mosas2000 Apr 16, 2026
f0ace15
Polish navigation coverage
Mosas2000 Apr 16, 2026
d454468
Refine watchlist integration
Mosas2000 Apr 16, 2026
d95bbad
Harden watchlist storage
Mosas2000 Apr 16, 2026
dfde127
Add watchlist context coverage
Mosas2000 Apr 16, 2026
fcc14df
Cover watchlist page rendering
Mosas2000 Apr 16, 2026
ee3b627
Cover market card watchlist toggle
Mosas2000 Apr 16, 2026
99c2c52
Cover header watchlist link
Mosas2000 Apr 16, 2026
1dee9a6
Cover mobile watchlist route
Mosas2000 Apr 16, 2026
5a7b163
Validate trade page network reads
Mosas2000 Apr 16, 2026
faf9f93
Validate trade page explorer links
Mosas2000 Apr 16, 2026
ecdd2d3
Stabilize landing and footer links
Mosas2000 Apr 16, 2026
6c150a0
Polish watchlist persistence
Mosas2000 Apr 16, 2026
873fd81
Finalize watchlist cleanup
Mosas2000 Apr 16, 2026
c2b7eb2
Resolve watchlist merge conflict
Mosas2000 Apr 17, 2026
209823f
Merge latest main into watchlist branch
Mosas2000 Apr 17, 2026
b8a7c6f
Sync watchlist branch with main
Mosas2000 Apr 17, 2026
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
191 changes: 100 additions & 91 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { WalletProvider } from './components/WalletProvider';
import { TransactionProvider } from './components/TransactionProvider';
import { NetworkProvider } from './contexts/NetworkContext';
import { ThemeProvider } from './contexts/ThemeContext';
import { WatchlistProvider } from './contexts/WatchlistContext';
import { ErrorBoundary } from './components/ErrorBoundary';
import { PageErrorBoundary } from './components/PageErrorBoundary';
import { Header } from './components/Header';
Expand All @@ -25,104 +26,112 @@ import { LeaderboardPage } from './pages/LeaderboardPage';
import { MultiMarketsPage } from './pages/MultiMarketsPage';
import { MultiTradePage } from './pages/MultiTradePage';
import { CreateMultiMarketPage } from './pages/CreateMultiMarketPage';
import { WatchlistPage } from './pages/WatchlistPage';

function App() {
return (
<ErrorBoundary>
<ThemeProvider>
<NetworkProvider>
<WalletProvider>
<TransactionProvider>
<BrowserRouter>
<div className="min-h-screen flex flex-col bg-white dark:bg-black pb-16 md:pb-0">
<TestnetWarningBanner />
<Header />
<main className="flex-1">
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/markets" element={
<PageErrorBoundary pageName="Markets">
<MarketsPage />
</PageErrorBoundary>
} />
<Route path="/trade/:id" element={
<PageErrorBoundary pageName="Trade">
<TradePage />
</PageErrorBoundary>
} />
<Route path="/portfolio" element={
<PageErrorBoundary pageName="Portfolio">
<PortfolioPage />
</PageErrorBoundary>
} />
<Route path="/token" element={
<PageErrorBoundary pageName="Token">
<TokenPage />
</PageErrorBoundary>
} />
<Route path="/staking" element={
<PageErrorBoundary pageName="Staking">
<StakingPage />
</PageErrorBoundary>
} />
<Route path="/governance" element={
<PageErrorBoundary pageName="Governance">
<GovernancePage />
</PageErrorBoundary>
} />
<Route path="/transactions" element={
<PageErrorBoundary pageName="Transactions">
<TransactionHistoryPage />
</PageErrorBoundary>
} />
<Route path="/create-market" element={
<PageErrorBoundary pageName="Create Market">
<CreateMarketPage />
</PageErrorBoundary>
} />
<Route path="/oracle" element={
<PageErrorBoundary pageName="Oracle">
<OraclePage />
</PageErrorBoundary>
} />
<Route path="/liquidity" element={
<PageErrorBoundary pageName="Liquidity">
<LiquidityPage />
</PageErrorBoundary>
} />
<Route path="/analytics" element={
<PageErrorBoundary pageName="Analytics">
<AnalyticsPage />
</PageErrorBoundary>
} />
<Route path="/leaderboard" element={
<PageErrorBoundary pageName="Leaderboard">
<LeaderboardPage />
</PageErrorBoundary>
} />
<Route path="/multi-markets" element={
<PageErrorBoundary pageName="Multi Markets">
<MultiMarketsPage />
</PageErrorBoundary>
} />
<Route path="/multi-trade/:id" element={
<PageErrorBoundary pageName="Multi Trade">
<MultiTradePage />
</PageErrorBoundary>
} />
<Route path="/create-multi-market" element={
<PageErrorBoundary pageName="Create Multi Market">
<CreateMultiMarketPage />
</PageErrorBoundary>
} />
</Routes>
</main>
<Footer />
<MobileBottomNav />
</div>
</BrowserRouter>
</TransactionProvider>
</WalletProvider>
<WatchlistProvider>
<TransactionProvider>
<BrowserRouter>
<div className="min-h-screen flex flex-col bg-white dark:bg-black pb-16 md:pb-0">
<TestnetWarningBanner />
<Header />
<main className="flex-1">
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/markets" element={
<PageErrorBoundary pageName="Markets">
<MarketsPage />
</PageErrorBoundary>
} />
<Route path="/trade/:id" element={
<PageErrorBoundary pageName="Trade">
<TradePage />
</PageErrorBoundary>
} />
<Route path="/portfolio" element={
<PageErrorBoundary pageName="Portfolio">
<PortfolioPage />
</PageErrorBoundary>
} />
<Route path="/watchlist" element={
<PageErrorBoundary pageName="Watchlist">
<WatchlistPage />
</PageErrorBoundary>
} />
<Route path="/token" element={
<PageErrorBoundary pageName="Token">
<TokenPage />
</PageErrorBoundary>
} />
<Route path="/staking" element={
<PageErrorBoundary pageName="Staking">
<StakingPage />
</PageErrorBoundary>
} />
<Route path="/governance" element={
<PageErrorBoundary pageName="Governance">
<GovernancePage />
</PageErrorBoundary>
} />
<Route path="/transactions" element={
<PageErrorBoundary pageName="Transactions">
<TransactionHistoryPage />
</PageErrorBoundary>
} />
<Route path="/create-market" element={
<PageErrorBoundary pageName="Create Market">
<CreateMarketPage />
</PageErrorBoundary>
} />
<Route path="/oracle" element={
<PageErrorBoundary pageName="Oracle">
<OraclePage />
</PageErrorBoundary>
} />
<Route path="/liquidity" element={
<PageErrorBoundary pageName="Liquidity">
<LiquidityPage />
</PageErrorBoundary>
} />
<Route path="/analytics" element={
<PageErrorBoundary pageName="Analytics">
<AnalyticsPage />
</PageErrorBoundary>
} />
<Route path="/leaderboard" element={
<PageErrorBoundary pageName="Leaderboard">
<LeaderboardPage />
</PageErrorBoundary>
} />
<Route path="/multi-markets" element={
<PageErrorBoundary pageName="Multi Markets">
<MultiMarketsPage />
</PageErrorBoundary>
} />
<Route path="/multi-trade/:id" element={
<PageErrorBoundary pageName="Multi Trade">
<MultiTradePage />
</PageErrorBoundary>
} />
<Route path="/create-multi-market" element={
<PageErrorBoundary pageName="Create Multi Market">
<CreateMultiMarketPage />
</PageErrorBoundary>
} />
</Routes>
</main>
<Footer />
<MobileBottomNav />
</div>
</BrowserRouter>
</TransactionProvider>
</WatchlistProvider>
</WalletProvider>
</NetworkProvider>
</ThemeProvider>
</ErrorBoundary>
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Link } from 'react-router-dom';
import { Logo } from './Logo';
import { useNetwork } from '../contexts/NetworkContext';
import { getExplorerAddressUrl } from '../utils/transactions';

export function Footer() {
const { network, contractAddress } = useNetwork();

return (
<footer className="border-t border-neutral-300 dark:border-neutral-800/50 bg-white dark:bg-black mt-auto">
<div className="container py-16">
Expand Down Expand Up @@ -33,7 +37,7 @@ export function Footer() {
<li><Link to="/markets" className="text-neutral-600 dark:text-neutral-500 hover:text-black dark:hover:text-white text-sm transition-colors">Markets</Link></li>
<li><Link to="/portfolio" className="text-neutral-600 dark:text-neutral-500 hover:text-black dark:hover:text-white text-sm transition-colors">Portfolio</Link></li>
<li>
<a href="https://explorer.hiro.so/address/SP31PKQVQZVZCK3FM3NH67CGD6G1FMR17VQVS2W5T?chain=mainnet" target="_blank" rel="noopener noreferrer" className="text-neutral-600 dark:text-neutral-500 hover:text-black dark:hover:text-white text-sm transition-colors">
<a href={getExplorerAddressUrl(contractAddress, network)} target="_blank" rel="noopener noreferrer" className="text-neutral-600 dark:text-neutral-500 hover:text-black dark:hover:text-white text-sm transition-colors">
Contract
</a>
</li>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function Header() {
{ path: '/markets', label: 'Markets' },
{ path: '/create-market', label: 'Create', highlight: true },
{ path: '/portfolio', label: 'Portfolio' },
{ path: '/watchlist', label: 'Watchlist' },
{ path: '/multi-markets', label: 'Multi Markets' },
{ path: '/liquidity', label: 'Liquidity' },
{ path: '/analytics', label: 'Analytics' },
Expand Down
48 changes: 45 additions & 3 deletions frontend/src/components/MarketCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { Market } from '../types/market';
import { MarketStatus } from '../types/market';
import { calculateOdds, formatStx, getStatusLabel } from '../utils/helpers';
import { categorizeMarket, getCategoryConfig } from '../utils/marketCategories';
import { useWatchlist } from '../contexts/WatchlistContext';
import type { MouseEvent } from 'react';

interface MarketCardProps {
market: Market;
Expand All @@ -15,10 +17,50 @@ export function MarketCard({ market, showCategory = true }: MarketCardProps) {
const isActive = market.status === MarketStatus.ACTIVE;
const category = categorizeMarket(market.question);
const categoryConfig = getCategoryConfig(category);
const { isWatched, toggleMarket } = useWatchlist();
const watched = isWatched(market.id);

const handleToggleWatchlist = (event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
toggleMarket(market.id);
};

return (
<Link to={`/trade/${market.id}`} className="block h-full no-underline">
<div className="h-full p-5 sm:p-6 lg:p-8 bg-neutral-50 dark:bg-neutral-900 rounded-2xl border border-neutral-300 dark:border-neutral-800 hover:border-neutral-400 dark:hover:border-neutral-700 flex flex-col transition-colors cursor-pointer">
<div className="relative block h-full">
<Link
to={`/trade/${market.id}`}
aria-label={`Open market ${market.question}`}
className="absolute inset-0 z-0 pointer-events-auto rounded-2xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-black"
>
<span className="sr-only">Open market {market.question}</span>
</Link>
<div className="relative z-10 h-full p-5 sm:p-6 lg:p-8 bg-neutral-50 dark:bg-neutral-900 rounded-2xl border border-neutral-300 dark:border-neutral-800 hover:border-neutral-400 dark:hover:border-neutral-700 flex flex-col transition-colors cursor-pointer pointer-events-none">
<button
type="button"
onClick={handleToggleWatchlist}
aria-pressed={watched}
aria-label={watched ? 'Remove from watchlist' : 'Add to watchlist'}
className={`pointer-events-auto absolute top-4 right-4 inline-flex h-10 w-10 items-center justify-center rounded-full border transition-colors ${
watched
? 'border-rose-500/40 bg-rose-500/15 text-rose-400 hover:bg-rose-500/25'
: 'border-neutral-300 dark:border-neutral-700 bg-white/90 dark:bg-neutral-950/90 text-neutral-500 hover:border-blue-500/40 hover:text-blue-500'
}`}
>
<svg
className="h-5 w-5"
fill={watched ? 'currentColor' : 'none'}
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11.645 20.91l-1.157-1.051C5.696 15.855 2.5 12.968 2.5 9.4 2.5 6.51 4.73 4.25 7.5 4.25c1.56 0 3.042.75 3.966 1.94A5.13 5.13 0 0 1 15.5 4.25c2.77 0 5 2.26 5 5.15 0 3.568-3.196 6.455-7.988 10.459l-0.867 0.718z"
/>
</svg>
</button>
{/* Header */}
<div className="flex justify-between items-start gap-2 mb-4 sm:mb-5">
<div className="flex gap-2 items-center flex-wrap">
Expand Down Expand Up @@ -75,6 +117,6 @@ export function MarketCard({ market, showCategory = true }: MarketCardProps) {
</span>
</div>
</div>
</Link>
</div>
);
}
9 changes: 9 additions & 0 deletions frontend/src/components/MobileBottomNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ export function MobileBottomNav() {
</svg>
),
},
{
path: '/watchlist',
label: 'Watchlist',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.645 20.91l-1.157-1.051C5.696 15.855 2.5 12.968 2.5 9.4 2.5 6.51 4.73 4.25 7.5 4.25c1.56 0 3.042.75 3.966 1.94A5.13 5.13 0 0 1 15.5 4.25c2.77 0 5 2.26 5 5.15 0 3.568-3.196 6.455-7.988 10.459l-0.867 0.718z" />
</svg>
),
},
{
path: '/multi-markets',
label: 'Multi',
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/__tests__/Footer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { Footer } from '../Footer';

vi.mock('../../contexts/NetworkContext', () => ({
useNetwork: () => ({
network: 'testnet',
contractAddress: 'STTESTCONTRACT',
}),
}));

describe('Footer', () => {
it('links the contract to the active network explorer', () => {
render(
<MemoryRouter>
<Footer />
</MemoryRouter>
);

const contractLink = screen.getByRole('link', { name: 'Contract' });
expect(contractLink).toHaveAttribute(
'href',
'https://explorer.hiro.so/address/STTESTCONTRACT?chain=testnet'
);
});
});
44 changes: 44 additions & 0 deletions frontend/src/components/__tests__/Header.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { Header } from '../Header';

vi.mock('../WalletProvider', () => ({
useWallet: () => ({
isConnected: false,
address: null,
connect: vi.fn(),
disconnect: vi.fn(),
}),
}));

vi.mock('../../contexts/NetworkContext', () => ({
useNetwork: () => ({
isTestnet: false,
networkConfig: {
color: '#10B981',
label: 'Mainnet',
},
}),
}));

vi.mock('../NetworkSelector', () => ({
NetworkSelector: () => React.createElement('div', { 'data-testid': 'network-selector' }),
}));

vi.mock('../ThemeSwitcher', () => ({
ThemeSwitcher: () => React.createElement('div', { 'data-testid': 'theme-switcher' }),
}));

describe('Header', () => {
it('includes the watchlist navigation link', () => {
render(
<MemoryRouter initialEntries={['/']}>
<Header />
</MemoryRouter>
);

expect(screen.getByRole('link', { name: 'Watchlist' })).toHaveAttribute('href', '/watchlist');
});
});
Loading