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:
@@ -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;
|
||||
@@ -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}`),
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -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>,
|
||||
)
|
||||
Reference in New Issue
Block a user