feat: add social features, leaderboard, auth enhancements, and admin fixes
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Implement Social Features: Shared Tasks, Global Access, Privacy Settings (Leaderboard/Searchable). - Add Leaderboard Page and API. - Enhance Auth: Support Email/Username login, explicit duplicate registration errors. - Fix: Admin login password hash regression. - Refactor: Move to wouter for routing, add Admin Dashboard and User Management. - Add Setup Wizard. - Update UI with Sidebar and Gamification elements.
This commit is contained in:
+373
-307
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -20,11 +21,16 @@ import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, Chec
|
||||
import { Task, Label } from '@shared/schema';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { motion, PanInfo, useAnimation } from 'framer-motion';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { triggerConfetti } from "@/lib/confetti";
|
||||
import { playSuccessSound } from "@/lib/sounds";
|
||||
import { simulateAIDecomposition } from "@/lib/ai-simulator";
|
||||
import { Wand2, Loader2 } from "lucide-react";
|
||||
|
||||
interface TaskCardProps {
|
||||
task: Task;
|
||||
@@ -33,11 +39,32 @@ interface TaskCardProps {
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
onStatusChange?: (status: Task['status']) => void;
|
||||
onUpdate?: (updates: Partial<Task>) => void;
|
||||
isDragging?: boolean;
|
||||
}
|
||||
|
||||
export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDelete, onStatusChange, isDragging }: TaskCardProps) {
|
||||
export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDelete, onStatusChange, onUpdate, isDragging }: TaskCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
|
||||
const handleAIMagic = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (isAnalyzing) return;
|
||||
|
||||
setIsAnalyzing(true);
|
||||
try {
|
||||
const aiContent = await simulateAIDecomposition(task.title);
|
||||
const newDescription = (task.description || '') + aiContent;
|
||||
|
||||
onUpdate?.({ description: newDescription });
|
||||
triggerConfetti(0.5, 0.5); // Small burst for "Magic"
|
||||
playSuccessSound();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch labels to get the label color
|
||||
const { data: labels = [] } = useQuery<Label[]>({
|
||||
@@ -92,330 +119,369 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
if (task.status === 'done') {
|
||||
onStatusChange?.('todo');
|
||||
} else {
|
||||
triggerConfetti(e.clientX / window.innerWidth, e.clientY / window.innerHeight);
|
||||
playSuccessSound();
|
||||
onStatusChange?.('done');
|
||||
}
|
||||
};
|
||||
|
||||
const controls = useAnimation();
|
||||
|
||||
const handleDragEnd = async (event: any, info: PanInfo) => {
|
||||
if (info.offset.x > 100) {
|
||||
// Swiped right -> Complete
|
||||
triggerConfetti(0.5, 0.5);
|
||||
playSuccessSound();
|
||||
onStatusChange?.('done');
|
||||
await controls.start({ x: 500, opacity: 0 });
|
||||
} else if (info.offset.x < -100 && onDelete) {
|
||||
// Swiped left -> Delete (optional, maybe just shake for now)
|
||||
onDelete();
|
||||
} else {
|
||||
controls.start({ x: 0 });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<Card
|
||||
className={`p-4 hover-elevate active-elevate-2 transition-all duration-200 relative ${
|
||||
isDragging ? 'rotate-1 scale-105 shadow-lg' : ''
|
||||
} ${task.status === 'done' ? 'opacity-60' : ''}`}
|
||||
style={taskLabel ? {
|
||||
borderColor: taskLabel.color,
|
||||
borderWidth: '2px',
|
||||
borderStyle: 'solid'
|
||||
} : {}}
|
||||
data-testid={`card-task-${task.id}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Checkbox for quick completion */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Checkbox
|
||||
checked={task.status === 'done'}
|
||||
onClick={handleToggleComplete}
|
||||
data-testid={`checkbox-complete-${task.id}`}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('taskCard.toggleComplete')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
className="flex-1 min-w-0 cursor-pointer"
|
||||
onClick={() => {
|
||||
onEdit?.();
|
||||
console.log(`Task card clicked: ${task.title}`);
|
||||
}}
|
||||
>
|
||||
<h3 className={`font-medium text-sm leading-tight truncate ${task.status === 'done' ? 'line-through' : ''}`} data-testid={`text-task-title-${task.id}`}>
|
||||
{task.title}
|
||||
</h3>
|
||||
{task.description && (
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-2" data-testid={`text-task-description-${task.id}`}>
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-3 flex-wrap">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`text-xs ${getPriorityColor(task.priority)}`}
|
||||
data-testid={`badge-priority-${task.id}`}
|
||||
>
|
||||
{t(`priority.${task.priority}`)}
|
||||
</Badge>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs ${getStatusColor(task.status)}`}
|
||||
data-testid={`badge-status-${task.id}`}
|
||||
>
|
||||
{t(`status.${task.status}`)}
|
||||
</Badge>
|
||||
<motion.div
|
||||
drag="x"
|
||||
dragConstraints={{ left: -50, right: 150 }} // Limit swipe distance
|
||||
dragElastic={0.1}
|
||||
onDragEnd={handleDragEnd}
|
||||
animate={controls}
|
||||
whileDrag={{ scale: 1.02, cursor: 'grabbing' }}
|
||||
className="touch-pan-y" // Allow vertical scroll, horizontal swipe
|
||||
>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<Card
|
||||
className={`p-5 hover-elevate active-elevate-2 transition-all duration-300 relative hover:-translate-y-1 hover:shadow-xl ${isDragging ? 'rotate-1 scale-105 shadow-lg' : ''
|
||||
} ${task.status === 'done' ? 'opacity-60' : ''}`}
|
||||
style={taskLabel ? {
|
||||
borderColor: taskLabel.color,
|
||||
borderWidth: '2px',
|
||||
borderStyle: 'solid'
|
||||
} : {}}
|
||||
data-testid={`card-task-${task.id}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Checkbox for quick completion */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Checkbox
|
||||
checked={task.status === 'done'}
|
||||
onClick={handleToggleComplete}
|
||||
data-testid={`checkbox-complete-${task.id}`}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('taskCard.toggleComplete')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{task.dueDate && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span data-testid={`text-due-date-${task.id}`}>
|
||||
{task.dueDate.toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(task.timeTracked > 0 || task.isTracking) && (
|
||||
<div className="flex items-center gap-1 mt-2 text-xs">
|
||||
<Clock className={`w-3 h-3 ${task.isTracking ? 'text-primary animate-pulse' : 'text-muted-foreground'}`} />
|
||||
<span
|
||||
data-testid={`text-time-tracked-${task.id}`}
|
||||
className={task.isTracking ? 'text-primary font-medium' : 'text-muted-foreground'}
|
||||
>
|
||||
{task.timeTracked > 0 ? formatTime(task.timeTracked) : '0m'}
|
||||
{task.isTracking && ` (${t('taskCard.running')})`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-1">
|
||||
{/* Quick delete button */}
|
||||
{onDelete && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
data-testid={`button-delete-${task.id}`}
|
||||
className="w-6 h-6 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('taskCard.quickDelete')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
data-testid={`button-menu-${task.id}`}
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<MoreHorizontal className="w-3 h-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
<div
|
||||
className="flex-1 min-w-0 cursor-pointer"
|
||||
onClick={() => {
|
||||
onEdit?.();
|
||||
console.log(`Edit task: ${task.title}`);
|
||||
console.log(`Task card clicked: ${task.title}`);
|
||||
}}
|
||||
data-testid={`menu-edit-${task.id}`}
|
||||
>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.editTask')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggleTimer();
|
||||
}}
|
||||
data-testid={`menu-timer-${task.id}`}
|
||||
>
|
||||
<Timer className="w-4 h-4 mr-2" />
|
||||
{task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{task.status !== 'done' && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('done');
|
||||
console.log(`Mark task as done: ${task.title}`);
|
||||
}}
|
||||
data-testid={`menu-complete-${task.id}`}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.markAsDone')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'todo' && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('inProgress');
|
||||
console.log(`Start working on: ${task.title}`);
|
||||
}}
|
||||
data-testid={`menu-start-${task.id}`}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.startWorking')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'inProgress' && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('todo');
|
||||
console.log(`Move back to todo: ${task.title}`);
|
||||
}}
|
||||
data-testid={`menu-todo-${task.id}`}
|
||||
>
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.moveToTodo')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'done' && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('inProgress');
|
||||
console.log(`Reopen task: ${task.title}`);
|
||||
}}
|
||||
data-testid={`menu-reopen-${task.id}`}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.reopenTask')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{onDelete && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
console.log(`Delete task: ${task.title}`);
|
||||
}}
|
||||
className="text-destructive"
|
||||
data-testid={`menu-delete-${task.id}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.deleteTask')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit?.();
|
||||
}}
|
||||
data-testid={`context-menu-edit-${task.id}`}
|
||||
>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.editTask')}
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggleTimer();
|
||||
}}
|
||||
data-testid={`context-menu-timer-${task.id}`}
|
||||
>
|
||||
<Timer className="w-4 h-4 mr-2" />
|
||||
{task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')}
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
{task.status !== 'done' && (
|
||||
<ContextMenuItem
|
||||
<h3 className={`font-medium text-sm leading-tight truncate ${task.status === 'done' ? 'line-through' : ''}`} data-testid={`text-task-title-${task.id}`}>
|
||||
{task.title}
|
||||
</h3>
|
||||
{task.description && (
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-2" data-testid={`text-task-description-${task.id}`}>
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-3 flex-wrap">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`text-xs ${getPriorityColor(task.priority)}`}
|
||||
data-testid={`badge-priority-${task.id}`}
|
||||
>
|
||||
{t(`priority.${task.priority}`)}
|
||||
</Badge>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs ${getStatusColor(task.status)}`}
|
||||
data-testid={`badge-status-${task.id}`}
|
||||
>
|
||||
{t(`status.${task.status}`)}
|
||||
</Badge>
|
||||
|
||||
{task.dueDate && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span data-testid={`text-due-date-${task.id}`}>
|
||||
{task.dueDate.toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{task.energyLevel && task.energyLevel !== 'medium' && (
|
||||
<Badge variant="secondary" className="text-xs bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300">
|
||||
{t(`energy.${task.energyLevel}`)}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{task.estimatedDuration && (
|
||||
<Badge variant="outline" className="text-xs text-muted-foreground border-dashed">
|
||||
⏳ {task.estimatedDuration}m
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(task.timeTracked > 0 || task.isTracking) && (
|
||||
<div className="flex items-center gap-1 mt-2 text-xs">
|
||||
<Clock className={`w-3 h-3 ${task.isTracking ? 'text-primary animate-pulse' : 'text-muted-foreground'}`} />
|
||||
<span
|
||||
data-testid={`text-time-tracked-${task.id}`}
|
||||
className={task.isTracking ? 'text-primary font-medium' : 'text-muted-foreground'}
|
||||
>
|
||||
{task.timeTracked > 0 ? formatTime(task.timeTracked) : '0m'}
|
||||
{task.isTracking && ` (${t('taskCard.running')})`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-1">
|
||||
{/* Quick delete button */}
|
||||
{onDelete && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
data-testid={`button-delete-${task.id}`}
|
||||
className="w-6 h-6 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('taskCard.quickDelete')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
data-testid={`button-menu-${task.id}`}
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<MoreHorizontal className="w-3 h-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit?.();
|
||||
console.log(`Edit task: ${task.title}`);
|
||||
}}
|
||||
data-testid={`menu-edit-${task.id}`}
|
||||
>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.editTask')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggleTimer();
|
||||
}}
|
||||
data-testid={`menu-timer-${task.id}`}
|
||||
>
|
||||
<Timer className="w-4 h-4 mr-2" />
|
||||
{task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{task.status !== 'done' && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('done');
|
||||
console.log(`Mark task as done: ${task.title}`);
|
||||
}}
|
||||
data-testid={`menu-complete-${task.id}`}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.markAsDone')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'todo' && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('inProgress');
|
||||
console.log(`Start working on: ${task.title}`);
|
||||
}}
|
||||
data-testid={`menu-start-${task.id}`}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.startWorking')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'inProgress' && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('todo');
|
||||
console.log(`Move back to todo: ${task.title}`);
|
||||
}}
|
||||
data-testid={`menu-todo-${task.id}`}
|
||||
>
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.moveToTodo')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'done' && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('inProgress');
|
||||
console.log(`Reopen task: ${task.title}`);
|
||||
}}
|
||||
data-testid={`menu-reopen-${task.id}`}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.reopenTask')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{onDelete && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
console.log(`Delete task: ${task.title}`);
|
||||
}}
|
||||
className="text-destructive"
|
||||
data-testid={`menu-delete-${task.id}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.deleteTask')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('done');
|
||||
onEdit?.();
|
||||
}}
|
||||
data-testid={`context-menu-complete-${task.id}`}
|
||||
data-testid={`context-menu-edit-${task.id}`}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.markAsDone')}
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.editTask')}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'todo' && (
|
||||
<ContextMenuItem
|
||||
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('inProgress');
|
||||
handleToggleTimer();
|
||||
}}
|
||||
data-testid={`context-menu-start-${task.id}`}
|
||||
data-testid={`context-menu-timer-${task.id}`}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.startWorking')}
|
||||
<Timer className="w-4 h-4 mr-2" />
|
||||
{task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'inProgress' && (
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('todo');
|
||||
}}
|
||||
data-testid={`context-menu-todo-${task.id}`}
|
||||
>
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.moveToTodo')}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'done' && (
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('inProgress');
|
||||
}}
|
||||
data-testid={`context-menu-reopen-${task.id}`}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.reopenTask')}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
{onDelete && (
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
className="text-destructive"
|
||||
data-testid={`context-menu-delete-${task.id}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.deleteTask')}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
{task.status !== 'done' && (
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('done');
|
||||
}}
|
||||
data-testid={`context-menu-complete-${task.id}`}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.markAsDone')}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'todo' && (
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('inProgress');
|
||||
}}
|
||||
data-testid={`context-menu-start-${task.id}`}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.startWorking')}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'inProgress' && (
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('todo');
|
||||
}}
|
||||
data-testid={`context-menu-todo-${task.id}`}
|
||||
>
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.moveToTodo')}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
|
||||
{task.status === 'done' && (
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onStatusChange?.('inProgress');
|
||||
}}
|
||||
data-testid={`context-menu-reopen-${task.id}`}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.reopenTask')}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
{onDelete && (
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
className="text-destructive"
|
||||
data-testid={`context-menu-delete-${task.id}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
{t('taskCard.deleteTask')}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user