feat: bulk delete, team dispatch, calendar views, timeline colors
Deploy / deploy (push) Successful in 11s
Deploy / deploy (push) Successful in 11s
- Multi-select bulk delete in all 5 list views (Artefacts, Posts, Tasks, Issues, Assets) with cascade deletes and confirmation modals - Team-based issue dispatch: team picker on public issue form, team filter on Issues page, copy public link from Team page and Issues header, team assignment in IssueDetailPanel - Month/Week toggle on PostCalendar and TaskCalendarView - Month/Week/Day zoom on project and campaign timelines (InteractiveTimeline) and ProjectDetail GanttView, with Month as default - Custom timeline bar colors: clickable color dot with 12-color palette popover on project, campaign, and task timeline bars - Artefacts default view changed to list - BulkSelectBar reusable component - i18n keys for all new features (en + ar) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon } from 'lucide-react'
|
||||
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, CalendarDays } from 'lucide-react'
|
||||
import { AppContext } from '../App'
|
||||
import { useLanguage } from '../i18n/LanguageContext'
|
||||
import { api, PLATFORMS } from '../utils/api'
|
||||
@@ -39,6 +39,19 @@ function getMonthData(year, month) {
|
||||
return cells
|
||||
}
|
||||
|
||||
function getWeekData(startDate) {
|
||||
const cells = []
|
||||
const start = new Date(startDate)
|
||||
// Align to Sunday
|
||||
start.setDate(start.getDate() - start.getDay())
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(start)
|
||||
d.setDate(start.getDate() + i)
|
||||
cells.push({ day: d.getDate(), current: true, date: d })
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
||||
function dateKey(d) {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
@@ -53,6 +66,10 @@ export default function PostCalendar() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [filters, setFilters] = useState({ brand: '', platform: '', status: '' })
|
||||
const [selectedPost, setSelectedPost] = useState(null)
|
||||
const [calView, setCalView] = useState('month') // 'month' | 'week'
|
||||
const [weekStart, setWeekStart] = useState(() => {
|
||||
const d = new Date(); d.setDate(d.getDate() - d.getDay()); return d
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
loadPosts()
|
||||
@@ -61,7 +78,7 @@ export default function PostCalendar() {
|
||||
const loadPosts = async () => {
|
||||
try {
|
||||
const res = await api.get('/posts')
|
||||
setPosts(res.data || res || [])
|
||||
setPosts(Array.isArray(res) ? res : [])
|
||||
} catch (err) {
|
||||
console.error('Failed to load posts:', err)
|
||||
} finally {
|
||||
@@ -69,7 +86,7 @@ export default function PostCalendar() {
|
||||
}
|
||||
}
|
||||
|
||||
const cells = getMonthData(year, month)
|
||||
const cells = calView === 'month' ? getMonthData(year, month) : getWeekData(weekStart)
|
||||
const todayKey = dateKey(today)
|
||||
|
||||
// Filter posts
|
||||
@@ -105,9 +122,22 @@ export default function PostCalendar() {
|
||||
if (month === 11) { setMonth(0); setYear(y => y + 1) }
|
||||
else setMonth(m => m + 1)
|
||||
}
|
||||
const goToday = () => { setYear(today.getFullYear()); setMonth(today.getMonth()) }
|
||||
const prevWeek = () => setWeekStart(d => { const n = new Date(d); n.setDate(n.getDate() - 7); return n })
|
||||
const nextWeek = () => setWeekStart(d => { const n = new Date(d); n.setDate(n.getDate() + 7); return n })
|
||||
|
||||
const goToday = () => {
|
||||
setYear(today.getFullYear()); setMonth(today.getMonth())
|
||||
const d = new Date(); d.setDate(d.getDate() - d.getDay()); setWeekStart(d)
|
||||
}
|
||||
|
||||
const monthLabel = new Date(year, month).toLocaleString('default', { month: 'long', year: 'numeric' })
|
||||
const weekLabel = (() => {
|
||||
const start = new Date(weekStart)
|
||||
start.setDate(start.getDate() - start.getDay())
|
||||
const end = new Date(start); end.setDate(start.getDate() + 6)
|
||||
const fmt = (d) => d.toLocaleString('default', { month: 'short', day: 'numeric' })
|
||||
return `${fmt(start)} – ${fmt(end)}, ${end.getFullYear()}`
|
||||
})()
|
||||
|
||||
const handlePostClick = (post) => {
|
||||
setSelectedPost(post)
|
||||
@@ -176,17 +206,37 @@ export default function PostCalendar() {
|
||||
{/* Nav */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={prevMonth} className="p-2 rounded-lg hover:bg-surface-tertiary text-text-tertiary hover:text-text-primary transition-colors">
|
||||
<button onClick={calView === 'month' ? prevMonth : prevWeek} className="p-2 rounded-lg hover:bg-surface-tertiary text-text-tertiary hover:text-text-primary transition-colors">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<h3 className="text-lg font-semibold text-text-primary min-w-[180px] text-center">{monthLabel}</h3>
|
||||
<button onClick={nextMonth} className="p-2 rounded-lg hover:bg-surface-tertiary text-text-tertiary hover:text-text-primary transition-colors">
|
||||
<h3 className="text-lg font-semibold text-text-primary min-w-[220px] text-center">
|
||||
{calView === 'month' ? monthLabel : weekLabel}
|
||||
</h3>
|
||||
<button onClick={calView === 'month' ? nextMonth : nextWeek} className="p-2 rounded-lg hover:bg-surface-tertiary text-text-tertiary hover:text-text-primary transition-colors">
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={goToday} className="px-4 py-2 text-sm font-medium text-brand-primary hover:bg-brand-primary/5 rounded-lg transition-colors">
|
||||
Today
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex bg-surface-tertiary rounded-lg p-0.5">
|
||||
<button
|
||||
onClick={() => setCalView('month')}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors flex items-center gap-1.5 ${calView === 'month' ? 'bg-white shadow-sm text-text-primary' : 'text-text-tertiary'}`}
|
||||
>
|
||||
<CalendarIcon className="w-3.5 h-3.5" />
|
||||
Month
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCalView('week')}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors flex items-center gap-1.5 ${calView === 'week' ? 'bg-white shadow-sm text-text-primary' : 'text-text-tertiary'}`}
|
||||
>
|
||||
<CalendarDays className="w-3.5 h-3.5" />
|
||||
Week
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={goToday} className="px-4 py-2 text-sm font-medium text-brand-primary hover:bg-brand-primary/5 rounded-lg transition-colors">
|
||||
Today
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Day headers */}
|
||||
@@ -207,7 +257,7 @@ export default function PostCalendar() {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`border-r border-b border-border min-h-[110px] p-2 ${
|
||||
className={`border-r border-b border-border ${calView === 'week' ? 'min-h-[300px]' : 'min-h-[110px]'} p-2 ${
|
||||
cell.current ? 'bg-surface' : 'bg-surface-secondary/30'
|
||||
} ${i % 7 === 6 ? 'border-r-0' : ''}`}
|
||||
>
|
||||
@@ -217,7 +267,7 @@ export default function PostCalendar() {
|
||||
{cell.day}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{dayPosts.slice(0, 3).map(post => (
|
||||
{dayPosts.slice(0, calView === 'week' ? 10 : 3).map(post => (
|
||||
<button
|
||||
key={post.Id || post._id}
|
||||
onClick={() => handlePostClick(post)}
|
||||
@@ -229,9 +279,9 @@ export default function PostCalendar() {
|
||||
{post.title}
|
||||
</button>
|
||||
))}
|
||||
{dayPosts.length > 3 && (
|
||||
{dayPosts.length > (calView === 'week' ? 10 : 3) && (
|
||||
<div className="text-[9px] text-text-tertiary text-center font-medium">
|
||||
+{dayPosts.length - 3} more
|
||||
+{dayPosts.length - (calView === 'week' ? 10 : 3)} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user