refactor(admin): replace inline avatar logic with shared UserAvatar component
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
|
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
|
||||||
import { ChevronDownIcon, User03Icon } from '@zen/core/shared/icons';
|
import { ChevronDownIcon, User03Icon } from '@zen/core/shared/icons';
|
||||||
|
import { UserAvatar } from '@zen/core/shared/components';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { useTheme, getThemeIcon } from '@zen/core/themes';
|
import { useTheme, getThemeIcon } from '@zen/core/themes';
|
||||||
|
|
||||||
@@ -10,11 +11,6 @@ const AdminHeader = ({ isMobileMenuOpen, setIsMobileMenuOpen, user, onLogout, ap
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const getImageUrl = (imageKey) => {
|
|
||||||
if (!imageKey) return null;
|
|
||||||
return `/zen/api/storage/${imageKey}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
if (onLogout) {
|
if (onLogout) {
|
||||||
@@ -34,16 +30,6 @@ const AdminHeader = ({ isMobileMenuOpen, setIsMobileMenuOpen, user, onLogout, ap
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserInitials = (name) => {
|
|
||||||
if (!name) return 'U';
|
|
||||||
return name
|
|
||||||
.split(' ')
|
|
||||||
.map(n => n[0])
|
|
||||||
.join('')
|
|
||||||
.toUpperCase()
|
|
||||||
.slice(0, 2);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { theme, toggle, systemIsDark } = useTheme();
|
const { theme, toggle, systemIsDark } = useTheme();
|
||||||
const ThemeIcon = getThemeIcon(theme, systemIsDark);
|
const ThemeIcon = getThemeIcon(theme, systemIsDark);
|
||||||
const themeLabel = theme === 'light' ? 'Mode clair' : theme === 'dark' ? 'Mode sombre' : 'Thème système';
|
const themeLabel = theme === 'light' ? 'Mode clair' : theme === 'dark' ? 'Mode sombre' : 'Thème système';
|
||||||
@@ -63,8 +49,6 @@ const AdminHeader = ({ isMobileMenuOpen, setIsMobileMenuOpen, user, onLogout, ap
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
const quickLinks = [];
|
const quickLinks = [];
|
||||||
const imageUrl = getImageUrl(user?.image);
|
|
||||||
const userInitials = getUserInitials(user?.name);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white dark:bg-black border-b border-neutral-200 dark:border-neutral-800/70 sticky top-0 z-30 h-12 flex items-center w-full">
|
<header className="bg-white dark:bg-black border-b border-neutral-200 dark:border-neutral-800/70 sticky top-0 z-30 h-12 flex items-center w-full">
|
||||||
@@ -111,18 +95,7 @@ const AdminHeader = ({ isMobileMenuOpen, setIsMobileMenuOpen, user, onLogout, ap
|
|||||||
{/* User Profile Menu */}
|
{/* User Profile Menu */}
|
||||||
<Menu as="div" className="relative">
|
<Menu as="div" className="relative">
|
||||||
<MenuButton className="cursor-pointer flex items-center gap-2.5 px-2.5 py-1.5 rounded-xl hover:bg-black/5 dark:hover:bg-white/5 transition-colors duration-200 outline-none group">
|
<MenuButton className="cursor-pointer flex items-center gap-2.5 px-2.5 py-1.5 rounded-xl hover:bg-black/5 dark:hover:bg-white/5 transition-colors duration-200 outline-none group">
|
||||||
{/* Avatar */}
|
<UserAvatar user={user} size="sm" />
|
||||||
{imageUrl ? (
|
|
||||||
<img
|
|
||||||
src={imageUrl}
|
|
||||||
alt={user?.name || 'User'}
|
|
||||||
className="w-[26px] h-[26px] rounded-full object-cover shrink-0"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="w-[26px] h-[26px] rounded-full bg-black dark:bg-white flex items-center justify-center shrink-0">
|
|
||||||
<span className="text-[10px] font-medium text-white dark:text-black">{userInitials}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Name — desktop only */}
|
{/* Name — desktop only */}
|
||||||
<span className="hidden sm:block text-[13px] leading-none font-medium text-neutral-800 dark:text-white">
|
<span className="hidden sm:block text-[13px] leading-none font-medium text-neutral-800 dark:text-white">
|
||||||
{user?.name || 'User'}
|
{user?.name || 'User'}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Card, Table, Badge, StatusBadge, Button } from '@zen/core/shared/components';
|
import { Card, Table, Badge, StatusBadge, Button, UserAvatar } from '@zen/core/shared/components';
|
||||||
import { PencilEdit01Icon } from '@zen/core/shared/icons';
|
import { PencilEdit01Icon } from '@zen/core/shared/icons';
|
||||||
import { useToast } from '@zen/core/toast';
|
import { useToast } from '@zen/core/toast';
|
||||||
|
|
||||||
@@ -29,27 +29,20 @@ const UsersPageClient = () => {
|
|||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
key: 'name',
|
key: 'name',
|
||||||
label: 'Nom',
|
label: 'Utilisateur',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (user) => (
|
render: (user) => (
|
||||||
<div>
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-sm font-medium text-neutral-900 dark:text-white">{user.name}</div>
|
<UserAvatar user={user} />
|
||||||
<div className="text-xs text-neutral-500 dark:text-gray-400">ID: {user.id.slice(0, 8)}...</div>
|
<div>
|
||||||
|
<div className="text-sm font-medium text-neutral-900 dark:text-white">{user.name}</div>
|
||||||
|
<div className="text-xs text-neutral-500 dark:text-gray-400">{user.email}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
skeleton: {
|
skeleton: {
|
||||||
height: 'h-4', width: '60%',
|
height: 'h-4', width: '60%',
|
||||||
secondary: { height: 'h-3', width: '40%' }
|
secondary: { height: 'h-3', width: '40%' }
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'email',
|
|
||||||
label: 'Email',
|
|
||||||
sortable: true,
|
|
||||||
render: (user) => <div className="text-sm font-medium text-neutral-900 dark:text-white">{user.email}</div>,
|
|
||||||
skeleton: {
|
|
||||||
height: 'h-4',
|
|
||||||
width: '60%',
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
const getImageUrl = (imageKey) => {
|
||||||
|
if (!imageKey) return null;
|
||||||
|
return `/zen/api/storage/${imageKey}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserInitials = (name) => {
|
||||||
|
if (!name) return 'U';
|
||||||
|
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeMap = {
|
||||||
|
sm: { wrapper: 'w-[26px] h-[26px]', text: 'text-[10px]' },
|
||||||
|
md: { wrapper: 'w-8 h-8', text: 'text-[11px]' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserAvatar = ({ user, size = 'md' }) => {
|
||||||
|
const imageUrl = getImageUrl(user?.image);
|
||||||
|
const initials = getUserInitials(user?.name);
|
||||||
|
const { wrapper, text } = sizeMap[size] ?? sizeMap.md;
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt={user?.name || 'User'}
|
||||||
|
className={`${wrapper} rounded-full object-cover shrink-0`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={`${wrapper} rounded-full bg-black dark:bg-white flex items-center justify-center shrink-0`}>
|
||||||
|
<span className={`${text} font-medium text-white dark:text-black`}>{initials}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserAvatar;
|
||||||
@@ -26,3 +26,4 @@ export { default as FilterTabs } from './FilterTabs';
|
|||||||
export { default as Breadcrumb } from './Breadcrumb';
|
export { default as Breadcrumb } from './Breadcrumb';
|
||||||
export { default as Switch } from './Switch';
|
export { default as Switch } from './Switch';
|
||||||
export { default as TagInput } from './TagInput';
|
export { default as TagInput } from './TagInput';
|
||||||
|
export { default as UserAvatar } from './UserAvatar';
|
||||||
|
|||||||
Reference in New Issue
Block a user