saas-veille / components /AppShell.tsx
PrestaGhis's picture
Update components/AppShell.tsx
3f09099 verified
'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>
);
}