Task Management Building and managing tasks in ShipKit with progress tracking, assignments, due dates, and prioritization
Task Management
ShipKit provides a comprehensive task management system for tracking work items, managing assignments, monitoring progress, and handling priorities. This guide covers setup, usage, and best practices.
Setup
Layout Configuration
// src/app/(app)/(task)/layout.tsx
import { TaskNav } from "@/components/task/nav"
import { TaskHeader } from "@/components/task/header"
import { auth } from "@/lib/auth/auth"
import { redirect } from "next/navigation"
export default async function TaskLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await auth()
if (!session?.user) {
redirect("/auth/signin?callbackUrl=/tasks")
}
return (
<div className="flex min-h-screen flex-col">
<TaskHeader user={session.user} />
<div className="flex flex-1">
<TaskNav />
<main className="flex-1 p-6">
{children}
</main>
</div>
</div>
)
}
Navigation Component
// src/components/task/nav.tsx
import { cn } from "@/lib/utils"
import { usePathname } from "next/navigation"
import Link from "next/link"
const navItems = [
{
title: "All Tasks",
href: "/tasks",
icon: "ListTodo",
},
{
title: "My Tasks",
href: "/tasks/my",
icon: "User",
},
{
title: "Calendar",
href: "/tasks/calendar",
icon: "Calendar",
},
{
title: "Reports",
href: "/tasks/reports",
icon: "BarChart",
},
{
title: "Settings",
href: "/tasks/settings",
icon: "Settings",
},
]
export function TaskNav() {
const pathname = usePathname()
return (
<nav className="w-64 border-r bg-background p-6">
<div className="space-y-4">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center space-x-3 rounded-lg px-3 py-2",
"hover:bg-accent hover:text-accent-foreground",
pathname === item.href && "bg-accent text-accent-foreground"
)}
>
<item.icon className="h-5 w-5" />
<span>{item.title}</span>
</Link>
))}
</div>
</nav>
)
}
Features
Task List
// src/app/(app)/(task)/page.tsx
import { Suspense } from "react"
import { TaskList } from "@/components/task/task-list"
import { TaskFilters } from "@/components/task/task-filters"
import { CreateTask } from "@/components/task/create-task"
import { Loading } from "@/components/ui/loading"
export default function TasksPage() {
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">Tasks</h1>
<CreateTask />
</div>
<div className="grid gap-6 lg:grid-cols-[300px,1fr]">
<TaskFilters />
<Suspense fallback={<Loading />}>
<TaskList />
</Suspense>
</div>
</div>
)
}
Task List Component
// src/components/task/task-list.tsx
import { fetchTasks } from "@/lib/task/tasks"
import { DataTable } from "@/components/ui/data-table"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { formatDate } from "@/lib/utils"
const columns = [
{
accessorKey: "title",
header: "Title",
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<Badge variant={getStatusVariant(row.original.status)}>
{row.original.status}
</Badge>
),
},
{
accessorKey: "assignee",
header: "Assignee",
cell: ({ row }) => row.original.assignee?.name || "Unassigned",
},
{
accessorKey: "dueDate",
header: "Due Date",
cell: ({ row }) => formatDate(row.original.dueDate),
},
{
accessorKey: "priority",
header: "Priority",
cell: ({ row }) => (
<Badge variant={getPriorityVariant(row.original.priority)}>
{row.original.priority}
</Badge>
),
},
{
id: "actions",
cell: ({ row }) => (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleEdit(row.original)}
>
Edit
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => handleDelete(row.original)}
>
Delete
</Button>
</div>
),
},
]
export async function TaskList() {
const tasks = await fetchTasks()
return (
<DataTable
columns={columns}
data={tasks}
searchKey="title"
pagination
/>
)
}
Create Task Dialog
// src/components/task/create-task.tsx
"use client"
import { useState } from "react"
import { Dialog } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Select } from "@/components/ui/select"
import { DatePicker } from "@/components/ui/date-picker"
import { createTask } from "@/lib/task/tasks"
export function CreateTask() {
const [open, setOpen] = useState(false)
const [task, setTask] = useState({
title: "",
description: "",
status: "TODO",
priority: "MEDIUM",
dueDate: null,
assigneeId: null,
})
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
try {
await createTask(task)
setOpen(false)
setTask({
title: "",
description: "",
status: "TODO",
priority: "MEDIUM",
dueDate: null,
assigneeId: null,
})
} catch (error) {
// Handle error
}
}
return (
<>
<Button onClick={() => setOpen(true)}>
Create Task
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<div className="p-6">
<h2 className="text-xl font-semibold">Create Task</h2>
<form onSubmit={handleSubmit} className="mt-4 space-y-4">
<div>
<label>Title</label>
<Input
value={task.title}
onChange={(e) =>
setTask((prev) => ({ ...prev, title: e.target.value }))
}
/>
</div>
<div>
<label>Description</label>
<Textarea
value={task.description}
onChange={(e) =>
setTask((prev) => ({
...prev,
description: e.target.value,
}))
}
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label>Status</label>
<Select
value={task.status}
onValueChange={(value) =>
setTask((prev) => ({ ...prev, status: value }))
}
>
<SelectItem value="TODO">To Do</SelectItem>
<SelectItem value="IN_PROGRESS">In Progress</SelectItem>
<SelectItem value="DONE">Done</SelectItem>
</Select>
</div>
<div>
<label>Priority</label>
<Select
value={task.priority}
onValueChange={(value) =>
setTask((prev) => ({ ...prev, priority: value }))
}
>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
</Select>
</div>
</div>
<div>
<label>Due Date</label>
<DatePicker
selected={task.dueDate}
onChange={(date) =>
setTask((prev) => ({ ...prev, dueDate: date }))
}
/>
</div>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
>
Cancel
</Button>
<Button type="submit">Create</Button>
</div>
</form>
</div>
</Dialog>
</>
)
}
Task Calendar
// src/app/(app)/(task)/calendar/page.tsx
import { TaskCalendar } from "@/components/task/task-calendar"
import { TaskCalendarFilters } from "@/components/task/task-calendar-filters"
export default function CalendarPage() {
return (
<div className="space-y-8">
<h1 className="text-3xl font-bold">Task Calendar</h1>
<TaskCalendarFilters />
<TaskCalendar />
</div>
)
}
Task Reports
// src/app/(app)/(task)/reports/page.tsx
import { TaskStats } from "@/components/task/task-stats"
import { TaskChart } from "@/components/task/task-chart"
import { TaskTrends } from "@/components/task/task-trends"
export default function ReportsPage() {
return (
<div className="space-y-8">
<h1 className="text-3xl font-bold">Task Reports</h1>
<TaskStats />
<div className="grid gap-6 lg:grid-cols-2">
<TaskChart />
<TaskTrends />
</div>
</div>
)
}
Data Model
// src/lib/task/types.ts
export interface Task {
id: string
title: string
description: string
status: "TODO" | "IN_PROGRESS" | "DONE"
priority: "LOW" | "MEDIUM" | "HIGH"
dueDate: Date | null
assigneeId: string | null
createdAt: Date
updatedAt: Date
}
export interface TaskStats {
total: number
completed: number
overdue: number
unassigned: number
}
export interface TaskTrend {
date: Date
completed: number
created: number
}
Server Actions
// src/server/actions/tasks.ts
"use server"
import { db } from "@/lib/db"
import { revalidatePath } from "next/cache"
import { type Task } from "@/lib/task/types"
export async function createTask(data: Omit<Task, "id" | "createdAt" | "updatedAt">) {
const task = await db.task.create({
data: {
...data,
createdAt: new Date(),
updatedAt: new Date(),
},
})
revalidatePath("/tasks")
return task
}
export async function updateTask(id: string, data: Partial<Task>) {
const task = await db.task.update({
where: { id },
data: {
...data,
updatedAt: new Date(),
},
})
revalidatePath("/tasks")
return task
}
export async function deleteTask(id: string) {
await db.task.delete({
where: { id },
})
revalidatePath("/tasks")
}
Error Handling
// src/components/task/error-boundary.tsx
"use client"
import { useEffect } from "react"
import { Button } from "@/components/ui/button"
import { toast } from "@/components/ui/toast"
interface ErrorBoundaryProps {
error: Error
reset: () => void
}
export default function ErrorBoundary({
error,
reset,
}: ErrorBoundaryProps) {
useEffect(() => {
// Log task errors
console.error("Task Error:", error)
}, [error])
return (
<div className="flex min-h-[400px] flex-col items-center justify-center">
<h2 className="text-2xl font-bold">Task Error</h2>
<p className="mt-2 text-muted-foreground">
{error.message}
</p>
<Button
className="mt-4"
onClick={() => {
reset()
toast({
title: "Reset",
description: "The task view has been reset.",
})
}}
>
Try Again
</Button>
</div>
)
}
Testing
// src/tests/task.test.tsx
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { TaskList } from "@/components/task/task-list"
import { CreateTask } from "@/components/task/create-task"
describe("Task Management", () => {
it("renders task list", async () => {
render(await TaskList())
expect(screen.getByText("Tasks")).toBeInTheDocument()
})
it("creates new task", async () => {
const user = userEvent.setup()
render(<CreateTask />)
await user.click(screen.getByText("Create Task"))
await user.type(screen.getByLabelText("Title"), "New Task")
await user.click(screen.getByText("Create"))
// Add assertions for task creation
})
})
Performance Optimization
Data Loading
Implement pagination for task lists
Use React Suspense for loading states
Cache frequently accessed task data
Component Optimization
Memoize expensive computations
Use virtualization for long lists
Implement infinite scrolling
State Management
Use server actions for data mutations
Implement optimistic updates
Handle concurrent modifications
Notes
Tasks are protected by authentication
All task changes are tracked in history
Task assignments trigger notifications
Calendar view supports drag-and-drop
Reports update in real-time
Error boundaries catch task errors
Testing covers critical task functions