今回はTypescript, React, Tailwind CSS, Shadcnを用いてNavigation barにUser buttonを配置する方法を紹介します。User buttonを押すとDropdown menuが出現し、Logoutなどの処理を行える様にします。主にShadcnのDropdown menuとAvatarを使用します。またUser objectはFirebaseから取得しているため、適宜変更してください。
自分のアウトプット目的の記事ですが、もしも誰かの役に立てたなら幸いです。

完成品こんな感じ

参考動画:Build a Jira Clone With Nextjs, React, Tailwind, Hono.js | Part 1/2 (2024)
参考資料:Avatar – shadcn/ui, Dropdown Menu – shadcn/ui

  • 使用技術
    • React19, Tailwind CSS, Shadcn

Navigation bar

まずUser buttonを配置するためのnavigation barを作成します。

//@/components/navbar
import { UserButton } from "@/components/user-button";

export const Navbar = () => {
  return (
    <nav className="pt-5 px-6 flex items-center justify-between">
      {/* ここにNavigationを入れてください */}
      <UserButton />
    </nav> 
  );
};

このnavigation barはlayout.tsxに配置してください。

//layout例
//@/app/layout
import { Navbar } from "@/components/navbar";
import { Sidebar } from "@/components/sidebar";
import React from "react";

const Layout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div className="min-h-screen">
      <div className="flex w-full h-full">
        <div className="fixed left-0 top-0 hidden lg:block lg:w-[264px] h-full overflow-y-auto">
          <Sidebar />
        </div>
        <div className="lg:pl-[264px] w-full">
          <div className="mx-auto max-x-screen-2xl h-full">
            <Navbar />{/* ここにNavbarを配置 */}
            <main className="h-full py-8 px-6 flex flex-col">{children}</main>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Layout;

ちなみに余談ですが Tailwindで "hidden lg:block" とすることで、モバイルデバイスなど幅1024px未満の時はSidebarが表示されず、幅がそれ以上になるとCSS上で display:hiddendisplay:block で上書きされるため、Sidebarが表示されます。

User button

Dropdown Menu – shadcn

まずはShadcnの公式documentでDropdown menuの使い方を確認しましょう。
Dropdown Menu – shadcn/ui

User buttonをDropdownMenuTriggerで囲えば良さそうです。

Avatar – shadcn

Avatarの方はもっと簡単ですね。
Avatar – shadcn/ui

Imageが存在するならAvatarImageが表示されて、imageの取得に失敗した時にAvatarFallbackが表示される様です。

User button実装

Login中のuserはContext APIを使用して取得しています。こちらは別の記事で紹介しています。BaaSとしてFirebaseを使用しており、User objectの構造が異なる場合は適宜書き換えてください。

Userの取得

まずuserを取得します。Loading中はLucide reactのLoader circleを回転させて表示しています。もしuserが存在しない場合はUser buttonは表示されません。userのdisplayName, email, photoURLを取り出しています。
Loader circle – Lucide react

<LoaderCircle className="animate-spin" />は個人的によく使います。おすすめです。

const { user, isLoading } = useAuthContext();
const logoutMutation = useLogout();

if (isLoading) {
  return (
    <div className="size-10 rounded-full flex items-center justify-center bg-neutral-200 border border-neutral-300">
      <LoaderCircle className="size-4 animate-spin text-muted-foreground" />
    </div>
  );
}

if (!user) {
  return null;
}
const { displayName: name, email, photoURL } = user;

AvatarFallback

AvatarFallbackで表示される文字を設定します。userのdisplayNameの頭文字を取得し大文字にします。displayNameが存在しない場合はemailの頭文字を、それも存在しない場合は大文字のUを表示させる様にします。

const avatarFallback = name
  ? name.charAt(0).toUpperCase()
  : email?.charAt(0).toUpperCase() ?? "U";

TSXの実装

AvatarをDropdownのtriggerに設定します。Imageが存在する場合AvatarImageが表示され、Imageの取得に失敗した時にAvatarFallbackが表示されます。
src={photoURL ?? undefined} とNull合体演算子で書いているのはAvatarImageのsrc属性がnullを受け付けないためです。

