feat: add theme toggle, shared KanbanCard, keyboard shortcuts
Deploy / deploy (push) Successful in 11s
Deploy / deploy (push) Successful in 11s
Previously unstaged files from prior sessions: ThemeContext, ThemeToggle, KanbanCard, useKeyboardShortcuts hook, and updated Header, KanbanBoard, Issues, Tasks, PostProduction, index.css. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+38
-101
@@ -1,10 +1,12 @@
|
||||
import { useState, useEffect, useContext, useMemo } from 'react'
|
||||
import { Plus, CheckSquare, Trash2, Search, LayoutGrid, List, CalendarDays, X, SlidersHorizontal } from 'lucide-react'
|
||||
import { Plus, CheckSquare, Search, LayoutGrid, List, CalendarDays, X, SlidersHorizontal } from 'lucide-react'
|
||||
import { AppContext } from '../App'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { useLanguage } from '../i18n/LanguageContext'
|
||||
import { api, PRIORITY_CONFIG, getBrandColor } from '../utils/api'
|
||||
import TaskCard from '../components/TaskCard'
|
||||
import KanbanBoard from '../components/KanbanBoard'
|
||||
import KanbanCard from '../components/KanbanCard'
|
||||
import TaskDetailPanel from '../components/TaskDetailPanel'
|
||||
import BulkSelectBar from '../components/BulkSelectBar'
|
||||
import Modal from '../components/Modal'
|
||||
@@ -31,8 +33,6 @@ export default function Tasks() {
|
||||
// UI state
|
||||
const [viewMode, setViewMode] = useState('board')
|
||||
const [selectedTask, setSelectedTask] = useState(null)
|
||||
const [draggedTask, setDraggedTask] = useState(null)
|
||||
const [dragOverCol, setDragOverCol] = useState(null)
|
||||
|
||||
// Filters
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
@@ -209,11 +209,14 @@ export default function Tasks() {
|
||||
}
|
||||
|
||||
const handleMove = async (taskId, newStatus) => {
|
||||
// Optimistic update — move the card instantly
|
||||
const prev = tasks
|
||||
setTasks(tasks.map(t => (t._id || t.id) === taskId ? { ...t, status: newStatus } : t))
|
||||
try {
|
||||
await api.patch(`/tasks/${taskId}`, { status: newStatus })
|
||||
toast.success(t('tasks.statusUpdated'))
|
||||
loadTasks()
|
||||
} catch (err) {
|
||||
setTasks(prev)
|
||||
if (err.message?.includes('403')) toast.error(t('tasks.canOnlyEditOwn'))
|
||||
else toast.error(t('common.updateFailed'))
|
||||
}
|
||||
@@ -223,45 +226,6 @@ export default function Tasks() {
|
||||
setSelectedTask(task)
|
||||
}
|
||||
|
||||
// ─── Drag and drop (Kanban) ─────────────────────────
|
||||
const handleDragStart = (e, task) => {
|
||||
setDraggedTask(task)
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
setTimeout(() => { if (e.target) e.target.style.opacity = '0.4' }, 0)
|
||||
}
|
||||
const handleDragEnd = (e) => {
|
||||
e.target.style.opacity = '1'
|
||||
setDraggedTask(null)
|
||||
setDragOverCol(null)
|
||||
}
|
||||
const handleDragOver = (e, colStatus) => {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
setDragOverCol(colStatus)
|
||||
}
|
||||
const handleDragLeave = (e) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) setDragOverCol(null)
|
||||
}
|
||||
const handleDrop = (e, colStatus) => {
|
||||
e.preventDefault()
|
||||
setDragOverCol(null)
|
||||
if (draggedTask && draggedTask.status !== colStatus) {
|
||||
handleMove(draggedTask._id || draggedTask.id, colStatus)
|
||||
}
|
||||
setDraggedTask(null)
|
||||
}
|
||||
|
||||
// ─── Kanban columns ──────────────────────────────────
|
||||
const todoTasks = filteredTasks.filter(t => t.status === 'todo')
|
||||
const inProgressTasks = filteredTasks.filter(t => t.status === 'in_progress')
|
||||
const doneTasks = filteredTasks.filter(t => t.status === 'done')
|
||||
|
||||
const columns = [
|
||||
{ label: t('tasks.todo'), status: 'todo', items: todoTasks, color: 'bg-gray-400' },
|
||||
{ label: t('tasks.in_progress'), status: 'in_progress', items: inProgressTasks, color: 'bg-blue-400' },
|
||||
{ label: t('tasks.done'), status: 'done', items: doneTasks, color: 'bg-emerald-400' },
|
||||
]
|
||||
|
||||
// ─── List view sorting ────────────────────────────────
|
||||
const [sortBy, setSortBy] = useState('due_date')
|
||||
const [sortDir, setSortDir] = useState('asc')
|
||||
@@ -560,67 +524,40 @@ export default function Tasks() {
|
||||
<>
|
||||
{/* ─── Board View ──────────────────────── */}
|
||||
{viewMode === 'board' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{columns.map(col => {
|
||||
const isOver = dragOverCol === col.status && draggedTask?.status !== col.status
|
||||
<KanbanBoard
|
||||
columns={[
|
||||
{ id: 'todo', label: t('tasks.todo'), color: 'bg-gray-400' },
|
||||
{ id: 'in_progress', label: t('tasks.in_progress'), color: 'bg-blue-400' },
|
||||
{ id: 'done', label: t('tasks.done'), color: 'bg-emerald-400' },
|
||||
]}
|
||||
items={filteredTasks}
|
||||
getItemId={(t) => t._id || t.id}
|
||||
onMove={handleMove}
|
||||
emptyLabel={t('tasks.noTasks')}
|
||||
renderCard={(task) => {
|
||||
const dueDate = task.due_date || task.dueDate
|
||||
const isOverdue = dueDate && new Date(dueDate) < new Date() && task.status !== 'done'
|
||||
const assignee = task.assigned_name || task.assignedName
|
||||
const brandName = task.brand_name || task.brandName
|
||||
const projectName = task.project_name || task.projectName
|
||||
return (
|
||||
<div key={col.status}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className={`w-2.5 h-2.5 rounded-full ${col.color}`} />
|
||||
<h4 className="text-sm font-semibold text-text-primary">{col.label}</h4>
|
||||
<span className="text-xs font-medium text-text-tertiary bg-surface-tertiary px-2 py-0.5 rounded-full">
|
||||
{col.items.length}
|
||||
<KanbanCard
|
||||
title={task.title}
|
||||
thumbnail={task.thumbnail_url}
|
||||
brandName={brandName}
|
||||
assigneeName={assignee}
|
||||
date={dueDate}
|
||||
dateOverdue={isOverdue}
|
||||
onClick={() => openTask(task)}
|
||||
tags={projectName && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-surface-tertiary text-text-tertiary">
|
||||
{projectName}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`kanban-column rounded-xl p-2 space-y-2 min-h-[200px] border-2 transition-colors ${
|
||||
isOver
|
||||
? 'bg-brand-primary/5 border-brand-primary/40 border-dashed'
|
||||
: 'bg-surface-secondary border-border-light border-solid'
|
||||
}`}
|
||||
onDragOver={(e) => handleDragOver(e, col.status)}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={(e) => handleDrop(e, col.status)}
|
||||
>
|
||||
{col.items.length === 0 ? (
|
||||
<div className={`py-8 text-center text-xs ${isOver ? 'text-brand-primary font-medium' : 'text-text-tertiary'}`}>
|
||||
{isOver ? t('posts.dropHere') : t('tasks.noTasks')}
|
||||
</div>
|
||||
) : (
|
||||
col.items.map(task => {
|
||||
const canEdit = canEditResource('task', task)
|
||||
const canDelete = canDeleteResource('task', task)
|
||||
return (
|
||||
<div
|
||||
key={task._id || task.id}
|
||||
draggable={canEdit}
|
||||
onDragStart={(e) => canEdit && handleDragStart(e, task)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className={canEdit ? 'cursor-grab active:cursor-grabbing' : ''}
|
||||
>
|
||||
<div className="relative group" onClick={() => openTask(task)}>
|
||||
<TaskCard task={task} onMove={canEdit ? handleMove : undefined} showProject />
|
||||
{canDelete && (
|
||||
<div className="absolute top-2 right-2 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handlePanelDelete(task._id || task.id) }}
|
||||
className="p-1 hover:bg-red-50 rounded text-text-tertiary hover:text-red-500"
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ─── List View ───────────────────────── */}
|
||||
|
||||
Reference in New Issue
Block a user