RegWatch frontend : React/Vite -> nginx + Docker/CI homelab
Build & Deploy / build (push) Successful in 18s

- URL d'API externalisée (VITE_API_URL bakée au build), plus de localhost en dur
- Dockerfile multi-stage pnpm build -> nginx (fallback SPA)
- docker-compose (Watchtower) + .gitea/workflows/build.yml

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-24 18:43:29 +02:00
co-authored by Claude Opus 4.8
commit 2307d9a134
49 changed files with 6678 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
import { useEffect, useState } from "react";
import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom";
import AuthPage from "./pages/AuthPage";
import HomePage from "./pages/Home";
import DocumentsPage from "./pages/Documents";
import IngredientSearchPage from "./pages/IngredientSearchPage";
import RecentUpdatesPage from "./pages/RecentUpdatesPage";
import AboutPage from "./pages/AboutPage";
import ContactPage from "./pages/ContactPage";
import CGU from "./pages/CGU";
import MentionsLegales from "./pages/MentionsLegales";
import Sidebar from "./components/Sidebar";
import Footer from "./components/Footer";
import styles from "./App.module.css";
import { getDocuments, type ApiDocument } from "./services/theapi";
type NavItem =
| "home"
| "ingredient-search"
| "documents"
| "recent-updates"
| "about"
| "contact"
| "cgu"
| "mentions-legales";
interface User {
id: number;
email: string;
full_name: string | null;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
loading: boolean;
}
export default function App() {
const [recentDocuments, setRecentDocuments] = useState<ApiDocument[]>([]);
const [authState, setAuthState] = useState<AuthState>(() => {
const savedToken = localStorage.getItem("authToken");
const savedUser = localStorage.getItem("user");
if (savedToken && savedUser) {
const parsedUser = JSON.parse(savedUser);
return {
user: parsedUser,
isAuthenticated: true,
loading: false,
};
}
return {
user: null,
isAuthenticated: false,
loading: false,
};
});
const location = useLocation();
const navigate = useNavigate();
const handleAuthSuccess = (newToken: string, newUser: User) => {
localStorage.setItem("authToken", newToken);
localStorage.setItem("user", JSON.stringify(newUser));
setAuthState({
user: newUser,
isAuthenticated: true,
loading: false,
});
navigate("/");
};
const handleLogout = () => {
localStorage.removeItem("authToken");
localStorage.removeItem("user");
setAuthState({
user: null,
isAuthenticated: false,
loading: false,
});
navigate("/");
};
const activeNav: NavItem =
location.pathname === "/ingredient-search" ? "ingredient-search"
: location.pathname === "/documents" ? "documents"
: location.pathname === "/recent-updates" ? "recent-updates"
: location.pathname === "/about" ? "about"
: location.pathname === "/contact" ? "contact"
: location.pathname === "/cgu" ? "cgu"
: location.pathname === "/mentions-legales"? "mentions-legales"
: "home";
// LOAD RECENT DOCUMENTS
useEffect(() => {
if (authState.isAuthenticated) {
getDocuments()
.then((data) => {
setRecentDocuments(Array.isArray(data) ? data.slice(0, 4) : []);
})
.catch((err) => {
console.error("API ERROR:", err);
setRecentDocuments([]);
});
}
}, [authState.isAuthenticated]);
const handleNavClick = (item: NavItem) => {
const path =
item === "home" ? "/"
: item === "documents" ? "/documents"
: item === "ingredient-search" ? "/ingredient-search"
: item === "recent-updates" ? "/recent-updates"
: item === "about" ? "/about"
: item === "contact" ? "/contact"
: item === "cgu" ? "/cgu"
: "/mentions-legales";
navigate(path);
};
if (authState.loading) {
return (
<div style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "#f8fafc" }}>
<p style={{ color: "#475569", fontSize: "1.125rem" }}>Loading application...</p>
</div>
);
}
// Si pas authentifié, afficher la page de login
if (!authState.isAuthenticated) {
return <AuthPage onAuthSuccess={handleAuthSuccess} />;
}
return (
<div className={styles.app}>
<div className={styles.contentWrapper}>
<Sidebar activeNav={activeNav} onNavClick={handleNavClick} onLogout={handleLogout} user={authState.user} />
<main className={styles.main}>
<Routes>
<Route path="/" element={<HomePage handleNavClick={handleNavClick} recentDocuments={recentDocuments} />} />
<Route path="/documents" element={<DocumentsPage />} />
<Route path="/ingredient-search" element={<IngredientSearchPage />} />
<Route path="/recent-updates" element={<RecentUpdatesPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="/cgu" element={<CGU />} />
<Route path="/mentions-legales" element={<MentionsLegales />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</main>
</div>
<Footer onNavClick={handleNavClick} />
</div>
);
}