Add core components for task management and navigation
This commit introduces the foundational UI components and logic for the task management application, including task creation, calendar views, Kanban boards, and navigation elements. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/yy9YLEW
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
import { useState } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Plus, Calendar as CalendarIcon, Clock, Copy, Play } from 'lucide-react';
|
||||
import { Task } from './TaskCard';
|
||||
|
||||
interface TemplateTask {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
dayOffset: number; // Days relative to project start date (negative for before, positive for after)
|
||||
estimatedHours?: number;
|
||||
}
|
||||
|
||||
interface ProjectTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
tasks: TemplateTask[];
|
||||
category: string;
|
||||
}
|
||||
|
||||
interface ProjectTemplateProps {
|
||||
templates?: ProjectTemplate[];
|
||||
onCreateFromTemplate?: (templateId: string, startDate: Date, projectName: string) => void;
|
||||
onCreateTemplate?: (template: Omit<ProjectTemplate, 'id'>) => void;
|
||||
}
|
||||
|
||||
const defaultTemplates: ProjectTemplate[] = [
|
||||
{
|
||||
id: 'web-project',
|
||||
name: 'Website Launch',
|
||||
description: 'Complete website development and launch process',
|
||||
category: 'Development',
|
||||
tasks: [
|
||||
{ id: '1', title: 'Project planning', priority: 'high', dayOffset: -14, estimatedHours: 4 },
|
||||
{ id: '2', title: 'Design mockups', priority: 'high', dayOffset: -10, estimatedHours: 16 },
|
||||
{ id: '3', title: 'Frontend development', priority: 'high', dayOffset: -7, estimatedHours: 40 },
|
||||
{ id: '4', title: 'Backend API', priority: 'medium', dayOffset: -5, estimatedHours: 24 },
|
||||
{ id: '5', title: 'Testing and QA', priority: 'high', dayOffset: -2, estimatedHours: 8 },
|
||||
{ id: '6', title: 'Deploy to production', priority: 'high', dayOffset: 0, estimatedHours: 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'product-launch',
|
||||
name: 'Product Launch',
|
||||
description: 'Marketing and launch campaign for new product',
|
||||
category: 'Marketing',
|
||||
tasks: [
|
||||
{ id: '1', title: 'Market research', priority: 'high', dayOffset: -21, estimatedHours: 12 },
|
||||
{ id: '2', title: 'Create marketing materials', priority: 'medium', dayOffset: -14, estimatedHours: 20 },
|
||||
{ id: '3', title: 'Press release', priority: 'medium', dayOffset: -7, estimatedHours: 4 },
|
||||
{ id: '4', title: 'Social media campaign', priority: 'high', dayOffset: -3, estimatedHours: 8 },
|
||||
{ id: '5', title: 'Launch event', priority: 'high', dayOffset: 0, estimatedHours: 6 }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export default function ProjectTemplate({
|
||||
templates = defaultTemplates,
|
||||
onCreateFromTemplate,
|
||||
onCreateTemplate
|
||||
}: ProjectTemplateProps) {
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<ProjectTemplate | null>(null);
|
||||
const [projectName, setProjectName] = useState('');
|
||||
const [startDate, setStartDate] = useState<Date | undefined>();
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [isLaunchOpen, setIsLaunchOpen] = useState(false);
|
||||
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
|
||||
|
||||
const handleLaunchProject = () => {
|
||||
if (selectedTemplate && startDate && projectName.trim()) {
|
||||
onCreateFromTemplate?.(selectedTemplate.id, startDate, projectName.trim());
|
||||
console.log('Launching project:', {
|
||||
template: selectedTemplate.name,
|
||||
name: projectName,
|
||||
startDate: startDate.toLocaleDateString()
|
||||
});
|
||||
|
||||
setProjectName('');
|
||||
setStartDate(undefined);
|
||||
setSelectedTemplate(null);
|
||||
setIsLaunchOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDayOffset = (offset: number) => {
|
||||
if (offset === 0) return 'Launch day';
|
||||
if (offset < 0) return `${Math.abs(offset)} days before`;
|
||||
return `${offset} days after`;
|
||||
};
|
||||
|
||||
const getTotalEstimatedHours = (template: ProjectTemplate) => {
|
||||
return template.tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0);
|
||||
};
|
||||
|
||||
const getTemplatesByCategory = () => {
|
||||
const categories = Array.from(new Set(templates.map(t => t.category)));
|
||||
return categories.map(category => ({
|
||||
category,
|
||||
templates: templates.filter(t => t.category === category)
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold" data-testid="text-templates-title">
|
||||
Project Templates
|
||||
</h2>
|
||||
|
||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" data-testid="button-create-template">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Template
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Template</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4 text-center text-muted-foreground">
|
||||
Template creation form would go here
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Templates by Category */}
|
||||
{getTemplatesByCategory().map(({ category, templates: categoryTemplates }) => (
|
||||
<div key={category} className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
|
||||
{category}
|
||||
</h3>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{categoryTemplates.map((template) => (
|
||||
<Card key={template.id} className="p-4 hover-elevate active-elevate-2" data-testid={`template-${template.id}`}>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-sm" data-testid={`text-template-name-${template.id}`}>
|
||||
{template.name}
|
||||
</h4>
|
||||
{template.description && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{template.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{template.tasks.length} tasks
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{getTotalEstimatedHours(template)}h estimated</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<CalendarIcon className="w-3 h-3" />
|
||||
<span>{Math.abs(Math.min(...template.tasks.map(t => t.dayOffset)))} day timeline</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-medium text-muted-foreground">Sample tasks:</div>
|
||||
<div className="space-y-1">
|
||||
{template.tasks.slice(0, 3).map((task) => (
|
||||
<div key={task.id} className="flex items-center justify-between text-xs">
|
||||
<span className="truncate flex-1">{task.title}</span>
|
||||
<Badge variant="outline" className="text-xs ml-2 flex-shrink-0">
|
||||
{formatDayOffset(task.dayOffset)}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
{template.tasks.length > 3 && (
|
||||
<div className="text-xs text-muted-foreground text-center py-1">
|
||||
+{template.tasks.length - 3} more tasks
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
console.log('Copying template:', template.name);
|
||||
}}
|
||||
data-testid={`button-copy-${template.id}`}
|
||||
>
|
||||
<Copy className="w-3 h-3 mr-1" />
|
||||
Copy
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
setSelectedTemplate(template);
|
||||
setProjectName(template.name);
|
||||
setIsLaunchOpen(true);
|
||||
}}
|
||||
data-testid={`button-use-${template.id}`}
|
||||
>
|
||||
<Play className="w-3 h-3 mr-1" />
|
||||
Use Template
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Launch Project Dialog */}
|
||||
<Dialog open={isLaunchOpen} onOpenChange={setIsLaunchOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Play className="w-4 h-4" />
|
||||
Launch Project from Template
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedTemplate && (
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-sm font-medium">{selectedTemplate.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{selectedTemplate.tasks.length} tasks • {getTotalEstimatedHours(selectedTemplate)}h estimated
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Project name"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
data-testid="input-project-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
data-testid="button-project-start-date"
|
||||
>
|
||||
<CalendarIcon className="w-4 h-4 mr-2" />
|
||||
{startDate ? startDate.toLocaleDateString() : 'Select start date'}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => {
|
||||
setStartDate(date);
|
||||
setIsCalendarOpen(false);
|
||||
}}
|
||||
disabled={(date) => date < new Date()}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsLaunchOpen(false)}
|
||||
className="flex-1"
|
||||
data-testid="button-cancel-launch"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleLaunchProject}
|
||||
disabled={!projectName.trim() || !startDate}
|
||||
className="flex-1"
|
||||
data-testid="button-launch-project"
|
||||
>
|
||||
Launch Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user