Skip to content

Discordクローン作成 8. チャンネル作成

公開日

表紙

15. チャンネル作成

チャンネル登録のためのモーダルを作成する。

15-1

モーダル作成

ストアにチャンネル作成を追加

hooks/use-modal-store.ts
import { Server } from "@prisma/client";
import { create } from "zustand";
 
export type ModalType =
  | "createServer"
  | "invite"
  | "editServer"
  | "members"
  | "createChannel";

モーダルのコンポーネントを作成

  • 中身はサーバ作成モーダルのものをコピーして作成
    • components/modals/create-channel-modal.tsx
  • 画像アップロード機能を削除する(チャンネルには不要なので)

モーダルプロバイダに追加

components/providers/modal-provider.tsx
  return (
    <>
      <CreateServerModal />
      <InviteModal />
      <EditServerModal />
      <MembersModal />
      <CreateChannelModal />
    </>
  );

画面から呼び出し追加

components/server/server-header.tsx
{isModerator && (
  <DropdownMenuItem
    onClick={() => onOpen("createChannel")}
    className="px-3 py-2 text-sm cursor-pointer"
  >
    チャンネルの作成
    <PlusCircle className="h-4 w-4 ml-auto" />
  </DropdownMenuItem>
)}

この段階で以下モーダル呼び出しができる状態。

15-2

チャンネルタイプのセレクトボックス・入力チェックの追加

  • Shadcn/uiのSelectコンポーネントを利用する。
  • チャンネルタイプはTEXT, AUDIO, VIDEOが選べる。
Select
Displays a list of options for the user to pick from—triggered by a button.
Select favicon https://ui.shadcn.com/docs/components/select
Select
terminal
pnpm dlx shadcn-ui@latest add select
  • name: generalチャンネルはデフォルトで1つしか作成できないようにするため、入力チェックを追加する。
    • useFormのrefineを用いてカスタムバリデーションを作成する
  • type: prismaのスキーマでenumを定義しているので、nativeEnumを利用する。
components/modals/create-channel-modal.tsx
const formSchema = z.object({
  name: z
    .string()
    .min(1, {
      message: "チャンネル名を入力してください",
    })
    .refine((name) => name !== "general", {
      message: "'general' は予約語です",
    }),
  type: z.nativeEnum(ChannelType),
});
  • formのデフォルトも更新しておく
components/modals/create-channel-modal.tsx
const form = useForm({
  resolver: zodResolver(formSchema),
  defaultValues: {
    name: "",
    type: ChannelType.TEXT,
  },
});

15-3

チャンネルタイプのセレクトボックスをフォームに追加する

  • 動画の通りだと以下。
  • セレクトボックスはエラーが発生しないので、<FormControl></FormControl>が不要に思えたので実際は削除している(後続処理で問題が発生したら戻すため、記事上は残しておく)
components/modals/create-channel-modal.tsx
<FormField
  control={form.control}
  name="type"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Channel Type</FormLabel>
      <Select
        disabled={isLoading}
        onValueChange={field.onChange}
        defaultValue={field.value}
      >
        <FormControl>
          <SelectTrigger className=" bg-zinc-300/50 border-0 focus:ring-0 text-black ring-offset-0 focus:ring-offset-0 capitalize outline-none">
            <SelectValue placeholder="チャンネルを選択してください"></SelectValue>
          </SelectTrigger>
        </FormControl>
        <SelectContent>
          {Object.values(ChannelType).map((type) => (
            <SelectItem
              key={type}
              value={type}
              className="capitalize"
            >
              {type.toLowerCase()}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
      <FormMessage />
    </FormItem>
  )}
/>

セレクトボックスの中身をenumから生成するには、Object.valuesを利用する。

Object.values() - JavaScript | MDN
Object.values() 静的メソッドは、指定されたオブジェクトが持つ列挙可能なプロパティの文字列キーのプロパティ値を配列で返します。
Object.values() - JavaScript | MDN favicon https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/values
Object.values() - JavaScript | MDN
<SelectContent>
  {Object.values(ChannelType).map(type => (
    <SelectItem key={type} value={type} className="capitalize">
      {type.toLowerCase()}
    </SelectItem>
  ))}
</SelectContent>

enumに関しては以下

列挙型 (enum) | TypeScript入門『サバイバルTypeScript』
TypeScriptでは、列挙型(enum)を用いると、定数のセットに意味を持たせたコード表現ができます。
列挙型 (enum) | TypeScript入門『サバイバルTypeScript』 favicon https://typescriptbook.jp/reference/values-types-variables/enum
列挙型 (enum) | TypeScript入門『サバイバルTypeScript』

15-4

[Tips]FormControl何やってるの?

  • パッと分かるほど理解してないが、エラー表示ができなくなるのだと思われる。
  • Slotに関してはRadix uiへの理解がまだまだ浅い。
    • asChild の仕組みを実現するためのものと理解している
    • childrenで渡したコンポーネントに対して機能を追加できる(この場合はエラーハンドリングによる表示
Slot – Radix Primitives
Merges its props onto its immediate child.
Slot – Radix Primitives favicon https://www.radix-ui.com/primitives/docs/utilities/slot
Slot – Radix Primitives
components/ui/form.tsx
const FormControl = React.forwardRef<
  React.ElementRef<typeof Slot>,
  React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
 
  return (
    <Slot
      ref={ref}
      id={formItemId}
      aria-describedby={
        !error
          ? `${formDescriptionId}`
          : `${formDescriptionId} ${formMessageId}`
      }
      aria-invalid={!!error}
      {...props}
    />
  )
})
FormControl.displayName = "FormControl"

