From 25f93526a52f317d85fba826d6a6ec23f3824bf6 Mon Sep 17 00:00:00 2001 From: Hyko Date: Fri, 24 Apr 2026 15:31:28 -0400 Subject: [PATCH] feat(admin): add RoleBadge component and integrate it in user management views - add new RoleBadge shared component for consistent role display - export RoleBadge from shared components index - replace inline Badge usage with RoleBadge in UsersPage role column - use RoleBadge via renderTag prop in UserEditModal role TagInput - simplify TagInput Pill to a generic unstyled pill, removing color logic --- .../admin/components/UserEditModal.client.js | 5 +- src/features/admin/pages/UsersPage.client.js | 6 +- src/shared/components/RoleBadge.js | 45 +++++++++++++++ src/shared/components/TagInput.js | 55 ++++++------------- src/shared/components/index.js | 1 + 5 files changed, 68 insertions(+), 44 deletions(-) create mode 100644 src/shared/components/RoleBadge.js diff --git a/src/features/admin/components/UserEditModal.client.js b/src/features/admin/components/UserEditModal.client.js index 1ee67a2..c272496 100644 --- a/src/features/admin/components/UserEditModal.client.js +++ b/src/features/admin/components/UserEditModal.client.js @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import { Input, TagInput, Modal } from '@zen/core/shared/components'; +import { Input, TagInput, Modal, RoleBadge } from '@zen/core/shared/components'; import { useToast } from '@zen/core/toast'; const UserEditModal = ({ userId, currentUserId, isOpen, onClose, onSaved }) => { @@ -225,6 +225,9 @@ const UserEditModal = ({ userId, currentUserId, isOpen, onClose, onSaved }) => { value={selectedRoleIds} onChange={setSelectedRoleIds} placeholder="Rechercher un rôle..." + renderTag={(opt, onRemove) => ( + + )} /> )} diff --git a/src/features/admin/pages/UsersPage.client.js b/src/features/admin/pages/UsersPage.client.js index c5b2eaf..f562959 100644 --- a/src/features/admin/pages/UsersPage.client.js +++ b/src/features/admin/pages/UsersPage.client.js @@ -2,7 +2,7 @@ import { registerPage } from '../registry.js'; import { useState, useEffect } from 'react'; -import { Card, Table, Badge, StatusBadge, Button, UserAvatar, RelativeDate } from '@zen/core/shared/components'; +import { Card, Table, Badge, StatusBadge, Button, UserAvatar, RelativeDate, RoleBadge } from '@zen/core/shared/components'; import { PencilEdit01Icon } from '@zen/core/shared/icons'; import { useToast } from '@zen/core/toast'; import AdminHeader from '../components/AdminHeader.js'; @@ -54,9 +54,7 @@ const UsersPageClient = ({ currentUserId }) => { return (
{visible.map(role => ( - - {role.name} - + ))} {overflow > 0 && +{overflow}} {roles.length === 0 && } diff --git a/src/shared/components/RoleBadge.js b/src/shared/components/RoleBadge.js new file mode 100644 index 0000000..4fb70a7 --- /dev/null +++ b/src/shared/components/RoleBadge.js @@ -0,0 +1,45 @@ +'use client'; + +const hexToRgb = (hex) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } + : null; +}; + +const RoleBadge = ({ name, color, onRemove }) => { + const rgb = color ? hexToRgb(color) : null; + const style = rgb + ? { backgroundColor: `rgba(${rgb.r},${rgb.g},${rgb.b},0.15)`, borderColor: `rgba(${rgb.r},${rgb.g},${rgb.b},0.4)`, color } + : {}; + const fallback = !rgb + ? 'bg-neutral-100 dark:bg-neutral-700 border-neutral-200 dark:border-neutral-600 text-neutral-700 dark:text-neutral-300' + : ''; + + return ( + + {rgb && ( + + )} + {name} + {onRemove && ( + + )} + + ); +}; + +export default RoleBadge; diff --git a/src/shared/components/TagInput.js b/src/shared/components/TagInput.js index cdd6495..196e5d5 100644 --- a/src/shared/components/TagInput.js +++ b/src/shared/components/TagInput.js @@ -3,45 +3,19 @@ import { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; -const hexToRgb = (hex) => { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result - ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } - : null; -}; - -const Pill = ({ option, onRemove }) => { - const rgb = option.color ? hexToRgb(option.color) : null; - const pillStyle = rgb - ? { backgroundColor: `rgba(${rgb.r},${rgb.g},${rgb.b},0.15)`, borderColor: `rgba(${rgb.r},${rgb.g},${rgb.b},0.4)`, color: option.color } - : {}; - const fallbackClass = !rgb - ? 'bg-neutral-100 dark:bg-neutral-700 border-neutral-200 dark:border-neutral-600 text-neutral-700 dark:text-neutral-300' - : ''; - - return ( - ( + + {option.label} + - - ); -}; + × + + +); const TagInput = ({ options = [], @@ -51,6 +25,7 @@ const TagInput = ({ description, placeholder = 'Ajouter...', error, + renderTag, }) => { const [inputValue, setInputValue] = useState(''); const [isOpen, setIsOpen] = useState(false); @@ -129,7 +104,9 @@ const TagInput = ({ onClick={() => { inputRef.current?.focus(); setIsOpen(true); }} > {selectedOptions.map(opt => ( - removeOption(opt.value)} /> + renderTag + ? renderTag(opt, () => removeOption(opt.value)) + : removeOption(opt.value)} /> ))}