- Discordクローン作成 1. 環境構築〜Clerkの設定
- Discordクローン作成 2. ダークモード〜Prisma, PlanetScale
- Discordクローン作成 3. モーダルUI, 画像(uploadthing)
- Discordクローン作成 4. サーバ作成, ナビゲーション
- Discordクローン作成 5. サーバー作成用モーダル、サイドバー作成
- Discordクローン作成 6. 招待、サーバ設定
- Discordクローン作成 7. メンバーの管理
- Discordクローン作成 8. チャンネル作成、サーバ削除・退出 👈ココ
- Discordクローン作成 9. サーバ検索、チャンネルリスト
- Discordクローン作成 10. チャンネルページ作成
- Discordクローン作成 11. 会話ページ作成
- Discordクローン作成 12. メッセージ送信
- Discordクローン作成 13. リアルタイムチャット
15. チャンネル作成
チャンネル登録のためのモーダルを作成する。
モーダル作成
ストアにチャンネル作成を追加
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>
)}
この段階で以下モーダル呼び出しができる状態。
チャンネルタイプのセレクトボックス・入力チェックの追加
- Shadcn/uiのSelectコンポーネントを利用する。
- チャンネルタイプはTEXT, AUDIO, VIDEOが選べる。
Select
Displays a list of options for the user to pick from—triggered by a button.

terminal
pnpm dlx shadcn-ui@latest add select
- name: generalチャンネルはデフォルトで1つしか作成できないようにするため、入力チェックを追加する。
- useFormの
refine
を用いてカスタムバリデーションを作成する
- useFormの
- 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,
},
});
チャンネルタイプのセレクトボックスをフォームに追加する
- 動画の通りだと以下。
- セレクトボックスはエラーが発生しないので、
<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() 静的メソッドは、指定されたオブジェクトが持つ列挙可能なプロパティの文字列キーのプロパティ値を配列で返します。

<SelectContent>
{Object.values(ChannelType).map(type => (
<SelectItem key={type} value={type} className="capitalize">
{type.toLowerCase()}
</SelectItem>
))}
</SelectContent>
enumに関しては以下
列挙型 (enum) | TypeScript入門『サバイバルTypeScript』
TypeScriptでは、列挙型(enum)を用いると、定数のセットに意味を持たせたコード表現ができます。
.png?pattern=cross&md=0&fontSize=75px&textColor=%23ffffff&textStrongColor=%238340BB&overlay=https%3A%2F%2Fraw.githubusercontent.com%2Fyytypescript%2Fog-image%2Fmain%2Fpublic%2Fogp-overlay.svg)
[Tips]FormControl何やってるの?
- パッと分かるほど理解してないが、エラー表示ができなくなるのだと思われる。
- Slotに関してはRadix uiへの理解がまだまだ浅い。
asChild
の仕組みを実現するためのものと理解しているchildren
で渡したコンポーネントに対して機能を追加できる(この場合はエラーハンドリングによる表示
Slot – Radix Primitives
Merges its props onto its immediate child.

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 オブジェクトを返します。

- Requestについて(備忘)
Request - Web API | MDN
Request はフェッチ API のインターフェイスで、リソースのリクエストを表します。

- チャンネルは
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. サーバ削除/退出処理
以下の処理を実装していく。
サーバ退出処理
まずは他の手順と一緒でコピーして呼び出せる状態にする。
モーダルストアに退出処理を追加
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 />
</>
);
- ヘッダから呼び出せるようにする
サーバ退出処理の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
を実行しないと画面に反映されなかった(動画ではなかったはずだが)。