チャンネル作成のAPIを呼び出し

components/modals/create-channel-modal.tsx
export const CreateChannelModal = () => {
  const { isOpen, onClose, type } = useModal();
  const router = useRouter();
  const params = useParams();
 
  const isModalOpen = isOpen && type === "createChannel";
 
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: "",
      type: ChannelType.TEXT,
    },
  });
 
  const isLoading = form.formState.isSubmitting;
 
  const onSubmit = async (values: z.infer<typeof formSchema>) => {
    try {
      const url = qs.stringifyUrl({
        url: "/api/channels",
        query: {
          serverId: params?.serverId,
        },
      });
 
      await axios.post(url, values);

チャンネル作成のAPIを作成

  • searchParamsについて(備忘)
URL: searchParams プロパティ - Web API | MDN
searchParams は URL インターフェイスの読み取り専用プロパティで、URL に含まれる GET デコードされたクエリー引数へのアクセスを可能にする URLSearchParams オブジェクトを返します。
URL: searchParams プロパティ - Web API | MDN favicon https://developer.mozilla.org/ja/docs/Web/API/URL/searchParams
URL: searchParams プロパティ - Web API | MDN
  • Requestについて(備忘)
Request - Web API | MDN
Request はフェッチ API のインターフェイスで、リソースのリクエストを表します。
Request - Web API | MDN favicon https://developer.mozilla.org/ja/docs/Web/API/Request
Request - Web API | MDN
  • チャンネルはAdmin, Moderatorのみ作成できるようにする。
  • generalは予約語として作成できないようにする。
app/api/channels/route.ts
import { currentProfile } from "@/lib/current-profile";
import { db } from "@/lib/db";
import { MemberRole } from "@prisma/client";
import { NextResponse } from "next/server";
 
export async function POST(req: Request) {
  try {
    const profile = await currentProfile();
 
    const { name, type } = await req.json();
    const { searchParams } = new URL(req.url);
 
    const serverId = searchParams.get("serverId");
 
    if (!profile) {
      return new NextResponse("Unauthorized", { status: 401 });
    }
 
    if (!serverId) {
      return new NextResponse("Server ID missing", { status: 400 });
    }
 
    if (name === "general") {
      return new NextResponse("Invalid channel name 'general'", {
        status: 400,
      });
    }
 
    const server = await db.server.update({
      where: {
        id: serverId,
        members: {
          some: {
            profileId: profile.id,
            role: {
              in: [MemberRole.ADMIN, MemberRole.MODERATOR],
            },
          },
        },
      },
      data: {
        channels: {
          create: {
            profileId: profile.id,
            name,
            type,
          },
        },
      },
    });
 
    return NextResponse.json(server);
  } catch (error) {
    console.error("CHANNELS_POST", error);
    return new NextResponse("Internal Server Error", { status: 500 });
  }
}

16. サーバ削除/退出処理

以下の処理を実装していく。

16-1 16-2

サーバ退出処理

まずは他の手順と一緒でコピーして呼び出せる状態にする。

モーダルストアに退出処理を追加

hooks/use-modal-store.ts
export type ModalType =
  | "createServer"
  | "invite"
  | "editServer"
  | "members"
  | "createChannel"
  | "leaveServer";
  • 招待モーダルをコピーして作成する。

  • モーダルプロバイダに追加する

components/providers/modal-provider.tsx
  return (
    <>
      <CreateServerModal />
      <InviteModal />
      <EditServerModal />
      <MembersModal />
      <CreateChannelModal />
      <LeaveServerModal />
    </>
  );
  • ヘッダから呼び出せるようにする

16-3

サーバ退出処理のAPIを作成

app/api/servers[serverId]/leave/route.ts
import { currentProfile } from "@/lib/current-profile";
import { db } from "@/lib/db";
import { NextResponse } from "next/server";
 
export async function PATCH(
  req: Request,
  { params }: { params: { serverId: string } }
) {
  try {
    const profile = await currentProfile();
    if (!profile) {
      return new NextResponse("Unauthorized", { status: 401 });
    }
    if (!params.serverId) {
      return new NextResponse("Server ID Missing", { status: 400 });
    }
 
    //サーバ作成者の場合は退出できないようにするため、not: profile.idとする
    const server = await db.server.update({
      where: {
        id: params.serverId,
        profileId: {
          not: profile.id,
        },
        members: {
          some: {
            profileId: profile.id,
          },
        },
      },
      data: {
        members: {
          //deleteはユニークキーである必要があるので、deleteManyを使用
          deleteMany: {
            profileId: profile.id,
          },
        },
      },
    });
 
    return NextResponse.json(server);
  } catch (error) {
    console.error("[SERVERS_SERVERID_LEAVE_PATCH]", error);
    return new NextResponse("Internal Server Error", { status: 500 });
  }
}
 

サーバ削除の追加

基本的には一緒の流れなので省略

削除したあと、window.location.reload を実行しないと画面に反映されなかった(動画ではなかったはずだが)。

16-4