185 lines
6.6 KiB
React
185 lines
6.6 KiB
React
|
|
import React, { useState } from 'react';
|
||
|
|
import { NavLink, useLocation } from 'react-router-dom';
|
||
|
|
import { Home, Globe, Users, Menu, X, Gamepad2, Map } from 'lucide-react';
|
||
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
||
|
|
|
||
|
|
// 사이드바 네비게이션 컴포넌트
|
||
|
|
const Sidebar = ({ isMobile = false }) => {
|
||
|
|
const [isOpen, setIsOpen] = useState(false);
|
||
|
|
const location = useLocation();
|
||
|
|
|
||
|
|
const menuItems = [
|
||
|
|
{ path: '/', icon: Home, label: '홈' },
|
||
|
|
{ path: '/players', icon: Users, label: '플레이어', matchPaths: ['/players', '/player/'] },
|
||
|
|
{ path: '/worlds', icon: Globe, label: '월드 정보' },
|
||
|
|
{ path: '/worldmap', icon: Map, label: '월드맵' },
|
||
|
|
];
|
||
|
|
|
||
|
|
// 커스텀 활성 상태 확인 함수
|
||
|
|
const isMenuActive = (item) => {
|
||
|
|
if (item.matchPaths) {
|
||
|
|
return item.matchPaths.some(path => location.pathname.startsWith(path));
|
||
|
|
}
|
||
|
|
return location.pathname === item.path;
|
||
|
|
};
|
||
|
|
|
||
|
|
// 모바일: 상단 툴바 + 햄버거 메뉴 사이드바
|
||
|
|
if (isMobile) {
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
{/* 상단 툴바 */}
|
||
|
|
<header className="fixed top-0 left-0 right-0 z-50 bg-mc-dark/95 backdrop-blur-xl border-b border-white/10 safe-area-top">
|
||
|
|
<div className="flex items-center justify-between h-14 px-4">
|
||
|
|
{/* 햄버거 메뉴 버튼 */}
|
||
|
|
<button
|
||
|
|
onClick={() => setIsOpen(true)}
|
||
|
|
className="p-2 -ml-2 rounded-lg text-white hover:bg-white/10 transition-all"
|
||
|
|
>
|
||
|
|
<Menu size={24} />
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{/* 로고/타이틀 */}
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Gamepad2 className="text-mc-green" size={20} />
|
||
|
|
<span className="font-bold text-white">마인크래프트</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 우측 공간 (균형용) */}
|
||
|
|
<div className="w-10" />
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
{/* 상단 툴바 높이만큼 공간 확보 */}
|
||
|
|
<div className="h-14" />
|
||
|
|
|
||
|
|
{/* 오버레이 */}
|
||
|
|
<AnimatePresence>
|
||
|
|
{isOpen && (
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
exit={{ opacity: 0 }}
|
||
|
|
transition={{ duration: 0.2 }}
|
||
|
|
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[60]"
|
||
|
|
onClick={() => setIsOpen(false)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</AnimatePresence>
|
||
|
|
|
||
|
|
{/* 사이드바 (슬라이드) */}
|
||
|
|
<AnimatePresence>
|
||
|
|
{isOpen && (
|
||
|
|
<motion.aside
|
||
|
|
initial={{ x: -280 }}
|
||
|
|
animate={{ x: 0 }}
|
||
|
|
exit={{ x: -280 }}
|
||
|
|
transition={{ type: 'tween', duration: 0.25, ease: 'easeOut' }}
|
||
|
|
className="fixed left-0 top-0 bottom-0 w-72 bg-mc-dark/98 backdrop-blur-xl border-r border-white/10 z-[70] flex flex-col safe-area-left"
|
||
|
|
>
|
||
|
|
{/* 사이드바 헤더 */}
|
||
|
|
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="p-2 rounded-xl bg-gradient-to-br from-mc-green/20 to-mc-emerald/20 border border-mc-green/30">
|
||
|
|
<Gamepad2 className="text-mc-green" size={20} />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h1 className="text-base font-bold text-white">마인크래프트</h1>
|
||
|
|
<p className="text-xs text-gray-500">서버 대시보드</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={() => setIsOpen(false)}
|
||
|
|
className="p-2 rounded-lg text-gray-400 hover:text-white hover:bg-white/10 transition-all"
|
||
|
|
>
|
||
|
|
<X size={20} />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 메뉴 */}
|
||
|
|
<nav className="flex-1 p-3 space-y-1">
|
||
|
|
{menuItems.map((item) => {
|
||
|
|
const active = isMenuActive(item);
|
||
|
|
return (
|
||
|
|
<NavLink
|
||
|
|
key={item.path}
|
||
|
|
to={item.path}
|
||
|
|
onClick={() => setIsOpen(false)}
|
||
|
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-colors outline-none focus:ring-0 border ${
|
||
|
|
active
|
||
|
|
? 'bg-mc-green/10 text-mc-green border-mc-green/20'
|
||
|
|
: 'text-gray-400 hover:text-white hover:bg-white/5 border-transparent'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<item.icon size={20} />
|
||
|
|
<span className="font-medium">{item.label}</span>
|
||
|
|
</NavLink>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
|
||
|
|
</motion.aside>
|
||
|
|
)}
|
||
|
|
</AnimatePresence>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// PC: 기존 사이드바
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
{/* 데스크탑 사이드바 (항상 표시) */}
|
||
|
|
<aside className="hidden md:flex fixed left-0 top-0 bottom-0 w-64 bg-mc-dark/95 backdrop-blur-xl border-r border-white/10 z-30 flex-col">
|
||
|
|
<SidebarContent menuItems={menuItems} isMenuActive={isMenuActive} />
|
||
|
|
</aside>
|
||
|
|
|
||
|
|
{/* 데스크톱용 사이드바 spacer */}
|
||
|
|
<div className="hidden md:block w-64 shrink-0" />
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 사이드바 내용 컴포넌트 (PC 전용)
|
||
|
|
const SidebarContent = ({ menuItems, isMenuActive, onClose }) => (
|
||
|
|
<>
|
||
|
|
{/* 로고 */}
|
||
|
|
<div className="p-6 border-b border-white/10">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="p-2 rounded-xl bg-gradient-to-br from-mc-green/20 to-mc-emerald/20 border border-mc-green/30">
|
||
|
|
<Gamepad2 className="text-mc-green" size={24} />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h1 className="text-lg font-bold text-white">마인크래프트</h1>
|
||
|
|
<p className="text-xs text-gray-500">서버 대시보드</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 메뉴 */}
|
||
|
|
<nav className="flex-1 p-4 space-y-2">
|
||
|
|
{menuItems.map((item) => {
|
||
|
|
const active = isMenuActive(item);
|
||
|
|
return (
|
||
|
|
<NavLink
|
||
|
|
key={item.path}
|
||
|
|
to={item.path}
|
||
|
|
onClick={onClose}
|
||
|
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-colors outline-none focus:ring-0 border ${
|
||
|
|
active
|
||
|
|
? 'bg-mc-green/10 text-mc-green border-mc-green/20'
|
||
|
|
: 'text-gray-400 hover:text-white hover:bg-white/5 border-transparent'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<item.icon size={20} />
|
||
|
|
<span className="font-medium">{item.label}</span>
|
||
|
|
</NavLink>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
|
||
|
|
export default Sidebar;
|