<DropdownMenu modal={false}>
  <DropdownMenuTrigger className="outline-none relative">
    <Avatar className="size-10 hover:opacity-75 transition border border-neutral-300">
      <AvatarImage src={photoURL ?? undefined} />
      <AvatarFallback className="bg-neutral-200 font-medium text-neutral-500 flex items-center justify-center">
        {avatarFallback}
      </AvatarFallback>
    </Avatar>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    {/* ここにDropdown menuの内容を記述 */}
  </DropdownMenuContent>
</DropdownMenu>

DropdownMenuContent

Dropdownの出現場所などを設定します。
alignとsideで出現位置を設定します。sideOffsetは主軸からどれだけ離すかを指定します。今回の例では下方向に10px移動します。

<DropdownMenuContent
  align="end"
  side="bottom"
  className="w-60"
  sideOffset={10}
>
align="center" // 中央揃え <- default
align="start"  // Start揃え、sideが上下なら左端揃え
align="end"    // End揃え、sideが上下なら右端揃え
side="bottom"  // 下側に表示(デフォルト)
side="top"     // 上側に表示
side="left"    // 左側に表示  
side="right"   // 右側に表示
sideOffset={0}   // ピッタリくっつける
sideOffset={10}  // 10px離す

Dropdownの中身はAvatar, User名, Email, Logout buttonを表示しています。ちなみにLogoutのための関数はTanstack queryのuseMutationで作成しています。こちらも適宜書き換えてください。

"use client";

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { useAuthContext } from "@/context/auth-context";
import { useLogout } from "@/features/auth/custom-hooks/use-logout";
import { LoaderCircle, LogOut } from "lucide-react";

export const UserButton = () => {
  const { user, loading } = useAuthContext();
  const logoutMutation = useLogout(); //Tanstack queryのuseMutationを使用しています。

  if (loading) {
    return (
      <div className="size-10 rounded-full flex items-center justify-center bg-neutral-200 border border-neutral-300">
        <LoaderCircle className="size-4 animate-spin text-muted-foreground" />
      </div>
    );
  }

  if (!user) {
    return null;
  }
  const { displayName: name, email, photoURL } = user;

  const avatarFallback = name
    ? name.charAt(0).toUpperCase()
    : email?.charAt(0).toUpperCase() ?? "U";

  return (
    <DropdownMenu modal={false}>
      <DropdownMenuTrigger className="outline-none relative">
        <Avatar className="size-10 hover:opacity-75 transition border border-neutral-300">
          <AvatarImage src={photoURL ?? undefined} />
          <AvatarFallback className="bg-neutral-200 font-medium text-neutral-500 flex items-center justify-center">
            {avatarFallback}
          </AvatarFallback>
        </Avatar>
      </DropdownMenuTrigger>
      <DropdownMenuContent
        align="end"
        side="bottom"
        className="w-60"
        sideOffset={10}
      >
        {/* Dropdownの内容は適宜書き換えてください。 */}
        <div className="flex flex-col items-center justify-center gap-2 px-2.5 py-4">
          <Avatar className="size-[52px] transition border border-neutral-300">
            <AvatarImage src={photoURL ? photoURL : undefined} />
            <AvatarFallback className="bg-neutral-200 font-medium text-neutral-500 text-xl flex items-center justify-center">
              {avatarFallback}
            </AvatarFallback>
          </Avatar>
          <div className="flex flex-col items-center justify-center">
            <p className="text-sm font-medium text-neutral-900">
              {name || "User"}
            </p>
            <p className="text-xs  text-neutral-500">{email}</p>
          </div>
          <Separator />
          <DropdownMenuItem
            className="h-10 flex items-center justify-center text-amber-700 font-medium cursor-pointer"
            onClick={() => logoutMutation.mutate()}
          >
            <LogOut className="size-4 mr-2" />
            Log out
          </DropdownMenuItem>
        </div>
      </DropdownMenuContent>
    </DropdownMenu>
  );
};

投稿者 Hiro

元医者でエンジニアを目指しています。卒後3年目。 Python (flask, tensorflow), JS・TS (React, Next.js), SQLなど使用。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です