Skip to content

Commit efe80b9

Browse files
tianzhouclaude
andauthored
chore: remove home tab (#129)
* docs: add design for removing home tab Documents the design for removing the home tab and defaulting to the first data source on initial load. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add detailed implementation plan for removing home tab * feat: add general 404 not found page * feat: redirect root to first source and add 404 route * feat: redirect invalid source IDs to 404 page * feat: remove Home navigation item from sidebar * feat: remove unused HomeView component * chore: cleanup plan --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3bbfd43 commit efe80b9

File tree

9 files changed

+212
-195
lines changed

9 files changed

+212
-195
lines changed

frontend/src/App.tsx

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
11
import { useEffect, useState } from 'react';
2-
import { BrowserRouter, Routes, Route } from 'react-router-dom';
2+
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
33
import Layout from './components/Layout';
4-
import HomeView from './components/views/HomeView';
54
import SourceDetailView from './components/views/SourceDetailView';
5+
import NotFoundView from './components/views/NotFoundView';
66
import Toast from './components/Toast';
7+
import ErrorBoundary from './components/ErrorBoundary';
78
import { fetchSources } from './api/sources';
9+
import { ApiError } from './api/errors';
810
import type { DataSource } from './types/datasource';
911

12+
function RedirectToFirstSource({ sources, isLoading }: { sources: DataSource[]; isLoading: boolean }) {
13+
if (isLoading) {
14+
return (
15+
<div className="container mx-auto px-8 py-12">
16+
<div className="text-muted-foreground">Loading...</div>
17+
</div>
18+
);
19+
}
20+
21+
if (sources.length === 0) {
22+
// This should never happen as backend validates at least one source
23+
return (
24+
<div className="container mx-auto px-8 py-12">
25+
<div className="text-destructive">No data sources configured</div>
26+
</div>
27+
);
28+
}
29+
30+
return <Navigate to={`/source/${sources[0].id}`} replace />;
31+
}
32+
1033
function App() {
1134
const [sources, setSources] = useState<DataSource[]>([]);
1235
const [isLoading, setIsLoading] = useState(true);
@@ -18,23 +41,27 @@ function App() {
1841
setSources(data);
1942
setIsLoading(false);
2043
})
21-
.catch((error) => {
22-
console.error('Failed to fetch sources:', error);
23-
setError(error instanceof Error ? error.message : 'Failed to load data sources');
44+
.catch((err) => {
45+
console.error('Failed to fetch sources:', err);
46+
const message = err instanceof ApiError ? err.message : 'Failed to load data sources';
47+
setError(message);
2448
setIsLoading(false);
2549
});
2650
}, []);
2751

2852
return (
29-
<BrowserRouter>
30-
<Routes>
31-
<Route path="/" element={<Layout sources={sources} isLoading={isLoading} />}>
32-
<Route index element={<HomeView />} />
33-
<Route path="source/:sourceId" element={<SourceDetailView />} />
34-
</Route>
35-
</Routes>
36-
{error && <Toast message={error} type="error" onClose={() => setError(null)} />}
37-
</BrowserRouter>
53+
<ErrorBoundary>
54+
<BrowserRouter>
55+
<Routes>
56+
<Route path="/" element={<Layout sources={sources} isLoading={isLoading} />}>
57+
<Route index element={<RedirectToFirstSource sources={sources} isLoading={isLoading} />} />
58+
<Route path="source/:sourceId" element={<SourceDetailView />} />
59+
<Route path="*" element={<NotFoundView />} />
60+
</Route>
61+
</Routes>
62+
{error && <Toast message={error} type="error" onClose={() => setError(null)} />}
63+
</BrowserRouter>
64+
</ErrorBoundary>
3865
);
3966
}
4067

frontend/src/api/errors.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Custom error class for API-related errors with HTTP status codes
3+
*/
4+
export class ApiError extends Error {
5+
constructor(
6+
message: string,
7+
public readonly status: number,
8+
public readonly statusText?: string
9+
) {
10+
super(message);
11+
this.name = 'ApiError';
12+
}
13+
}

frontend/src/api/sources.ts

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { DataSource } from '../types/datasource';
2+
import { ApiError } from './errors';
23

34
const API_BASE = '/api';
45

@@ -9,43 +10,62 @@ async function parseJsonResponse<T>(response: Response): Promise<T> {
910
}
1011
// Fallback to text if not JSON
1112
const text = await response.text();
12-
throw new Error(text || response.statusText);
13+
throw new ApiError(text || response.statusText, response.status, response.statusText);
1314
}
1415

1516
export async function fetchSources(): Promise<DataSource[]> {
16-
const response = await fetch(`${API_BASE}/sources`);
17+
try {
18+
const response = await fetch(`${API_BASE}/sources`);
1719

18-
if (!response.ok) {
19-
const errorMessage = await parseJsonResponse<{ error: string }>(response)
20-
.then((data) => data.error)
21-
.catch(() => response.statusText);
22-
throw new Error(`Failed to fetch sources: ${errorMessage}`);
23-
}
20+
if (!response.ok) {
21+
const errorMessage = await parseJsonResponse<{ error: string }>(response)
22+
.then((data) => data.error)
23+
.catch(() => response.statusText);
24+
throw new ApiError(`Failed to fetch sources: ${errorMessage}`, response.status, response.statusText);
25+
}
2426

25-
return response.json();
27+
return response.json();
28+
} catch (err) {
29+
// Ensure all errors are ApiError instances
30+
if (err instanceof ApiError) {
31+
throw err;
32+
}
33+
// Network errors, timeout, etc.
34+
throw new ApiError(err instanceof Error ? err.message : 'Network error', 0);
35+
}
2636
}
2737

