Marketing Hub: RBAC, i18n (AR/EN), tasks overhaul, team/user merge, tutorial

Features:
- Full RBAC with 3 roles (superadmin/manager/contributor)
- Ownership tracking on posts, tasks, campaigns, projects
- Task system: assign to anyone, filter combobox, visibility scoping
- Team members merged into users table (single source of truth)
- Post thumbnails on kanban cards from attachments
- Publication link validation before publishing
- Interactive onboarding tutorial with Settings restart
- Full Arabic/English i18n with RTL layout support
- Language toggle in sidebar, IBM Plex Sans Arabic font
- Brand-based visibility filtering for non-superadmins
- Manager can only create contributors
- Profile completion flow for new users
- Cookie-based sessions (express-session + SQLite)
This commit is contained in:
fahed
2026-02-08 20:46:58 +03:00
commit 35d84b6bff
2240 changed files with 846749 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
import { useState, useMemo } from 'react'
import {
startOfMonth, endOfMonth, startOfWeek, endOfWeek,
eachDayOfInterval, format, isSameMonth, isToday,
addMonths, subMonths, isBefore, isAfter, isSameDay
} from 'date-fns'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { getBrandColor } from '../utils/api'
const CAMPAIGN_COLORS = [
'bg-indigo-400', 'bg-pink-400', 'bg-emerald-400', 'bg-amber-400',
'bg-purple-400', 'bg-cyan-400', 'bg-rose-400', 'bg-teal-400',
]
export default function CampaignCalendar({ campaigns = [] }) {
const [currentMonth, setCurrentMonth] = useState(new Date())
const days = useMemo(() => {
const monthStart = startOfMonth(currentMonth)
const monthEnd = endOfMonth(currentMonth)
const calStart = startOfWeek(monthStart, { weekStartsOn: 0 })
const calEnd = endOfWeek(monthEnd, { weekStartsOn: 0 })
return eachDayOfInterval({ start: calStart, end: calEnd })
}, [currentMonth])
const getCampaignsForDay = (day) => {
return campaigns.filter((c) => {
const start = new Date(c.startDate)
const end = new Date(c.endDate)
return (isSameDay(day, start) || isAfter(day, start)) &&
(isSameDay(day, end) || isBefore(day, end))
})
}
const isStartOfCampaign = (day, campaign) => {
return isSameDay(day, new Date(campaign.startDate))
}
const isEndOfCampaign = (day, campaign) => {
return isSameDay(day, new Date(campaign.endDate))
}
return (
<div className="bg-white rounded-xl border border-border overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<h3 className="text-lg font-semibold text-text-primary">
{format(currentMonth, 'MMMM yyyy')}
</h3>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}
className="p-1.5 rounded-lg hover:bg-surface-tertiary text-text-secondary"
>
<ChevronLeft className="w-5 h-5" />
</button>
<button
onClick={() => setCurrentMonth(new Date())}
className="px-3 py-1 text-sm font-medium rounded-lg hover:bg-surface-tertiary text-text-secondary"
>
Today
</button>
<button
onClick={() => setCurrentMonth(addMonths(currentMonth, 1))}
className="p-1.5 rounded-lg hover:bg-surface-tertiary text-text-secondary"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
{/* Day names */}
<div className="calendar-grid border-b border-border">
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((d) => (
<div key={d} className="px-2 py-2 text-center text-xs font-semibold text-text-tertiary uppercase tracking-wider">
{d}
</div>
))}
</div>
{/* Calendar grid */}
<div className="calendar-grid">
{days.map((day, i) => {
const dayCampaigns = getCampaignsForDay(day)
const inMonth = isSameMonth(day, currentMonth)
const today = isToday(day)
return (
<div
key={i}
className={`min-h-[80px] p-1 border-b border-r border-border-light relative ${
!inMonth ? 'bg-surface-secondary/50' : ''
}`}
>
<span className={`text-xs font-medium inline-flex items-center justify-center w-6 h-6 rounded-full ${
today ? 'bg-brand-primary text-white' :
inMonth ? 'text-text-primary' : 'text-text-tertiary'
}`}>
{format(day, 'd')}
</span>
<div className="space-y-0.5 mt-0.5">
{dayCampaigns.slice(0, 3).map((campaign, ci) => {
const colorIndex = campaigns.indexOf(campaign) % CAMPAIGN_COLORS.length
const isStart = isStartOfCampaign(day, campaign)
const isEnd = isEndOfCampaign(day, campaign)
return (
<div
key={campaign._id || ci}
className={`h-5 text-[10px] font-medium text-white flex items-center px-1 truncate ${CAMPAIGN_COLORS[colorIndex]} ${
isStart ? 'rounded-l-full ml-0' : '-ml-1'
} ${isEnd ? 'rounded-r-full mr-0' : '-mr-1'}`}
title={campaign.name}
>
{isStart ? campaign.name : ''}
</div>
)
})}
{dayCampaigns.length > 3 && (
<div className="text-[10px] text-text-tertiary px-1">
+{dayCampaigns.length - 3} more
</div>
)}
</div>
</div>
)
})}
</div>
</div>
)
}