e1d1c392eb
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>
123 lines
5.5 KiB
React
123 lines
5.5 KiB
React
import { useContext } from 'react'
|
|
import { format } from 'date-fns'
|
|
import { ArrowRight } from 'lucide-react'
|
|
import { getInitials } from '../utils/api'
|
|
import { useLanguage } from '../i18n/LanguageContext'
|
|
import { AppContext } from '../App'
|
|
import BrandBadge from './BrandBadge'
|
|
import StatusBadge from './StatusBadge'
|
|
import { PlatformIcons } from './PlatformIcon'
|
|
|
|
export default function PostCard({ post, onClick, onMove, compact = false, checkboxSlot }) {
|
|
const { t } = useLanguage()
|
|
const { getBrandName } = useContext(AppContext)
|
|
const brandName = getBrandName(post.brand_id || post.brandId) || post.brand_name || post.brand
|
|
// Support both single platform and platforms array
|
|
const platforms = post.platforms?.length > 0
|
|
? post.platforms
|
|
: (post.platform ? [post.platform] : [])
|
|
|
|
const assigneeName = post.assignedToName || post.assignedName || post.assigned_name || (typeof post.assignedTo === 'object' ? post.assignedTo?.name : null)
|
|
|
|
if (compact) {
|
|
return (
|
|
<div
|
|
onClick={onClick}
|
|
className="bg-surface rounded-lg p-3 border border-border hover:border-brand-primary/30 cursor-pointer transition-all group overflow-hidden card-hover"
|
|
>
|
|
{post.thumbnail_url && (
|
|
<div className="w-[calc(100%+1.5rem)] h-32 -mx-3 -mt-3 mb-2 rounded-t-lg overflow-hidden">
|
|
<img src={post.thumbnail_url} alt="" className="w-full h-full object-cover" loading="lazy" />
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-start justify-between gap-2 mb-2">
|
|
<h5 className="text-sm font-medium text-text-primary line-clamp-2 leading-snug">{post.title}</h5>
|
|
<div className="shrink-0 mt-0.5">
|
|
<PlatformIcons platforms={platforms} size={14} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
{brandName && <BrandBadge brand={brandName} />}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between mt-3 pt-2 border-t border-border-light">
|
|
{assigneeName ? (
|
|
<div className="flex items-center gap-1.5">
|
|
<div className="w-5 h-5 rounded-full bg-brand-primary/10 text-brand-primary flex items-center justify-center text-[10px] font-semibold">
|
|
{getInitials(assigneeName)}
|
|
</div>
|
|
<span className="text-xs text-text-tertiary">{assigneeName}</span>
|
|
</div>
|
|
) : (
|
|
<span className="text-xs text-text-tertiary">{t('common.unassigned')}</span>
|
|
)}
|
|
|
|
{post.scheduledDate && (
|
|
<span className="text-[10px] text-text-tertiary">
|
|
{format(new Date(post.scheduledDate), 'MMM d')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Quick move buttons */}
|
|
{onMove && (
|
|
<div className="hidden group-hover:flex items-center gap-1 mt-2 pt-2 border-t border-border-light">
|
|
{post.status === 'draft' && (
|
|
<button onClick={(e) => { e.stopPropagation(); onMove(post._id, 'in_review') }}
|
|
className="text-[10px] text-amber-600 hover:bg-amber-50 px-2 py-0.5 rounded-full flex items-center gap-1">
|
|
{t('posts.sendToReview')} <ArrowRight className="w-3 h-3" />
|
|
</button>
|
|
)}
|
|
{post.status === 'in_review' && (
|
|
<button onClick={(e) => { e.stopPropagation(); onMove(post._id, 'approved') }}
|
|
className="text-[10px] text-blue-600 hover:bg-blue-50 px-2 py-0.5 rounded-full flex items-center gap-1">
|
|
{t('posts.approve')} <ArrowRight className="w-3 h-3" />
|
|
</button>
|
|
)}
|
|
{post.status === 'approved' && (
|
|
<button onClick={(e) => { e.stopPropagation(); onMove(post._id, 'scheduled') }}
|
|
className="text-[10px] text-purple-600 hover:bg-purple-50 px-2 py-0.5 rounded-full flex items-center gap-1">
|
|
{t('posts.schedule')} <ArrowRight className="w-3 h-3" />
|
|
</button>
|
|
)}
|
|
{post.status === 'scheduled' && (
|
|
<button onClick={(e) => { e.stopPropagation(); onMove(post._id, 'published') }}
|
|
className="text-[10px] text-emerald-600 hover:bg-emerald-50 px-2 py-0.5 rounded-full flex items-center gap-1">
|
|
{t('posts.publish')} <ArrowRight className="w-3 h-3" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Table row view
|
|
return (
|
|
<tr onClick={onClick} className="hover:bg-surface-secondary cursor-pointer group">
|
|
{checkboxSlot && <td className="w-10 px-3 py-3" onClick={e => e.stopPropagation()}>{checkboxSlot}</td>}
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="shrink-0">
|
|
<PlatformIcons platforms={platforms} size={16} />
|
|
</div>
|
|
<span className="text-sm font-medium text-text-primary">{post.title}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">{brandName && <BrandBadge brand={brandName} />}</td>
|
|
<td className="px-4 py-3"><StatusBadge status={post.status} /></td>
|
|
<td className="px-4 py-3">
|
|
<PlatformIcons platforms={platforms} size={16} gap="gap-1.5" />
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<span className="text-sm text-text-secondary">{assigneeName || '—'}</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-text-tertiary">
|
|
{post.scheduledDate ? format(new Date(post.scheduledDate), 'MMM d, yyyy') : '—'}
|
|
</td>
|
|
</tr>
|
|
)
|
|
}
|