feat(frontend): add API client, layout, and routing

- Create Axios-based API client with sources, jobs, and dashboard endpoints
- Add responsive Layout component with navigation sidebar
- Set up React Router with Dashboard, Backups, and Settings routes
- Configure TanStack Query provider with default options
- Use lucide-react for navigation icons
This commit is contained in:
2026-05-11 21:47:20 +02:00
parent 7676438466
commit d3b92593d5
14073 changed files with 2182272 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Layout } from './components/Layout';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
retry: 1,
},
},
});
function DashboardPage() {
return (
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">Dashboard</h2>
<p className="text-gray-600">Welcome to the Backup Tool dashboard.</p>
</div>
);
}
function BackupsPage() {
return (
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">Backups</h2>
<p className="text-gray-600">Manage your backup jobs here.</p>
</div>
);
}
function SettingsPage() {
return (
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">Settings</h2>
<p className="text-gray-600">Configure your backup sources and preferences.</p>
</div>
);
}
function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<DashboardPage />} />
<Route path="/backups" element={<BackupsPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Layout>
</BrowserRouter>
</QueryClientProvider>
);
}
export default App;
+61
View File
@@ -0,0 +1,61 @@
import axios from 'axios';
const API_BASE_URL = '/api';
export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
export interface BackupSource {
id: string;
name: string;
type: string;
config: Record<string, unknown>;
created_at: string;
updated_at: string;
}
export interface BackupJob {
id: string;
source_id: string;
status: 'pending' | 'running' | 'completed' | 'failed';
started_at: string | null;
completed_at: string | null;
error_message: string | null;
created_at: string;
}
export interface DashboardStats {
total_sources: number;
total_jobs: number;
completed_jobs: number;
failed_jobs: number;
pending_jobs: number;
}
export const sourcesApi = {
getAll: () => apiClient.get<BackupSource[]>('/sources'),
getById: (id: string) => apiClient.get<BackupSource>(`/sources/${id}`),
create: (data: Omit<BackupSource, 'id' | 'created_at' | 'updated_at'>) =>
apiClient.post<BackupSource>('/sources', data),
update: (id: string, data: Partial<BackupSource>) =>
apiClient.put<BackupSource>(`/sources/${id}`, data),
delete: (id: string) => apiClient.delete(`/sources/${id}`),
};
export const jobsApi = {
getAll: () => apiClient.get<BackupJob[]>('/jobs'),
getById: (id: string) => apiClient.get<BackupJob>(`/jobs/${id}`),
create: (sourceId: string) =>
apiClient.post<BackupJob>('/jobs', { source_id: sourceId }),
getLogs: (id: string) => apiClient.get<string>(`/jobs/${id}/logs`),
};
export const dashboardApi = {
getStats: () => apiClient.get<DashboardStats>('/dashboard/stats'),
getRecentJobs: (limit: number = 10) =>
apiClient.get<BackupJob[]>(`/dashboard/recent-jobs?limit=${limit}`),
};
+80
View File
@@ -0,0 +1,80 @@
import { Link, useLocation } from 'react-router-dom';
import { LayoutDashboard, Archive, Settings, Menu } from 'lucide-react';
import { useState } from 'react';
interface LayoutProps {
children: React.ReactNode;
}
const navItems = [
{ path: '/', label: 'Dashboard', icon: LayoutDashboard },
{ path: '/backups', label: 'Backups', icon: Archive },
{ path: '/settings', label: 'Settings', icon: Settings },
];
export function Layout({ children }: LayoutProps) {
const location = useLocation();
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className="flex h-screen bg-gray-50">
{/* Mobile sidebar overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={`fixed lg:static inset-y-0 left-0 z-50 w-64 bg-white border-r border-gray-200 transform transition-transform duration-200 ease-in-out lg:transform-none ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex items-center h-16 px-6 border-b border-gray-200">
<h1 className="text-xl font-bold text-gray-900">Backup Tool</h1>
</div>
<nav className="p-4 space-y-1">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.path;
return (
<Link
key={item.path}
to={item.path}
onClick={() => setSidebarOpen(false)}
className={`flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${
isActive
? 'bg-blue-50 text-blue-700'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<Icon className="w-5 h-5" />
{item.label}
</Link>
);
})}
</nav>
</aside>
{/* Main content */}
<div className="flex-1 flex flex-col min-w-0">
{/* Mobile header */}
<header className="lg:hidden flex items-center h-16 px-4 bg-white border-b border-gray-200">
<button
onClick={() => setSidebarOpen(true)}
className="p-2 -ml-2 text-gray-600 hover:bg-gray-100 rounded-lg"
>
<Menu className="w-6 h-6" />
</button>
<span className="ml-3 text-lg font-semibold text-gray-900">
Backup Tool
</span>
</header>
<main className="flex-1 overflow-auto p-6">{children}</main>
</div>
</div>
);
}
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)