This commit is contained in:
Garret Patti
2026-06-29 10:41:29 -04:00
parent d48c1e973e
commit 2b0b19eb91
23 changed files with 969 additions and 44 deletions

View File

@@ -1,13 +1,34 @@
import { useEffect, useState } from "react";
import { BrowserRouter, Routes, Route, NavLink } from "react-router-dom";
import { BrowserRouter, Routes, Route, NavLink, Navigate, useNavigate } from "react-router-dom";
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
import { api, type Library } from "./api/client";
import { AuthProvider } from "./auth/AuthContext";
import { useAuth } from "./auth/useAuth";
import BrowserPage from "./pages/BrowserPage";
import SettingsPage from "./pages/SettingsPage";
import SearchPage from "./pages/SearchPage";
import LoginPage from "./pages/LoginPage";
const queryClient = new QueryClient();
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%" }}>
<span style={{ color: "var(--text-secondary)" }}>Loading</span>
</div>
);
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
}
function useTheme() {
const [dark, setDark] = useState(
() => document.documentElement.getAttribute("data-theme") === "dark"
@@ -22,9 +43,12 @@ function useTheme() {
}
function Sidebar({ onToggleTheme, dark, onClose }: { onToggleTheme: () => void; dark: boolean; onClose?: () => void }) {
const { user, logout } = useAuth();
const navigate = useNavigate();
const { data: libraries = [] } = useQuery<Library[]>({
queryKey: ["libraries"],
queryFn: api.libraries.list,
enabled: !!user,
});
const linkStyle = ({ isActive }: { isActive: boolean }) => ({
@@ -37,6 +61,11 @@ function Sidebar({ onToggleTheme, dark, onClose }: { onToggleTheme: () => void;
fontWeight: isActive ? 600 : 400,
});
function handleLogout() {
logout();
navigate("/login");
}
return (
<nav style={{
width: 220,
@@ -76,12 +105,24 @@ function Sidebar({ onToggleTheme, dark, onClose }: { onToggleTheme: () => void;
>
{dark ? "☀ Light mode" : "☾ Dark mode"}
</button>
{user && (
<div style={{ fontSize: 11, color: "var(--text-muted)", padding: "4px 12px 0" }}>
{user.username}
</div>
)}
<button
onClick={handleLogout}
style={{ textAlign: "left", border: "none", background: "transparent", padding: "6px 12px", color: "var(--danger)", borderRadius: 4 }}
>
Sign out
</button>
</div>
</nav>
);
}
function AppShell() {
const { isAuthenticated } = useAuth();
const { dark, toggle } = useTheme();
const [isMobile, setIsMobile] = useState(() => window.innerWidth < 768);
const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -97,6 +138,15 @@ function AppShell() {
return () => mq.removeEventListener("change", handler);
}, []);
if (!isAuthenticated) {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
);
}
return (
<div style={{ display: "flex", height: "100vh", background: "var(--bg)", color: "var(--text)" }}>
{/* Mobile hamburger button */}
@@ -129,10 +179,11 @@ function AppShell() {
<main style={{ flex: 1, overflow: "auto", background: "var(--bg)", paddingTop: isMobile ? 48 : 0 }}>
<Routes>
<Route path="/" element={<SearchPage />} />
<Route path="/search" element={<SearchPage />} />
<Route path="/library/:libraryId" element={<BrowserPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/" element={<ProtectedRoute><SearchPage /></ProtectedRoute>} />
<Route path="/search" element={<ProtectedRoute><SearchPage /></ProtectedRoute>} />
<Route path="/library/:libraryId" element={<ProtectedRoute><BrowserPage /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><SettingsPage /></ProtectedRoute>} />
<Route path="/login" element={<LoginPage />} />
</Routes>
</main>
</div>
@@ -142,9 +193,11 @@ function AppShell() {
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AppShell />
</BrowserRouter>
<AuthProvider>
<BrowserRouter>
<AppShell />
</BrowserRouter>
</AuthProvider>
</QueryClientProvider>
);
}