Task Create

PreviousNext

Form to create new tasks with all necessary fields including assignees, projects, and due dates.

Docs
hextauiui

Preview

Loading preview…
registry/new-york/blocks/tasks/task-create.tsx
"use client";

import { Calendar, Loader2, Plus, X } from "lucide-react";
import { useState } from "react";
import { Button } from "@/registry/new-york/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/registry/new-york/ui/card";
import { Field, FieldContent, FieldLabel } from "@/registry/new-york/ui/field";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/registry/new-york/ui/input-group";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/registry/new-york/ui/select";
import { Separator } from "@/registry/new-york/ui/separator";
import { Textarea } from "@/registry/new-york/ui/textarea";
import type { TaskPriority, TaskStatus } from "./task-list";

export interface TaskCreateProps {
  onSubmit?: (data: {
    title: string;
    description?: string;
    status: TaskStatus;
    priority: TaskPriority;
    dueDate?: Date;
    assigneeIds?: string[];
    tags?: string[];
    projectId?: string;
  }) => Promise<void>;
  onCancel?: () => void;
  className?: string;
  defaultStatus?: TaskStatus;
  defaultProjectId?: string;
  availableAssignees?: Array<{ id: string; name: string; avatar?: string }>;
  availableProjects?: Array<{ id: string; name: string }>;
  isLoading?: boolean;
}