2838
export async function fetchSource(sourceId: string): Promise<DataSource> {
29-
// Validate sourceId to prevent path traversal attacks
30-
if (!sourceId || sourceId.trim() === '') {
31-
throw new Error('Source ID cannot be empty');
32-
}
33-
if (sourceId.includes('/') || sourceId.includes('..')) {
34-
throw new Error('Invalid source ID format');
35-
}
39+
try {
40+
// Validate sourceId to prevent path traversal attacks
41+
if (!sourceId || sourceId.trim() === '') {
42+
throw new ApiError('Source ID cannot be empty', 400);
43+
}
44+
if (sourceId.includes('/') || sourceId.includes('..')) {
45+
throw new ApiError('Invalid source ID format', 400);
46+
}
3647

37-
const response = await fetch(`${API_BASE}/sources/${encodeURIComponent(sourceId)}`);
48+
const response = await fetch(`${API_BASE}/sources/${encodeURIComponent(sourceId)}`);
3849

39-
if (!response.ok) {
40-
const errorMessage = await parseJsonResponse<{ error: string }>(response)
41-
.then((data) => data.error)
42-
.catch(() => response.statusText);
50+
if (!response.ok) {
51+
const errorMessage = await parseJsonResponse<{ error: string }>(response)
52+
.then((data) => data.error)
53+
.catch(() => response.statusText);
4354

44-
if (response.status === 404) {
45-
throw new Error(`Source not found: ${sourceId}`);
55+
throw new ApiError(
56+
response.status === 404 ? `Source not found: ${sourceId}` : `Failed to fetch source: ${errorMessage}`,
57+
response.status,
58+
response.statusText
59+
);
4660
}
47-
throw new Error(`Failed to fetch source: ${errorMessage}`);
48-
}
4961

50-
return response.json();
62+
return response.json();
63+
} catch (err) {
64+
// Ensure all errors are ApiError instances
65+
if (err instanceof ApiError) {
66+
throw err;
67+
}
68+
// Network errors, timeout, etc.
69+
throw new ApiError(err instanceof Error ? err.message : 'Network error', 0);
70+
}
5171
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Component, ReactNode, ErrorInfo } from 'react';
2+
import { ApiError } from '../api/errors';
3+
4+
interface Props {
5+
children: ReactNode;
6+
}
7+
8+
interface State {
9+
error: Error | null;
10+
}
11+
12+
/**
13+
* Error Boundary to catch unexpected errors in the component tree
14+
*/
15+
export default class ErrorBoundary extends Component<Props, State> {
16+
constructor(props: Props) {
17+
super(props);
18+
this.state = { error: null };
19+
}
20+
21+
static getDerivedStateFromError(error: Error): State {
22+
return { error };
23+
}
24+
25+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
26+
console.error('Uncaught error:', error, errorInfo);
27+
}
28+
29+
render() {
30+
if (this.state.error) {
31+
const error = this.state.error;
32+
const isApiError = error instanceof ApiError;
33+
34+
return (
35+
<div className="min-h-screen flex items-center justify-center bg-background px-4">
36+
<div className="max-w-md w-full">
37+
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-6">
38+
<h1 className="text-2xl font-bold text-destructive mb-4">
39+
Something went wrong
40+
</h1>
41+
<p className="text-destructive/90 mb-4">
42+
{error.message}
43+
</p>
44+
{isApiError && (
45+
<p className="text-sm text-muted-foreground mb-4">
46+
Error code: {error.status}
47+
</p>
48+
)}
49+
<button
50+
onClick={() => window.location.reload()}
51+
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
52+
>
53+
Reload page
54+
</button>
55+
</div>
56+
</div>
57+
</div>
58+
);
59+
}
60+
61+
return this.props.children;
62+
}
63+
}

frontend/src/components/Sidebar/NavItem.tsx

Lines changed: 0 additions & 29 deletions
This file was deleted.

frontend/src/components/Sidebar/Sidebar.tsx

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import Logo from './Logo';
2-
import NavItem from './NavItem';
32
import SourceList from './SourceList';
43
import type { DataSource } from '../../types/datasource';
54

@@ -8,31 +7,11 @@ interface SidebarProps {
87
isLoading: boolean;
98
}
109

11-
function HomeIcon() {
12-
return (
13-
<svg
14-
xmlns="http://www.w3.org/2000/svg"
15-
width="20"
16-
height="20"
17-
viewBox="0 0 24 24"
18-
fill="none"
19-
stroke="currentColor"
20-
strokeWidth="2"
21-
strokeLinecap="round"
22-
strokeLinejoin="round"
23-
>
24-
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
25-
<polyline points="9 22 9 12 15 12 15 22" />
26-
</svg>
27-
);
28-
}
29-
3010
export default function Sidebar({ sources, isLoading }: SidebarProps) {
3111
return (
3212
<aside className="w-[200px] sm:w-[220px] md:w-[240px] lg:w-[280px] border-r border-border bg-card flex flex-col" aria-label="Navigation sidebar">
3313
<Logo />
3414
<nav className="flex-1 flex flex-col overflow-hidden" aria-label="Sidebar navigation">
35-
<NavItem to="/" icon={<HomeIcon />} label="Home" />
3615
<SourceList sources={sources} isLoading={isLoading} />
3716
</nav>
3817
</aside>

0 commit comments

Comments
 (0)