Spaces:
Running
Running
| 'use client'; | |
| import { useAuth } from '@/lib/auth'; | |
| import { usePathname, useRouter } from 'next/navigation'; | |
| import React, { useEffect, useState } from 'react'; | |
| import { Sidebar } from './Sidebar'; | |
| import Chatbot from './Chatbot'; | |
| import { Loader2, AlertTriangle, RefreshCw, Menu, Newspaper } from 'lucide-react'; | |
| import { supabase } from '@/lib/supabase'; | |
| export function AppShell({ children }: { children: React.ReactNode }) { | |
| const { user, profile, loading } = useAuth(); | |
| const pathname = usePathname(); | |
| const router = useRouter(); | |
| const [loadingTimeout, setLoadingTimeout] = useState(false); | |
| const [sidebarOpen, setSidebarOpen] = useState(false); | |
| const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); | |
| const [minLoadingDone, setMinLoadingDone] = useState(false); | |
| const [isSystemPaused, setIsSystemPaused] = useState(false); | |
| const isLoginPage = pathname === '/login'; | |
| const isDebugPage = pathname === '/debug-supabase'; | |
| const isPublicPage = isLoginPage || isDebugPage; | |
| // Check pause status | |
| useEffect(() => { | |
| if (!user || isPublicPage) return; | |
| const checkPauseStatus = async () => { | |
| try { | |
| // Determine source table based on role | |
| const isClientRole = profile?.role === 'client' && profile?.client_id; | |
| if (isClientRole) { | |
| const { data } = await supabase.from('client_settings').select('is_paused').eq('client_id', profile.client_id).maybeSingle(); | |
| if (data) setIsSystemPaused(!!data.is_paused); | |
| } else { | |
| // Fallback for admin or special cases | |
| const { data } = await supabase.from('settings').select('is_paused').eq('id', 1).maybeSingle(); | |
| if (data) setIsSystemPaused(!!data.is_paused); | |
| } | |
| } catch (e) { | |
| console.warn("Failed to fetch pause status:", e); | |
| } | |
| }; | |
| checkPauseStatus(); | |
| const interval = setInterval(checkPauseStatus, 30000); // Check every 30s | |
| return () => clearInterval(interval); | |
| }, [user, profile, isPublicPage]); | |
| // Minimum loading duration (3 seconds) to ensure a smooth transition | |
| useEffect(() => { | |
| const timer = setTimeout(() => { | |
| setMinLoadingDone(true); | |
| }, 3000); | |
| return () => clearTimeout(timer); | |
| }, []); | |
| // Fermer la sidebar mobile quand on change de page | |
| useEffect(() => { | |
| setSidebarOpen(false); | |
| }, [pathname]); | |
| // Persistance de l'état réduit de la sidebar | |
| useEffect(() => { | |
| const syncState = () => { | |
| const saved = localStorage.getItem('sidebar-collapsed') === 'true'; | |
| setIsSidebarCollapsed(saved); | |
| }; | |
| syncState(); | |
| if (localStorage.getItem('sidebar-collapsed') === 'true') { | |
| document.documentElement.classList.add('sidebar-collapsed'); | |
| } else { | |
| document.documentElement.classList.remove('sidebar-collapsed'); | |
| } | |
| window.addEventListener('sidebar-toggled', syncState); | |
| return () => window.removeEventListener('sidebar-toggled', syncState); | |
| }, [pathname]); // Refresh on pathname change ensures consistency | |
| const toggleSidebar = () => { | |
| const newValue = !isSidebarCollapsed; | |
| setIsSidebarCollapsed(newValue); | |
| localStorage.setItem('sidebar-collapsed', String(newValue)); | |
| if (newValue) { | |
| document.documentElement.classList.add('sidebar-collapsed'); | |
| } else { | |
| document.documentElement.classList.remove('sidebar-collapsed'); | |
| } | |
| }; | |
| // Timeout fallback: si loading dure plus de 15 secondes, on affiche une erreur | |
| useEffect(() => { | |
| if (loading) { | |
| const timer = setTimeout(() => { | |
| setLoadingTimeout(true); | |
| }, 15000); | |
| return () => clearTimeout(timer); | |
| } else { | |
| setLoadingTimeout(false); | |
| } | |
| }, [loading]); | |
| useEffect(() => { | |
| if (!loading && !user && !isPublicPage) { | |
| router.push('/login'); | |
| } | |
| }, [user, loading, isPublicPage, router]); | |
| const [progress, setProgress] = useState(0); | |
| const [statusText, setStatusText] = useState("Initialisation..."); | |
| const isReallyLoading = loading || !minLoadingDone; | |
| useEffect(() => { | |
| if (isReallyLoading) { | |
| const interval = setInterval(() => { | |
| setProgress((prev: number) => { | |
| if (prev >= 98) return 98; | |
| const inc = Math.random() * 10; | |
| const next = prev + inc; | |
| if (next > 20 && next < 50) setStatusText("Authentification sécurisée..."); | |
| if (next >= 50 && next < 80) setStatusText("Récupération de vos radars..."); | |
| if (next >= 80) setStatusText("Préparation de l'interface..."); | |
| return next > 98 ? 98 : next; | |
| }); | |
| }, 300); | |
| return () => clearInterval(interval); | |
| } else { | |
| setStatusText("Prêt !"); | |
| setProgress(100); | |
| } | |
| }, [isReallyLoading]); | |
| // Debug page - always accessible | |
| if (isDebugPage) { | |
| return <>{children}</>; | |
| } | |
| // Loading state with timeout fallback | |
| if (loading) { | |
| if (loadingTimeout) { | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center bg-[var(--bg-main)]"> | |
| <div className="flex flex-col items-center gap-4 max-w-md text-center p-6"> | |
| <AlertTriangle size={48} className="text-amber-500" /> | |
| <h2 className="text-xl font-bold theme-title">Connexion lente</h2> | |
| <p className="text-sm theme-description"> | |
| La connexion à Supabase prend plus de temps que prévu. | |
| Cela peut être dû à un problème réseau. | |
| </p> | |
| <div className="flex gap-3 mt-4"> | |
| <button | |
| onClick={() => window.location.reload()} | |
| className="flex items-center gap-2 px-4 py-2 bg-[var(--accent)] text-[var(--accent-foreground)] rounded-lg font-medium hover:opacity-90" | |
| > | |
| <RefreshCw size={16} /> | |
| Réessayer | |
| </button> | |
| <button | |
| onClick={() => { | |
| localStorage.clear(); | |
| sessionStorage.clear(); | |
| window.location.href = '/login'; | |
| }} | |
| className="px-4 py-2 theme-card-sec theme-title rounded-lg font-medium hover:opacity-80" | |
| > | |
| Réinitialiser | |
| </button> | |
| </div> | |
| <a | |
| href="/debug-supabase" | |
| className="text-xs text-blue-500 hover:underline mt-4" | |
| > | |
| Page de diagnostic Supabase | |
| </a> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center bg-[#0a0a0c]"> | |
| <div className="w-64 space-y-4"> | |
| <div className="flex items-center justify-between text-[10px] font-black uppercase tracking-widest"> | |
| <span className="text-indigo-400">{statusText}</span> | |
| <span className="theme-metadata tabular-nums">{Math.round(progress)}%</span> | |
| </div> | |
| <div className="h-1.5 w-full bg-white/5 rounded-full overflow-hidden border border-white/5 p-[1px]"> | |
| <div | |
| className="h-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 rounded-full transition-all duration-500 ease-out shadow-[0_0_15px_rgba(99,102,241,0.4)]" | |
| style={{ width: `${progress}%` }} | |
| /> | |
| </div> | |
| <div className="flex justify-center"> | |
| <p className="text-[10px] theme-metadata font-bold uppercase tracking-[0.2em] opacity-40 animate-pulse">SaaS Veille v3</p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Login page - no sidebar | |
| if (isLoginPage) { | |
| return <>{children}</>; | |
| } | |
| // Not authenticated - redirect will happen | |
| if (!user) { | |
| return null; | |
| } | |
| // Authenticated - show app with sidebar | |
| return ( | |
| <div className="flex min-h-screen bg-[var(--bg-main)] max-w-screen-fix"> | |
| {/* Sidebar (responsive) */} | |
| <Sidebar | |
| isOpen={sidebarOpen} | |
| onClose={() => setSidebarOpen(false)} | |
| /> | |
| {/* Main content */} | |
| <main className="flex-1 min-h-screen flex flex-col w-full relative transition-all duration-300"> | |
| {/* Desktop Sidebar Toggle (Burger) — Toujours visible car FIXED */} | |
| <div className="hidden lg:flex fixed top-4 z-[60]" style={{ left: "calc(var(--sidebar-w, 256px) + 16px)" }}> | |
| <button | |
| onClick={toggleSidebar} | |
| className={`p-1.5 bg-[var(--bg-sidebar)] border border-[var(--border-subtle)] rounded-full text-[var(--accent)] shadow-xl hover:scale-110 transition-all group ${isSidebarCollapsed ? 'rotate-180' : ''}`} | |
| title="Réduire/Agrandir la navigation" | |
| > | |
| <Menu size={14} className="group-hover:rotate-90 transition-transform duration-300" /> | |
| </button> | |
| </div> | |
| {/* Mobile Header avec bouton hamburger */} | |
| <header className="lg:hidden sticky top-0 z-30 flex items-center justify-between px-4 py-4 bg-[var(--bg-sidebar)] border-b border-[var(--border-subtle)] backdrop-blur-xl"> | |
| <button | |
| onClick={() => setSidebarOpen(true)} | |
| className="p-2 -ml-2 rounded-xl hover:bg-[var(--accent)]/10 theme-title transition-colors" | |
| aria-label="Ouvrir le menu" | |
| > | |
| <Menu size={24} /> | |
| </button> | |
| <div className="flex items-center gap-2"> | |
| < Newspaper size={20} className="theme-accent" /> | |
| <span className="text-sm font-black tracking-tight theme-title">ARGOS <span className="opacity-50 theme-accent">VEILLE</span></span> | |
| </div> | |
| <div className="w-10 h-10 rounded-xl bg-[var(--accent)]/10 border border-[var(--accent)]/30 flex items-center justify-center theme-accent text-xs font-bold"> | |
| {profile?.email?.charAt(0).toUpperCase() || 'U'} | |
| </div> | |
| </header> | |
| {/* PAUSE BANNER */} | |
| {isSystemPaused && ( | |
| <div className="sticky top-0 lg:top-0 z-40 animate-in slide-in-from-top duration-500"> | |
| <div className="bg-gradient-to-r from-amber-600 via-amber-500 to-amber-600 px-4 py-3 flex items-center justify-center gap-3 shadow-2xl"> | |
| <AlertTriangle size={18} className="text-white animate-pulse" /> | |
| <p className="text-[11px] font-black uppercase tracking-[0.2em] text-white"> | |
| Mode Veille Activé : L'agent automatique est actuellement à l'arrêt | |
| </p> | |
| <button | |
| onClick={() => router.push('/config')} | |
| className="ml-4 px-3 py-1 bg-white/20 hover:bg-white/30 rounded-lg text-[9px] font-black uppercase text-white border border-white/20 transition-all" | |
| > | |
| Réactiver | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Page content */} | |
| <div className="flex-1 flex flex-col h-full"> | |
| {children} | |
| </div> | |
| </main> | |
| {/* Chatbot - Disponible sur toutes les pages */} | |
| <Chatbot clientId={profile?.client_id || undefined} /> | |
| </div> | |
| ); | |
| } | |