Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | 62x 62x 62x 62x 35x 35x 62x 35x 35x 14x 21x 35x 35x 62x 3x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 2x 62x 62x 62x 62x 1x 1x 1x 1x 1x 1x 1x | "use client";
import { AnimatePresence, motion, Variants } from "framer-motion";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useFirebaseAuth } from "@/app/auth/FirebaseAuthProvider";
/**
* ヘッダーメニューコンポーネント。
*
* 画面右上のハンバーガーメニューを提供し、ナビゲーション機能と認証状態に応じたアクション(ログアウトなど)を管理する。
* Framer Motionを使用して、スムーズな開閉アニメーションを実現している。
*
* 主な機能:
* - ナビゲーションリンクの表示(認証状態による切り替えあり)。
* - ログアウト処理の実行(確認ダイアログ付き)。
* - メニュー展開時の背景スクロールロック。
*/
export default function HeaderMenu() {
const [isOpen, setIsOpen] = useState(false);
const { user } = useFirebaseAuth();
const [isFitbitAuthCompleted, setIsFitbitAuthCompleted] = useState(false);
useEffect(() => {
Eif (typeof window !== "undefined") {
setIsFitbitAuthCompleted(
localStorage.getItem("fitbitAuthCompleted") === "true",
);
}
}, [isOpen]); // メニューが開くときに再確認
// Prevent scrolling when menu is open
useEffect(() => {
const bodyClassList = document.body.classList;
if (isOpen) {
bodyClassList.add("overflow-hidden");
} else {
bodyClassList.remove("overflow-hidden");
}
return () => {
bodyClassList.remove("overflow-hidden");
};
}, [isOpen]);
const handleLogout = async () => {
if (confirm("Fitbit連携を解除してログアウトしますか?")) {
Eif (typeof window !== "undefined") {
localStorage.removeItem("fitbitAuthCompleted");
localStorage.removeItem("redirectRemembered");
}
try {
const { getAuth, signOut } = await import("firebase/auth");
const auth = getAuth();
await signOut(auth);
// Reload to force state cleanup
window.location.href = "/";
} catch (error) {
console.error("ログアウトエラー:", error);
alert("ログアウトに失敗しました");
}
setIsOpen(false);
}
};
const toggleMenu = () => setIsOpen(!isOpen);
// Animation variants
const sidebarVariants: Variants = {
closed: {
x: "100%",
transition: { type: "spring", stiffness: 300, damping: 30 },
},
open: { x: 0, transition: { type: "spring", stiffness: 300, damping: 30 } },
};
const overlayVariants: Variants = {
closed: { opacity: 0 },
open: { opacity: 1 },
};
return (
<div className="relative z-50">
<button
onClick={toggleMenu}
className="p-2 text-gray-300 hover:text-white hover:bg-gray-800 rounded-full transition-colors focus:outline-none"
aria-label="メニュー"
data-testid="header-menu-button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="1" />
<circle cx="19" cy="12" r="1" />
<circle cx="5" cy="12" r="1" />
</svg>
</button>
<AnimatePresence>
{isOpen && (
<>
{/* Overlay */}
<motion.div
initial="closed"
animate="open"
exit="closed"
variants={overlayVariants}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-40"
onClick={() => setIsOpen(false)}
data-testid="header-menu-overlay"
/>
{/* Sidebar (Drawer) */}
<motion.div
initial="closed"
animate="open"
exit="closed"
variants={sidebarVariants}
className="fixed top-0 right-0 bottom-0 w-72 bg-gray-900 border-l border-gray-800 shadow-2xl z-50 overflow-y-auto"
data-testid="header-menu-drawer"
>
<div className="p-6 flex flex-col h-full">
<div className="flex justify-between items-center mb-8">
<h2 className="text-lg font-bold text-gray-100">メニュー</h2>
<button
onClick={() => setIsOpen(false)}
className="p-2 -mr-2 text-gray-400 hover:text-white rounded-full hover:bg-gray-800 transition-colors"
aria-label="閉じる"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
<nav className="flex-1 space-y-2">
<Link
href="/?show_top=true"
className="block px-4 py-3 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors"
onClick={() => setIsOpen(false)}
>
アプリトップ
</Link>
<div className="my-4 border-t border-gray-800" />
{user && isFitbitAuthCompleted && (
<Link
href="/register"
className="block px-4 py-3 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors"
onClick={() => setIsOpen(false)}
>
JSON登録
</Link>
)}
<Link
href="/instructions"
className="block px-4 py-3 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors"
onClick={() => setIsOpen(false)}
>
設定手順
</Link>
<Link
href="/tips"
className="block px-4 py-3 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors"
onClick={() => setIsOpen(false)}
>
使い方のヒント
</Link>
<div className="my-4 border-t border-gray-800" />
<Link
href="/terms"
className="block px-4 py-3 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors"
onClick={() => setIsOpen(false)}
>
利用規約
</Link>
<Link
href="/privacy"
className="block px-4 py-3 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors"
onClick={() => setIsOpen(false)}
>
プライバシーポリシー
</Link>
{user && isFitbitAuthCompleted && (
<>
<div className="my-4 border-t border-gray-800" />
<button
onClick={handleLogout}
className="w-full text-left px-4 py-3 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors"
>
連携解除
</button>
</>
)}
</nav>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}
|