📚

【NextJs14】NextJs14 と 便利なライブラリ【#30Create Todo Form】

2024/02/12に公開

【#30Create Todo Form】

YouTube: https://youtu.be/5SH2X-o-iPQ

https://youtu.be/5SH2X-o-iPQ

今回はTodoを作成するフォームを実装します。

app/(main)/account/page.tsx
import { CheckCircle, Trash2 } from "lucide-react";
import { formatDistanceToNow } from "date-fns";

import { db } from "@/lib/db";

import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";

import { TodoForm } from "./_components/todo-form";

const getTodos = async () => {
  const todos = await db.todo.findMany({
    orderBy: {
      createdAt: "asc",
    },
  });
  return todos;
};

const AccountPage = async () => {
  const todos = await getTodos();

  return (
    <div className="p-6 w-full h-full">
      <div className="w-full h-full flex flex-col gap-y-3">
        <div className="flex flex-col md:flex-row items-center justify-between gap-2">
          <h2 className="text-center md:text-left text-3xl font-bold">Todos</h2>
          <TodoForm />
        </div>
        <Separator />
        <ul className="w-full space-y-3">
          {todos.map((todo, idx) => (
            <li
              key={todo.id}
              className="flex flex-col md:flex-row items-center justify-between gap-x-4 gap-y-2"
            >
              <div className="flex flex-col md:flex-row flex-1 items-center justify-between gap-x-3">
                <p className="text-xl font-semibold">
                  <span className="pr-2">{idx + 1}</span>
                  <span className={todo.isCompleted ? "line-through" : ""}>
                    {todo.title}
                  </span>
                </p>
                <p>
                  {formatDistanceToNow(todo.createdAt, { addSuffix: true })}
                </p>
              </div>
              <div className="flex items-center gap-x-2">
                <Button variant={todo.isCompleted ? "Completed" : "secondary"}>
                  <CheckCircle className="h-5 w-5" />
                </Button>
                <Button variant="destructive">
                  <Trash2 className="h-5 w-5" />
                </Button>
              </div>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};

export default AccountPage;
app/(main)/account/_components/todo-form.tsx
"use client";
import * as z from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

import { CreateTodoSchema } from "@/types/schema";

import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

export const TodoForm = () => {
  const form = useForm<z.infer<typeof CreateTodoSchema>>({
    resolver: zodResolver(CreateTodoSchema),
    defaultValues: {
      title: "",
    },
  });

  const onSubmit = (values: z.infer<typeof CreateTodoSchema>) => {
    alert(JSON.stringify(values));
  };

  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="flex items-center gap-x-1 w-full max-w-xl"
      >
        <FormField
          control={form.control}
          name="title"
          render={({ field }) => (
            <FormItem className="w-full">
              <FormControl>
                <Input {...field} type="text" placeholder="Enter your todo" />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit" variant="primary">
          Create
        </Button>
      </form>
    </Form>
  );
};
types/schema.ts
import * as z from "zod";

export const CreateTodoSchema = z.object({
  title: z.string().min(1, {
    message: "Title is required.",
  }),
});

Discussion