Tasks
Full task management with create, edit, complete, and delete — optimistic updates included.
@prisma/client@prisma/adapter-pgpgdotenvzustandlucide-reactprisma@types/pg
shadcn/uibuttoninputtextareaskeletonauto-installed by CLI
Add these to your
.env before installingThe CLI checks for these at install time — missing values mean the database step gets skipped, and the feature won't work until it's added.
DATABASE_URLrequiredPostgreSQL connection string.
e.g. postgresql://user:pass@localhost:5432/mydb
Install via CLI
npx feature101@latest add tasksImport
import { TaskList } from '@/features/tasks'Usage
<TaskList currentUserId="user_123" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
currentUserId* | string | — | ID of the user whose tasks are loaded and managed. |
className | string | — | Custom CSS classes for the task list container. |
emptyMessage | string | No tasks yet. Add one below. | Message shown when the user has no tasks. |
Data Flow
client
What the user clicks and types.
zustand
Updates the screen instantly, before the server replies.
server
Validates and saves the change securely.
prisma
Your data, permanently written to the database.
client
What the user clicks and types.
zustand
Updates the screen instantly, before the server replies.
server
Validates and saves the change securely.
prisma
Your data, permanently written to the database.
Files12
Files
components/TaskCreateInput.tsx
"use client";
import { memo, useState, useRef } from "react";
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import type { TaskCreateInputProps } from "../tasks.types";
const TaskCreateInput = memo(
({
onCreate,
placeholder = "Add a task",
className,
onTaskCreated,
}: TaskCreateInputProps) => {
const [expanded, setExpanded] = useState(false);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const titleRef = useRef<HTMLInputElement>(null);
function handleOpen() {
setExpanded(true);
setTimeout(() => titleRef.current?.focus(), 0);
}
function handleCancel() {
setExpanded(false);
setTitle("");
setDescription("");
}
async function handleCreate() {
const trimmedTitle = title.trim();
if (!trimmedTitle) return;
// Capture description before clearing state
const trimmedDesc = description.trim();
setExpanded(false);
setTitle("");
setDescription("");
const task = await onCreate(trimmedTitle, trimmedDesc || undefined);
if (task) onTaskCreated?.(task);
}
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter") handleCreate();
if (e.key === "Escape") handleCancel();
}
if (!expanded) {
return (
<button
onClick={handleOpen}
className={cn(
"flex items-center gap-2 w-full px-2 py-2 text-sm text-muted-foreground",
"hover:text-foreground transition-colors rounded-lg hover:bg-muted/50",
className,
)}
>
<Plus className="h-4 w-4 text-primary shrink-0" />
<span>{placeholder}</span>
</button>
);
}
return (
<div className={cn("flex flex-col gap-1.5 px-2 py-1.5", className)}>
<Input
ref={titleRef}
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Task name"
className="shadow-none focus-visible:ring-0 bg-transparent font-medium !text-base h-auto p-3"
/>
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Description (optional)"
rows={2}
className="shadow-none focus-visible:ring-0 bg-transparent resize-none text-muted-foreground p-3"
/>
<div className="flex items-center justify-end gap-2 pt-0.5">
<Button
variant="ghost"
size="sm"
onClick={handleCancel}
className="h-7 text-sm"
>
Cancel
</Button>
<Button
size="sm"
onClick={handleCreate}
disabled={!title.trim()}
className="h-7 text-sm"
>
Add task
</Button>
</div>
</div>
);
},
);
TaskCreateInput.displayName = "TaskCreateInput";
export { TaskCreateInput };