feat: 항해 종료 시 버튼을 꾹 눌러서 종료
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Menu as MenuIcon, X as CloseIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
@@ -22,7 +24,12 @@ export function I18nLayoutShell({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const isLobby = pathname === "/";
|
||||
const [locale, setLocale] = useState<Locale>(DEFAULT_LOCALE);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
const menuButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const initialLocale = resolveInitialLocale();
|
||||
@@ -44,41 +51,115 @@ export function I18nLayoutShell({
|
||||
|
||||
const t = (key: I18nKey) => translateText(locale, key);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMenuOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) return;
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (!menuRef.current) return;
|
||||
const target = event.target as Node | null;
|
||||
if (target && !menuRef.current.contains(target)) {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Escape") return;
|
||||
setIsMenuOpen(false);
|
||||
menuButtonRef.current?.focus();
|
||||
};
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown);
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handlePointerDown);
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
};
|
||||
}, [isMenuOpen]);
|
||||
|
||||
return (
|
||||
<I18nProvider locale={locale} setLocale={handleSetLocale}>
|
||||
<div className="relative z-10 mx-auto flex min-h-screen max-w-6xl flex-col">
|
||||
<header className="flex items-center justify-between px-6 py-4">
|
||||
<div
|
||||
className={`relative z-10 mx-auto flex min-h-screen flex-col ${
|
||||
isLobby ? "w-full max-w-none" : "max-w-6xl"
|
||||
}`}
|
||||
>
|
||||
<header
|
||||
className={`flex items-center justify-between px-6 py-4 ${
|
||||
isLobby ? "lg:hidden" : ""
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
href="/"
|
||||
className="z-50 text-lg font-bold tracking-wider text-indigo-400 transition-colors hover:text-indigo-300"
|
||||
>
|
||||
FOCUSTELLA
|
||||
</Link>
|
||||
<nav className="z-50 flex items-center gap-4 text-sm font-medium text-slate-400">
|
||||
<Link href="/log" className="transition-colors hover:text-slate-200">
|
||||
{t("layout.nav.log")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="transition-colors hover:text-slate-200"
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="relative z-50"
|
||||
onBlur={(event) => {
|
||||
if (!isMenuOpen) return;
|
||||
const nextFocused = event.relatedTarget as Node | null;
|
||||
if (nextFocused && event.currentTarget.contains(nextFocused)) {
|
||||
return;
|
||||
}
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<button
|
||||
ref={menuButtonRef}
|
||||
type="button"
|
||||
onClick={() => setIsMenuOpen((prev) => !prev)}
|
||||
aria-expanded={isMenuOpen}
|
||||
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||
className="rounded-lg border border-slate-700 bg-slate-900/70 p-2 text-slate-200 transition-colors hover:border-slate-500 hover:bg-slate-800/80"
|
||||
>
|
||||
{t("layout.nav.settings")}
|
||||
</Link>
|
||||
<label className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<span>{t("layout.nav.language")}</span>
|
||||
<select
|
||||
value={locale}
|
||||
onChange={handleLocaleChange}
|
||||
className="rounded border border-slate-700 bg-slate-900/70 px-2 py-1 text-xs text-slate-200 outline-none transition-colors focus:border-indigo-400"
|
||||
>
|
||||
{SUPPORTED_LOCALES.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{LOCALE_LABELS[item]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</nav>
|
||||
{isMenuOpen ? (
|
||||
<CloseIcon className="h-5 w-5" aria-hidden />
|
||||
) : (
|
||||
<MenuIcon className="h-5 w-5" aria-hidden />
|
||||
)}
|
||||
</button>
|
||||
{isMenuOpen && (
|
||||
<div className="absolute right-0 mt-2 w-64 rounded-xl border border-slate-700 bg-slate-900/95 p-3 shadow-2xl backdrop-blur">
|
||||
<nav className="flex flex-col gap-2 text-sm font-medium text-slate-300">
|
||||
<Link
|
||||
href="/log"
|
||||
className="rounded-md px-3 py-2 transition-colors hover:bg-slate-800 hover:text-slate-100"
|
||||
>
|
||||
{t("layout.nav.log")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="rounded-md px-3 py-2 transition-colors hover:bg-slate-800 hover:text-slate-100"
|
||||
>
|
||||
{t("layout.nav.settings")}
|
||||
</Link>
|
||||
<div className="rounded-md px-3 py-2">
|
||||
<label className="flex items-center justify-between gap-3 text-xs text-slate-300">
|
||||
<span>{t("layout.nav.language")}</span>
|
||||
<select
|
||||
value={locale}
|
||||
onChange={handleLocaleChange}
|
||||
className="rounded border border-slate-700 bg-slate-900/70 px-2 py-1 text-xs text-slate-200 outline-none transition-colors focus:border-indigo-400"
|
||||
>
|
||||
{SUPPORTED_LOCALES.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{LOCALE_LABELS[item]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<main className="relative flex w-full flex-1 flex-col">{children}</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user