- Discordクローン作成 1. 環境構築〜Clerkの設定
- Discordクローン作成 2. ダークモード〜Prisma, PlanetScale
- Discordクローン作成 3. モーダルUI, 画像(uploadthing)
- Discordクローン作成 4. サーバ作成, ナビゲーション 👈ココ
- Discordクローン作成 5. サーバー作成用モーダル、サイドバー作成
- Discordクローン作成 7. メンバーの管理
- Discordクローン作成 8. チャンネル作成、サーバ削除・退出
- Discordクローン作成 9. サーバ検索、チャンネルリスト
- Discordクローン作成 10. チャンネルページ作成
- Discordクローン作成 11. 会話ページ作成
- Discordクローン作成 12. メッセージ送信
- Discordクローン作成 13. リアルタイムチャット
8. サーバの作成
ここのcreateの処理を作成していく
サーバ作成API呼び出し実装
どこかでfetchの方がいいと見かけた記憶があるのだけど、パフォーマンス上問題ないならaxiosの方が楽(なはず)。

Axiosのインストール
pnpm add axios
Submit処理の実装
以下のonSubmit処理をAxiosを利用して実装していく
const onSubmit = async (values: z.infer<typeof formSchema>) => {
console.log(values);
};
"use client";
import axios from "axios";
import { useRouter } from "next/navigation";
~~~
export const InitialModal = () => {
const [isMounted, setIsMounted] = useState(false);
const router = useRouter();
useEffect(() => {
setIsMounted(true);
}, []);
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
imageUrl: "",
},
});
const isLoading = form.formState.isSubmitting;
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await axios.post("/api/servers", values);
//初期化
form.reset();
router.refresh();
window.location.reload();
} catch (error) {
console.error(error);
}
};
~~~
[Tips]useRouterのrefreshとwindow.location.reload
refresh
だけでいいのでは?と思ったので聞いてみるも、正確なところはわからず。refresh
することで「サーバがある状態」でデータ更新をするので、作成したサーバに入った状態とできるはず。
router.refresh();
:これはNext.jsのルーターを使用して現在のページをリフレッシュします。これにより、サーバーサイドで生成されるデータが更新されます。window.location.reload();
:これはブラウザのJavaScript APIを使用してページ全体をリロードします。これにより、クライアントサイドの状態がリセットされ、ページ全体が再読み込みされます。ただし、これら2つの行は似た動作をするため、通常はどちらか一方だけを使用します。両方を使用すると、ページが2回リロードされる可能性があります。
追記
サーバ削除処理においてwindow.location.reload
をつけないとリフレッシュが効かなかった。router.refresh
だけだと効かないパターンがあるらしい(ここはまだ理解が浅い)
[Tips]react-hook-formのreset
必ず空にする、というわけではないので関数の使い方には注意が必要