export default function TaskCreate({
  onSubmit,
  onCancel,
  className,
  defaultStatus = "todo",
  defaultProjectId,
  availableAssignees = [],
  availableProjects = [],
  isLoading = false,
}: TaskCreateProps) {
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [status, setStatus] = useState<TaskStatus>(defaultStatus);
  const [priority, setPriority] = useState<TaskPriority>("medium");
  const [dueDate, setDueDate] = useState("");
  const [selectedAssignees, setSelectedAssignees] = useState<string[]>([]);
  const [tags, setTags] = useState<string[]>([]);
  const [tagInput, setTagInput] = useState("");
  const [projectId, setProjectId] = useState(defaultProjectId || "");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!title.trim()) return;

    await onSubmit?.({
      title: title.trim(),
      description: description.trim() || undefined,
      status,
      priority,
      dueDate: dueDate ? new Date(dueDate) : undefined,
      assigneeIds: selectedAssignees.length > 0 ? selectedAssignees : undefined,
      tags: tags.length > 0 ? tags : undefined,
      projectId: projectId || undefined,
    });

    // Reset form
    setTitle("");
    setDescription("");
    setStatus(defaultStatus);
    setPriority("medium");
    setDueDate("");
    setSelectedAssignees([]);
    setTags([]);
    setTagInput("");
    setProjectId(defaultProjectId || "");
  };

  const handleAddTag = () => {
    if (tagInput.trim() && !tags.includes(tagInput.trim())) {
      setTags([...tags, tagInput.trim()]);
      setTagInput("");
    }
  };

  const handleRemoveTag = (tagToRemove: string) => {
    setTags(tags.filter((tag) => tag !== tagToRemove));
  };

  const handleToggleAssignee = (assigneeId: string) => {
    setSelectedAssignees((prev) =>
      prev.includes(assigneeId)
        ? prev.filter((id) => id !== assigneeId)
        : [...prev, assigneeId]
    );
  };

  return (
    <Card className={className}>
      <CardHeader>
        <div className="flex flex-col gap-1">
          <CardTitle>Create Task</CardTitle>
          <CardDescription>Add a new task to your project</CardDescription>
        </div>
      </CardHeader>
      <CardContent>
        <form className="flex flex-col gap-4" onSubmit={handleSubmit}>
          <Field>
            <FieldLabel htmlFor="title">
              Title <span className="text-destructive">*</span>
            </FieldLabel>
            <FieldContent>
              <InputGroup>
                <InputGroupInput
                  id="title"
                  onChange={(e) => setTitle(e.target.value)}
                  placeholder="Task title…"
                  required
                  value={title}
                />
              </InputGroup>
            </FieldContent>
          </Field>

          <Field>
            <FieldLabel htmlFor="description">Description</FieldLabel>
            <FieldContent>
              <Textarea
                id="description"
                onChange={(e) => setDescription(e.target.value)}
                placeholder="Task description…"
                rows={4}
                value={description}
              />
            </FieldContent>
          </Field>

          <div className="grid gap-4 sm:grid-cols-2">
            <Field>
              <FieldLabel htmlFor="status">Status</FieldLabel>
              <FieldContent>
                <Select
                  onValueChange={(value) => setStatus(value as TaskStatus)}
                  value={status}
                >
                  <SelectTrigger id="status">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="todo">To Do</SelectItem>
                    <SelectItem value="in_progress">In Progress</SelectItem>
                    <SelectItem value="done">Done</SelectItem>
                    <SelectItem value="cancelled">Cancelled</SelectItem>
                  </SelectContent>
                </Select>
              </FieldContent>
            </Field>

            <Field>
              <FieldLabel htmlFor="priority">Priority</FieldLabel>
              <FieldContent>
                <Select
                  onValueChange={(value) => setPriority(value as TaskPriority)}
                  value={priority}
                >
                  <SelectTrigger id="priority">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="low">Low</SelectItem>
                    <SelectItem value="medium">Medium</SelectItem>
                    <SelectItem value="high">High</SelectItem>
                    <SelectItem value="urgent">Urgent</SelectItem>
                  </SelectContent>
                </Select>
              </FieldContent>
            </Field>
          </div>

          <Field>
            <FieldLabel htmlFor="dueDate">Due Date</FieldLabel>
            <FieldContent>
              <InputGroup>
                <InputGroupAddon>
                  <Calendar className="size-4" />
                </InputGroupAddon>
                <InputGroupInput
                  id="dueDate"
                  onChange={(e) => setDueDate(e.target.value)}
                  type="date"
                  value={dueDate}
                />
              </InputGroup>
            </FieldContent>
          </Field>

          {availableProjects.length > 0 && (
            <Field>
              <FieldLabel htmlFor="project">Project</FieldLabel>
              <FieldContent>
                <Select
                  onValueChange={setProjectId}
                  value={projectId || undefined}
                >
                  <SelectTrigger id="project">
                    <SelectValue placeholder="Select project" />
                  </SelectTrigger>
                  <SelectContent>
                    {availableProjects.map((project) => (
                      <SelectItem key={project.id} value={project.id}>
                        {project.name}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </FieldContent>
            </Field>
          )}

          {availableAssignees.length > 0 && (
            <Field>
              <FieldLabel>Assignees</FieldLabel>
              <FieldContent>
                <div className="flex flex-wrap gap-2">
                  {availableAssignees.map((assignee) => {
                    const isSelected = selectedAssignees.includes(assignee.id);
                    return (
                      <Button
                        key={assignee.id}
                        onClick={() => handleToggleAssignee(assignee.id)}
                        size="sm"
                        type="button"
                        variant={isSelected ? "default" : "outline"}
                      >
                        {assignee.name}
                      </Button>
                    );
                  })}
                </div>
              </FieldContent>
            </Field>
          )}

          <Field>
            <FieldLabel htmlFor="tags">Tags</FieldLabel>
            <FieldContent>
              <div className="flex flex-col gap-2">
                <div className="flex gap-2">
                  <InputGroup>
                    <InputGroupInput
                      id="tags"
                      onChange={(e) => setTagInput(e.target.value)}
                      onKeyDown={(e) => {
                        if (e.key === "Enter") {
                          e.preventDefault();
                          handleAddTag();
                        }
                      }}
                      placeholder="Add tag…"
                      value={tagInput}
                    />
                  </InputGroup>
                  <Button
                    onClick={handleAddTag}
                    size="icon"
                    type="button"
                    variant="outline"
                  >
                    <Plus className="size-4" />
                  </Button>
                </div>
                {tags.length > 0 && (
                  <div className="flex flex-wrap gap-2">
                    {tags.map((tag) => (
                      <div
                        className="flex items-center gap-1 rounded-full border bg-secondary px-2 py-1 text-sm"
                        key={tag}
                      >
                        <span>{tag}</span>
                        <button
                          aria-label={`Remove ${tag}`}
                          onClick={() => handleRemoveTag(tag)}
                          type="button"
                        >
                          <X className="size-3" />
                        </button>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            </FieldContent>
          </Field>

          <Separator />

          <div className="flex justify-end gap-2">
            {onCancel && (
              <Button
                disabled={isLoading}
                onClick={onCancel}
                type="button"
                variant="outline"
              >
                Cancel
              </Button>
            )}
            <Button disabled={isLoading || !title.trim()} type="submit">
              {isLoading ? (
                <>
                  <Loader2 className="size-4 animate-spin" />
                  Creating…
                </>
              ) : (
                <>
                  <Plus className="size-4" />
                  Create Task
                </>
              )}
            </Button>
          </div>
        </form>
      </CardContent>
    </Card>
  );
}

Installation

npx shadcn@latest add @hextaui/task-create

Usage

import { TaskCreate } from "@/components/ui/task-create"
<TaskCreate />