refactor: update UserDetailPage styles and introduce avatar gradient utility
- Refactored role metadata styles in UserDetailPage for consistency with design tokens. - Replaced hardcoded avatar colors with a utility function to generate gradients based on user ID. - Improved InfoCard component styles for better hover effects and accessibility. - Removed deprecated color classes and adjusted background gradients for various components. - Updated theme token tests to reflect the removal of deferred files and ensure compliance with design standards. - Added new avatarColors utility file to manage avatar gradient definitions.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { avatarGradient } from '../lib/avatarColors';
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -71,11 +72,11 @@ const MEDICAL_SECTIONS: {
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
const ROLE_META: Record<string, { label: string; cls: string; dot: string }> = {
|
||||
admin: { label: 'ادمین', cls: 'bg-violet-100 dark:bg-violet-400/10 text-violet-700 dark:text-violet-300', dot: '#8b5cf6' },
|
||||
doctor: { label: 'پزشک', cls: 'bg-[var(--info-bg)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300', dot: '#3b82f6' },
|
||||
secretary: { label: 'منشی', cls: 'bg-orange-100 dark:bg-orange-400/10 text-orange-700 dark:text-orange-300', dot: '#f97316' },
|
||||
clinic: { label: 'کلینیک', cls: 'bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300', dot: '#10b981' },
|
||||
patient: { label: 'بیمار', cls: 'bg-[var(--surface-2)] dark:bg-slate-400/10 text-[var(--text-2)] dark:text-[var(--text-3)]', dot: '#94a3b8' },
|
||||
admin: { label: 'ادمین', cls: 'bg-[var(--violet-bg)] text-[var(--violet)]', dot: 'var(--violet)' },
|
||||
doctor: { label: 'پزشک', cls: 'bg-[var(--info-bg)] text-[var(--info)]', dot: 'var(--info)' },
|
||||
secretary: { label: 'منشی', cls: 'bg-[var(--accent-bg)] text-[var(--accent)]', dot: 'var(--accent)' },
|
||||
clinic: { label: 'کلینیک', cls: 'bg-[var(--success-bg)] text-[var(--success)]', dot: 'var(--success)' },
|
||||
patient: { label: 'بیمار', cls: 'bg-[var(--surface-2)] text-[var(--text-2)]', dot: 'var(--text-3)' },
|
||||
};
|
||||
|
||||
const ROLE_MAP: Record<string, string[]> = {
|
||||
@@ -94,20 +95,13 @@ function getPrimaryRole(roles: string[]): string {
|
||||
return 'patient';
|
||||
}
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
'from-violet-500 to-purple-600',
|
||||
'from-blue-500 to-cyan-600',
|
||||
'from-emerald-500 to-teal-600',
|
||||
'from-pink-500 to-rose-600',
|
||||
'from-amber-500 to-orange-600',
|
||||
];
|
||||
|
||||
function BigAvatar({ name, id }: { name: string | null; id: number }) {
|
||||
const initials = name
|
||||
? name.split(' ').filter(Boolean).map(w => w[0]).join('').toUpperCase().slice(0, 2)
|
||||
: '؟';
|
||||
return (
|
||||
<div className={`w-20 h-20 rounded-2xl bg-gradient-to-br ${AVATAR_COLORS[id % AVATAR_COLORS.length]} flex items-center justify-center text-[var(--on-primary)] text-2xl font-bold shadow-lg shrink-0`}>
|
||||
<div className={`w-20 h-20 rounded-2xl bg-gradient-to-br ${avatarGradient(id)} flex items-center justify-center text-[var(--on-primary)] text-2xl font-bold shadow-lg shrink-0`}>
|
||||
{initials}
|
||||
</div>
|
||||
);
|
||||
@@ -124,13 +118,13 @@ function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }:
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors group">
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] hover:bg-[var(--surface-2)] transition-colors group">
|
||||
<div className="w-9 h-9 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0">
|
||||
<Icon className="w-4.5 h-4.5 text-[var(--text-2)]" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-0.5">{label}</p>
|
||||
<p className={`text-sm font-medium text-[var(--text)] dark:text-[var(--text)] truncate ${mono ? 'font-mono' : ''}`}
|
||||
<p className={`text-sm font-medium text-[var(--text)] truncate ${mono ? 'font-mono' : ''}`}
|
||||
dir={mono ? 'ltr' : undefined}>
|
||||
{value ?? <span className="text-[var(--text-3)] font-normal">ثبت نشده</span>}
|
||||
</p>
|
||||
@@ -138,7 +132,7 @@ function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }:
|
||||
{copyable && typeof value === 'string' && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="opacity-0 group-hover:opacity-100 w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--text-2)] dark:hover:text-[var(--text-3)] hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)] transition-all shrink-0"
|
||||
className="opacity-0 group-hover:opacity-100 w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--text-2)] hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)] transition-all shrink-0"
|
||||
>
|
||||
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -180,15 +174,15 @@ function ChangeRoleModal({ current, loading, onSave, onClose }: {
|
||||
key={key}
|
||||
className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer border-2 transition-all ${
|
||||
selected === key
|
||||
? 'border-primary-500 bg-primary-50 dark:bg-primary-500/10'
|
||||
: 'border-transparent hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] border border-[var(--border)] dark:border-[var(--border)]'
|
||||
? 'border-[var(--primary)] bg-[var(--primary-soft)] dark:bg-[color-mix(in_srgb,var(--primary)_10%,transparent)]'
|
||||
: 'border-transparent hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] border border-[var(--border)]'
|
||||
}`}
|
||||
>
|
||||
<input type="radio" name="role" value={key} checked={selected === key}
|
||||
onChange={() => setSelected(key)} className="sr-only" />
|
||||
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: meta.dot }} />
|
||||
<span className="text-sm font-medium text-[var(--text)] flex-1">{meta.label}</span>
|
||||
{selected === key && <CheckCircleIcon className="w-4 h-4 text-primary-600 dark:text-primary-400" />}
|
||||
{selected === key && <CheckCircleIcon className="w-4 h-4 text-[var(--primary)] dark:text-[var(--primary)]" />}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -320,7 +314,7 @@ export default function UserDetailPage() {
|
||||
return (
|
||||
<div className="animate-slide-up">
|
||||
<div className="cp-card p-16 flex flex-col items-center gap-4 text-center">
|
||||
<div className="w-16 h-16 rounded-2xl bg-[var(--danger-bg)] dark:bg-red-400/10 flex items-center justify-center">
|
||||
<div className="w-16 h-16 rounded-2xl bg-[var(--danger-bg)] flex items-center justify-center">
|
||||
<XCircleIcon className="w-8 h-8 text-[var(--danger)]" />
|
||||
</div>
|
||||
<p className="font-semibold text-[var(--text)]">کاربر یافت نشد</p>
|
||||
@@ -356,14 +350,14 @@ export default function UserDetailPage() {
|
||||
<div className="cp-card overflow-hidden">
|
||||
|
||||
{/* Cover strip */}
|
||||
<div className="h-24 bg-gradient-to-l from-primary-600/20 via-primary-500/10 to-transparent dark:from-primary-500/20" />
|
||||
<div className="h-24 bg-gradient-to-l from-[color-mix(in_srgb,var(--primary)_20%,transparent)] via-[color-mix(in_srgb,var(--primary)_10%,transparent)] to-transparent dark:from-[color-mix(in_srgb,var(--primary)_20%,transparent)]" />
|
||||
|
||||
{/* Profile header */}
|
||||
<div className="px-6 pb-6 -mt-10 flex flex-col sm:flex-row sm:items-end gap-4">
|
||||
<BigAvatar name={user.name} id={user.id} />
|
||||
|
||||
<div className="flex-1 min-w-0 sm:mb-1">
|
||||
<h1 className="text-xl font-bold text-[var(--text)] dark:text-slate-50 leading-tight">
|
||||
<h1 className="text-xl font-bold text-[var(--text)] leading-tight">
|
||||
{user.name || <span className="text-[var(--text-3)] font-normal">نام ثبت نشده</span>}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
||||
@@ -372,12 +366,12 @@ export default function UserDetailPage() {
|
||||
{roleMeta.label}
|
||||
</span>
|
||||
{user.is_active ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--success-bg)] dark:bg-green-400/10 text-[var(--success)] dark:text-green-300">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />فعال
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--success-bg)] text-[var(--success)]">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--success)] animate-pulse" />فعال
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--surface-2)] dark:bg-slate-400/10 text-[var(--text-2)]">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-slate-400" />غیرفعال
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-2)]">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--text-3)]" />غیرفعال
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-[var(--text-3)]">#{user.id}</span>
|
||||
@@ -411,7 +405,7 @@ export default function UserDetailPage() {
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--text)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{user.is_active
|
||||
? <XCircleIcon className="w-4 h-4 text-orange-400 shrink-0" />
|
||||
? <XCircleIcon className="w-4 h-4 text-[var(--accent)] shrink-0" />
|
||||
: <CheckCircleIcon className="w-4 h-4 text-[var(--success)] shrink-0" />
|
||||
}
|
||||
{user.is_active ? 'غیرفعال کردن حساب' : 'فعالسازی حساب'}
|
||||
@@ -420,13 +414,13 @@ export default function UserDetailPage() {
|
||||
onClick={() => { setRoleOpen(true); setMenuOpen(false); }}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--text)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors"
|
||||
>
|
||||
<ShieldCheckIcon className="w-4 h-4 text-violet-400 shrink-0" />
|
||||
<ShieldCheckIcon className="w-4 h-4 text-[var(--violet)] shrink-0" />
|
||||
تغییر نقش کاربر
|
||||
</button>
|
||||
<div className="border-t border-[var(--border)] my-1" />
|
||||
<button
|
||||
onClick={() => { setDeleteOpen(true); setMenuOpen(false); }}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--danger)] dark:text-red-400 hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10 transition-colors"
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--danger)] hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10 transition-colors"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4 shrink-0" />
|
||||
حذف این کاربر
|
||||
@@ -469,7 +463,7 @@ export default function UserDetailPage() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setRoleOpen(true)}
|
||||
className="w-full mt-1 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right"
|
||||
className="w-full mt-1 text-xs text-[var(--primary)] dark:text-[var(--primary)] hover:underline text-right"
|
||||
>
|
||||
تغییر نقش ←
|
||||
</button>
|
||||
@@ -484,7 +478,7 @@ export default function UserDetailPage() {
|
||||
</p>
|
||||
<button
|
||||
onClick={() => { navigator.clipboard.writeText(user.uuid); toast.success('UUID کپی شد'); }}
|
||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--text-2)] dark:hover:text-[var(--text-3)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface-2)] transition-colors shrink-0"
|
||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--text-2)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface-2)] transition-colors shrink-0"
|
||||
>
|
||||
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -496,20 +490,20 @@ export default function UserDetailPage() {
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-[var(--text)]">وضعیت حساب</h2>
|
||||
{user.is_active
|
||||
? <span className="text-xs font-medium text-[var(--success)] dark:text-green-400">فعال</span>
|
||||
? <span className="text-xs font-medium text-[var(--success)]">فعال</span>
|
||||
: <span className="text-xs font-medium text-[var(--text-2)]">غیرفعال</span>
|
||||
}
|
||||
</div>
|
||||
<div className={`w-full h-2 rounded-full ${user.is_active ? 'bg-green-200 dark:bg-green-400/20' : 'bg-[var(--surface-3)] dark:bg-[var(--surface-2)]'}`}>
|
||||
<div className={`h-full rounded-full transition-all ${user.is_active ? 'w-full bg-green-500' : 'w-0'}`} />
|
||||
<div className={`w-full h-2 rounded-full ${user.is_active ? 'bg-[var(--success-bg)]' : 'bg-[var(--surface-3)]'}`}>
|
||||
<div className={`h-full rounded-full transition-all ${user.is_active ? 'w-full bg-[var(--success)]' : 'w-0'}`} />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => toggleStatusMut.mutate()}
|
||||
disabled={toggleStatusMut.isPending}
|
||||
className={`w-full mt-4 text-sm font-medium py-2 rounded-xl transition-colors disabled:opacity-50 ${
|
||||
user.is_active
|
||||
? 'bg-orange-50 dark:bg-orange-400/10 text-orange-600 dark:text-orange-400 hover:bg-orange-100 dark:hover:bg-orange-400/20'
|
||||
: 'bg-[var(--success-bg)] dark:bg-green-400/10 text-[var(--success)] dark:text-green-400 hover:bg-[var(--success-bg)] dark:hover:bg-green-400/20'
|
||||
? 'bg-[var(--accent-bg)] text-[var(--accent)] hover:bg-[var(--accent-bg)]'
|
||||
: 'bg-[var(--success-bg)] text-[var(--success)] hover:bg-[var(--success-bg)]'
|
||||
}`}
|
||||
>
|
||||
{toggleStatusMut.isPending
|
||||
@@ -561,14 +555,14 @@ export default function UserDetailPage() {
|
||||
.map(format)
|
||||
.filter((s) => s && s.trim());
|
||||
return (
|
||||
<div key={key} className="p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60">
|
||||
<div key={key} className="p-4 rounded-xl bg-[var(--surface-2)]">
|
||||
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-1.5">{label}</p>
|
||||
{items.length === 0 ? (
|
||||
<p className="text-sm text-[var(--text-3)]">موردی ثبت نشده</p>
|
||||
) : (
|
||||
<ul className="flex flex-wrap gap-1.5">
|
||||
{items.map((text, i) => (
|
||||
<li key={i} className="text-xs px-2 py-0.5 rounded-lg bg-[var(--surface)] text-[var(--text-2)] border border-[var(--border)] dark:border-gray-600">
|
||||
<li key={i} className="text-xs px-2 py-0.5 rounded-lg bg-[var(--surface)] text-[var(--text-2)] border border-[var(--border)]">
|
||||
{text}
|
||||
</li>
|
||||
))}
|
||||
@@ -624,10 +618,10 @@ export default function UserDetailPage() {
|
||||
<input
|
||||
type="text"
|
||||
placeholder="مثال: علی احمدی"
|
||||
className={`cp-input ${errors.name ? 'border-red-400 dark:border-red-500' : ''}`}
|
||||
className={`cp-input ${errors.name ? 'border-[var(--danger)]' : ''}`}
|
||||
{...register('name')}
|
||||
/>
|
||||
{errors.name && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.name.message}</p>}
|
||||
{errors.name && <p className="text-xs text-[var(--danger)] mt-1">{errors.name.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
@@ -639,10 +633,10 @@ export default function UserDetailPage() {
|
||||
type="email"
|
||||
placeholder="example@email.com"
|
||||
dir="ltr"
|
||||
className={`cp-input text-left ${errors.email ? 'border-red-400 dark:border-red-500' : ''}`}
|
||||
className={`cp-input text-left ${errors.email ? 'border-[var(--danger)]' : ''}`}
|
||||
{...register('email')}
|
||||
/>
|
||||
{errors.email && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.email.message}</p>}
|
||||
{errors.email && <p className="text-xs text-[var(--danger)] mt-1">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
@@ -657,11 +651,11 @@ export default function UserDetailPage() {
|
||||
type="password"
|
||||
placeholder="حداقل ۶ کاراکتر"
|
||||
dir="ltr"
|
||||
className={`cp-input pr-9 text-left ${errors.password ? 'border-red-400 dark:border-red-500' : ''}`}
|
||||
className={`cp-input pr-9 text-left ${errors.password ? 'border-[var(--danger)]' : ''}`}
|
||||
{...register('password')}
|
||||
/>
|
||||
</div>
|
||||
{errors.password && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.password.message}</p>}
|
||||
{errors.password && <p className="text-xs text-[var(--danger)] mt-1">{errors.password.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Mobile (read-only) */}
|
||||
@@ -675,7 +669,7 @@ export default function UserDetailPage() {
|
||||
value={user.mobile_number}
|
||||
readOnly
|
||||
dir="ltr"
|
||||
className="cp-input text-left bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 text-[var(--text-3)] cursor-not-allowed"
|
||||
className="cp-input text-left bg-[var(--surface-2)] text-[var(--text-3)] cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user