'use client';
import { cn } from '@/lib/utils';
import {
MessagesSquare,
Plus,
Newspaper,
TrendingUp,
Plane,
LayoutPanelLeft,
User,
Stethoscope,
Building2,
Package,
SlidersHorizontal,
Baby,
GraduationCap,
HeartPulse,
Brain,
Trophy,
ShoppingCart,
Gamepad2,
Receipt,
Scale,
MoreHorizontal,
} from 'lucide-react';
import Link from 'next/link';
import { useSelectedLayoutSegments } from 'next/navigation';
import React, { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from '@/lib/localization/context';
import { getSidebarMenuConfig } from '@/lib/config/sidebarMenu';
import MenuSettingsPanel from './Sidebar/MenuSettingsPanel';
import Layout from './Layout';
interface ChatItem {
id: string;
title: string;
createdAt: string;
}
const VerticalIconContainer = ({ children }: { children: ReactNode }) => {
return
{children}
;
};
interface HistorySubmenuProps {
onMouseEnter: () => void;
onMouseLeave: () => void;
}
interface MoreSubmenuProps {
onMouseEnter: () => void;
onMouseLeave: () => void;
restIds: string[];
linkMap: Record;
itemLabels: Record;
submenuStyle: { top: number; left: number };
isMobile: boolean;
cancelMoreHide: () => void;
}
const MoreSubmenu = ({
onMouseEnter,
onMouseLeave,
restIds,
linkMap,
itemLabels,
submenuStyle,
isMobile,
cancelMoreHide,
}: MoreSubmenuProps) => {
const { t } = useTranslation();
const [settingsHovered, setSettingsHovered] = useState(false);
return (
);
};
const HistorySubmenu = ({ onMouseEnter, onMouseLeave }: HistorySubmenuProps) => {
const { t } = useTranslation();
const [chats, setChats] = useState([]);
const [loading, setLoading] = useState(true);
const fetchChats = useCallback(async () => {
try {
const token =
typeof window !== 'undefined'
? localStorage.getItem('auth_token') ?? localStorage.getItem('access_token')
: null;
if (!token) {
const { getGuestChats } = await import('@/lib/guest-storage');
const guestChats = getGuestChats();
setChats(
guestChats.map((c) => ({
id: c.id,
title: c.title,
createdAt: c.createdAt,
sources: c.sources,
files: c.files,
})),
);
setLoading(false);
return;
}
const res = await fetch('/api/v1/library/threads', {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();
setChats((data.chats ?? []).reverse());
} catch {
setChats([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchChats();
}, [fetchChats]);
useEffect(() => {
const onMigrated = () => fetchChats();
window.addEventListener('gooseek:guest-migrated', onMigrated);
return () => window.removeEventListener('gooseek:guest-migrated', onMigrated);
}, []);
return (
{loading ? (
{t('common.loading')}
) : chats.length === 0 ? (
—
) : (
)}
);
};
type NavLink = {
icon: React.ComponentType<{ size?: number | string; className?: string }>;
href?: string;
active: boolean;
label: string;
};
const Sidebar = ({ children }: { children: React.ReactNode }) => {
const segments = useSelectedLayoutSegments();
const { t } = useTranslation();
const [historyOpen, setHistoryOpen] = useState(false);
const [moreOpen, setMoreOpen] = useState(false);
const historyRef = useRef(null);
const moreRef = useRef(null);
const hideTimeoutRef = useRef | null>(null);
const moreHideRef = useRef | null>(null);
const [submenuStyle, setSubmenuStyle] = useState({ top: 0, left: 0 });
const [tooltip, setTooltip] = useState<{ label: string; x: number; y: number } | null>(null);
const [menuConfig, setMenuConfig] = useState(() =>
typeof window !== 'undefined' ? getSidebarMenuConfig() : { order: [], visible: {} },
);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
if (typeof window === 'undefined') return;
const mq = window.matchMedia('(max-width: 1023px)');
setIsMobile(mq.matches);
const handler = () => setIsMobile(mq.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
const discoverLink = {
icon: Newspaper,
href: '/discover',
active: segments.includes('discover'),
label: t('nav.discover'),
};
const libraryLink = {
icon: MessagesSquare,
href: '/library',
active: segments.includes('library'),
label: t('nav.messageHistory'),
};
const financeLink = {
icon: TrendingUp,
href: '/finance',
active: segments.includes('finance'),
label: t('nav.finance'),
};
const travelLink = {
icon: Plane,
href: '/travel',
active: segments.includes('travel'),
label: t('nav.travel'),
};
const spacesLink = {
icon: LayoutPanelLeft,
href: '/spaces',
active: segments.includes('spaces'),
label: t('nav.spaces'),
};
const profileLink = {
icon: User,
href: '/profile',
active: segments.includes('profile'),
label: t('nav.profile'),
};
const placeholderLink = (
icon: React.ComponentType<{ size?: number | string; className?: string }>,
labelKey: string,
) => ({
icon,
href: undefined as string | undefined,
active: false,
label: t(labelKey),
});
const medicineLink = placeholderLink(Stethoscope, 'nav.medicine');
const realEstateLink = placeholderLink(Building2, 'nav.realEstate');
const goodsLink = placeholderLink(Package, 'nav.goods');
const childrenLink = placeholderLink(Baby, 'nav.children');
const educationLink = placeholderLink(GraduationCap, 'nav.education');
const healthLink = placeholderLink(HeartPulse, 'nav.health');
const psychologyLink = placeholderLink(Brain, 'nav.psychology');
const sportsLink = placeholderLink(Trophy, 'nav.sports');
const shoppingLink = placeholderLink(ShoppingCart, 'nav.shopping');
const gamesLink = placeholderLink(Gamepad2, 'nav.games');
const taxesLink = placeholderLink(Receipt, 'nav.taxes');
const legislationLink = placeholderLink(Scale, 'nav.legislation');
const placeholderLinks = [
medicineLink,
realEstateLink,
goodsLink,
childrenLink,
educationLink,
healthLink,
psychologyLink,
sportsLink,
shoppingLink,
gamesLink,
taxesLink,
legislationLink,
];
const linkMap: Record = {
discover: discoverLink,
library: libraryLink,
finance: financeLink,
travel: travelLink,
spaces: spacesLink,
medicine: medicineLink,
realEstate: realEstateLink,
goods: goodsLink,
children: childrenLink,
education: educationLink,
health: healthLink,
psychology: psychologyLink,
sports: sportsLink,
shopping: shoppingLink,
games: gamesLink,
taxes: taxesLink,
legislation: legislationLink,
profile: profileLink,
};
const itemLabels: Record = Object.fromEntries(
Object.entries(linkMap).map(([id, link]) => [id, link.label]),
);
const orderedVisibleIds = menuConfig.order.filter((id) => menuConfig.visible[id] !== false);
const mainIds = orderedVisibleIds.filter((id) => id !== 'profile' && id !== 'spaces');
const topMainIds = mainIds.slice(0, 5);
const restMainIds = mainIds.slice(5);
const showSpaces = menuConfig.visible['spaces'] !== false;
const showProfile = menuConfig.visible['profile'] !== false;
const refreshMenuConfig = useCallback(() => {
setMenuConfig(getSidebarMenuConfig());
}, []);
useEffect(() => {
const handler = () => refreshMenuConfig();
window.addEventListener('client-config-changed', handler);
return () => window.removeEventListener('client-config-changed', handler);
}, [refreshMenuConfig]);
const handleHistoryMouseEnter = () => {
setTooltip(null);
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
if (historyRef.current) {
const rect = historyRef.current.getBoundingClientRect();
setSubmenuStyle({ top: 0, left: rect.right + 4 });
}
setHistoryOpen(true);
};
const handleHistoryMouseLeave = () => {
hideTimeoutRef.current = setTimeout(() => setHistoryOpen(false), 150);
};
const cancelHide = () => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
};
const handleMoreEnter = () => {
setTooltip(null);
if (moreHideRef.current) {
clearTimeout(moreHideRef.current);
moreHideRef.current = null;
}
if (moreRef.current) {
const rect = moreRef.current.getBoundingClientRect();
setSubmenuStyle({ top: 0, left: rect.right + 4 });
}
setMoreOpen(true);
};
const handleMoreLeave = () => {
moreHideRef.current = setTimeout(() => setMoreOpen(false), 150);
};
const cancelMoreHide = () => {
if (moreHideRef.current) {
clearTimeout(moreHideRef.current);
moreHideRef.current = null;
}
};
useEffect(() => {
return () => {
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
if (moreHideRef.current) clearTimeout(moreHideRef.current);
};
}, []);
useEffect(() => {
if (!moreOpen || !isMobile) return;
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') setMoreOpen(false);
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [moreOpen, isMobile]);
const showTooltip = (label: string, e: React.MouseEvent) => {
const rect = e.currentTarget.getBoundingClientRect();
setTooltip({ label, x: rect.right + 8, y: rect.top + rect.height / 2 });
};
const hideTooltip = () => setTooltip(null);
return (
{tooltip &&
typeof document !== 'undefined' &&
createPortal(
{tooltip.label}
,
document.body,
)}
showTooltip(t('chat.newChat'), e)}
onMouseLeave={hideTooltip}
>
{showSpaces && (
showTooltip(spacesLink.label, e)}
onMouseLeave={hideTooltip}
>
)}
{topMainIds.map((id) => {
if (id === 'library') {
return (
showTooltip(libraryLink.label, e)}
onMouseLeave={hideTooltip}
>
{historyOpen &&
typeof document !== 'undefined' &&
createPortal(
,
document.body,
)}
);
}
const link = linkMap[id];
if (!link) return null;
if (link.href) {
return (
showTooltip(link.label, e)}
onMouseLeave={hideTooltip}
>
);
}
return (
showTooltip(link.label, e)}
onMouseLeave={hideTooltip}
>
);
})}
{moreOpen &&
typeof document !== 'undefined' &&
createPortal(
isMobile ? (
setMoreOpen(false)}
role="button"
tabIndex={0}
aria-label={t('common.close')}
>
e.stopPropagation()}
onMouseEnter={cancelMoreHide}
onMouseLeave={() => {}}
>
setMoreOpen(false)}
cancelMoreHide={cancelMoreHide}
/>
) : (
),
document.body,
)}
{showProfile && (
showTooltip(profileLink.label, e)}
onMouseLeave={hideTooltip}
>
)}
{topMainIds.map((id) => {
const link = linkMap[id];
if (!link) return null;
return link.href ? (
{link.active && (
)}
{link.label}
) : (
{link.label}
);
})}
{showProfile && (
{profileLink.active && (
)}
{profileLink.label}
)}
{children}
);
};
export default Sidebar;