Skip to content

Commit 8771f55

Browse files
fix(frontend): Handle 404 gracefully in campaign detail panel
Implements issue #600 - Add notFoundCampaignId prop to CampaignDetailPanel - Show 'Campaign not found' message with back button when campaign doesn't exist - Update tests to cover the not found state The panel now properly handles non-existent campaign IDs instead of showing a blank panel. Closes #600
1 parent 3e2dd95 commit 8771f55

3 files changed

Lines changed: 78 additions & 1 deletion

File tree

frontend/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,7 @@ function App() {
743743
isConnectingWallet={isConnectingWallet}
744744
isPledgePending={pendingPledgeCampaignId === selectedCampaignId}
745745
isLoading={isSelectedLoading || initialLoad}
746+
notFoundCampaignId={invalidUrlCampaignId}
746747
onConnectWallet={handleConnectWallet}
747748
onDisconnectWallet={handleDisconnectWallet}
748749
onPledge={handlePledge}

frontend/src/components/CampaignDetailPanel.test.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { render, screen } from '@testing-library/react';
2+
import { BrowserRouter } from 'react-router-dom';
23

34
import { CampaignDetailPanel } from './CampaignDetailPanel';
45
import { AppConfig, Campaign } from '../types/campaign';
@@ -50,6 +51,57 @@ const mockCampaign: Campaign = {
5051
};
5152

5253
describe('CampaignDetailPanel', () => {
54+
it('renders loading state', () => {
55+
render(
56+
<BrowserRouter>
57+
<CampaignDetailPanel
58+
campaign={null}
59+
appConfig={mockConfig}
60+
isLoading={true}
61+
/>
62+
</BrowserRouter>
63+
);
64+
expect(screen.getByRole('region')).toBeInTheDocument();
65+
});
66+
67+
it('renders not found state when notFoundCampaignId is provided', () => {
68+
render(
69+
<BrowserRouter>
70+
<CampaignDetailPanel
71+
campaign={null}
72+
appConfig={mockConfig}
73+
notFoundCampaignId="999"
74+
/>
75+
</BrowserRouter>
76+
);
77+
expect(screen.getByText('Campaign not found')).toBeInTheDocument();
78+
expect(screen.getByText(/campaign #999 does not exist/i)).toBeInTheDocument();
79+
expect(screen.getByRole('link', { name: 'Back to campaigns' })).toBeInTheDocument();
80+
});
81+
82+
it('renders empty state when no campaign is selected', () => {
83+
render(
84+
<BrowserRouter>
85+
<CampaignDetailPanel
86+
campaign={null}
87+
appConfig={mockConfig}
88+
isLoading={false}
89+
/>
90+
</BrowserRouter>
91+
);
92+
expect(screen.getByText('Campaign actions')).toBeInTheDocument();
93+
});
5394

95+
it('renders campaign details when campaign is provided', () => {
96+
render(
97+
<BrowserRouter>
98+
<CampaignDetailPanel
99+
campaign={mockCampaign}
100+
appConfig={mockConfig}
101+
isLoading={false}
102+
/>
103+
</BrowserRouter>
104+
);
105+
expect(screen.getByText('Test Campaign')).toBeInTheDocument();
54106
});
55107
});

frontend/src/components/CampaignDetailPanel.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11

22

3+
4+
5+
36
import { FormEvent, useState, useEffect } from 'react';
4-
import { MousePointer2 } from 'lucide-react';
7+
import { MousePointer2, AlertCircle } from 'lucide-react';
8+
import { Link } from 'react-router-dom';
59
import { Campaign, AppConfig } from '../types/campaign';
610
import CopyButton from './CopyButton';
711
import { AddressAvatar } from './AddressAvatar';
@@ -16,6 +20,7 @@ interface CampaignDetailPanelProps {
1620
isConnectingWallet?: boolean;
1721
isLoading?: boolean;
1822
isPledgePending?: boolean;
23+
notFoundCampaignId?: string | null;
1924
onConnectWallet?: () => Promise<void>;
2025
onDisconnectWallet?: () => void;
2126
onPledge?: (campaignId: string, amount: number, assetCode: string) => Promise<void>;
@@ -66,6 +71,7 @@ export function CampaignDetailPanel({
6671
isConnectingWallet = false,
6772
isLoading = false,
6873
isPledgePending = false,
74+
notFoundCampaignId = null,
6975
onConnectWallet = async () => {},
7076
onDisconnectWallet = () => {},
7177
onPledge = async () => {},
@@ -109,6 +115,24 @@ export function CampaignDetailPanel({
109115
);
110116
}
111117

118+
if (notFoundCampaignId) {
119+
return (
120+
<section className="card detail-panel">
121+
<div className="section-heading">
122+
<h2>Campaign not found</h2>
123+
<p className="muted">
124+
The campaign <code>#{notFoundCampaignId}</code> does not exist or may have been removed.
125+
</p>
126+
</div>
127+
<div style={{ marginTop: 24 }}>
128+
<Link to="/" className="btn-ghost">
129+
Back to campaigns
130+
</Link>
131+
</div>
132+
</section>
133+
);
134+
}
135+
112136
if (!campaign) {
113137
return (
114138
<EmptyState

0 commit comments

Comments
 (0)