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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user