refactor(web): split AdminView into admin-view module
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a862479ffc
commit
4400a8a58d
9 changed files with 208 additions and 127 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import { AdminView } from './AdminView';
|
import { AdminView, AdminViewSkeleton } from './admin-view';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AdminView - Vue principale d'administration
|
* AdminView - Vue principale d'administration
|
||||||
|
|
@ -8,42 +8,45 @@ import { AdminView } from './AdminView';
|
||||||
* pour basculer entre les différentes vues admin.
|
* pour basculer entre les différentes vues admin.
|
||||||
*/
|
*/
|
||||||
const meta: Meta<typeof AdminView> = {
|
const meta: Meta<typeof AdminView> = {
|
||||||
title: 'Components/Features/Views/AdminView',
|
title: 'Components/Features/Views/AdminView',
|
||||||
component: AdminView,
|
component: AdminView,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'fullscreen',
|
layout: 'fullscreen',
|
||||||
docs: {
|
docs: {
|
||||||
description: {
|
description: {
|
||||||
component: 'Vue conteneur admin avec navigation sidebar vers sous-vues.',
|
component:
|
||||||
},
|
'Vue conteneur admin avec navigation sidebar vers sous-vues.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tags: ['autodocs'],
|
},
|
||||||
argTypes: {
|
tags: ['autodocs'],
|
||||||
currentSubView: {
|
argTypes: {
|
||||||
control: 'select',
|
currentSubView: {
|
||||||
options: ['dashboard', 'users', 'moderation', 'audit', 'settings'],
|
control: 'select',
|
||||||
description: 'Sous-vue à afficher par défaut',
|
options: ['dashboard', 'users', 'moderation', 'audit', 'settings'],
|
||||||
},
|
description: 'Sous-vue à afficher par défaut',
|
||||||
},
|
},
|
||||||
decorators: [
|
},
|
||||||
(Story) => (
|
decorators: [
|
||||||
<div className="bg-kodo-background min-h-screen p-4">
|
(Story) => (
|
||||||
<Story />
|
<div className="bg-kodo-background min-h-layout-page p-4">
|
||||||
</div>
|
<Story />
|
||||||
),
|
</div>
|
||||||
],
|
),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Vue dashboard par défaut.
|
|
||||||
*/
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
name: 'Par défaut (Dashboard)',
|
name: 'Par défaut (Dashboard)',
|
||||||
args: {
|
args: {
|
||||||
currentSubView: 'dashboard',
|
currentSubView: 'dashboard',
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Loading: Story = {
|
||||||
|
render: () => <AdminViewSkeleton />,
|
||||||
|
name: 'Chargement',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,96 +1 @@
|
||||||
import React, { useState } from 'react';
|
export { AdminView } from './admin-view';
|
||||||
import { AdminDashboardView } from '../admin/AdminDashboardView';
|
|
||||||
import { AdminUsersView } from '../admin/AdminUsersView';
|
|
||||||
import { AdminModerationView } from '../admin/AdminModerationView';
|
|
||||||
import { AdminAuditLogsView } from '../admin/AdminAuditLogsView';
|
|
||||||
import { AdminSettingsView } from '../admin/AdminSettingsView';
|
|
||||||
import { LayoutDashboard, Users, ShieldAlert, Settings, History } from 'lucide-react';
|
|
||||||
|
|
||||||
interface AdminViewProps {
|
|
||||||
currentSubView?: string; // 'dashboard' | 'users' | 'moderation' | 'audit' | 'settings'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AdminView: React.FC<AdminViewProps> = ({
|
|
||||||
currentSubView = 'dashboard',
|
|
||||||
}) => {
|
|
||||||
// Local state for internal navigation if not driven by props, but props is better for deep linking simulation
|
|
||||||
const [activeTab, setActiveTab] = useState(currentSubView);
|
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
switch (activeTab) {
|
|
||||||
case 'users':
|
|
||||||
return <AdminUsersView />;
|
|
||||||
case 'moderation':
|
|
||||||
return <AdminModerationView />;
|
|
||||||
case 'audit':
|
|
||||||
return <AdminAuditLogsView />;
|
|
||||||
case 'settings':
|
|
||||||
return <AdminSettingsView />;
|
|
||||||
case 'dashboard':
|
|
||||||
default:
|
|
||||||
return <AdminDashboardView />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col lg:flex-row gap-8 animate-fadeIn h-[calc(100vh-140px)]">
|
|
||||||
{/* Admin Sidebar */}
|
|
||||||
<div className="w-full lg:w-64 flex-shrink-0">
|
|
||||||
<div className="bg-kodo-red/10 border border-kodo-red/30 p-4 rounded-xl mb-6 flex items-center gap-4">
|
|
||||||
<ShieldAlert className="w-6 h-6 text-kodo-red" />
|
|
||||||
<div>
|
|
||||||
<h3 className="font-bold text-white text-sm">Admin Area</h3>
|
|
||||||
<p className="text-[10px] text-kodo-content-dim">Restricted Access</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
id: 'dashboard',
|
|
||||||
label: 'Dashboard',
|
|
||||||
icon: <LayoutDashboard className="w-4 h-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'users',
|
|
||||||
label: 'Users',
|
|
||||||
icon: <Users className="w-4 h-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'moderation',
|
|
||||||
label: 'Moderation',
|
|
||||||
icon: <ShieldAlert className="w-4 h-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'audit',
|
|
||||||
label: 'Audit Logs',
|
|
||||||
icon: <History className="w-4 h-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'settings',
|
|
||||||
label: 'Settings',
|
|
||||||
icon: <Settings className="w-4 h-4" />,
|
|
||||||
},
|
|
||||||
].map((item) => (
|
|
||||||
<button
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => setActiveTab(item.id)}
|
|
||||||
className={`w-full flex items-center gap-4 px-4 py-4 rounded-lg text-sm font-medium transition-all ${activeTab === item.id
|
|
||||||
? 'bg-white/10 text-white border-l-2 border-kodo-red'
|
|
||||||
: 'text-kodo-content-dim hover:text-white hover:bg-white/5 border-l-2 border-transparent'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.icon}
|
|
||||||
{item.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content Area */}
|
|
||||||
<div className="flex-1 overflow-y-auto pr-2 pb-10 custom-scrollbar">
|
|
||||||
{renderContent()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
26
apps/web/src/components/views/admin-view/AdminView.tsx
Normal file
26
apps/web/src/components/views/admin-view/AdminView.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { AdminViewSidebar } from './AdminViewSidebar';
|
||||||
|
import { AdminViewContent } from './AdminViewContent';
|
||||||
|
import { useAdminView } from './useAdminView';
|
||||||
|
import type { AdminViewProps, AdminSubViewId } from './types';
|
||||||
|
|
||||||
|
const DEFAULT_SUB_VIEW: AdminSubViewId = 'dashboard';
|
||||||
|
|
||||||
|
export function AdminView({
|
||||||
|
currentSubView = DEFAULT_SUB_VIEW,
|
||||||
|
}: AdminViewProps = {}) {
|
||||||
|
const { activeTab, setActiveTab, tabs } = useAdminView(currentSubView);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col lg:flex-row gap-8 animate-fadeIn min-h-layout-main">
|
||||||
|
<AdminViewSidebar
|
||||||
|
tabs={tabs}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
/>
|
||||||
|
<div className="flex-1 overflow-y-auto pr-2 pb-10 custom-scrollbar">
|
||||||
|
<AdminViewContent activeTab={activeTab} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { AdminDashboardView } from '@/components/admin/AdminDashboardView';
|
||||||
|
import { AdminUsersView } from '@/components/admin/AdminUsersView';
|
||||||
|
import { AdminModerationView } from '@/components/admin/AdminModerationView';
|
||||||
|
import { AdminAuditLogsView } from '@/components/admin/AdminAuditLogsView';
|
||||||
|
import { AdminSettingsView } from '@/components/admin/AdminSettingsView';
|
||||||
|
import type { AdminSubViewId } from './types';
|
||||||
|
|
||||||
|
interface AdminViewContentProps {
|
||||||
|
activeTab: AdminSubViewId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdminViewContent({ activeTab }: AdminViewContentProps) {
|
||||||
|
switch (activeTab) {
|
||||||
|
case 'users':
|
||||||
|
return <AdminUsersView />;
|
||||||
|
case 'moderation':
|
||||||
|
return <AdminModerationView />;
|
||||||
|
case 'audit':
|
||||||
|
return <AdminAuditLogsView />;
|
||||||
|
case 'settings':
|
||||||
|
return <AdminSettingsView />;
|
||||||
|
case 'dashboard':
|
||||||
|
default:
|
||||||
|
return <AdminDashboardView />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ShieldAlert } from 'lucide-react';
|
||||||
|
import type { AdminSubViewId, AdminTabConfig } from './types';
|
||||||
|
|
||||||
|
interface AdminViewSidebarProps {
|
||||||
|
tabs: AdminTabConfig[];
|
||||||
|
activeTab: AdminSubViewId;
|
||||||
|
onTabChange: (id: AdminSubViewId) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdminViewSidebar({
|
||||||
|
tabs,
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
|
}: AdminViewSidebarProps) {
|
||||||
|
return (
|
||||||
|
<div className="w-full lg:w-64 flex-shrink-0">
|
||||||
|
<div className="bg-kodo-red/10 border border-kodo-red/30 p-4 rounded-xl mb-6 flex items-center gap-4">
|
||||||
|
<ShieldAlert className="w-6 h-6 text-kodo-red" />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-white text-sm">Admin Area</h3>
|
||||||
|
<p className="text-xs text-kodo-content-dim">Restricted Access</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
{tabs.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onTabChange(item.id)}
|
||||||
|
className={`w-full flex items-center gap-4 px-4 py-4 rounded-lg text-sm font-medium transition-all ${
|
||||||
|
activeTab === item.id
|
||||||
|
? 'bg-white/10 text-white border-l-2 border-kodo-red'
|
||||||
|
: 'text-kodo-content-dim hover:text-white hover:bg-white/5 border-l-2 border-transparent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
||||||
|
export function AdminViewSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col lg:flex-row gap-8 animate-fadeIn min-h-layout-main">
|
||||||
|
<div className="w-full lg:w-64 flex-shrink-0">
|
||||||
|
<Skeleton className="h-20 w-full rounded-xl mb-6" />
|
||||||
|
<div className="space-y-1">
|
||||||
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
|
<Skeleton key={i} className="h-12 w-full rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-4">
|
||||||
|
<Skeleton className="h-10 w-48" />
|
||||||
|
<Skeleton className="h-40 w-full rounded-xl" />
|
||||||
|
<Skeleton className="h-40 w-full rounded-xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
apps/web/src/components/views/admin-view/index.ts
Normal file
4
apps/web/src/components/views/admin-view/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { AdminView } from './AdminView';
|
||||||
|
export { AdminViewSkeleton } from './AdminViewSkeleton';
|
||||||
|
export { useAdminView } from './useAdminView';
|
||||||
|
export type { AdminViewProps, AdminSubViewId, AdminTabConfig } from './types';
|
||||||
18
apps/web/src/components/views/admin-view/types.ts
Normal file
18
apps/web/src/components/views/admin-view/types.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export type AdminSubViewId =
|
||||||
|
| 'dashboard'
|
||||||
|
| 'users'
|
||||||
|
| 'moderation'
|
||||||
|
| 'audit'
|
||||||
|
| 'settings';
|
||||||
|
|
||||||
|
export interface AdminViewProps {
|
||||||
|
currentSubView?: AdminSubViewId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminTabConfig {
|
||||||
|
id: AdminSubViewId;
|
||||||
|
label: string;
|
||||||
|
icon: ReactNode;
|
||||||
|
}
|
||||||
31
apps/web/src/components/views/admin-view/useAdminView.tsx
Normal file
31
apps/web/src/components/views/admin-view/useAdminView.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
Users,
|
||||||
|
ShieldAlert,
|
||||||
|
Settings,
|
||||||
|
History,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import type { AdminSubViewId, AdminTabConfig } from './types';
|
||||||
|
|
||||||
|
const ADMIN_TABS: AdminTabConfig[] = [
|
||||||
|
{ id: 'dashboard', label: 'Dashboard', icon: <LayoutDashboard className="w-4 h-4" /> },
|
||||||
|
{ id: 'users', label: 'Users', icon: <Users className="w-4 h-4" /> },
|
||||||
|
{ id: 'moderation', label: 'Moderation', icon: <ShieldAlert className="w-4 h-4" /> },
|
||||||
|
{ id: 'audit', label: 'Audit Logs', icon: <History className="w-4 h-4" /> },
|
||||||
|
{ id: 'settings', label: 'Settings', icon: <Settings className="w-4 h-4" /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function useAdminView(initialSubView: AdminSubViewId = 'dashboard') {
|
||||||
|
const [activeTab, setActiveTab] = useState<AdminSubViewId>(initialSubView);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveTab(initialSubView);
|
||||||
|
}, [initialSubView]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeTab,
|
||||||
|
setActiveTab,
|
||||||
|
tabs: ADMIN_TABS,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue