Files
marketing-app/client/src/components/MemberCard.jsx
T
fahed e1d1c392eb feat: comprehensive UI overhaul + budget allocation redesign
Audit & Quality:
- RTL: replaced 121 LTR-only utilities (text-left, pl-, left-) with logical properties
- A11y: focus traps + ARIA on Modal/SlidePanel/TabbedModal, clickable divs→buttons
- Theming: bg-white→bg-surface (170 instances), text-gray→semantic tokens
- Performance: useMemo on filters, loading="lazy" on 24 images
- CSS: prefers-reduced-motion, removed dead animations

Component Splits:
- PostDetailPanel: 1332→623 lines + 4 sub-components
- ArtefactDetailPanel: 972→590 lines + 1 sub-component

Brand Identity — Rawaj (رواج):
- New name, DM Sans font, deep teal palette (#0d9488)
- Custom SVG logo, forest-tinted dark mode
- All emails branded with app name in subject line

Design Refinement:
- Dashboard: merged posts+deadlines into tabbed ActivityFeed, inline stats
- Quieter: removed card lift, brand glow, gradient text, mesh backgrounds
- CampaignDetail: prominent budget card, compact team avatars, Lucide icons
- Consistent page titles via Header.jsx, standardized section headers
- Finance page fully i18n'd (20+ hardcoded strings replaced)

Budget Allocation Redesign:
- Single source of truth: BudgetEntries (Campaign.budget deprecated)
- Validation at all levels: main→campaign→track, expenses blocked if insufficient
- Budget request workflow with CEO approval via public link
- BudgetRequests table, CRUD routes, public approval page
- Budget mutex for race condition prevention
- Idempotent migration for existing campaign budgets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 15:36:19 +03:00

78 lines
3.2 KiB
React

import { getInitials } from '../utils/api'
import BrandBadge from './BrandBadge'
const ROLE_BADGES = {
manager: { bg: 'bg-indigo-50', text: 'text-indigo-700', label: 'Manager' },
approver: { bg: 'bg-emerald-50', text: 'text-emerald-700', label: 'Approver' },
publisher: { bg: 'bg-blue-50', text: 'text-blue-700', label: 'Publisher' },
content_creator: { bg: 'bg-amber-50', text: 'text-amber-700', label: 'Content Creator' },
producer: { bg: 'bg-purple-50', text: 'text-purple-700', label: 'Producer' },
designer: { bg: 'bg-pink-50', text: 'text-pink-700', label: 'Designer' },
content_writer: { bg: 'bg-orange-50', text: 'text-orange-700', label: 'Content Writer' },
social_media_manager: { bg: 'bg-teal-50', text: 'text-teal-700', label: 'Social Media Manager' },
photographer: { bg: 'bg-cyan-50', text: 'text-cyan-700', label: 'Photographer' },
videographer: { bg: 'bg-sky-50', text: 'text-sky-700', label: 'Videographer' },
strategist: { bg: 'bg-rose-50', text: 'text-rose-700', label: 'Strategist' },
superadmin: { bg: 'bg-red-50', text: 'text-red-700', label: 'Super Admin' },
contributor: { bg: 'bg-slate-50', text: 'text-slate-700', label: 'Contributor' },
default: { bg: 'bg-gray-50', text: 'text-text-secondary', label: 'Team Member' },
}
export default function MemberCard({ member, onClick }) {
const role = ROLE_BADGES[member.team_role || member.role] || ROLE_BADGES.default
const avatarColors = [
'from-indigo-400 to-purple-500',
'from-pink-400 to-rose-500',
'from-emerald-400 to-teal-500',
'from-amber-400 to-orange-500',
'from-cyan-400 to-blue-500',
]
const colorIndex = (member.name?.charCodeAt(0) || 0) % avatarColors.length
return (
<div
onClick={() => onClick?.(member)}
className="bg-surface rounded-xl border border-border p-5 card-hover cursor-pointer text-center"
>
{/* Avatar */}
<div className={`w-16 h-16 rounded-full bg-gradient-to-br ${avatarColors[colorIndex]} flex items-center justify-center text-white text-xl font-bold mx-auto mb-3`}>
{getInitials(member.name)}
</div>
{/* Name */}
<h4 className="text-base font-semibold text-text-primary">{member.name}</h4>
{/* Role badge */}
<span className={`inline-block text-xs font-medium px-2 py-0.5 rounded-full mt-1 ${role.bg} ${role.text}`}>
{role.label}
</span>
{/* Email */}
{member.email && (
<p className="text-xs text-text-tertiary mt-2">{member.email}</p>
)}
{/* Brands */}
{member.brands && member.brands.length > 0 && (
<div className="flex flex-wrap gap-1 justify-center mt-3 pt-3 border-t border-border-light">
{member.brands.map((brand) => (
<BrandBadge key={brand} brand={brand} />
))}
</div>
)}
{/* Teams */}
{member.teams && member.teams.length > 0 && (
<div className="flex flex-wrap gap-1 justify-center mt-2">
{member.teams.map((team) => (
<span key={team.id} className="text-[10px] px-2 py-0.5 rounded-full bg-blue-50 text-blue-600 font-medium">
{team.name}
</span>
))}
</div>
)}
</div>
)
}