Tasks
ProductivityFull task management with create, edit, complete, and delete — optimistic updates included.
@prisma/client@prisma/adapter-pgpgdotenvzustandlucide-reactprisma@types/pg
shadcn/uibuttoninputtextareaskeleton— auto-installed by CLI
11 files generated
Preview
This feature includes an interactive live preview.
Live PreviewBefore running the command, ensure these are in your
.env Without these in your.env your database won't be set up — the feature will break at runtime.
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. |
File layers:clientui componentzustandhook / storeserverserver actionprismaprisma model
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 };