S SmartDocs
シリーズ: Golden-Hour-backend-layer-2 typescript 192 行 · 更新日 2026-02-22

Sidebar.tsx

Golden-Hour-backend-layer-2/components/Sidebar.tsx


import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ModuleType } from '../types';

interface SidebarProps {
  activeModule: ModuleType;
  isOpen: boolean;
}

const navItems = [
  { 
    id: 'I', 
    label: 'Overview', 
    module: ModuleType.DASHBOARD,
    icon: 'fa-compass',
    subItems: [{ id: '1-1', label: 'Dashboard', path: '/dashboard' }]
  },
  { 
    id: 'II', 
    label: 'Content Management', 
    subLabel: '(Create, Edit, Delete, Search)',
    module: ModuleType.CONTENT,
    icon: 'fa-feather-pointed',
    subItems: [
      { id: '2-1', label: 'Maisons', path: '/content/maisons' },
      { id: '2-2', label: 'Experiences', path: '/content/experiences' },
      { id: '2-3', label: 'Services', path: '/content/services' },
      { id: '2-4', label: 'Products', path: '/content/products' },
      { id: '2-5', label: 'Blog', path: '/content/blog' },
      { id: '2-6', label: 'Downloads', path: '/content/downloads' }
    ]
  },
  { 
    id: 'III', 
    label: 'Order Management', 
    subLabel: '(Create, Edit, Delete, Search)',
    module: ModuleType.ORDERS,
    icon: 'fa-bag-shopping',
    subItems: [
      { id: '3-1', label: 'Maisons Orders', path: '/orders/maisons' },
      { id: '3-2', label: 'Experiences Orders', path: '/orders/experiences' },
      { id: '3-3', label: 'Services Orders', path: '/orders/services' },
      { id: '3-4', label: 'Products Orders', path: '/orders/products' },
      { id: '3-5', label: 'Returns / Refunds', path: '/orders/returns' }
    ]
  },
  { 
    id: 'IV', 
    label: 'Customer Information', 
    subLabel: '(Create, Edit, Delete, Search)',
    module: ModuleType.CUSTOMERS,
    icon: 'fa-user-tag',
    subItems: [{ id: '4-1', label: 'Customer List', path: '/customers' }]
  },
  { 
    id: 'V', 
    label: 'Customer Service', 
    subLabel: '(Guest Relations Protocol)',
    module: ModuleType.SERVICE,
    icon: 'fa-bell-concierge',
    subItems: [
      { id: '5-1', label: 'System Email', path: '/service/email' },
      { id: '5-2', label: 'Check-in System', path: '/service/checkin' },
      { id: '5-3', label: 'Messages', path: '/service/messages' },
      { id: '5-4', label: 'FAQ / Templates', path: '/service/faq' }
    ]
  },
  { 
    id: 'VI', 
    label: 'Payments ( Stripe )', 
    module: ModuleType.PAYMENTS,
    icon: 'fa-vault'
  },
  { 
    id: 'VII', 
    label: 'Invoices', 
    subLabel: '(Create, Edit, Delete, Search)',
    module: ModuleType.INVOICES,
    icon: 'fa-file-invoice-dollar',
    subItems: [
      { id: '7-1', label: 'Company Invoices', path: '/invoices/company' },
      { id: '7-2', label: 'Customer Invoices', path: '/invoices/customer' },
      { id: '7-3', label: 'Supplier Invoices', path: '/invoices/supplier' },
      { id: '7-4', label: 'Procurement Invoices', path: '/invoices/procurement' },
      { id: '7-5', label: 'Invoice Settings', path: '/invoices/settings' }
    ]
  },
  { 
    id: 'VIII', 
    label: 'Staff Management', 
    module: ModuleType.STAFF,
    icon: 'fa-user-tie',
    subItems: [
      { id: '8-1', label: 'Staff dashboard', path: '/staff' },
      { id: '8-2', label: 'Attendance / Scheduling', path: '/staff' },
      { id: '8-3', label: 'Staff setting', path: '/staff' }
    ]
  },
  { 
    id: 'IX', 
    label: 'Permissions Management', 
    module: ModuleType.PERMISSIONS,
    icon: 'fa-shield-halved',
    subItems: [
      { id: '9-1', label: 'Role Settings (Admin, Manager, Clerk, etc.)', path: '/permissions' },
      { id: '9-2', label: 'Role-to-Module Access Mapping', path: '/permissions' },
      { id: '9-3', label: 'Permissions setting', path: '/permissions' }
    ]
  },
];

