feat(admin): add bottom navigation items and settings page to admin panel
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import AdminShell from './components/AdminShell.js';
|
||||
import { protectAdmin } from './protect.js';
|
||||
import { buildNavigationSections } from './navigation.js';
|
||||
import { buildNavigationSections, buildBottomNavItems } from './navigation.js';
|
||||
import { logoutAction } from '@zen/core/features/auth/actions';
|
||||
import { getAppName } from '@zen/core';
|
||||
import './widgets/index.server.js';
|
||||
@@ -9,6 +9,7 @@ export default async function AdminLayout({ children }) {
|
||||
const session = await protectAdmin();
|
||||
const appName = getAppName();
|
||||
const navigationSections = buildNavigationSections('/');
|
||||
const bottomNavItems = buildBottomNavItems('/');
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
@@ -16,6 +17,7 @@ export default async function AdminLayout({ children }) {
|
||||
onLogout={logoutAction}
|
||||
appName={appName}
|
||||
navigationSections={navigationSections}
|
||||
bottomNavItems={bottomNavItems}
|
||||
>
|
||||
{children}
|
||||
</AdminShell>
|
||||
|
||||
@@ -5,9 +5,10 @@ import './pages/DashboardPage.client.js';
|
||||
import './pages/UsersPage.client.js';
|
||||
import './pages/RolesPage.client.js';
|
||||
import './pages/ProfilePage.client.js';
|
||||
import './pages/SettingsPage.client.js';
|
||||
import './widgets/index.client.js';
|
||||
|
||||
export default function AdminPageClient({ params, user, widgetData }) {
|
||||
export default function AdminPageClient({ params, user, widgetData, appConfig }) {
|
||||
const parts = params?.admin || [];
|
||||
const [first] = parts;
|
||||
|
||||
@@ -17,10 +18,11 @@ export default function AdminPageClient({ params, user, widgetData }) {
|
||||
if (!page) return null;
|
||||
|
||||
const { Component } = page;
|
||||
// Le tableau de bord reçoit les données collectées côté serveur ; les
|
||||
// autres pages ne connaissent pas le widget data.
|
||||
if (slug === 'dashboard') {
|
||||
return <Component user={user} stats={widgetData} />;
|
||||
}
|
||||
if (slug === 'settings') {
|
||||
return <Component user={user} appConfig={appConfig} />;
|
||||
}
|
||||
return <Component user={user} params={parts} />;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import AdminPageClient from './AdminPage.client.js';
|
||||
import { protectAdmin } from './protect.js';
|
||||
import { collectWidgetData } from './registry.js';
|
||||
import { getAppConfig, getPublicBaseUrl } from '@zen/core';
|
||||
|
||||
export default async function AdminPage({ params }) {
|
||||
const resolvedParams = await params;
|
||||
const session = await protectAdmin();
|
||||
const widgetData = await collectWidgetData();
|
||||
const appConfig = { ...getAppConfig(), siteUrl: getPublicBaseUrl() };
|
||||
|
||||
return (
|
||||
<AdminPageClient
|
||||
params={resolvedParams}
|
||||
user={session.user}
|
||||
widgetData={widgetData}
|
||||
appConfig={appConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
||||
import AdminSidebar from './AdminSidebar.js';
|
||||
import AdminTop from './AdminTop.js';
|
||||
|
||||
export default function AdminShell({ children, user, onLogout, appName, navigationSections }) {
|
||||
export default function AdminShell({ children, user, onLogout, appName, navigationSections, bottomNavItems }) {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -14,6 +14,7 @@ export default function AdminShell({ children, user, onLogout, appName, navigati
|
||||
setIsMobileMenuOpen={setIsMobileMenuOpen}
|
||||
appName={appName}
|
||||
navigationSections={navigationSections}
|
||||
bottomNavItems={bottomNavItems}
|
||||
/>
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<AdminTop
|
||||
|
||||
@@ -20,7 +20,7 @@ function resolveIcon(iconNameOrComponent) {
|
||||
return Icons.DashboardSquare03Icon;
|
||||
}
|
||||
|
||||
const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledModules, navigationSections: serverNavigationSections }) => {
|
||||
const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledModules, navigationSections: serverNavigationSections, bottomNavItems = [] }) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const [collapsedSections, setCollapsedSections] = useState(() => {
|
||||
@@ -190,9 +190,31 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
</Link>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-2 py-2 overflow-y-auto flex flex-col pb-12">
|
||||
<nav className="flex-1 px-2 py-2 overflow-y-auto flex flex-col">
|
||||
{navigationSections.map(renderNavSection)}
|
||||
</nav>
|
||||
|
||||
{/* Bottom pinned items */}
|
||||
{bottomNavItems.length > 0 && (
|
||||
<div className="px-2 py-2 border-t border-neutral-200 dark:border-neutral-800/70 shrink-0">
|
||||
{bottomNavItems.map((item) => {
|
||||
const Icon = resolveIcon(item.icon);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={handleMobileLinkClick}
|
||||
className={`${base} ${item.current ? parentActif : inactive}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-[15px] w-[15px] flex-shrink-0" />
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ registerNavSection({ id: 'system', title: 'Utilisateurs', icon: 'UserMulti
|
||||
registerNavItem({ id: 'dashboard', label: 'Tableau de bord', icon: 'DashboardSquare03Icon', href: '/admin/dashboard', sectionId: 'dashboard', order: 10 });
|
||||
registerNavItem({ id: 'users', label: 'Utilisateurs', icon: 'UserMultiple02Icon', href: '/admin/users', sectionId: 'system', order: 10 });
|
||||
registerNavItem({ id: 'roles', label: 'Rôles', icon: 'Crown03Icon', href: '/admin/roles', sectionId: 'system', order: 20 });
|
||||
registerNavItem({ id: 'settings', label: 'Paramètres', icon: 'Settings02Icon', href: '/admin/settings', position: 'bottom', order: 10 });
|
||||
|
||||
/**
|
||||
* Build sections for AdminSidebar. Items are sérialisables (pas de composants),
|
||||
@@ -19,7 +20,7 @@ registerNavItem({ id: 'roles', label: 'Rôles', icon: 'Crown03Icon'
|
||||
*/
|
||||
export function buildNavigationSections(pathname) {
|
||||
const sections = getNavSections();
|
||||
const items = getNavItems();
|
||||
const items = getNavItems().filter(item => item.position !== 'bottom');
|
||||
|
||||
const bySection = new Map();
|
||||
for (const item of items) {
|
||||
@@ -37,3 +38,18 @@ export function buildNavigationSections(pathname) {
|
||||
.filter(s => bySection.has(s.id))
|
||||
.map(s => ({ id: s.id, title: s.title, icon: s.icon, items: bySection.get(s.id) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the list of bottom-pinned nav items for AdminSidebar.
|
||||
*/
|
||||
export function buildBottomNavItems(pathname) {
|
||||
return getNavItems()
|
||||
.filter(item => item.position === 'bottom')
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
.map(item => ({
|
||||
name: item.label,
|
||||
href: item.href,
|
||||
icon: item.icon,
|
||||
current: pathname === item.href || pathname.startsWith(item.href + '/'),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { registerPage } from '../registry.js';
|
||||
import AdminHeader from '../components/AdminHeader.js';
|
||||
import { Card, Input, Select, TabNav } from '@zen/core/shared/components';
|
||||
import { applyTheme, getStoredTheme } from '@zen/core/themes';
|
||||
|
||||
const TABS = [
|
||||
{ id: 'general', label: 'Général' },
|
||||
{ id: 'appearance', label: 'Apparence' },
|
||||
];
|
||||
|
||||
const THEME_OPTIONS = [
|
||||
{ value: 'light', label: 'Mode clair' },
|
||||
{ value: 'dark', label: 'Mode sombre' },
|
||||
{ value: 'auto', label: 'Thème système' },
|
||||
];
|
||||
|
||||
const SettingsPage = ({ appConfig = {} }) => {
|
||||
const [activeTab, setActiveTab] = useState('general');
|
||||
const [theme, setTheme] = useState('auto');
|
||||
|
||||
useEffect(() => {
|
||||
setTheme(getStoredTheme());
|
||||
}, []);
|
||||
|
||||
const handleThemeChange = (value) => {
|
||||
setTheme(value);
|
||||
applyTheme(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 sm:gap-6 lg:gap-8">
|
||||
<AdminHeader title="Paramètres" description="Configuration de votre espace ZEN" />
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<TabNav tabs={TABS} activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
|
||||
{activeTab === 'general' && (
|
||||
<Card title="Informations générales">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Nom du site"
|
||||
value={appConfig.name || ''}
|
||||
readOnly
|
||||
disabled
|
||||
/>
|
||||
<Input
|
||||
label="URL du site"
|
||||
value={appConfig.siteUrl || ''}
|
||||
readOnly
|
||||
disabled
|
||||
description="URL publique de votre site"
|
||||
/>
|
||||
<Input
|
||||
label="Fuseau horaire"
|
||||
value={appConfig.timezone || ''}
|
||||
readOnly
|
||||
disabled
|
||||
/>
|
||||
<Input
|
||||
label="Format de date"
|
||||
value={appConfig.dateFormat || ''}
|
||||
readOnly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'appearance' && (
|
||||
<Card title="Thème">
|
||||
<div className="max-w-xs">
|
||||
<Select
|
||||
label="Thème de l'interface"
|
||||
value={theme}
|
||||
onChange={handleThemeChange}
|
||||
options={THEME_OPTIONS}
|
||||
description="S'applique immédiatement et persiste entre les sessions"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
|
||||
registerPage({ slug: 'settings', title: 'Paramètres', Component: SettingsPage });
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const TabNav = ({ tabs = [], activeTab, onTabChange }) => {
|
||||
return (
|
||||
<div className="flex border-b border-neutral-200 dark:border-neutral-800/70">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.id === activeTab;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
className={`cursor-pointer px-4 py-2.5 text-[13px] font-medium transition-colors duration-[120ms] ease-out border-b-2 -mb-px ${
|
||||
isActive
|
||||
? 'border-neutral-900 dark:border-white text-neutral-900 dark:text-white'
|
||||
: 'border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNav;
|
||||
@@ -29,3 +29,4 @@ export { default as TagInput } from './TagInput';
|
||||
export { default as UserAvatar } from './UserAvatar';
|
||||
export { default as ColorPicker } from './ColorPicker.client';
|
||||
export { default as RelativeDate } from './RelativeDate.client';
|
||||
export { default as TabNav } from './TabNav';
|
||||
|
||||
Reference in New Issue
Block a user