Files
marketing-app/client/src/components/SkeletonLoader.jsx
T
fahed ce4d6025d7 feat: post composition redesign + budget allocation + brand identity (Rawaj)
Post Workflow:
- PostDetail full page (/posts/:id) replaces slide panel approach
- Post = 1 Caption Copy + 1 Body Copy + 1 Design + 0-1 Video
- copy_type field on Translations (caption/body)
- Composition endpoint returns rich data (content preview, languages, thumbnails)
- Stage auto-advances on translation/artefact changes (both link and unlink)
- "Translations" renamed to "Copy" in navigation
- GET /api/posts/:id, /api/translations/:id, /api/artefacts/:id routes added
- PostProduction: "New Post" creates → navigates to full page
- CampaignDetail: click post → navigates to full page
- Inline link picker (no modals) with search + rich item display
- PostComposition sub-components for caption, copy, designs, video, formats, readiness

Budget Allocation:
- Single source of truth: BudgetEntries (Campaign.budget deprecated)
- Budget mutex for race conditions
- Validation at all levels (main → campaign → track, expenses)
- CEO approval workflow: BudgetRequests table, public approval page
- Finance page: request budget UI, budget requests section
- Settings: CEO email field
- All emails branded with "Rawaj —" prefix

Brand Identity:
- Name: Rawaj (رواج) — trending/virality
- Deep teal palette (#0d9488), forest-tinted dark mode
- DM Sans font, custom SVG logo
- Consistent across login, sidebar, emails, public pages

Approval Workflow:
- Single reviewer per artefact (not multi-select)
- Reviewer redirect on public review page
- Server blocks submit-review without reviewer
- Review URLs use APP_URL (not server URL)

UI/UX:
- Scroll clipping fix: Modal, TabbedModal, SlidePanel restructured
  to avoid overflow-y-auto clipping native select dropdowns
- section-card overflow-hidden → overflow-clip
- All page titles via Header.jsx (removed duplicate h1s)
- CampaignDetail redesigned: prominent budget card, compact team

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

162 lines
6.1 KiB
React

// Reusable skeleton components for loading states
export function SkeletonCard() {
return (
<div className="bg-surface rounded-xl border border-border p-5 animate-pulse">
<div className="h-4 bg-surface-tertiary rounded w-3/4 mb-3"></div>
<div className="h-3 bg-surface-tertiary rounded w-1/2 mb-2"></div>
<div className="h-3 bg-surface-tertiary rounded w-2/3"></div>
</div>
)
}
export function SkeletonStatCard() {
return (
<div className="bg-surface rounded-xl border border-border p-5 animate-pulse">
<div className="flex items-start justify-between mb-4">
<div className="w-10 h-10 bg-surface-tertiary rounded-lg"></div>
<div className="h-3 bg-surface-tertiary rounded w-16"></div>
</div>
<div className="h-8 bg-surface-tertiary rounded w-20 mb-2"></div>
<div className="h-3 bg-surface-tertiary rounded w-24"></div>
</div>
)
}
export function SkeletonTable({ rows = 5, cols = 6 }) {
return (
<div className="bg-surface rounded-xl border border-border overflow-clip animate-pulse">
<div className="border-b border-border bg-surface-secondary p-4">
<div className="flex gap-4">
{[...Array(cols)].map((_, i) => (
<div key={i} className="h-3 bg-surface-tertiary rounded w-20"></div>
))}
</div>
</div>
<div className="divide-y divide-border-light">
{[...Array(rows)].map((_, i) => (
<div key={i} className="p-4">
<div className="flex gap-4">
{[...Array(cols)].map((_, j) => (
<div key={j} className="h-4 bg-surface-tertiary rounded flex-1"></div>
))}
</div>
</div>
))}
</div>
</div>
)
}
export function SkeletonKanbanBoard() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4">
{[...Array(5)].map((_, colIdx) => (
<div key={colIdx} className="animate-pulse">
<div className="flex items-center gap-2 mb-3">
<div className="w-2.5 h-2.5 bg-surface-tertiary rounded-full"></div>
<div className="h-4 bg-surface-tertiary rounded w-24"></div>
<div className="h-5 bg-surface-tertiary rounded-full w-8"></div>
</div>
<div className="bg-surface-secondary rounded-xl p-2 space-y-2 min-h-[400px]">
{[...Array(3)].map((_, cardIdx) => (
<div key={cardIdx} className="bg-surface rounded-lg border border-border p-3">
<div className="h-4 bg-surface-tertiary rounded w-full mb-2"></div>
<div className="h-3 bg-surface-tertiary rounded w-3/4 mb-3"></div>
<div className="flex gap-2">
<div className="h-5 bg-surface-tertiary rounded w-16"></div>
<div className="h-5 bg-surface-tertiary rounded w-20"></div>
</div>
</div>
))}
</div>
</div>
))}
</div>
)
}
export function SkeletonCalendar() {
return (
<div className="bg-surface rounded-xl border border-border overflow-clip animate-pulse">
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<div className="h-6 bg-surface-tertiary rounded w-40"></div>
<div className="h-8 bg-surface-tertiary rounded w-20"></div>
</div>
<div className="grid grid-cols-7 border-b border-border bg-surface-secondary">
{[...Array(7)].map((_, i) => (
<div key={i} className="text-center py-3">
<div className="h-3 bg-surface-tertiary rounded w-8 mx-auto"></div>
</div>
))}
</div>
<div className="grid grid-cols-7">
{[...Array(35)].map((_, i) => (
<div key={i} className="border-r border-b border-border min-h-[100px] p-2">
<div className="h-5 w-5 bg-surface-tertiary rounded-full mb-2"></div>
<div className="space-y-1">
<div className="h-3 bg-surface-tertiary rounded w-full"></div>
{i % 3 === 0 && <div className="h-3 bg-surface-tertiary rounded w-3/4"></div>}
</div>
</div>
))}
</div>
</div>
)
}
export function SkeletonAssetGrid({ count = 10 }) {
return (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
{[...Array(count)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="aspect-square bg-surface-tertiary rounded-xl"></div>
<div className="mt-2 h-3 bg-surface-tertiary rounded w-3/4"></div>
<div className="mt-1 h-3 bg-surface-tertiary rounded w-1/2"></div>
</div>
))}
</div>
)
}
export function SkeletonDashboard() {
return (
<div className="space-y-6">
{/* Header */}
<div className="animate-pulse">
<div className="h-8 w-64 bg-surface-tertiary rounded-lg mb-2"></div>
<div className="h-4 w-48 bg-surface-tertiary rounded"></div>
</div>
{/* Stat cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<SkeletonStatCard key={i} />
))}
</div>
{/* Content cards */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{[...Array(2)].map((_, i) => (
<div key={i} className="bg-surface rounded-xl border border-border animate-pulse">
<div className="px-5 py-4 border-b border-border">
<div className="h-5 bg-surface-tertiary rounded w-32"></div>
</div>
<div className="divide-y divide-border-light">
{[...Array(5)].map((_, j) => (
<div key={j} className="px-5 py-3 flex gap-3">
<div className="flex-1 space-y-2">
<div className="h-4 bg-surface-tertiary rounded w-2/3"></div>
<div className="h-3 bg-surface-tertiary rounded w-1/2"></div>
</div>
<div className="h-6 bg-surface-tertiary rounded w-16"></div>
</div>
))}
</div>
</div>
))}
</div>
</div>
)
}