Files
chat-website/src/components/Sidebar.tsx
T
LucasDLTG df0428f240
CI / CI - Validation Technique (push) Successful in 35s
Publish & Deploy (Versioned) / Build and Push Docker Image (push) Successful in 27s
Publish & Deploy (Versioned) / Deploy via SSH (push) Successful in 8s
fix: some design
2026-04-29 15:37:02 +02:00

188 lines
4.7 KiB
TypeScript

'use client';
import { useState, useSyncExternalStore } from 'react';
import { CloseIcon } from './icons/CloseIcon';
export type SidebarPanel = {
id: string;
label: string;
icon: React.ReactNode;
component: React.ReactNode;
};
function useIsWide(): boolean {
return useSyncExternalStore(
(cb) => {
window.addEventListener('resize', cb);
return () => window.removeEventListener('resize', cb);
},
() => window.innerWidth >= 768,
() => false,
);
}
/* -----------------------------
Shared sidebar logic
------------------------------*/
function useSidebarState(panels: SidebarPanel[], isWide: boolean) {
const [activeId, setActiveId] = useState<string | null>(null);
// open first panel by default ONLY on first mount (desktop only)
const [initialized, setInitialized] = useState(false);
if (!initialized && isWide) {
setActiveId(panels[0]?.id ?? null);
setInitialized(true);
}
const setClosed = () => setActiveId(null);
const toggle = (id: string) => {
setActiveId((prev) => (prev === id ? null : id));
};
return {
activeId,
setActiveId,
setClosed,
toggle,
};
}
/* -----------------------------
Desktop UI
------------------------------*/
function DesktopSidebar({
activePanel,
isOpen,
setClosed,
}: {
activePanel?: SidebarPanel;
isOpen: boolean;
setClosed: () => void;
}) {
return (
<div
className={`
flex transition-all duration-300 ease overflow-hidden
${isOpen ? 'w-64 opacity-100' : 'w-0 opacity-0'}
`}
>
{activePanel && (
<div className="w-64 flex flex-col border-r border-muted transition-theme">
<div className="flex items-center justify-between px-4 py-3 border-b border-muted shrink-0 transition-theme">
<span className="text-sm font-semibold text-accent">
{activePanel.label}
</span>
<button onClick={setClosed}>
<CloseIcon className="w-4 h-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-3 min-h-0">
{activePanel.component}
</div>
</div>
)}
</div>
);
}
/* -----------------------------
Mobile UI (drawer)
------------------------------*/
function MobileSidebar({
activePanel,
setClosed,
}: {
activePanel?: SidebarPanel;
setClosed: () => void;
}) {
if (!activePanel) return null;
return (
<>
{/* Backdrop */}
<div className="fixed inset-0 bg-black/40 z-20" onClick={setClosed} />
{/* Drawer */}
<div className="fixed left-14 top-[var(--header-height)] bottom-0 z-30 bg-primary overflow-y-auto border border-muted w-64">
<div className="flex items-center justify-between px-4 py-3 border-b border-muted">
<span className="text-sm font-semibold text-accent">
{activePanel.label}
</span>
<button onClick={setClosed}>
<CloseIcon className="w-4 h-4" />
</button>
</div>
<div className="p-3">{activePanel.component}</div>
</div>
</>
);
}
/* -----------------------------
Icon rail (shared)
------------------------------*/
function IconRail({
panels,
effectiveId,
toggle,
}: {
panels: SidebarPanel[];
effectiveId: string | null;
toggle: (id: string, effectiveId: string | null) => void;
}) {
return (
<div className="w-12 flex flex-col items-center py-3 gap-1 z-30">
{panels.map((panel) => (
<button
key={panel.id}
onClick={() => toggle(panel.id, effectiveId)}
title={panel.label}
className={`w-8 h-8 rounded-lg flex items-center justify-center text-secondary ${
effectiveId === panel.id ? 'border-2 border-[var(--accent2)]' : ''
}`}
>
{panel.icon}
</button>
))}
</div>
);
}
/* -----------------------------
Main Sidebar (controller)
------------------------------*/
export default function Sidebar({ panels }: { panels: SidebarPanel[] }) {
const isWide = useIsWide();
const { activeId, setClosed, toggle } = useSidebarState(panels, isWide);
const effectiveId = activeId;
const activePanel = panels.find((p) => p.id === effectiveId);
const isOpen = !!activePanel;
return (
<div className="flex border-r border-t border-muted transition-theme">
{/* Desktop */}
{isWide && (
<DesktopSidebar
activePanel={activePanel}
isOpen={isOpen}
setClosed={setClosed}
/>
)}
{/* Mobile */}
{!isWide && (
<MobileSidebar activePanel={activePanel} setClosed={setClosed} />
)}
{/* Icon rail */}
<IconRail panels={panels} effectiveId={effectiveId} toggle={toggle} />
</div>
);
}