780 lines
35 KiB
TypeScript
780 lines
35 KiB
TypeScript
import React, { useState } from "react";
|
|
import {
|
|
House, Layers, Zap, Palette, Mic2, Users, FileText, BookOpen,
|
|
MessageSquare, CreditCard, Shield, ChevronRight,
|
|
ChevronLeft, Settings, LogOut, Plus, X, Sun, Moon,
|
|
} from "lucide-react";
|
|
import { useBreakpoint } from "../hooks/useBreakpoint";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useTheme } from "../contexts/ThemeContext";
|
|
import { useAuth } from "../contexts/AuthContext";
|
|
import { navItemAllowed, usePermission } from "../contexts/PermissionContext";
|
|
|
|
/* ─────────────────────────────────────
|
|
Types
|
|
───────────────────────────────────── */
|
|
interface SubItem { id: string; label: string; isAction?: boolean; }
|
|
interface NavItem { id: string; label: string; icon: React.ReactNode; children?: SubItem[]; }
|
|
|
|
/* ─────────────────────────────────────
|
|
Sidebar events list (accordion data)
|
|
───────────────────────────────────── */
|
|
interface SidebarEventItem {
|
|
id: string;
|
|
name: string;
|
|
status: "Live" | "Upcoming" | "Completed";
|
|
}
|
|
|
|
const SIDEBAR_EVENTS: SidebarEventItem[] = [
|
|
{ id: "e1", name: "Napcon Jaipur 2026", status: "Live" },
|
|
{ id: "e2", name: "Napcon Mumbai 2025", status: "Completed" },
|
|
{ id: "e3", name: "CHEST India 2026", status: "Upcoming" },
|
|
{ id: "e4", name: "ICS Annual Conf. 2026", status: "Upcoming" },
|
|
{ id: "e5", name: "COPD Workshop", status: "Completed" },
|
|
{ id: "e6", name: "Interventional Pulmo.", status: "Completed" },
|
|
{ id: "e7", name: "Resp. Physiology Lab", status: "Upcoming" },
|
|
{ id: "e8", name: "IMA Annual Summit 2025", status: "Completed" },
|
|
{ id: "e9", name: "AIIMS Research Conclave", status: "Upcoming" },
|
|
];
|
|
|
|
const EVENT_STATUS_DOT: Record<"Live" | "Upcoming" | "Completed", string> = {
|
|
Live: "#16a34a",
|
|
Upcoming: "#2563eb",
|
|
Completed: "var(--muted)",
|
|
};
|
|
|
|
/* ─────────────────────────────────────
|
|
Nav structure — Portfolio / All Events
|
|
───────────────────────────────────── */
|
|
const PORTFOLIO_NAV: NavItem[] = [
|
|
{ id: "home", label: "Home", icon: <House size={16} /> },
|
|
{ id: "events", label: "All Events", icon: <Layers size={16} /> },
|
|
{ id: "activities", label: "Activities", icon: <Zap size={16} /> },
|
|
{
|
|
id: "design", label: "Design", icon: <Palette size={16} />,
|
|
children: [
|
|
{ id: "design-brochure", label: "Brochure" },
|
|
{ id: "design-badges", label: "Badges" },
|
|
{ id: "design-emailers", label: "Emailers" },
|
|
{ id: "design-whatsapp-templates", label: "WhatsApp Templates" },
|
|
],
|
|
},
|
|
{ id: "faculty", label: "Faculty & Speakers", icon: <Mic2 size={16} /> },
|
|
{ id: "all-participants", label: "Participants", icon: <Users size={16} /> },
|
|
{
|
|
id: "forms", label: "Forms & Workflow", icon: <FileText size={16} />,
|
|
children: [
|
|
{ id: "forms-feedback", label: "Feedback" },
|
|
{ id: "forms-registration", label: "Registration" },
|
|
{ id: "forms-other", label: "Other Forms" },
|
|
{ id: "forms-add", label: "Add New Form", isAction: true },
|
|
],
|
|
},
|
|
{
|
|
id: "content", label: "Content", icon: <BookOpen size={16} />,
|
|
children: [
|
|
{ id: "content-library", label: "Library" },
|
|
{ id: "content-collections", label: "Collections" },
|
|
],
|
|
},
|
|
{
|
|
id: "messaging", label: "Messaging", icon: <MessageSquare size={16} />,
|
|
children: [
|
|
{ id: "messaging-email", label: "Email" },
|
|
{ id: "messaging-whatsapp", label: "WhatsApp" },
|
|
{ id: "messaging-whatsapp-chatbots", label: "WhatsApp Chatbots" },
|
|
],
|
|
},
|
|
{ id: "payment", label: "Payment Settings", icon: <CreditCard size={16} /> },
|
|
{ id: "user-management", label: "User Management", icon: <Shield size={16} /> },
|
|
{ id: "settings", label: "Settings", icon: <Settings size={16} /> },
|
|
];
|
|
|
|
/* ─────────────────────────────────────
|
|
Helpers
|
|
───────────────────────────────────── */
|
|
function isChildActive(item: NavItem, active: string) {
|
|
return !!item.children?.some(c => c.id === active);
|
|
}
|
|
function isItemActive(item: NavItem, active: string) {
|
|
return active === item.id || isChildActive(item, active);
|
|
}
|
|
|
|
/* ─────────────────────────────────────
|
|
Tooltip (collapsed mode)
|
|
───────────────────────────────────── */
|
|
function Tooltip({ label, children }: { label: string; children: React.ReactNode }) {
|
|
const [show, setShow] = useState(false);
|
|
return (
|
|
<div style={{ position: "relative" }} onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
|
|
{children}
|
|
{show && (
|
|
<div style={{
|
|
position: "absolute", left: "calc(100% + 8px)", top: "50%", transform: "translateY(-50%)",
|
|
background: "var(--foreground)", color: "#fff", padding: "4px 10px",
|
|
borderRadius: "var(--radius-button)", fontSize: "12px", fontFamily: "var(--font-noto-sans)",
|
|
whiteSpace: "nowrap", zIndex: 300, pointerEvents: "none", boxShadow: "var(--elevation-sm)",
|
|
}}>
|
|
{label}
|
|
<span style={{ position: "absolute", right: "100%", top: "50%", transform: "translateY(-50%)", borderWidth: "4px", borderStyle: "solid", borderColor: "transparent var(--foreground) transparent transparent" }} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ─────────────────────────────────────
|
|
Inner nav list — shared between drawer and desktop
|
|
───────────────────────────────────── */
|
|
function NavList({
|
|
activeItem, onNavigate, showCollapsed, selectedEventId, onEventSelect,
|
|
}: {
|
|
activeItem: string;
|
|
onNavigate: (id: string) => void;
|
|
showCollapsed: boolean;
|
|
selectedEventId?: string | null;
|
|
onEventSelect?: (id: string) => void;
|
|
}) {
|
|
const { canModule } = usePermission();
|
|
const visibleNav = PORTFOLIO_NAV.filter((item) => navItemAllowed(item.id, canModule));
|
|
const defaultOpen = visibleNav.filter(n => n.children).reduce<Record<string, boolean>>((acc, item) => {
|
|
acc[item.id] = isChildActive(item, activeItem);
|
|
return acc;
|
|
}, {});
|
|
|
|
// Events accordion starts open by default
|
|
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({ events: true, ...defaultOpen });
|
|
const toggle = (id: string) => setOpenGroups(p => ({ ...p, [id]: !p[id] }));
|
|
|
|
const btnStyle = (isActive: boolean): React.CSSProperties => ({
|
|
width: "100%", display: "flex", alignItems: "center",
|
|
gap: showCollapsed ? 0 : "9px",
|
|
padding: showCollapsed ? "8px 0" : "7px 10px",
|
|
justifyContent: showCollapsed ? "center" : "flex-start",
|
|
borderRadius: "var(--radius-button)",
|
|
background: isActive ? "var(--sidebar-accent)" : "transparent",
|
|
border: "none", cursor: "pointer", textAlign: "left",
|
|
fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)",
|
|
fontWeight: isActive ? "var(--font-weight-medium)" : "var(--font-weight-normal)",
|
|
color: isActive ? "var(--sidebar-accent-foreground)" : "var(--sidebar-foreground)",
|
|
transition: "background 0.12s",
|
|
marginBottom: "1px", whiteSpace: "nowrap", overflow: "hidden",
|
|
});
|
|
|
|
const iconStyle = (isActive: boolean): React.CSSProperties => ({
|
|
color: isActive ? "var(--sidebar-primary)" : "var(--muted)",
|
|
flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center",
|
|
});
|
|
|
|
return (
|
|
<>
|
|
{visibleNav.map(item => {
|
|
|
|
/* ── Special: All Events accordion ── */
|
|
if (item.id === "events") {
|
|
const isEvtActive = activeItem === "events";
|
|
const isOpen = !!openGroups["events"] && !showCollapsed;
|
|
|
|
if (showCollapsed) {
|
|
return (
|
|
<Tooltip key="events" label="All Events">
|
|
<button
|
|
onClick={() => onNavigate("events")}
|
|
style={{ ...btnStyle(isEvtActive || !!selectedEventId), position: "relative" }}
|
|
onMouseEnter={e => { if (!isEvtActive) (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }}
|
|
onMouseLeave={e => { if (!isEvtActive) (e.currentTarget as HTMLElement).style.background = (isEvtActive || !!selectedEventId) ? "var(--sidebar-accent)" : "transparent"; }}
|
|
>
|
|
<span style={iconStyle(isEvtActive || !!selectedEventId)}>{item.icon}</span>
|
|
{/* Dot indicator when an event is selected */}
|
|
{selectedEventId && (
|
|
<span style={{
|
|
position: "absolute", top: "6px", right: "6px",
|
|
width: "6px", height: "6px", borderRadius: "50%",
|
|
background: "#16a34a",
|
|
borderWidth: "1px", borderStyle: "solid", borderColor: "var(--sidebar)",
|
|
}} />
|
|
)}
|
|
</button>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div key="events" style={{ marginBottom: "1px" }}>
|
|
{/* Header row: label navigates, chevron toggles */}
|
|
<div style={{ display: "flex", alignItems: "center" }}>
|
|
<button
|
|
onClick={() => { onNavigate("events"); if (!isOpen) setOpenGroups(p => ({ ...p, events: true })); }}
|
|
style={{
|
|
...btnStyle(isEvtActive),
|
|
flex: 1,
|
|
borderRadius: "var(--radius-button) 0 0 var(--radius-button)",
|
|
marginBottom: 0,
|
|
overflow: "hidden",
|
|
}}
|
|
onMouseEnter={e => { if (!isEvtActive) (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }}
|
|
onMouseLeave={e => { if (!isEvtActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
|
>
|
|
<span style={iconStyle(isEvtActive)}>{item.icon}</span>
|
|
<span style={{ flex: 1, fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
{item.label}
|
|
</span>
|
|
{/* Dot when event selected but list is closed */}
|
|
{selectedEventId && !isOpen && (
|
|
<span style={{ width: "5px", height: "5px", borderRadius: "50%", background: "var(--primary)", flexShrink: 0, marginRight: "2px" }} />
|
|
)}
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ── Standard accordion items (Design, Forms, Content, Messaging) ── */
|
|
const active = isItemActive(item, activeItem);
|
|
const isOpen = !!openGroups[item.id] && !showCollapsed;
|
|
|
|
if (item.children) {
|
|
const GroupBtn = (
|
|
<button
|
|
onClick={() => toggle(item.id)}
|
|
style={btnStyle(active)}
|
|
onMouseEnter={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }}
|
|
onMouseLeave={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
|
>
|
|
<span style={iconStyle(active)}>{item.icon}</span>
|
|
{!showCollapsed && <>
|
|
<span style={{ flex: 1, fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)" }}>{item.label}</span>
|
|
<ChevronRight size={13} style={{ color: "var(--muted)", flexShrink: 0, transform: isOpen ? "rotate(90deg)" : "none", transition: "transform 0.18s" }} />
|
|
</>}
|
|
</button>
|
|
);
|
|
|
|
return (
|
|
<div key={item.id}>
|
|
{showCollapsed ? <Tooltip label={item.label}>{GroupBtn}</Tooltip> : GroupBtn}
|
|
{isOpen && (
|
|
<div style={{ paddingLeft: "14px", marginBottom: "2px" }}>
|
|
<div style={{ borderLeft: "1px solid var(--sidebar-border)", paddingLeft: "8px" }}>
|
|
{item.children!.filter((child) => navItemAllowed(child.id, canModule)).map(child => {
|
|
const cActive = activeItem === child.id;
|
|
return (
|
|
<button key={child.id} onClick={() => onNavigate(child.id)}
|
|
style={{ ...btnStyle(cActive), padding: "5px 8px", fontSize: "var(--text-xs)", gap: "6px" }}
|
|
onMouseEnter={e => { if (!cActive) (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }}
|
|
onMouseLeave={e => { if (!cActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
|
>
|
|
{child.isAction && <Plus size={11} style={{ color: "var(--primary)", flexShrink: 0 }} />}
|
|
<span style={{ fontFamily: "var(--font-noto-sans)", fontSize: "var(--text-xs)", color: cActive ? "var(--sidebar-accent-foreground)" : child.isAction ? "var(--primary)" : "var(--card-foreground)" }}>
|
|
{child.label}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ── Leaf items (Faculty, Participants, Payment, User Mgmt) ── */
|
|
const LeafBtn = (
|
|
<button key={item.id} onClick={() => onNavigate(item.id)} style={btnStyle(active)}
|
|
onMouseEnter={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }}
|
|
onMouseLeave={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
|
>
|
|
<span style={iconStyle(active)}>{item.icon}</span>
|
|
{!showCollapsed && <>
|
|
<span style={{ flex: 1, fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)" }}>{item.label}</span>
|
|
{active && <span style={{ width: "5px", height: "5px", borderRadius: "50%", background: "var(--primary)", flexShrink: 0 }} />}
|
|
</>}
|
|
</button>
|
|
);
|
|
|
|
return showCollapsed
|
|
? <Tooltip key={item.id} label={item.label}>{LeafBtn}</Tooltip>
|
|
: LeafBtn;
|
|
})}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/* ─────────────────────────────────────
|
|
Bottom user row
|
|
───────────────────────────────────── */
|
|
function sidebarUserDisplayName(user: { firstName?: string; lastName?: string; email: string } | null): string {
|
|
if (!user) return "User";
|
|
if (user.firstName || user.lastName) return [user.firstName, user.lastName].filter(Boolean).join(" ").trim();
|
|
return user.email?.split("@")[0] ?? "User";
|
|
}
|
|
function sidebarUserInitials(user: { firstName?: string; lastName?: string; email: string } | null): string {
|
|
const name = sidebarUserDisplayName(user);
|
|
return name.slice(0, 2).toUpperCase() || "?";
|
|
}
|
|
|
|
function BottomUser({ collapsed, onNavigate }: { collapsed: boolean; onNavigate: (id: string) => void }) {
|
|
const { theme, setTheme } = useTheme();
|
|
const { user, logout } = useAuth();
|
|
const navigate = useNavigate();
|
|
const isPlatformAdmin = user?.platform_admin === "yes";
|
|
const displayName = sidebarUserDisplayName(user ?? null);
|
|
const initials = sidebarUserInitials(user ?? null);
|
|
const email = user?.email ?? "";
|
|
|
|
const handleLogout = () => {
|
|
logout();
|
|
navigate("/login", { replace: true });
|
|
};
|
|
|
|
const btnStyle: React.CSSProperties = {
|
|
width: "100%", display: "flex", alignItems: "center",
|
|
gap: collapsed ? 0 : "9px",
|
|
padding: collapsed ? "8px 0" : "7px 10px",
|
|
justifyContent: collapsed ? "center" : "flex-start",
|
|
borderRadius: "var(--radius-button)", background: "transparent",
|
|
border: "none", cursor: "pointer", transition: "background 0.12s",
|
|
marginBottom: "1px", whiteSpace: "nowrap", overflow: "hidden",
|
|
};
|
|
|
|
return (
|
|
<div style={{ borderTopWidth: "1px", borderTopStyle: "solid", borderTopColor: "var(--sidebar-border)", padding: collapsed ? "8px 4px 12px" : "8px 8px 12px", display: "flex", flexDirection: "column", alignItems: collapsed ? "center" : "stretch", gap: "2px", flexShrink: 0 }}>
|
|
|
|
{/* ── Appearance toggle ── */}
|
|
{collapsed ? (
|
|
<Tooltip label={theme === "dark" ? "Switch to Light" : "Switch to Dark"}>
|
|
<button
|
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
style={{ ...btnStyle, marginBottom: "4px" }}
|
|
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }}
|
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
|
>
|
|
<span style={{ color: "var(--muted)", flexShrink: 0, display: "flex" }}>
|
|
{theme === "dark" ? <Sun size={16} /> : <Moon size={16} />}
|
|
</span>
|
|
</button>
|
|
</Tooltip>
|
|
) : (
|
|
<div style={{ marginBottom: "4px" }}>
|
|
<p style={{ fontSize: "10px", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", fontWeight: "700", letterSpacing: "0.07em", textTransform: "uppercase", margin: "0 0 6px 2px" }}>
|
|
Appearance
|
|
</p>
|
|
<div style={{
|
|
display: "flex", alignItems: "center",
|
|
background: "var(--sidebar-accent)",
|
|
borderTopWidth: "1px", borderRightWidth: "1px", borderBottomWidth: "1px", borderLeftWidth: "1px",
|
|
borderTopStyle: "solid", borderRightStyle: "solid", borderBottomStyle: "solid", borderLeftStyle: "solid",
|
|
borderTopColor: "var(--sidebar-border)", borderRightColor: "var(--sidebar-border)",
|
|
borderBottomColor: "var(--sidebar-border)", borderLeftColor: "var(--sidebar-border)",
|
|
borderRadius: "var(--radius-button)", padding: "2px", gap: "2px",
|
|
}}>
|
|
{(["light", "dark"] as const).map(mode => {
|
|
const isActive = theme === mode;
|
|
return (
|
|
<button
|
|
key={mode}
|
|
onClick={() => setTheme(mode)}
|
|
style={{
|
|
flex: 1, display: "flex", alignItems: "center", justifyContent: "center",
|
|
padding: "5px",
|
|
borderRadius: "var(--radius-button)",
|
|
border: "none", cursor: "pointer",
|
|
background: isActive ? "var(--sidebar)" : "transparent",
|
|
boxShadow: isActive ? "var(--elevation-sm)" : "none",
|
|
color: isActive ? "var(--sidebar-foreground)" : "var(--muted)",
|
|
transition: "background 0.15s, color 0.15s, box-shadow 0.15s",
|
|
}}
|
|
onMouseEnter={e => { if (!isActive) (e.currentTarget as HTMLElement).style.color = "var(--sidebar-foreground)"; }}
|
|
onMouseLeave={e => { if (!isActive) (e.currentTarget as HTMLElement).style.color = "var(--muted)"; }}
|
|
title={mode === "light" ? "Light mode" : "Dark mode"}
|
|
>
|
|
{mode === "light" ? <Sun size={13} /> : <Moon size={13} />}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isPlatformAdmin &&
|
|
(collapsed ? (
|
|
<Tooltip label="Go to Super Admin Panel">
|
|
<button
|
|
type="button"
|
|
style={btnStyle}
|
|
onClick={() => navigate("/super-admin")}
|
|
onMouseEnter={(e) => {
|
|
(e.currentTarget as HTMLElement).style.background = "var(--accent)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
(e.currentTarget as HTMLElement).style.background = "transparent";
|
|
}}
|
|
>
|
|
<span style={{ color: "var(--muted)", flexShrink: 0, display: "flex" }}>
|
|
<Shield size={16} />
|
|
</span>
|
|
</button>
|
|
</Tooltip>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
style={btnStyle}
|
|
onClick={() => navigate("/super-admin")}
|
|
onMouseEnter={(e) => {
|
|
(e.currentTarget as HTMLElement).style.background = "var(--accent)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
(e.currentTarget as HTMLElement).style.background = "transparent";
|
|
}}
|
|
>
|
|
<span style={{ color: "var(--muted)", flexShrink: 0, display: "flex" }}>
|
|
<Shield size={16} />
|
|
</span>
|
|
<span style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--sidebar-foreground)" }}>
|
|
Go to Super Admin Panel
|
|
</span>
|
|
</button>
|
|
))}
|
|
|
|
{collapsed ? (
|
|
<Tooltip label="Settings">
|
|
<button style={btnStyle}
|
|
onClick={() => onNavigate("settings")}
|
|
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }}
|
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = "transparent"; }}>
|
|
<span style={{ color: "var(--muted)", flexShrink: 0, display: "flex" }}><Settings size={16} /></span>
|
|
</button>
|
|
</Tooltip>
|
|
) : (
|
|
<button style={btnStyle}
|
|
onClick={() => onNavigate("settings")}
|
|
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }}
|
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = "transparent"; }}>
|
|
<span style={{ color: "var(--muted)", flexShrink: 0, display: "flex" }}><Settings size={16} /></span>
|
|
<span style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--sidebar-foreground)" }}>Settings</span>
|
|
</button>
|
|
)}
|
|
|
|
{collapsed ? (
|
|
<>
|
|
<Tooltip label={`${displayName} · Settings`}>
|
|
<button
|
|
type="button"
|
|
onClick={() => onNavigate("settings")}
|
|
style={{ ...btnStyle, padding: "6px 0" }}
|
|
onMouseEnter={(e) => {
|
|
(e.currentTarget as HTMLElement).style.background = "var(--accent)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
(e.currentTarget as HTMLElement).style.background = "transparent";
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: "28px",
|
|
height: "28px",
|
|
borderRadius: "50%",
|
|
background: "var(--primary)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
<span style={{ color: "#fff", fontSize: "10px", fontFamily: "var(--font-inter)", fontWeight: "600" }}>
|
|
{initials}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
</Tooltip>
|
|
<Tooltip label="Log out">
|
|
<button
|
|
type="button"
|
|
onClick={handleLogout}
|
|
style={btnStyle}
|
|
onMouseEnter={(e) => {
|
|
(e.currentTarget as HTMLElement).style.background = "var(--accent)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
(e.currentTarget as HTMLElement).style.background = "transparent";
|
|
}}
|
|
>
|
|
<span style={{ color: "var(--muted)", flexShrink: 0, display: "flex" }}>
|
|
<LogOut size={16} />
|
|
</span>
|
|
</button>
|
|
</Tooltip>
|
|
</>
|
|
) : (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "8px",
|
|
padding: "6px 10px",
|
|
borderRadius: "var(--radius-button)",
|
|
}}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => onNavigate("settings")}
|
|
title="Settings"
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "8px",
|
|
flex: 1,
|
|
minWidth: 0,
|
|
background: "none",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
padding: 0,
|
|
textAlign: "left",
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
const nameEl = e.currentTarget.querySelector("[data-user-name]") as HTMLElement | null;
|
|
if (nameEl) nameEl.style.color = "var(--primary)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
const nameEl = e.currentTarget.querySelector("[data-user-name]") as HTMLElement | null;
|
|
if (nameEl) nameEl.style.color = "var(--sidebar-foreground)";
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: "28px",
|
|
height: "28px",
|
|
borderRadius: "50%",
|
|
background: "var(--primary)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
<span style={{ color: "#fff", fontSize: "10px", fontFamily: "var(--font-inter)", fontWeight: "600" }}>
|
|
{initials}
|
|
</span>
|
|
</div>
|
|
<div style={{ flex: 1, overflow: "hidden" }}>
|
|
<p
|
|
data-user-name
|
|
style={{
|
|
fontSize: "var(--text-xs)",
|
|
fontFamily: "var(--font-noto-sans)",
|
|
fontWeight: "var(--font-weight-medium)",
|
|
color: "var(--sidebar-foreground)",
|
|
lineHeight: "1.3",
|
|
margin: 0,
|
|
}}
|
|
>
|
|
{displayName}
|
|
</p>
|
|
<p
|
|
style={{
|
|
fontSize: "10px",
|
|
fontFamily: "var(--font-noto-sans)",
|
|
color: "var(--muted)",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
whiteSpace: "nowrap",
|
|
margin: 0,
|
|
}}
|
|
>
|
|
{email || "—"}
|
|
</p>
|
|
</div>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleLogout}
|
|
title="Log out"
|
|
style={{
|
|
background: "none",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
color: "var(--muted)",
|
|
display: "flex",
|
|
padding: "2px",
|
|
flexShrink: 0,
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
(e.currentTarget as HTMLElement).style.color = "var(--foreground)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
(e.currentTarget as HTMLElement).style.color = "var(--muted)";
|
|
}}
|
|
>
|
|
<LogOut size={13} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ─────────────────────────────────────
|
|
Section label
|
|
───────────────────────────────────── */
|
|
function SectionLabel() {
|
|
return (
|
|
<div style={{ padding: "10px 14px 4px", flexShrink: 0 }}>
|
|
<p style={{ fontSize: "10px", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", fontWeight: "700", letterSpacing: "0.07em", textTransform: "uppercase", margin: 0 }}>Portfolio</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ─────────────────────────────────────
|
|
Public Sidebar component
|
|
───────────────────────────────────── */
|
|
interface SidebarProps {
|
|
activeItem: string;
|
|
onItemClick: (id: string) => void;
|
|
collapsed: boolean;
|
|
onToggleCollapse: () => void;
|
|
drawerOpen?: boolean;
|
|
onDrawerClose?: () => void;
|
|
selectedEventId?: string | null;
|
|
onEventSelect?: (id: string) => void;
|
|
}
|
|
|
|
export function Sidebar({
|
|
activeItem, onItemClick, collapsed, onToggleCollapse,
|
|
drawerOpen, onDrawerClose, selectedEventId, onEventSelect,
|
|
}: SidebarProps) {
|
|
const bp = useBreakpoint();
|
|
const isSmall = bp === "mobile" || bp === "tablet";
|
|
|
|
const handleNav = (id: string) => {
|
|
onItemClick(id);
|
|
if (isSmall && onDrawerClose) onDrawerClose();
|
|
};
|
|
|
|
const handleEventSelect = (id: string) => {
|
|
onEventSelect?.(id);
|
|
if (isSmall && onDrawerClose) onDrawerClose();
|
|
};
|
|
|
|
/* ── Mobile / Tablet: slide-in drawer ── */
|
|
if (isSmall) {
|
|
return (
|
|
<>
|
|
{/* Backdrop */}
|
|
<div
|
|
onClick={onDrawerClose}
|
|
style={{
|
|
position: "fixed", inset: 0,
|
|
background: "rgba(0,0,0,0.4)",
|
|
zIndex: 40,
|
|
opacity: drawerOpen ? 1 : 0,
|
|
pointerEvents: drawerOpen ? "auto" : "none",
|
|
transition: "opacity 0.22s",
|
|
}}
|
|
/>
|
|
{/* Panel */}
|
|
<aside style={{
|
|
position: "fixed", top: 0, left: 0, bottom: 0, width: "272px",
|
|
background: "var(--sidebar)",
|
|
borderRight: "1px solid var(--sidebar-border)",
|
|
zIndex: 50,
|
|
display: "flex", flexDirection: "column",
|
|
transform: drawerOpen ? "translateX(0)" : "translateX(-100%)",
|
|
transition: "transform 0.22s cubic-bezier(0.4,0,0.2,1)",
|
|
boxShadow: drawerOpen ? "var(--elevation-sm)" : "none",
|
|
overflow: "hidden",
|
|
}}>
|
|
{/* Header */}
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "14px 14px 14px 16px", borderBottom: "1px solid var(--sidebar-border)", flexShrink: 0, minHeight: "52px" }}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: "9px" }}>
|
|
<div style={{ width: "28px", height: "28px", borderRadius: "6px", background: "var(--primary)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
<span style={{ color: "#fff", fontSize: "11px", fontFamily: "var(--font-inter)", fontWeight: "var(--font-weight-medium)" }}>EB</span>
|
|
</div>
|
|
<span style={{ fontFamily: "var(--font-inter)", fontWeight: "var(--font-weight-medium)", fontSize: "var(--text-base)", color: "var(--sidebar-foreground)" }}>EventBuzz</span>
|
|
</div>
|
|
<button onClick={onDrawerClose}
|
|
style={{ width: "28px", height: "28px", display: "flex", alignItems: "center", justifyContent: "center", background: "none", border: "none", cursor: "pointer", borderRadius: "var(--radius-button)", color: "var(--muted)" }}
|
|
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }}
|
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = "none"; }}>
|
|
<X size={15} />
|
|
</button>
|
|
</div>
|
|
<SectionLabel />
|
|
<nav style={{ flex: 1, overflowY: "auto", overflowX: "hidden", padding: "4px 8px" }}>
|
|
<NavList
|
|
activeItem={activeItem}
|
|
onNavigate={handleNav}
|
|
showCollapsed={false}
|
|
selectedEventId={selectedEventId}
|
|
onEventSelect={handleEventSelect}
|
|
/>
|
|
</nav>
|
|
<BottomUser collapsed={false} onNavigate={handleNav} />
|
|
</aside>
|
|
</>
|
|
);
|
|
}
|
|
|
|
/* ── Desktop: inline collapsible sidebar ── */
|
|
const w = collapsed ? 60 : 240;
|
|
|
|
return (
|
|
<aside style={{
|
|
width: `${w}px`, flexShrink: 0, height: "100%",
|
|
display: "flex", flexDirection: "column",
|
|
background: "var(--sidebar)",
|
|
borderRight: "1px solid var(--sidebar-border)",
|
|
transition: "width 0.2s cubic-bezier(0.4,0,0.2,1)",
|
|
overflow: "hidden", zIndex: 30, position: "relative",
|
|
}}>
|
|
{/* Logo row */}
|
|
<div style={{
|
|
display: "flex", alignItems: "center",
|
|
justifyContent: collapsed ? "center" : "space-between",
|
|
padding: collapsed ? "14px 0" : "14px 12px 14px 14px",
|
|
borderBottom: "1px solid var(--sidebar-border)",
|
|
flexShrink: 0, minHeight: "52px",
|
|
}}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: "9px", overflow: "hidden" }}>
|
|
<div style={{ width: "28px", height: "28px", borderRadius: "6px", background: "var(--primary)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
|
|
<span style={{ color: "#fff", fontSize: "11px", fontFamily: "var(--font-inter)", fontWeight: "var(--font-weight-medium)" }}>EB</span>
|
|
</div>
|
|
{!collapsed && <span style={{ fontFamily: "var(--font-inter)", fontWeight: "var(--font-weight-medium)", fontSize: "var(--text-base)", color: "var(--sidebar-foreground)", whiteSpace: "nowrap", overflow: "hidden" }}>EventBuzz</span>}
|
|
</div>
|
|
{/* H3: Always show collapse/expand button — critical for user control */}
|
|
<button onClick={onToggleCollapse} title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
style={{ width: "24px", height: "24px", display: "flex", alignItems: "center", justifyContent: "center", background: "none", border: "none", cursor: "pointer", borderRadius: "var(--radius-button)", color: "var(--muted)", flexShrink: 0 }}
|
|
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = "var(--accent)"; (e.currentTarget as HTMLElement).style.color = "var(--primary)"; }}
|
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = "none"; (e.currentTarget as HTMLElement).style.color = "var(--muted)"; }}>
|
|
{collapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
|
|
</button>
|
|
</div>
|
|
|
|
{!collapsed && <SectionLabel />}
|
|
|
|
<nav style={{ flex: 1, overflowY: "auto", overflowX: "hidden", padding: collapsed ? "8px 4px" : "4px 8px" }}>
|
|
<NavList
|
|
activeItem={activeItem}
|
|
onNavigate={handleNav}
|
|
showCollapsed={collapsed}
|
|
selectedEventId={selectedEventId}
|
|
onEventSelect={handleEventSelect}
|
|
/>
|
|
</nav>
|
|
|
|
{/* Expand toggle when collapsed */}
|
|
{collapsed && (
|
|
<div style={{ borderTop: "1px solid var(--sidebar-border)", padding: "8px 4px 4px", display: "flex", justifyContent: "center" }}>
|
|
<button onClick={onToggleCollapse} title="Expand"
|
|
style={{ width: "36px", height: "28px", display: "flex", alignItems: "center", justifyContent: "center", background: "none", border: "none", cursor: "pointer", borderRadius: "var(--radius-button)", color: "var(--muted)" }}
|
|
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = "var(--accent)"; (e.currentTarget as HTMLElement).style.color = "var(--primary)"; }}
|
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = "none"; (e.currentTarget as HTMLElement).style.color = "var(--muted)"; }}>
|
|
<ChevronRight size={14} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<BottomUser collapsed={collapsed} onNavigate={handleNav} />
|
|
</aside>
|
|
);
|
|
} |