サーバ作成API
プロフィール取得ライブラリの作成
ログインユーザの情報を取得するためのライブラリを作成する。
import { db } from "@/lib/db";
import { auth } from "@clerk/nextjs";
export const currentProfile = async () => {
const { userId } = auth();
if (!userId) {
return null;
}
const profile = await db.profile.findUnique({
where: {
userId,
},
});
return profile;
};
uuidのインストール
serverテーブルのidを作成するためにuuidをインストールする。
pnpm add uuid
pnpm add -D @types/uuid
[Tips] プライマリキーをUUIDにする場合の注意点
普通に使ってる分には問題なさそうだけど、SNSのPOSTなどでUUIDを使う場合は速度面への考慮が必要。以下が詳しそう(全部読めていない)。
![MySQLでプライマリキーをUUIDにする前に知っておいて欲しいこと | Raccoon Tech Blog [株式会社ラクーンホールディングス 技術戦略部ブログ]](https://techblog.raccoon.ne.jp/wp-content/uploads/2021/07/board-755792_800.jpg)
UUIDには種類があって、UUID v7から時系列に基づいたUUIDが作成される。これによって上記のパフォーマンス問題が解消される(らしい)。まだドラフト?

サーバ作成APIの実装
- uuidはv4。
- サーバ作成時にgeneralチャンネルと管理者(自分)を追加する。
import { v4 as uuidv4 } from "uuid";
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 { name, imageUrl } = await req.json();
const profile = await currentProfile();
if (!profile) {
return new NextResponse("Unauthorized", { status: 401 });
}
const server = await db.server.create({
data: {
profileId: profile.id,
name,
imageUrl,
inviteCode: uuidv4(),
channels: {
create: [
{
name: "general",
profileId: profile.id,
},
],
},
members: {
create: {
profileId: profile.id,
role: MemberRole.ADMIN,
},
},
},
});
return NextResponse.json(server);
} catch (error) {
console.error("[SERVERS_POST]", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}
作成時にmemberとchannelに追加されることが確認できる。
リダイレクト
router.refresh()
を行うとサーバが作成された状態となるので、サーバページにリダイレクトする(この時点では未作成なので404エラー)
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
import { initialProfile } from "@/lib/initial-profile";
import { InitialModal } from "@/components/modals/initial-modal";
const SetupPage = async () => {
//プロフィールを取得する
const profile = await initialProfile();
//自分が所属するサーバから1件目を取得する
const server = await db.server.findFirst({
where: {
members: {
some: {
profileId: profile.id,
},
},
},
});
//サーバが存在する場合はそのサーバにリダイレクトする
if (server) {
return redirect(`/servers/${server.id}`);
}
return <InitialModal />;
};
export default SetupPage;
9. ナビゲーション
ナビゲーションバーを作成する
コンポーネントの作成
export const NavigationSidebar = () => {
return <div>Navigation Sidebar</div>;
};
レイアウトの設定
- md以上の場合に表示されるようにする
- サイドバーの幅は72px
inset-y-0
は左側固定。tailwindのレイアウト指定は以下の通り(inset-y-0
なら04
のレイアウト。)
https://tailwindcss.com/docs/top-right-bottom-left
import { NavigationSidebar } from "@/components/navigation/navigation-sidebar";
const MainLayout = async ({ children }: { children: React.ReactNode }) => {
return (
<div className="h-full">
<div className="hidden md:flex h-full w-[72px] z-30 flex-col fixed inset-y-0">
<NavigationSidebar />
</div>
<main className="md:pl-[72px] h-full">{children}</main>
</div>
);
};
export default MainLayout;
ナビゲーションバーの中身を実装
ツールチップとセパレータの導入
shadcn/uiから導入 https://ui.shadcn.com/docs/components/tooltip
pnpm dlx shadcn-ui@latest add tooltip
pnpm dlx shadcn-ui@latest add separator
ナビゲーションアクションコンポーネント
クライアントサイドのアクションがあるので分割して作成していく。
- navigation-sidebar
profile
とservers
を取得する- NavigationActionコンポーネントを表示する
- navigation-action
- プラスボタンを表示する。
- hover時のアクションはgroup指定で実施。
navigation-sidebar
import { currentProfile } from "@/lib/current-profile";
import { db } from "@/lib/db";
import { redirect } from "next/navigation";
export const NavigationSidebar = async () => {
const profile = await currentProfile();
if (!profile) {
return redirect("/");
}
const servers = await db.server.findMany({
where: {
members: {
some: {
profileId: profile.id,
},
},
},
});
return (
<div className="space-y-4 flex flex-col items-center h-full text-primary w-full dark:bg-[#1E1F22] py-3">
<NavigationAction />
</div>
);
};
navigation-action
group指定でhover時のアクションを実施する。group-hoverとすることによって複数要素を同じタイミングで変更できる。
"use client";
import { Plus } from "lucide-react";
export const NavigationAction = () => {
return (
<div>
<button className="group flex items-center">
<div
className="flex mx-3 h-[48px] w-[48px]
rounded-[24px] group-hover:rounded-[16px]
transition-all overflow-hidden items-center
justify-center bg-background dark:bg-neutral-700 group-hover:bg-emerald-500"
>
<Plus
className="group-hover:text-white transition text-emerald-500"
size={25}
/>
</div>
</button>
</div>
);
};
こんなアニメーション。
[Tips]transition-allとtransitionの違い
transition-all
を指定した箇所で transition
を指定しても同じ動きをしている。cssで見ると一緒っぽい(transition-property
に全種類入ってるのかは知らないけど……)
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
アクション用ツールチップの作成
プラスボタンで表示されるツールチップを作成する。
"use client";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface ActionTooltipProps {
children: React.ReactNode;
label: string;
side?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end";
}
export const ActionTooltip = ({
children,
label,
side,
align,
}: ActionTooltipProps) => {
return (
<TooltipProvider>
<Tooltip delayDuration={50}>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent side={side} align={align}>
<p className="font-semibold text-sm capitalize">
{label.toLowerCase()}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
asChild
についてはchildrenに対して親コンポーネントの機能をつけることができる浅い理解。以下ブログが事の背景から非常に詳しい。

ツールチップをナビゲーションアクションに追加
"use client";
import { Plus } from "lucide-react";
import { ActionTooltip } from "@/components/action-tooltip";
export const NavigationAction = () => {
return (
<div>
<ActionTooltip side="right" align="center" label="サーバーを追加する">
<button className="group flex items-center">
<div
className="flex mx-3 h-[48px] w-[48px]
rounded-[24px] group-hover:rounded-[16px]
transition-all overflow-hidden items-center
justify-center bg-background dark:bg-neutral-700 group-hover:bg-emerald-500"
>
<Plus
className="group-hover:text-white transition text-emerald-500"
size={25}
/>
</div>
</button>
</ActionTooltip>
</div>
);
};
hover時にツールチップが表示される。
ナビゲーションバーにサーバ一覧を表示する
アクションボタンの下にセパレータを追加してサーバ一覧を表示していく。ナビゲーションバー下部にはアカウント情報とダークモードの切り替えトグルも表示する。
セパレータ追加
import { Separator } from "@/components/ui/separator";
export const NavigationSidebar = async () => {
~~~
return (
<div className="space-y-4 flex flex-col items-center h-full text-primary w-full dark:bg-[#1E1F22] py-3">
<NavigationAction />
<Separator className="h-[2px] bg-zinc-200 dark:bg-zinc-700 rounded-md w-10 mx-auto" />
</div>
);
};
スクロールエリア追加

pnpm dlx shadcn-ui@latest add scroll-area
最終的には以下の形。
"use client";
import { ActionTooltip } from "@/components/action-tooltip";
import { useParams, useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import Image from "next/image";
interface NavigationItemProps {
id: string;
imageUrl: string;
name: string;
}
export const NavigationItem = ({ id, imageUrl, name }: NavigationItemProps) => {
const params = useParams();
const router = useRouter();
const onClick = () => {
router.push(`/servers/${id}`);
};
return (
<ActionTooltip side="right" align="center" label={name}>
<button onClick={onClick} className="group relative flex items-center">
<div
className={cn(
"absolute left-0 bg-primary rounded-r-full transition-all w-[4px]",
params?.serverId !== id && "group-hover:h-[20px]",
params?.serverId === id ? "h-[36px]" : "h-[8px]"
)}
/>
<div
className={cn(
" group flex mx-3 h-[48px] w-[48px] rounded-[24px] group-hover:rounded-[16px] transition-all overflow-hidden",
params?.serverId === id &&
"bg-primary/10 text-primary rounded-[16px]"
)}
>
<Image alt="Channel" src={imageUrl} fill />
</div>
</button>
</ActionTooltip>
);
};
チャンネルアイコンの左部にあるバーを表示している。現在のサーバの場合は36px、それ以外は8pxで、hover時に20pxになるように制御。
<div
className={cn(
"bg-primary absolute left-0 w-[4px] rounded-r-full transition-all",
params?.serverId !== id && "group-hover:h-[20px]",
params?.serverId === id ? "h-[36px]" : "h-[8px]"
)}
/>
サーバイメージのアイコン部。現在のサーバの場合は背景色を変更している(が、今のところ分からない)。relative, h-[48px] w-[48px]
でアイコンのサイズが固定されるようにしている。(が、Imageコンポーネントでfillではなくwidth, heightを指定することでも実現できそう。何が違うんだろう。relative指定した上でサイズ指定する必要があるのが苦手)
<div
className={cn(
"group relative mx-3 flex h-[48px] w-[48px] overflow-hidden rounded-[24px] transition-all group-hover:rounded-[16px]",
params?.serverId === id && "bg-primary/10 text-primary rounded-[16px]"
)}
>
<Image alt="Channel" src={imageUrl} fill />
</div>
チャンネルとダークモードのトグル。ユーザーアイコンを追加する。
import { ModeToggle } from "@/components/mode-toggle";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { currentProfile } from "@/lib/current-profile";
import { db } from "@/lib/db";
import { UserButton } from "@clerk/nextjs";
import { redirect } from "next/navigation";
import { NavigationAction } from "./navigation-action";
import { NavigationItem } from "./navigation-item";
export const NavigationSidebar = async () => {
/* 省略*/
return (
<div className="space-y-4 flex flex-col items-center h-full text-primary w-full dark:bg-[#1E1F22] py-3">
<NavigationAction />
<Separator className="h-[2px] bg-zinc-200 dark:bg-zinc-700 rounded-md w-10 mx-auto" />
<ScrollArea className="flex-1 w-full">
{servers.map((server) => (
<div key={server.id} className="mb-4">
<NavigationItem
id={server.id}
name={server.name}
imageUrl={server.imageUrl}
/>
</div>
))}
</ScrollArea>
<div className="pb-3 mt-auto flex items-center flex-col gap-y-4">
<ModeToggle />
<UserButton
afterSignOutUrl="/"
appearance={{
elements: {
avatarBox: "h-[48px] w-[48px]",
},
}}
/>
</div>
</div>
);
};