feat: slide panels, task calendar, team management, project editing, collapsible sections

- Add SlidePanel, TaskDetailPanel, PostDetailPanel, TeamPanel, TeamMemberPanel
- Add ProjectEditPanel, CollapsibleSection, DatePresetPicker, TaskCalendarView
- Update App, AuthContext, i18n (ar/en), PostProduction, ProjectDetail, Projects
- Update Settings, Tasks, Team pages
- Update InteractiveTimeline, MemberCard, ProjectCard, TaskCard components
- Update server API utilities
- Remove tracked server/node_modules (now properly gitignored)
This commit is contained in:
fahed
2026-02-19 11:35:42 +03:00
parent e76be78498
commit 4522edeea8
2207 changed files with 3767 additions and 831225 deletions
+176
View File
@@ -0,0 +1,176 @@
import { useState } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { PRIORITY_CONFIG } from '../utils/api'
import { useLanguage } from '../i18n/LanguageContext'
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
function getMonthData(year, month) {
const firstDay = new Date(year, month, 1).getDay()
const daysInMonth = new Date(year, month + 1, 0).getDate()
const prevDays = new Date(year, month, 0).getDate()
const cells = []
// Previous month trailing days
for (let i = firstDay - 1; i >= 0; i--) {
cells.push({ day: prevDays - i, current: false, date: new Date(year, month - 1, prevDays - i) })
}
// Current month
for (let d = 1; d <= daysInMonth; d++) {
cells.push({ day: d, current: true, date: new Date(year, month, d) })
}
// Next month leading days
const remaining = 42 - cells.length
for (let d = 1; d <= remaining; d++) {
cells.push({ day: d, current: false, date: new Date(year, month + 1, d) })
}
return cells
}
function dateKey(d) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
export default function TaskCalendarView({ tasks, onTaskClick }) {
const { t } = useLanguage()
const today = new Date()
const [year, setYear] = useState(today.getFullYear())
const [month, setMonth] = useState(today.getMonth())
const cells = getMonthData(year, month)
const todayKey = dateKey(today)
// Group tasks by due_date
const tasksByDate = {}
const unscheduled = []
for (const task of tasks) {
const dd = task.due_date || task.dueDate
if (dd) {
const key = dd.slice(0, 10) // yyyy-mm-dd
if (!tasksByDate[key]) tasksByDate[key] = []
tasksByDate[key].push(task)
} else {
unscheduled.push(task)
}
}
const prevMonth = () => {
if (month === 0) { setMonth(11); setYear(y => y - 1) }
else setMonth(m => m - 1)
}
const nextMonth = () => {
if (month === 11) { setMonth(0); setYear(y => y + 1) }
else setMonth(m => m + 1)
}
const goToday = () => { setYear(today.getFullYear()); setMonth(today.getMonth()) }
const monthLabel = new Date(year, month).toLocaleString('default', { month: 'long', year: 'numeric' })
const getPillColor = (task) => {
const p = task.priority || 'medium'
if (p === 'urgent') return 'bg-red-500 text-white'
if (p === 'high') return 'bg-orange-400 text-white'
if (p === 'medium') return 'bg-amber-400 text-amber-900'
return 'bg-gray-300 text-gray-700'
}
return (
<div className="flex gap-4">
{/* Calendar grid */}
<div className="flex-1">
{/* Nav */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<button onClick={prevMonth} className="p-1.5 rounded-lg hover:bg-surface-tertiary text-text-tertiary hover:text-text-primary transition-colors">
<ChevronLeft className="w-4 h-4" />
</button>
<h3 className="text-sm font-semibold text-text-primary min-w-[150px] text-center">{monthLabel}</h3>
<button onClick={nextMonth} className="p-1.5 rounded-lg hover:bg-surface-tertiary text-text-tertiary hover:text-text-primary transition-colors">
<ChevronRight className="w-4 h-4" />
</button>
</div>
<button onClick={goToday} className="px-3 py-1 text-xs font-medium text-brand-primary hover:bg-brand-primary/5 rounded-lg transition-colors">
{t('tasks.today')}
</button>
</div>
{/* Day headers */}
<div className="grid grid-cols-7 mb-1">
{DAYS.map(d => (
<div key={d} className="text-center text-[10px] font-medium text-text-tertiary uppercase py-1">
{d}
</div>
))}
</div>
{/* Cells */}
<div className="grid grid-cols-7 border-t border-l border-border">
{cells.map((cell, i) => {
const key = dateKey(cell.date)
const isToday = key === todayKey
const dayTasks = tasksByDate[key] || []
return (
<div
key={i}
className={`border-r border-b border-border min-h-[90px] p-1 ${
cell.current ? 'bg-white' : 'bg-surface-secondary/50'
}`}
>
<div className={`text-[11px] font-medium mb-0.5 w-6 h-6 flex items-center justify-center rounded-full ${
isToday ? 'bg-brand-primary text-white' : cell.current ? 'text-text-primary' : 'text-text-tertiary'
}`}>
{cell.day}
</div>
<div className="space-y-0.5">
{dayTasks.slice(0, 3).map(task => (
<button
key={task._id || task.id}
onClick={() => onTaskClick(task)}
className={`w-full text-left text-[10px] px-1.5 py-0.5 rounded truncate font-medium hover:opacity-80 transition-opacity ${
task.status === 'done' ? 'bg-emerald-100 text-emerald-700 line-through' : getPillColor(task)
}`}
title={task.title}
>
{task.title}
</button>
))}
{dayTasks.length > 3 && (
<div className="text-[9px] text-text-tertiary text-center font-medium">
+{dayTasks.length - 3} more
</div>
)}
</div>
</div>
)
})}
</div>
</div>
{/* Unscheduled sidebar */}
{unscheduled.length > 0 && (
<div className="w-48 shrink-0">
<h4 className="text-xs font-semibold text-text-tertiary uppercase mb-2">{t('tasks.unscheduled')}</h4>
<div className="space-y-1.5 max-h-[500px] overflow-y-auto">
{unscheduled.map(task => {
const priority = PRIORITY_CONFIG[task.priority] || PRIORITY_CONFIG.medium
return (
<button
key={task._id || task.id}
onClick={() => onTaskClick(task)}
className="w-full text-left bg-white border border-border rounded-lg p-2 hover:border-brand-primary/30 transition-colors"
>
<div className="flex items-center gap-1.5">
<div className={`w-2 h-2 rounded-full ${priority.color} shrink-0`} />
<span className={`text-xs font-medium truncate ${task.status === 'done' ? 'text-text-tertiary line-through' : 'text-text-primary'}`}>
{task.title}
</span>
</div>
</button>
)
})}
</div>
</div>
)}
</div>
)
}