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 installing

The 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_URLrequired

    PostgreSQL connection string.

    e.g. postgresql://user:pass@localhost:5432/mydb

Install via CLI

npx feature101@latest add tasks

Import

import { TaskList } from '@/features/tasks'

Usage

<TaskList currentUserId="user_123" />

Props

PropTypeDescription
currentUserId*
stringID of the user whose tasks are loaded and managed.
className
stringCustom CSS classes for the task list container.
emptyMessage
stringMessage 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.

Files12
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 };