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, useMemo, useRef, useCallback } from 'react';
|
||||
import { avatarGradient } from '../lib/avatarColors';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
@@ -84,18 +85,13 @@ const DEGREE_OPTIONS = [
|
||||
{ value: 'subspecialistplus', label: 'فلوشیپ' },
|
||||
];
|
||||
const GENDER_OPTIONS = [{ value: 'man', label: 'مرد' }, { value: 'woman', label: 'زن' }];
|
||||
const AVATAR_COLORS = [
|
||||
'from-blue-500 to-cyan-500', 'from-violet-500 to-purple-600',
|
||||
'from-emerald-500 to-teal-600', 'from-rose-500 to-pink-600',
|
||||
'from-amber-500 to-orange-600',
|
||||
];
|
||||
const SPECIALTY_COLORS = [
|
||||
'bg-[var(--danger-bg)] dark:bg-red-400/10 text-[var(--danger)] dark:text-red-300',
|
||||
'bg-[var(--info-bg)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300',
|
||||
'bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300',
|
||||
'bg-violet-100 dark:bg-violet-400/10 text-violet-700 dark:text-violet-300',
|
||||
'bg-orange-100 dark:bg-orange-400/10 text-orange-700 dark:text-orange-300',
|
||||
'bg-pink-100 dark:bg-pink-400/10 text-pink-700 dark:text-pink-300',
|
||||
'bg-[var(--danger-bg)] text-[var(--danger)]',
|
||||
'bg-[var(--info-bg)] text-[var(--info)]',
|
||||
'bg-[var(--success-bg)] text-[var(--success)]',
|
||||
'bg-[var(--violet-bg)] text-[var(--violet)]',
|
||||
'bg-[var(--accent-bg)] text-[var(--accent)]',
|
||||
'bg-[var(--violet-bg)] text-[var(--violet)]',
|
||||
];
|
||||
|
||||
// ── Image upload ───────────────────────────────────────────────────────────
|
||||
@@ -135,8 +131,8 @@ function DoctorAvatar({ name, img, idx, onUpload, uploading }: {
|
||||
onClick={() => onUpload && ref.current?.click()}
|
||||
>
|
||||
{img
|
||||
? <img src={img} alt={name} className="w-24 h-24 rounded-2xl object-cover shadow-xl ring-4 ring-white dark:ring-gray-900" />
|
||||
: <div className={`w-24 h-24 rounded-2xl bg-gradient-to-br ${AVATAR_COLORS[idx % AVATAR_COLORS.length]} flex items-center justify-center text-[var(--on-primary)] text-3xl font-bold shadow-xl ring-4 ring-white dark:ring-gray-900`}>{initials}</div>
|
||||
? <img src={img} alt={name} className="w-24 h-24 rounded-2xl object-cover shadow-xl ring-4 ring-[var(--surface)]" />
|
||||
: <div className={`w-24 h-24 rounded-2xl bg-gradient-to-br ${avatarGradient(idx)} flex items-center justify-center text-[var(--on-primary)] text-3xl font-bold shadow-xl ring-4 ring-[var(--surface)]`}>{initials}</div>
|
||||
}
|
||||
{onUpload && (
|
||||
<div className="absolute inset-0 rounded-2xl bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
@@ -161,13 +157,13 @@ function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }:
|
||||
mono?: boolean; copyable?: boolean;
|
||||
}) {
|
||||
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 h-4 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)] ${mono ? 'font-mono' : ''}`} dir={mono ? 'ltr' : undefined}>
|
||||
<p className={`text-sm font-medium text-[var(--text)] ${mono ? 'font-mono' : ''}`} dir={mono ? 'ltr' : undefined}>
|
||||
{value ?? <span className="text-[var(--text-3)] font-normal">ثبت نشده</span>}
|
||||
</p>
|
||||
</div>
|
||||
@@ -186,7 +182,7 @@ function StarRating({ rate }: { rate: number }) {
|
||||
<div className="flex items-center gap-1" dir="ltr">
|
||||
{Array.from({ length: 5 }).map((_, i) =>
|
||||
i < Math.round(rate)
|
||||
? <StarSolid key={i} className="w-4 h-4 text-amber-400" />
|
||||
? <StarSolid key={i} className="w-4 h-4 text-[var(--warning)]" />
|
||||
: <StarIcon key={i} className="w-4 h-4 text-[var(--text-3)]" />
|
||||
)}
|
||||
<span className="text-sm font-medium text-[var(--text-2)] mr-1">{rate.toFixed(1)}</span>
|
||||
@@ -306,7 +302,7 @@ function SearchableSelect({ options, value, onChange, placeholder, disabled = fa
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={openDropdown}
|
||||
className={`cp-input h-11 flex items-center justify-between text-right w-full ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} ${!selected ? 'text-[var(--text-3)] dark:text-[var(--text-2)]' : ''}`}
|
||||
className={`cp-input h-11 flex items-center justify-between text-right w-full ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} ${!selected ? 'text-[var(--text-3)]' : ''}`}
|
||||
>
|
||||
<span className="truncate flex-1">{selected?.label ?? placeholder ?? 'انتخاب کنید'}</span>
|
||||
<ChevronDownIcon className={`w-4 h-4 shrink-0 mr-2 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||
@@ -328,7 +324,7 @@ function SearchableSelect({ options, value, onChange, placeholder, disabled = fa
|
||||
{filtered.map(o => (
|
||||
<button key={o.value} type="button"
|
||||
onClick={() => { onChange(o.value, o.label); setOpen(false); setQ(''); }}
|
||||
className={`w-full text-right px-3 py-2 text-sm hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] ${value === o.value ? 'text-primary-600 dark:text-primary-400 font-medium bg-primary-50 dark:bg-primary-500/10' : 'text-[var(--text)] dark:text-[var(--text-3)]'}`}>
|
||||
className={`w-full text-right px-3 py-2 text-sm hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] ${value === o.value ? 'text-[var(--primary)] dark:text-[var(--primary)] font-medium bg-[var(--primary-soft)] dark:bg-[color-mix(in_srgb,var(--primary)_10%,transparent)]' : 'text-[var(--text)]'}`}>
|
||||
{o.label}
|
||||
</button>
|
||||
))}
|
||||
@@ -380,16 +376,16 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
|
||||
return (
|
||||
<div className="border border-[var(--border)] rounded-xl overflow-hidden">
|
||||
<div className="p-2 border-b border-[var(--border)] bg-[var(--surface-2)] dark:bg-[var(--surface)]/60">
|
||||
<div className="p-2 border-b border-[var(--border)] bg-[var(--surface-2)]">
|
||||
<input type="text" value={q} onChange={e => setQ(e.target.value)} placeholder="جستجوی تخصص..."
|
||||
className="cp-input text-sm h-8" />
|
||||
</div>
|
||||
{selectedLabels.length > 0 && (
|
||||
<div className="px-3 py-2 border-b border-[var(--border)] flex flex-wrap gap-1">
|
||||
{selectedLabels.map(({ id, name }) => (
|
||||
<span key={id} className="inline-flex items-center gap-1 text-xs px-2.5 py-0.5 rounded-full bg-primary-100 dark:bg-primary-500/20 text-primary-700 dark:text-primary-300">
|
||||
<span key={id} className="inline-flex items-center gap-1 text-xs px-2.5 py-0.5 rounded-full bg-[var(--primary-soft2)] dark:bg-[color-mix(in_srgb,var(--primary)_20%,transparent)] text-[var(--primary-700)] dark:text-[var(--primary)]">
|
||||
{name}
|
||||
<button type="button" onClick={() => toggleSelect(id)} className="hover:text-primary-900 dark:hover:text-primary-100 leading-none">×</button>
|
||||
<button type="button" onClick={() => toggleSelect(id)} className="hover:text-[var(--primary-700)] dark:hover:text-[var(--primary-soft2)] leading-none">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -400,7 +396,7 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
? filteredFlat.map(s => (
|
||||
<label key={s.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
||||
<input type="checkbox" checked={selected.includes(s.id)} onChange={() => toggleSelect(s.id)}
|
||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 shrink-0" />
|
||||
className="rounded border-[var(--border-2)] text-[var(--primary)] shrink-0" />
|
||||
<span className="text-sm text-[var(--text)]">{s.name}</span>
|
||||
{s.parent_id !== null && (
|
||||
<span className="text-[10px] text-[var(--text-3)] mr-auto truncate max-w-[100px]">
|
||||
@@ -424,13 +420,13 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
? <ChevronDownIcon className="w-4 h-4 text-[var(--text-3)] shrink-0" />
|
||||
: <ChevronRightIcon className="w-4 h-4 text-[var(--text-3)] shrink-0" />)
|
||||
: <input type="checkbox" checked={selected.includes(parent.id)} readOnly
|
||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 pointer-events-none shrink-0" />
|
||||
className="rounded border-[var(--border-2)] text-[var(--primary)] pointer-events-none shrink-0" />
|
||||
}
|
||||
<span className={`text-sm font-medium ${numSel > 0 ? 'text-primary-600 dark:text-primary-400' : 'text-[var(--text)] dark:text-[var(--text-3)]'}`}>
|
||||
<span className={`text-sm font-medium ${numSel > 0 ? 'text-[var(--primary)] dark:text-[var(--primary)]' : 'text-[var(--text)]'}`}>
|
||||
{parent.name}
|
||||
</span>
|
||||
{numSel > 0 && (
|
||||
<span className="mr-auto text-[10px] bg-primary-100 dark:bg-primary-500/20 text-primary-700 dark:text-primary-300 px-1.5 py-0.5 rounded-full">
|
||||
<span className="mr-auto text-[10px] bg-[var(--primary-soft2)] dark:bg-[color-mix(in_srgb,var(--primary)_20%,transparent)] text-[var(--primary-700)] dark:text-[var(--primary)] px-1.5 py-0.5 rounded-full">
|
||||
{numSel} انتخاب
|
||||
</span>
|
||||
)}
|
||||
@@ -438,7 +434,7 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
{isOpen && children.map(child => (
|
||||
<label key={child.id} className="flex items-center gap-2.5 px-3 py-2 pr-9 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
||||
<input type="checkbox" checked={selected.includes(child.id)} onChange={() => toggleSelect(child.id)}
|
||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 shrink-0" />
|
||||
className="rounded border-[var(--border-2)] text-[var(--primary)] shrink-0" />
|
||||
<span className="text-sm text-[var(--text-2)]">{child.name}</span>
|
||||
</label>
|
||||
))}
|
||||
@@ -487,7 +483,7 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
||||
|
||||
if (selectedSpecialtyIds.length === 0) {
|
||||
return (
|
||||
<div className="rounded-xl border border-dashed border-[var(--border-2)] dark:border-gray-600 py-8 text-center text-[var(--text-3)] text-sm">
|
||||
<div className="rounded-xl border border-dashed border-[var(--border-2)] py-8 text-center text-[var(--text-3)] text-sm">
|
||||
ابتدا تخصصهایی را انتخاب کنید تا خدمات نمایش داده شوند
|
||||
</div>
|
||||
);
|
||||
@@ -495,7 +491,7 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
||||
|
||||
if (relevantServices.length === 0) {
|
||||
return (
|
||||
<div className="rounded-xl border border-dashed border-[var(--border-2)] dark:border-gray-600 py-8 text-center text-[var(--text-3)] text-sm">
|
||||
<div className="rounded-xl border border-dashed border-[var(--border-2)] py-8 text-center text-[var(--text-3)] text-sm">
|
||||
خدمتی برای تخصصهای انتخابشده یافت نشد
|
||||
</div>
|
||||
);
|
||||
@@ -504,11 +500,11 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
||||
return (
|
||||
<div className="border border-[var(--border)] rounded-xl overflow-hidden">
|
||||
{tabs.length > 1 && (
|
||||
<div className="flex gap-1 px-2 py-2 border-b border-[var(--border)] bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 overflow-x-auto">
|
||||
<div className="flex gap-1 px-2 py-2 border-b border-[var(--border)] bg-[var(--surface-2)] overflow-x-auto">
|
||||
{[{ id: 'all' as const, name: 'همه' }, ...tabs].map(t => (
|
||||
<button key={String(t.id)} type="button"
|
||||
onClick={() => setActiveTab(t.id as number | 'all')}
|
||||
className={`px-2.5 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition-colors ${activeTab === t.id ? 'bg-primary-600 text-[var(--on-primary)]' : 'text-[var(--text-2)] dark:text-[var(--text-3)] hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)]'}`}>
|
||||
className={`px-2.5 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition-colors ${activeTab === t.id ? 'bg-[var(--primary)] text-[var(--on-primary)]' : 'text-[var(--text-2)] hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)]'}`}>
|
||||
{t.name}
|
||||
</button>
|
||||
))}
|
||||
@@ -521,9 +517,9 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
||||
{selectedLabels.length > 0 && (
|
||||
<div className="px-3 py-2 border-b border-[var(--border)] flex flex-wrap gap-1">
|
||||
{selectedLabels.map(({ id, name }) => (
|
||||
<span key={id} className="inline-flex items-center gap-1 text-xs px-2.5 py-0.5 rounded-full bg-emerald-100 dark:bg-emerald-500/20 text-emerald-700 dark:text-emerald-300">
|
||||
<span key={id} className="inline-flex items-center gap-1 text-xs px-2.5 py-0.5 rounded-full bg-[var(--success-bg)] text-[var(--success)]">
|
||||
{name}
|
||||
<button type="button" onClick={() => toggle(id)} className="hover:text-emerald-900 dark:hover:text-emerald-100 leading-none">×</button>
|
||||
<button type="button" onClick={() => toggle(id)} className="hover:text-[var(--success)] dark:hover:text-[var(--success)] leading-none">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -532,7 +528,7 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
||||
{visibleServices.map(svc => (
|
||||
<label key={svc.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
||||
<input type="checkbox" checked={selected.includes(svc.id)} onChange={() => toggle(svc.id)}
|
||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-[var(--success)] shrink-0" />
|
||||
className="rounded border-[var(--border-2)] text-[var(--success)] shrink-0" />
|
||||
<span className="text-sm text-[var(--text)]">{svc.name}</span>
|
||||
</label>
|
||||
))}
|
||||
@@ -741,7 +737,7 @@ function AddressCard({ addr, onEdit, onDelete, readOnly = false }: {
|
||||
addr: AddressData; onEdit: () => void; onDelete: () => void; readOnly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 border border-[var(--border)] group">
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] border border-[var(--border)] group">
|
||||
<div className="w-9 h-9 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0 mt-0.5">
|
||||
<MapPinIcon className="w-4 h-4 text-[var(--text-3)]" />
|
||||
</div>
|
||||
@@ -769,7 +765,7 @@ function AddressCard({ addr, onEdit, onDelete, readOnly = false }: {
|
||||
{!readOnly && (
|
||||
<div className="opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity shrink-0">
|
||||
<button onClick={onEdit}
|
||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-blue-500/10 transition-colors">
|
||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-[var(--info)]/10 transition-colors">
|
||||
<PencilIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button onClick={onDelete}
|
||||
@@ -1362,7 +1358,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
if (isError || !doctor) {
|
||||
return (
|
||||
<div className="animate-slide-up 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>
|
||||
@@ -1407,7 +1403,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
|
||||
{/* Profile Card */}
|
||||
<div className="cp-card overflow-hidden">
|
||||
<div className="h-28 bg-gradient-to-l from-blue-600/20 via-blue-500/10 to-transparent dark:from-blue-500/20" />
|
||||
<div className="h-28 bg-gradient-to-l from-[color-mix(in_srgb,var(--info)_20%,transparent)] via-[color-mix(in_srgb,var(--info)_10%,transparent)] to-transparent dark:from-[color-mix(in_srgb,var(--info)_20%,transparent)]" />
|
||||
<div className="px-6 pb-6 -mt-12 flex flex-col sm:flex-row sm:items-end gap-4">
|
||||
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
@@ -1427,11 +1423,11 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 sm:mb-1">
|
||||
<h1 className="text-xl font-bold text-[var(--text)] dark:text-slate-50">{displayDoctorName(doctor.name)}</h1>
|
||||
<h1 className="text-xl font-bold text-[var(--text)]">{displayDoctorName(doctor.name)}</h1>
|
||||
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
||||
{/* Active status badge */}
|
||||
{doctor.active
|
||||
? <span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300">
|
||||
? <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)]">
|
||||
<CheckCircleIcon className="w-3 h-3" />فعال
|
||||
</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)] text-[var(--text-2)]">
|
||||
@@ -1439,7 +1435,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
</span>
|
||||
}
|
||||
{doctor.degree && (
|
||||
<span className="inline-flex text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--info-bg)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300">{degreeLabel}</span>
|
||||
<span className="inline-flex text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--info-bg)] text-[var(--info)]">{degreeLabel}</span>
|
||||
)}
|
||||
{doctor.gender && (
|
||||
<span className="text-xs text-[var(--text-2)]">{doctor.gender === 'man' ? '♂ مرد' : '♀ زن'}</span>
|
||||
@@ -1455,7 +1451,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
{doctor.expertise.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{doctor.expertise.map(e => (
|
||||
<span key={e.id} className="text-[11px] px-2 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-2)] border border-[var(--border)] dark:border-gray-600">{e.name}</span>
|
||||
<span key={e.id} className="text-[11px] px-2 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-2)] border border-[var(--border)]">{e.name}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -1476,8 +1472,8 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
disabled={toggleMut.isPending}
|
||||
className={`inline-flex items-center gap-1.5 text-sm font-medium px-3 h-10 rounded-xl border transition-colors disabled:opacity-50 ${
|
||||
doctor.active
|
||||
? 'border-orange-300 dark:border-orange-500/40 text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-500/10 hover:bg-orange-100 dark:hover:bg-orange-500/20'
|
||||
: 'border-emerald-300 dark:border-emerald-500/40 text-[var(--success)] dark:text-emerald-400 bg-[var(--success-bg)] dark:bg-emerald-500/10 hover:bg-emerald-100 dark:hover:bg-emerald-500/20'
|
||||
? 'border-[var(--accent)] text-[var(--accent)] bg-[var(--accent-bg)] hover:bg-[var(--accent-bg)]'
|
||||
: 'border-[var(--success)] text-[var(--success)] bg-[var(--success-bg)] hover:bg-[var(--success-bg)]'
|
||||
}`}
|
||||
>
|
||||
{toggleMut.isPending
|
||||
@@ -1497,7 +1493,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
{menuOpen && (
|
||||
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-[var(--border)] py-1.5 animate-scale-in" data-dmenu>
|
||||
<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">
|
||||
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">
|
||||
<TrashIcon className="w-4 h-4 shrink-0" />حذف این پزشک
|
||||
</button>
|
||||
</div>
|
||||
@@ -1556,7 +1552,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
</h2>
|
||||
{!isReadOnly && (
|
||||
<button onClick={() => { setEditingAddr(null); setAddrModalOpen(true); }}
|
||||
className="flex items-center gap-1.5 text-xs font-medium text-primary-600 dark:text-primary-400 hover:underline">
|
||||
className="flex items-center gap-1.5 text-xs font-medium text-[var(--primary)] dark:text-[var(--primary)] hover:underline">
|
||||
<PlusIcon className="w-4 h-4" />افزودن آدرس
|
||||
</button>
|
||||
)}
|
||||
@@ -1576,7 +1572,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
<p className="text-sm">هنوز آدرسی ثبت نشده است</p>
|
||||
{!isReadOnly && (
|
||||
<button onClick={() => { setEditingAddr(null); setAddrModalOpen(true); }}
|
||||
className="mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline">
|
||||
className="mt-3 text-xs text-[var(--primary)] dark:text-[var(--primary)] hover:underline">
|
||||
اولین آدرس را اضافه کنید
|
||||
</button>
|
||||
)}
|
||||
@@ -1595,7 +1591,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{doctor.clinics.map(c => (
|
||||
<div key={c.uuid} className="flex items-start gap-3 p-3 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60">
|
||||
<div key={c.uuid} className="flex items-start gap-3 p-3 rounded-xl bg-[var(--surface-2)]">
|
||||
<div className="w-8 h-8 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0">
|
||||
<BuildingOfficeIcon className="w-4 h-4 text-[var(--text-3)]" />
|
||||
</div>
|
||||
@@ -1625,7 +1621,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
: <p className="text-sm text-[var(--text-3)]">تخصصی ثبت نشده</p>
|
||||
}
|
||||
{!isReadOnly && (
|
||||
<button onClick={() => setEditOpen(true)} className="w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right">
|
||||
<button onClick={() => setEditOpen(true)} className="w-full mt-3 text-xs text-[var(--primary)] dark:text-[var(--primary)] hover:underline text-right">
|
||||
ویرایش تخصصها ←
|
||||
</button>
|
||||
)}
|
||||
@@ -1644,7 +1640,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
: <p className="text-sm text-[var(--text-3)]">خدمتی ثبت نشده</p>
|
||||
}
|
||||
{!isReadOnly && (
|
||||
<button onClick={() => setEditOpen(true)} className="w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right">
|
||||
<button onClick={() => setEditOpen(true)} className="w-full mt-3 text-xs text-[var(--primary)] dark:text-[var(--primary)] hover:underline text-right">
|
||||
ویرایش خدمات ←
|
||||
</button>
|
||||
)}
|
||||
@@ -1661,7 +1657,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
<p className="text-xs text-[var(--text-2)] mb-1">درصد رضایت</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 rounded-full bg-[var(--surface-3)]">
|
||||
<div className="h-full rounded-full bg-amber-400 transition-all" style={{ width: `${doctor.satisfaction}%` }} />
|
||||
<div className="h-full rounded-full bg-[var(--warning)] transition-all" style={{ width: `${doctor.satisfaction}%` }} />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-[var(--text)]">{doctor.satisfaction}%</span>
|
||||
</div>
|
||||
|
||||
@@ -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