Tasks

Productivity

Full 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 Preview
Before 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_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.
File layers:clientui componentzustandhook / storeserverserver actionprismaprisma model
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 };