const Sidebar: React.FC<SidebarProps> = ({ activeModule, isOpen }) => {
  const navigate = useNavigate();

  const handleNavClick = (module: ModuleType, path?: string) => {
    // Only navigate when switching to a different module; avoid going back to first sub-item when already in that section
    if (activeModule !== module && path) {
      navigate(path);
    }
  };

  return (
    <aside className={`${isOpen ? 'w-64' : 'w-16'} flex-shrink-0 bg-white transition-all duration-700 h-screen overflow-hidden z-20 flex flex-col border-r border-[#c5a059]/10`}>
      <div className={`py-8 flex flex-col items-center transition-all duration-700 ${isOpen ? 'px-6' : 'px-0'}`}>
        <div className="text-[#c5a059] font-serif italic text-3xl mb-0 select-none tracking-tighter">G.H</div>
        <div className={`text-center transition-all duration-700 overflow-hidden whitespace-nowrap ${isOpen ? 'max-h-12 opacity-100 mt-2' : 'max-h-0 opacity-0 mt-0'}`}>
          <h1 className="font-medium text-[8px] uppercase tracking-[0.5em] text-slate-800 font-serif italic">Atelier Suite</h1>
          <div className="w-3 h-[0.5px] bg-[#c5a059]/30 mx-auto mt-2"></div>
        </div>
      </div>

      <nav className="mt-2 flex-1 overflow-y-auto no-scrollbar px-3 pb-8">
        {navItems.map((item) => (
          <div key={item.id} className="mb-0.5">
            <button
              onClick={() => handleNavClick(item.module, item.subItems?.[0]?.path)}
              className={`w-full flex items-center py-2 px-3 transition-all duration-500 group rounded-md ${
                activeModule === item.module 
                  ? 'bg-[#fdfcf9] text-slate-900 border border-[#c5a059]/10 shadow-sm' 
                  : 'text-slate-400 hover:text-slate-700 hover:bg-[#fdfcf9]/50'
              }`}
            >
              <div className={`flex items-center w-full ${!isOpen ? 'justify-center' : 'justify-start'}`}>
                <span className={`font-serif italic text-[11px] w-6 transition-all duration-700 ${isOpen ? 'mr-1 opacity-20' : 'opacity-0 w-0'}`}>
                  {item.id}
                </span>
                
                <i className={`fa-solid ${item.icon} text-[9px] transition-all duration-500 ${isOpen ? 'mr-3' : 'mx-0 scale-110'} ${activeModule === item.module ? 'text-[#c5a059]' : 'opacity-20 group-hover:opacity-100'}`}></i>
                
                <div className={`flex flex-col transition-all duration-700 ${isOpen ? 'max-w-xs opacity-100' : 'max-w-0 opacity-0'}`}>
                  <span className="text-[9px] font-bold uppercase tracking-[0.1em] whitespace-nowrap overflow-hidden">
                    {item.label}
                  </span>
                  {item.subLabel && isOpen && (
                    <span className="text-[6px] text-slate-400 font-medium uppercase tracking-[0.05em] mt-0.5 whitespace-nowrap overflow-hidden opacity-60">
                      {item.subLabel}
                    </span>
                  )}
                </div>
              </div>
            </button>
            
            <div className={`overflow-hidden transition-all duration-700 ${isOpen && activeModule === item.module ? 'max-h-[500px] opacity-100 mt-1' : 'max-h-0 opacity-0'}`}>
              <div className="pl-11 pr-2 pb-2 space-y-1.5 border-l border-[#c5a059]/5 ml-3.5">
                {item.subItems?.map((sub: any) => (
                  <button 
                    key={sub.id} 
                    onClick={() => navigate(sub.path)}
                    className="w-full text-left py-0.5 text-[8.5px] font-medium uppercase tracking-[0.08em] text-slate-400 hover:text-[#c5a059] transition-all flex items-center group/sub"
                  >
                    <span className="text-[10px] mr-2 opacity-30">•</span>
                    {sub.label}
                  </button>
                ))}
              </div>
            </div>
          </div>
        ))}
      </nav>

      <div className={`p-6 transition-all duration-700 overflow-hidden ${isOpen ? 'opacity-100' : 'opacity-0 h-0 p-0'}`}>
         <div className="flex items-center justify-between border-t border-[#c5a059]/10 pt-4">
            <span className="text-[7px] font-serif italic text-slate-300 uppercase tracking-widest">v4.0.2 Atelier</span>
            <div className="w-1 h-1 rounded-full bg-[#c5a059]/20"></div>
         </div>
      </div>
    </aside>
  );
};

export default Sidebar;

関連記事