@lastbrain/app 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/useAuthSession.d.ts.map +1 -1
- package/dist/auth/useAuthSession.js +8 -2
- package/dist/components/NotificationContainer.d.ts +2 -0
- package/dist/components/NotificationContainer.d.ts.map +1 -0
- package/dist/components/NotificationContainer.js +8 -0
- package/dist/hooks/useNotifications.d.ts +29 -0
- package/dist/hooks/useNotifications.d.ts.map +1 -0
- package/dist/hooks/useNotifications.js +165 -0
- package/dist/hooks/useNotificationsSimple.d.ts +20 -0
- package/dist/hooks/useNotificationsSimple.d.ts.map +1 -0
- package/dist/hooks/useNotificationsSimple.js +37 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/layouts/AdminLayoutWithSidebar.d.ts.map +1 -1
- package/dist/layouts/AdminLayoutWithSidebar.js +13 -4
- package/dist/layouts/AppProviders.d.ts +15 -2
- package/dist/layouts/AppProviders.d.ts.map +1 -1
- package/dist/layouts/AppProviders.js +21 -8
- package/dist/layouts/AuthLayoutWithSidebar.d.ts.map +1 -1
- package/dist/layouts/AuthLayoutWithSidebar.js +10 -2
- package/dist/layouts/PublicLayout.js +1 -1
- package/dist/layouts/PublicLayoutWithSidebar.d.ts +8 -0
- package/dist/layouts/PublicLayoutWithSidebar.d.ts.map +1 -0
- package/dist/layouts/PublicLayoutWithSidebar.js +19 -0
- package/dist/layouts/RootLayout.d.ts +3 -1
- package/dist/layouts/RootLayout.d.ts.map +1 -1
- package/dist/layouts/RootLayout.js +3 -2
- package/dist/scripts/init-app.d.ts.map +1 -1
- package/dist/scripts/init-app.js +209 -23
- package/dist/scripts/module-build.d.ts.map +1 -1
- package/dist/scripts/module-build.js +52 -0
- package/dist/styles.css +1 -1
- package/dist/templates/DefaultDoc.d.ts.map +1 -1
- package/dist/templates/DefaultDoc.js +64 -1
- package/dist/templates/DocPage.d.ts.map +1 -1
- package/dist/templates/DocPage.js +7 -0
- package/package.json +1 -2
- package/src/auth/useAuthSession.ts +6 -2
- package/src/components/NotificationContainer.tsx +20 -0
- package/src/hooks/useNotifications.ts +216 -0
- package/src/index.ts +4 -1
- package/src/layouts/AdminLayoutWithSidebar.tsx +18 -6
- package/src/layouts/AppProviders.tsx +50 -13
- package/src/layouts/AuthLayoutWithSidebar.tsx +16 -4
- package/src/layouts/PublicLayout.tsx +1 -1
- package/src/layouts/PublicLayoutWithSidebar.tsx +56 -0
- package/src/layouts/RootLayout.tsx +10 -2
- package/src/scripts/generate-realtime-config.mjs +183 -0
- package/src/scripts/init-app.ts +237 -23
- package/src/scripts/module-build.ts +63 -0
- package/src/templates/DefaultDoc.tsx +265 -0
- package/src/templates/DocPage.tsx +7 -0
|
@@ -33,7 +33,70 @@ const proxyUrl = await uploadFile(
|
|
|
33
33
|
import { storagePathToProxyUrl } from "@/lib/storage";
|
|
34
34
|
|
|
35
35
|
const proxyUrl = storagePathToProxyUrl("avatar/user_128_123456.webp");
|
|
36
|
-
// Retourne: "/api/storage/avatar/user_128_123456.webp"` }) })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Avantages du syst\u00E8me" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "URLs plus courtes et lisibles" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Contr\u00F4le d'acc\u00E8s granulaire" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Cache optimis\u00E9 (1 an)" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "S\u00E9curit\u00E9 am\u00E9lior\u00E9e" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "D\u00E9tection auto des types MIME" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Extensible facilement" })] })] })] })] })] }), _jsxs(Card, { id: "section-
|
|
36
|
+
// Retourne: "/api/storage/avatar/user_128_123456.webp"` }) })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Avantages du syst\u00E8me" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "URLs plus courtes et lisibles" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Contr\u00F4le d'acc\u00E8s granulaire" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Cache optimis\u00E9 (1 an)" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "S\u00E9curit\u00E9 am\u00E9lior\u00E9e" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "D\u00E9tection auto des types MIME" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Extensible facilement" })] })] })] })] })] }), _jsxs(Card, { id: "section-realtime", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Zap, { size: 24 }), "Syst\u00E8me Realtime"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsx("p", { className: "text-slate-600 dark:text-slate-400", children: "LastBrain int\u00E8gre un syst\u00E8me de notifications et de synchronisation en temps r\u00E9el bas\u00E9 sur Supabase Realtime. Le syst\u00E8me est enti\u00E8rement automatis\u00E9 et scalable." }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Configuration dans les modules" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-3", children: ["Ajoutez la configuration realtime dans votre", " ", _jsx("code", { className: "text-sm bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "build.config.ts" }), " ", "pour activer la synchronisation en temps r\u00E9el :"] }), _jsx("div", { className: "bg-slate-50 dark:bg-slate-900 p-4 rounded-lg text-sm font-mono overflow-x-auto", children: _jsx("pre", { children: `export default {
|
|
37
|
+
name: "Module Auth",
|
|
38
|
+
description: "Gestion de l'authentification",
|
|
39
|
+
|
|
40
|
+
// Configuration realtime
|
|
41
|
+
realtime: {
|
|
42
|
+
moduleId: "module-auth",
|
|
43
|
+
tables: [
|
|
44
|
+
{
|
|
45
|
+
schema: "public",
|
|
46
|
+
table: "user_notifications",
|
|
47
|
+
event: "*", // INSERT, UPDATE, DELETE ou *
|
|
48
|
+
filter: "owner_id=eq.\${USER_ID}",
|
|
49
|
+
broadcast: "user_notifications_updated"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
schema: "public",
|
|
53
|
+
table: "user_profiles",
|
|
54
|
+
event: "*",
|
|
55
|
+
filter: "owner_id=eq.\${USER_ID}",
|
|
56
|
+
broadcast: "user_profile_updated"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
pages: [
|
|
62
|
+
// vos pages...
|
|
63
|
+
]
|
|
64
|
+
}` }) })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "G\u00E9n\u00E9ration automatique de la configuration" }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-2", children: "Lors du build des modules, la configuration realtime est automatiquement g\u00E9n\u00E9r\u00E9e :" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm mb-3", children: "pnpm build:modules" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400", children: ["Cela g\u00E9n\u00E8re automatiquement", " ", _jsx("code", { className: "text-sm bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "config/realtime.ts" }), " ", "avec toutes les configurations des modules."] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Utilisation dans les composants" }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-3", children: "Utilisez les hooks pr\u00E9-construits pour \u00E9couter les changements :" }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h4", { className: "font-medium mb-2", children: "1. Hook g\u00E9n\u00E9rique pour une table" }), _jsx("div", { className: "bg-slate-50 dark:bg-slate-900 p-4 rounded-lg text-sm font-mono overflow-x-auto", children: _jsx("pre", { children: `import { useTableRealtime } from "@lastbrain/core";
|
|
65
|
+
|
|
66
|
+
function MonComposant() {
|
|
67
|
+
const signal = useTableRealtime("user_notifications");
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (signal.hasUpdates) {
|
|
71
|
+
console.log("Nouvelle notification!");
|
|
72
|
+
// Recharger vos données
|
|
73
|
+
refetchNotifications();
|
|
74
|
+
}
|
|
75
|
+
}, [signal.tick]);
|
|
76
|
+
}` }) })] }), _jsxs("div", { children: [_jsx("h4", { className: "font-medium mb-2", children: "2. Hook sp\u00E9cialis\u00E9 pour les notifications" }), _jsx("div", { className: "bg-slate-50 dark:bg-slate-900 p-4 rounded-lg text-sm font-mono overflow-x-auto", children: _jsx("pre", { children: `import { useNotifications } from "../hooks/useNotifications";
|
|
77
|
+
|
|
78
|
+
function NotificationBell() {
|
|
79
|
+
const { notifications, loading, hasUpdates } = useNotifications();
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div>
|
|
83
|
+
🔔 {notifications.length}
|
|
84
|
+
{hasUpdates && <span>•</span>}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}` }) })] }), _jsxs("div", { children: [_jsx("h4", { className: "font-medium mb-2", children: "3. Hook bas niveau avec EventBus" }), _jsx("div", { className: "bg-slate-50 dark:bg-slate-900 p-4 rounded-lg text-sm font-mono overflow-x-auto", children: _jsx("pre", { children: `import { useRealtimeSignal } from "@lastbrain/core";
|
|
88
|
+
|
|
89
|
+
function MonComposant() {
|
|
90
|
+
const signal = useRealtimeSignal("user_notifications_updated");
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (signal.hasUpdates) {
|
|
94
|
+
// Événement spécifique reçu
|
|
95
|
+
const payload = signal.lastPayload;
|
|
96
|
+
console.log("Données reçues:", payload);
|
|
97
|
+
}
|
|
98
|
+
}, [signal.tick]);
|
|
99
|
+
}` }) })] })] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Architecture du syst\u00E8me" }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "border-l-4 border-blue-500 pl-4", children: [_jsx("h4", { className: "font-semibold text-blue-600 dark:text-blue-400", children: "\uD83C\uDFAF EventBus Global" }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400", children: "Syst\u00E8me central de coordination des \u00E9v\u00E9nements realtime \u00E0 travers toute l'application." })] }), _jsxs("div", { className: "border-l-4 border-green-500 pl-4", children: [_jsx("h4", { className: "font-semibold text-green-600 dark:text-green-400", children: "\uD83D\uDD0C RealtimeProvider" }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400", children: "Provider React qui \u00E9tablit les connexions Supabase Realtime bas\u00E9es sur la configuration des modules." })] }), _jsxs("div", { className: "border-l-4 border-purple-500 pl-4", children: [_jsx("h4", { className: "font-semibold text-purple-600 dark:text-purple-400", children: "\u26A1 Hooks Scalables" }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400", children: "Collection de hooks React pour \u00E9couter des tables ou \u00E9v\u00E9nements sp\u00E9cifiques avec auto-refresh." })] }), _jsxs("div", { className: "border-l-4 border-orange-500 pl-4", children: [_jsx("h4", { className: "font-semibold text-orange-600 dark:text-orange-400", children: "\uD83D\uDEE0\uFE0F Build int\u00E9gr\u00E9" }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400", children: "G\u00E9n\u00E9ration automatique de la configuration TypeScript lors du build des modules." })] })] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Avantages du syst\u00E8me" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Configuration d\u00E9clarative dans les modules" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "G\u00E9n\u00E9ration automatique de config" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Hooks React pr\u00EAts \u00E0 l'emploi" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "S\u00E9curit\u00E9 RLS de Supabase" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Scalable pour n'importe quelle table" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "TypeScript avec types g\u00E9n\u00E9r\u00E9s" })] })] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Exemples d'utilisation" }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "p-3 bg-blue-50 dark:bg-blue-950 rounded-lg", children: [_jsx("h4", { className: "font-medium text-blue-700 dark:text-blue-300 mb-1", children: "\uD83D\uDD14 Notifications" }), _jsx("p", { className: "text-sm text-blue-600 dark:text-blue-400", children: "Affichage en temps r\u00E9el des nouvelles notifications avec badge et auto-refresh" })] }), _jsxs("div", { className: "p-3 bg-purple-50 dark:bg-purple-950 rounded-lg", children: [_jsx("h4", { className: "font-medium text-purple-700 dark:text-purple-300 mb-1", children: "\uD83D\uDC65 Utilisateurs en ligne" }), _jsx("p", { className: "text-sm text-purple-600 dark:text-purple-400", children: "Suivi des connexions/d\u00E9connexions des utilisateurs en direct" })] }), _jsxs("div", { className: "p-3 bg-green-50 dark:bg-green-950 rounded-lg", children: [_jsx("h4", { className: "font-medium text-green-700 dark:text-green-300 mb-1", children: "\uD83D\uDCCA Donn\u00E9es collaboratives" }), _jsx("p", { className: "text-sm text-green-600 dark:text-green-400", children: "Synchronisation automatique des modifications de donn\u00E9es entre utilisateurs" })] })] })] })] })] }), _jsxs(Card, { id: "section-module-docs", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(BookOpen, { size: 24 }), "Documenter ses modules"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsx("p", { className: "text-slate-600 dark:text-slate-400", children: "Chaque module peut exporter un composant de documentation qui sera automatiquement int\u00E9gr\u00E9 dans cette page." }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Cr\u00E9er une documentation" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-2", children: ["Dans votre module, cr\u00E9ez", " ", _jsx("code", { className: "text-sm bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "src/components/Doc.tsx" }), " ", ":"] }), _jsx("div", { className: "bg-slate-50 dark:bg-slate-900 p-4 rounded-lg text-sm font-mono overflow-x-auto", children: _jsx("pre", { children: `export function MonModuleDoc() {
|
|
37
100
|
return (
|
|
38
101
|
<div>
|
|
39
102
|
<h1>Mon Module</h1>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocPage.d.ts","sourceRoot":"","sources":["../../src/templates/DocPage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AA6FnD,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EACF,SAAS,GACT,WAAW,GACX,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,SAAS,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAClC;AAED,wBAAgB,OAAO,CAAC,EAAE,OAAY,EAAE,cAAc,EAAE,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"DocPage.d.ts","sourceRoot":"","sources":["../../src/templates/DocPage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AA6FnD,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EACF,SAAS,GACT,WAAW,GACX,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,SAAS,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAClC;AAED,wBAAgB,OAAO,CAAC,EAAE,OAAY,EAAE,cAAc,EAAE,EAAE,YAAY,2CAgTrE"}
|
|
@@ -32,6 +32,7 @@ export function DocPage({ modules = [], defaultContent }) {
|
|
|
32
32
|
"section-create-module",
|
|
33
33
|
"section-database",
|
|
34
34
|
"section-storage",
|
|
35
|
+
"section-realtime",
|
|
35
36
|
"section-ui",
|
|
36
37
|
"section-module-docs",
|
|
37
38
|
"section-links",
|
|
@@ -128,6 +129,12 @@ export function DocPage({ modules = [], defaultContent }) {
|
|
|
128
129
|
description: "Gestion des fichiers",
|
|
129
130
|
icon: HardDrive,
|
|
130
131
|
},
|
|
132
|
+
{
|
|
133
|
+
id: "section-realtime",
|
|
134
|
+
name: "Système Realtime",
|
|
135
|
+
description: "Synchronisation temps réel",
|
|
136
|
+
icon: Sparkles,
|
|
137
|
+
},
|
|
131
138
|
{
|
|
132
139
|
id: "section-ui",
|
|
133
140
|
name: "Interface utilisateur",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lastbrain/app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"description": "Framework modulaire Next.js avec CLI et système de modules",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@lastbrain/core": "^0.1.0",
|
|
35
|
-
"@lastbrain/module-auth": "^0.1.2",
|
|
36
35
|
"@lastbrain/ui": "^0.1.4",
|
|
37
36
|
"@supabase/supabase-js": "^2.36.0",
|
|
38
37
|
"chalk": "^5.3.0",
|
|
@@ -22,6 +22,9 @@ export function useAuthSession() {
|
|
|
22
22
|
} catch (error) {
|
|
23
23
|
console.error("Error checking superadmin status:", error);
|
|
24
24
|
setIsSuperAdmin(false);
|
|
25
|
+
} finally {
|
|
26
|
+
// Ne terminer le loading qu'après avoir vérifié le status admin
|
|
27
|
+
setLoading(false);
|
|
25
28
|
}
|
|
26
29
|
};
|
|
27
30
|
|
|
@@ -30,8 +33,9 @@ export function useAuthSession() {
|
|
|
30
33
|
setUser(session?.user ?? null);
|
|
31
34
|
if (session?.user) {
|
|
32
35
|
checkSuperAdmin(session.user.id);
|
|
36
|
+
} else {
|
|
37
|
+
setLoading(false);
|
|
33
38
|
}
|
|
34
|
-
setLoading(false);
|
|
35
39
|
});
|
|
36
40
|
|
|
37
41
|
// Écouter les changements d'auth
|
|
@@ -43,8 +47,8 @@ export function useAuthSession() {
|
|
|
43
47
|
checkSuperAdmin(session.user.id);
|
|
44
48
|
} else {
|
|
45
49
|
setIsSuperAdmin(false);
|
|
50
|
+
setLoading(false);
|
|
46
51
|
}
|
|
47
|
-
setLoading(false);
|
|
48
52
|
});
|
|
49
53
|
|
|
50
54
|
return () => subscription.unsubscribe();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Notification } from "@lastbrain/ui";
|
|
4
|
+
import { useNotifications } from "../layouts/AppProviders.js";
|
|
5
|
+
|
|
6
|
+
export function NotificationContainer() {
|
|
7
|
+
const { data, loading, markAsRead, markAllAsRead, deleteNotification } =
|
|
8
|
+
useNotifications();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<Notification
|
|
12
|
+
notifications={data.notifications}
|
|
13
|
+
unreadCount={data.unreadCount}
|
|
14
|
+
loading={loading}
|
|
15
|
+
onMarkAsRead={markAsRead}
|
|
16
|
+
onMarkAllAsRead={markAllAsRead}
|
|
17
|
+
onDelete={deleteNotification}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { useTableRealtime, supabaseBrowserClient } from "@lastbrain/core";
|
|
5
|
+
import type { User } from "@supabase/supabase-js";
|
|
6
|
+
|
|
7
|
+
export interface UserNotification {
|
|
8
|
+
id: string;
|
|
9
|
+
owner_id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
body?: string;
|
|
12
|
+
type: string;
|
|
13
|
+
read: boolean;
|
|
14
|
+
created_at: string;
|
|
15
|
+
updated_at: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NotificationsData {
|
|
19
|
+
notifications: UserNotification[];
|
|
20
|
+
unreadCount: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useNotifications(user?: User | null) {
|
|
24
|
+
const [data, setData] = useState<NotificationsData>({
|
|
25
|
+
notifications: [],
|
|
26
|
+
unreadCount: 0,
|
|
27
|
+
});
|
|
28
|
+
const [loading, setLoading] = useState(true);
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
const fetchNotifications = useCallback(async () => {
|
|
32
|
+
if (!user?.id) {
|
|
33
|
+
if (user === null) {
|
|
34
|
+
// Utilisateur explicitement non connecté
|
|
35
|
+
setData({ notifications: [], unreadCount: 0 });
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
// Si user === undefined, on est encore en train de charger l'auth, on ne fait rien
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
setLoading(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
|
|
46
|
+
// Utilisation directe du client Supabase côté navigateur
|
|
47
|
+
const { data: notifications, error: fetchError } =
|
|
48
|
+
await supabaseBrowserClient
|
|
49
|
+
.from("user_notifications")
|
|
50
|
+
.select("*")
|
|
51
|
+
.eq("owner_id", user.id)
|
|
52
|
+
.order("created_at", { ascending: false });
|
|
53
|
+
|
|
54
|
+
if (fetchError) {
|
|
55
|
+
throw fetchError;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const unreadCount = notifications?.filter((n) => !n.read).length || 0;
|
|
59
|
+
|
|
60
|
+
setData({
|
|
61
|
+
notifications: notifications || [],
|
|
62
|
+
unreadCount,
|
|
63
|
+
});
|
|
64
|
+
} catch (err: unknown) {
|
|
65
|
+
setError(
|
|
66
|
+
err instanceof Error
|
|
67
|
+
? err.message
|
|
68
|
+
: "Erreur lors du chargement des notifications",
|
|
69
|
+
);
|
|
70
|
+
console.error("[useNotifications] Erreur:", err);
|
|
71
|
+
} finally {
|
|
72
|
+
setLoading(false);
|
|
73
|
+
}
|
|
74
|
+
}, [user]);
|
|
75
|
+
|
|
76
|
+
// Utiliser le hook realtime scalable pour les notifications
|
|
77
|
+
const { tick: realtimeTick, lastPayload } = useTableRealtime(
|
|
78
|
+
"user_notifications",
|
|
79
|
+
{
|
|
80
|
+
eventName: "user_notifications_updated",
|
|
81
|
+
onUpdate: (payload) => {
|
|
82
|
+
console.log("🔔 Nouvelle notification détectée via realtime:", payload);
|
|
83
|
+
console.log("🔔 User ID actuel:", user?.id);
|
|
84
|
+
// Recharger immédiatement les notifications
|
|
85
|
+
if (user?.id) {
|
|
86
|
+
console.log("🔔 Rechargement des notifications...");
|
|
87
|
+
fetchNotifications();
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Marquer une notification comme lue
|
|
94
|
+
const markAsRead = useCallback(
|
|
95
|
+
async (notificationId: string) => {
|
|
96
|
+
if (!user?.id) return;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const { error: updateError } = await supabaseBrowserClient
|
|
100
|
+
.from("user_notifications")
|
|
101
|
+
.update({
|
|
102
|
+
read: true,
|
|
103
|
+
updated_at: new Date().toISOString(),
|
|
104
|
+
})
|
|
105
|
+
.eq("id", notificationId)
|
|
106
|
+
.eq("owner_id", user.id);
|
|
107
|
+
|
|
108
|
+
if (updateError) {
|
|
109
|
+
throw updateError;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Mettre à jour localement en attendant le realtime
|
|
113
|
+
setData((prev) => ({
|
|
114
|
+
...prev,
|
|
115
|
+
notifications: prev.notifications.map((n) =>
|
|
116
|
+
n.id === notificationId ? { ...n, read: true } : n,
|
|
117
|
+
),
|
|
118
|
+
unreadCount: Math.max(0, prev.unreadCount - 1),
|
|
119
|
+
}));
|
|
120
|
+
} catch (err: unknown) {
|
|
121
|
+
console.error("Erreur mark as read:", err);
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
[user?.id],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Marquer toutes les notifications comme lues
|
|
129
|
+
const markAllAsRead = useCallback(async () => {
|
|
130
|
+
if (!user?.id || data.unreadCount === 0) return;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const { error: updateError } = await supabaseBrowserClient
|
|
134
|
+
.from("user_notifications")
|
|
135
|
+
.update({
|
|
136
|
+
read: true,
|
|
137
|
+
updated_at: new Date().toISOString(),
|
|
138
|
+
})
|
|
139
|
+
.eq("owner_id", user.id)
|
|
140
|
+
.eq("read", false);
|
|
141
|
+
|
|
142
|
+
if (updateError) {
|
|
143
|
+
throw updateError;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Mettre à jour localement
|
|
147
|
+
setData((prev) => ({
|
|
148
|
+
...prev,
|
|
149
|
+
notifications: prev.notifications.map((n) => ({ ...n, read: true })),
|
|
150
|
+
unreadCount: 0,
|
|
151
|
+
}));
|
|
152
|
+
} catch (err: unknown) {
|
|
153
|
+
console.error("Erreur mark all as read:", err);
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}, [user?.id, data.unreadCount]);
|
|
157
|
+
|
|
158
|
+
// Supprimer une notification
|
|
159
|
+
const deleteNotification = useCallback(
|
|
160
|
+
async (notificationId: string) => {
|
|
161
|
+
if (!user?.id) return;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const { error: deleteError } = await supabaseBrowserClient
|
|
165
|
+
.from("user_notifications")
|
|
166
|
+
.delete()
|
|
167
|
+
.eq("id", notificationId)
|
|
168
|
+
.eq("owner_id", user.id);
|
|
169
|
+
|
|
170
|
+
if (deleteError) {
|
|
171
|
+
throw deleteError;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Mettre à jour localement
|
|
175
|
+
setData((prev) => {
|
|
176
|
+
const notification = prev.notifications.find(
|
|
177
|
+
(n) => n.id === notificationId,
|
|
178
|
+
);
|
|
179
|
+
return {
|
|
180
|
+
notifications: prev.notifications.filter(
|
|
181
|
+
(n) => n.id !== notificationId,
|
|
182
|
+
),
|
|
183
|
+
unreadCount:
|
|
184
|
+
notification && !notification.read
|
|
185
|
+
? Math.max(0, prev.unreadCount - 1)
|
|
186
|
+
: prev.unreadCount,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
} catch (err: unknown) {
|
|
190
|
+
console.error("Erreur delete notification:", err);
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
[user?.id],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Charger les notifications initiales
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
fetchNotifications();
|
|
200
|
+
}, [fetchNotifications]);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
data,
|
|
204
|
+
loading,
|
|
205
|
+
error,
|
|
206
|
+
refetch: fetchNotifications,
|
|
207
|
+
markAsRead,
|
|
208
|
+
markAllAsRead,
|
|
209
|
+
deleteNotification,
|
|
210
|
+
isEmpty: data.notifications.length === 0,
|
|
211
|
+
hasUnread: data.unreadCount > 0,
|
|
212
|
+
// Informations realtime
|
|
213
|
+
realtimeActive: realtimeTick > 0,
|
|
214
|
+
lastRealtimePayload: lastPayload,
|
|
215
|
+
};
|
|
216
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,12 +2,15 @@ export {
|
|
|
2
2
|
AppProviders,
|
|
3
3
|
useAuth,
|
|
4
4
|
useModules,
|
|
5
|
-
useNotifications,
|
|
5
|
+
useNotifications as useNotificationsContext,
|
|
6
6
|
} from "./layouts/AppProviders.js";
|
|
7
|
+
export { useNotifications } from "./hooks/useNotifications.js";
|
|
8
|
+
export { NotificationContainer } from "./components/NotificationContainer.js";
|
|
7
9
|
export { useAuthSession } from "./auth/useAuthSession.js";
|
|
8
10
|
export { signOut, signIn } from "./auth/authHelpers.js";
|
|
9
11
|
export { RootLayout } from "./layouts/RootLayout.js";
|
|
10
12
|
export { PublicLayout } from "./layouts/PublicLayout.js";
|
|
13
|
+
export { PublicLayoutWithSidebar } from "./layouts/PublicLayoutWithSidebar.js";
|
|
11
14
|
export { AuthLayout } from "./layouts/AuthLayout.js";
|
|
12
15
|
export { AuthLayoutWithSidebar } from "./layouts/AuthLayoutWithSidebar.js";
|
|
13
16
|
export { AdminLayout } from "./layouts/AdminLayout.js";
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { AdminLayout } from "./AdminLayout.js";
|
|
4
4
|
import { AppAside, AppAsideSkeleton } from "@lastbrain/ui";
|
|
5
5
|
import { useAuthSession } from "../auth/useAuthSession.js";
|
|
6
|
+
import { usePathname } from "next/navigation";
|
|
6
7
|
|
|
7
8
|
interface AdminLayoutWithSidebarProps {
|
|
8
9
|
children: React.ReactNode;
|
|
@@ -15,17 +16,27 @@ export function AdminLayoutWithSidebar({
|
|
|
15
16
|
menuConfig,
|
|
16
17
|
className = "",
|
|
17
18
|
}: AdminLayoutWithSidebarProps) {
|
|
18
|
-
const { isSuperAdmin } = useAuthSession();
|
|
19
|
+
const { isSuperAdmin, loading, user } = useAuthSession();
|
|
20
|
+
const pathname = usePathname();
|
|
19
21
|
|
|
20
22
|
// Détecter si on est dans la section admin pour le skeleton
|
|
21
|
-
const isAdminSection =
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const isAdminSection = pathname.startsWith("/admin");
|
|
24
|
+
|
|
25
|
+
// Vérifier si menuConfig est vraiment disponible et valide
|
|
26
|
+
const hasValidMenuConfig =
|
|
27
|
+
menuConfig &&
|
|
28
|
+
typeof menuConfig === "object" &&
|
|
29
|
+
(menuConfig.admin?.length > 0 ||
|
|
30
|
+
menuConfig.auth?.length > 0 ||
|
|
31
|
+
menuConfig.public?.length > 0);
|
|
32
|
+
|
|
33
|
+
// Afficher le skeleton pendant le chargement de la session ou si pas de menuConfig valide
|
|
34
|
+
const shouldShowSkeleton = loading || !hasValidMenuConfig;
|
|
24
35
|
|
|
25
36
|
return (
|
|
26
37
|
<div className="flex min-h-screen">
|
|
27
|
-
{/* Afficher le skeleton pendant le chargement ou si pas de menuConfig */}
|
|
28
|
-
{
|
|
38
|
+
{/* Afficher le skeleton pendant le chargement ou si pas de menuConfig valide */}
|
|
39
|
+
{shouldShowSkeleton ? (
|
|
29
40
|
<AppAsideSkeleton
|
|
30
41
|
className={className}
|
|
31
42
|
isAdminSection={isAdminSection}
|
|
@@ -34,6 +45,7 @@ export function AdminLayoutWithSidebar({
|
|
|
34
45
|
<AppAside
|
|
35
46
|
menuConfig={menuConfig}
|
|
36
47
|
isSuperAdmin={isSuperAdmin}
|
|
48
|
+
isAuthenticated={!!user}
|
|
37
49
|
className={className}
|
|
38
50
|
/>
|
|
39
51
|
)}
|
|
@@ -3,11 +3,40 @@
|
|
|
3
3
|
import { createContext, useContext, useMemo } from "react";
|
|
4
4
|
import { getModuleConfigs } from "../modules/module-loader.js";
|
|
5
5
|
import { ToastProvider } from "@lastbrain/ui";
|
|
6
|
+
import { RealtimeProvider } from "@lastbrain/core";
|
|
6
7
|
import { useAuthSession } from "../auth/useAuthSession.js";
|
|
8
|
+
import { useNotifications as useNotificationsHook } from "../hooks/useNotifications.js";
|
|
7
9
|
import type { User } from "@supabase/supabase-js";
|
|
10
|
+
import type { ModuleRealtimeConfig } from "@lastbrain/core";
|
|
11
|
+
import type { NotificationsData } from "../hooks/useNotifications.js";
|
|
8
12
|
|
|
9
13
|
const ModuleContext = createContext(getModuleConfigs());
|
|
10
|
-
const NotificationContext = createContext
|
|
14
|
+
const NotificationContext = createContext<{
|
|
15
|
+
data: NotificationsData;
|
|
16
|
+
loading: boolean;
|
|
17
|
+
error: string | null;
|
|
18
|
+
refetch: () => Promise<void>;
|
|
19
|
+
markAsRead: (id: string) => Promise<void>;
|
|
20
|
+
markAllAsRead: () => Promise<void>;
|
|
21
|
+
deleteNotification: (id: string) => Promise<void>;
|
|
22
|
+
isEmpty: boolean;
|
|
23
|
+
hasUnread: boolean;
|
|
24
|
+
realtimeActive: boolean;
|
|
25
|
+
lastRealtimePayload: unknown;
|
|
26
|
+
}>({
|
|
27
|
+
data: { notifications: [], unreadCount: 0 },
|
|
28
|
+
loading: true,
|
|
29
|
+
error: null,
|
|
30
|
+
refetch: async () => {},
|
|
31
|
+
markAsRead: async () => {},
|
|
32
|
+
markAllAsRead: async () => {},
|
|
33
|
+
deleteNotification: async () => {},
|
|
34
|
+
isEmpty: true,
|
|
35
|
+
hasUnread: false,
|
|
36
|
+
realtimeActive: false,
|
|
37
|
+
lastRealtimePayload: null,
|
|
38
|
+
});
|
|
39
|
+
|
|
11
40
|
const AuthContext = createContext<{
|
|
12
41
|
user: User | null;
|
|
13
42
|
loading: boolean;
|
|
@@ -30,17 +59,23 @@ export function useAuth() {
|
|
|
30
59
|
return useContext(AuthContext);
|
|
31
60
|
}
|
|
32
61
|
|
|
33
|
-
export function AppProviders({
|
|
62
|
+
export function AppProviders({
|
|
63
|
+
children,
|
|
64
|
+
realtimeConfig = [],
|
|
65
|
+
}: {
|
|
66
|
+
children: React.ReactNode;
|
|
67
|
+
realtimeConfig?: ModuleRealtimeConfig[];
|
|
68
|
+
}) {
|
|
34
69
|
const modules = useMemo(() => getModuleConfigs(), []);
|
|
35
|
-
const
|
|
36
|
-
const { user, loading, isSuperAdmin } = useAuthSession();
|
|
37
|
-
// const router = useRouter();
|
|
38
|
-
const authValue = useMemo(
|
|
39
|
-
() => ({ user, loading, isSuperAdmin }),
|
|
40
|
-
[user, loading, isSuperAdmin],
|
|
41
|
-
);
|
|
70
|
+
const { user, loading: authLoading, isSuperAdmin } = useAuthSession();
|
|
42
71
|
|
|
43
|
-
//
|
|
72
|
+
// Hook de notifications seulement quand l'auth est prête
|
|
73
|
+
const notificationsData = useNotificationsHook(user);
|
|
74
|
+
|
|
75
|
+
const authValue = useMemo(
|
|
76
|
+
() => ({ user, loading: authLoading, isSuperAdmin }),
|
|
77
|
+
[user, authLoading, isSuperAdmin],
|
|
78
|
+
); // const handleLogout = async () => {
|
|
44
79
|
// const { signOut } = await import("../auth/authHelpers.js");
|
|
45
80
|
// await signOut();
|
|
46
81
|
// router.push("/auth/signin");
|
|
@@ -49,9 +84,11 @@ export function AppProviders({ children }: { children: React.ReactNode }) {
|
|
|
49
84
|
return (
|
|
50
85
|
<AuthContext.Provider value={authValue}>
|
|
51
86
|
<ModuleContext.Provider value={modules}>
|
|
52
|
-
<NotificationContext.Provider value={
|
|
53
|
-
{
|
|
54
|
-
|
|
87
|
+
<NotificationContext.Provider value={notificationsData}>
|
|
88
|
+
<RealtimeProvider userId={user?.id} config={realtimeConfig}>
|
|
89
|
+
{children}
|
|
90
|
+
<ToastProvider placement="bottom-right" toastOffset={5} />
|
|
91
|
+
</RealtimeProvider>
|
|
55
92
|
</NotificationContext.Provider>
|
|
56
93
|
</ModuleContext.Provider>
|
|
57
94
|
</AuthContext.Provider>
|
|
@@ -15,16 +15,27 @@ export function AuthLayoutWithSidebar({
|
|
|
15
15
|
menuConfig,
|
|
16
16
|
className = "",
|
|
17
17
|
}: AuthLayoutWithSidebarProps) {
|
|
18
|
-
const { isSuperAdmin } = useAuthSession();
|
|
18
|
+
const { isSuperAdmin, loading, user } = useAuthSession();
|
|
19
19
|
|
|
20
20
|
// Pour la section auth, isAdminSection sera false
|
|
21
21
|
const isAdminSection = false;
|
|
22
22
|
|
|
23
|
+
// Vérifier si menuConfig est vraiment disponible et valide
|
|
24
|
+
const hasValidMenuConfig =
|
|
25
|
+
menuConfig &&
|
|
26
|
+
typeof menuConfig === "object" &&
|
|
27
|
+
(menuConfig.admin?.length > 0 ||
|
|
28
|
+
menuConfig.auth?.length > 0 ||
|
|
29
|
+
menuConfig.public?.length > 0);
|
|
30
|
+
|
|
31
|
+
// Afficher le skeleton pendant le chargement de la session ou si pas de menuConfig valide
|
|
32
|
+
const shouldShowSkeleton = loading || !hasValidMenuConfig;
|
|
33
|
+
|
|
23
34
|
return (
|
|
24
35
|
<div className="flex min-h-screen">
|
|
25
|
-
{/* Afficher le skeleton pendant le chargement ou si pas de menuConfig */}
|
|
26
|
-
{
|
|
27
|
-
<AppAsideSkeleton
|
|
36
|
+
{/* Afficher le skeleton pendant le chargement ou si pas de menuConfig valide */}
|
|
37
|
+
{shouldShowSkeleton ? (
|
|
38
|
+
<AppAsideSkeleton
|
|
28
39
|
className={className}
|
|
29
40
|
isAdminSection={isAdminSection}
|
|
30
41
|
/>
|
|
@@ -32,6 +43,7 @@ export function AuthLayoutWithSidebar({
|
|
|
32
43
|
<AppAside
|
|
33
44
|
menuConfig={menuConfig}
|
|
34
45
|
isSuperAdmin={isSuperAdmin}
|
|
46
|
+
isAuthenticated={!!user}
|
|
35
47
|
className={className}
|
|
36
48
|
/>
|
|
37
49
|
)}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PublicLayout } from "./PublicLayout.js";
|
|
4
|
+
import { AppAside, AppAsideSkeleton } from "@lastbrain/ui";
|
|
5
|
+
import { useAuthSession } from "../auth/useAuthSession.js";
|
|
6
|
+
|
|
7
|
+
interface PublicLayoutWithSidebarProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
menuConfig?: any;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function PublicLayoutWithSidebar({
|
|
14
|
+
children,
|
|
15
|
+
menuConfig,
|
|
16
|
+
className = "",
|
|
17
|
+
}: PublicLayoutWithSidebarProps) {
|
|
18
|
+
const { user, loading } = useAuthSession();
|
|
19
|
+
|
|
20
|
+
// Pour la section public, isAdminSection sera false
|
|
21
|
+
const isAdminSection = false;
|
|
22
|
+
|
|
23
|
+
// Vérifier si menuConfig est vraiment disponible et valide
|
|
24
|
+
const hasValidMenuConfig =
|
|
25
|
+
menuConfig &&
|
|
26
|
+
typeof menuConfig === "object" &&
|
|
27
|
+
(menuConfig.admin?.length > 0 ||
|
|
28
|
+
menuConfig.auth?.length > 0 ||
|
|
29
|
+
menuConfig.public?.length > 0);
|
|
30
|
+
|
|
31
|
+
// Afficher le skeleton pendant le chargement de la session ou si pas de menuConfig valide
|
|
32
|
+
const shouldShowSkeleton = loading || !hasValidMenuConfig;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex min-h-screen">
|
|
36
|
+
{/* Afficher le skeleton pendant le chargement ou si pas de menuConfig valide */}
|
|
37
|
+
{shouldShowSkeleton ? (
|
|
38
|
+
<AppAsideSkeleton
|
|
39
|
+
className={className}
|
|
40
|
+
isAdminSection={isAdminSection}
|
|
41
|
+
/>
|
|
42
|
+
) : (
|
|
43
|
+
<AppAside
|
|
44
|
+
menuConfig={menuConfig}
|
|
45
|
+
isSuperAdmin={false}
|
|
46
|
+
isAuthenticated={!!user}
|
|
47
|
+
className={className}
|
|
48
|
+
/>
|
|
49
|
+
)}
|
|
50
|
+
{/* Contenu principal avec marge pour la sidebar */}
|
|
51
|
+
<div className="flex-1 lg:ml-72">
|
|
52
|
+
<PublicLayout>{children}</PublicLayout>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|