feat: update sidebar ui

This commit is contained in:
ivannoskov
2026-01-21 10:36:08 +03:00
parent 1f36a074af
commit 0af5cd7458
8 changed files with 214 additions and 160 deletions

View File

@@ -8,6 +8,7 @@ import * as React from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import {
ArrowUpRight,
BarChart3,
BookmarkCheck,
Building2,
@@ -128,19 +129,12 @@ interface HelpItem {
action?: () => void;
}
interface SupportLink {
title: string;
icon: LucideIcon;
href: string;
external?: boolean;
}
const TRAINING_STEPS: TrainingStep[] = [
{
id: "dashboard",
title: "Главная панель",
description:
"Это стартовая точка: отсюда видно общее состояние закупок, статус проектов и свежие уведомления. Сюда возвращаемся, чтобы понять, что делать дальше и какие задачи горят.",
"Это стартовая точка: отсюда видно общее состояние закупов, статус проектов и свежие уведомления. Сюда возвращаемся, чтобы понять, что делать дальше и какие задачи горят.",
navItemTitle: "Главная",
icon: LayoutDashboard,
sections: [
@@ -509,53 +503,50 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
}, [isTrainingOpen, currentTrainingStep]);
const isTrainingVisible = isTrainingOpen && Boolean(currentTrainingStep);
const supportLinks = React.useMemo<SupportLink[]>(
const channelLink = React.useMemo<SupportLink>(
() => ({
title: "Наш канал",
href: "https://t.me/+xqHlRelJ2etkMGIy",
icon: Megaphone,
external: true,
}),
[]
);
const helpSubItems = React.useMemo<HelpItem[]>(
() => [
{
title: "Наш канал",
href: "https://t.me/+xqHlRelJ2etkMGIy",
icon: Megaphone,
external: true,
},
{
title: "Поддержка",
description: "Свяжитесь с нашей службой поддержки.",
href: "https://t.me/smartpost_help_bot",
icon: MessageCircle,
external: true,
},
{
title: "База знаний",
description: "Документация и инструкции по использованию.",
href: "https://docs.smartpost.ru",
icon: BookOpen,
external: true,
},
{
title: "Демо аккаунт",
description: "Посмотрите пример рабочего пространства.",
href: buildRoute.dashboard(DEMO_WORKSPACE_ID),
icon: Sparkles,
},
],
[]
);
const extraHelpItems = React.useMemo<HelpItem[]>(
() => [
{
title: "Демо дашборд",
description: "Посмотрите пример рабочего пространства со всеми разделами.",
href: buildRoute.dashboard(DEMO_WORKSPACE_ID),
icon: Sparkles,
},
{
title: "Открыть обучение",
description: "Запустите интерактивный тур по основным разделам.",
action: openTraining,
icon: MonitorPlay,
},
],
[openTraining]
);
const renderExtraHelpItem = React.useCallback((item: HelpItem) => {
const renderHelpSubItem = React.useCallback((item: HelpItem) => {
const content = (
<>
<item.icon className="mt-1.5 size-3 shrink-0 text-muted-foreground" />
<p className="text-xs font-light mt-1.5">{item.title}</p>
<item.icon className="mt-1.5 size-3 shrink-0 !text-muted-foreground/90" />
<p className="text-xs font-light mt-1.5 text-muted-foreground/90 flex-1">{item.title}</p>
{item.external && (
<ArrowUpRight className="mt-1.5 size-3 shrink-0 !text-muted-foreground/60" />
)}
</>
);
@@ -639,17 +630,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
style={expandedLogoDimensions}
>
<Image
src="/logo_full_dark.webp"
width={expandedLogoWidth}
height={expandedLogoHeight}
src="/logo_full_dark.png"
width={expandedLogoWidth*2}
height={expandedLogoHeight*2}
alt="Smartpost"
className="shrink-0 dark:hidden"
style={expandedLogoDimensions}
/>
<Image
src="/logo_full_white_2.png"
width={expandedLogoWidth}
height={expandedLogoHeight}
src="/logo_full_white.png"
width={expandedLogoWidth*2}
height={expandedLogoHeight*2}
alt="Smartpost"
className="hidden shrink-0 dark:block"
style={expandedLogoDimensions}
@@ -675,33 +666,40 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarGroup className="px-0">
<SidebarGroupContent>
<SidebarMenu>
{supportLinks.map((link) => (
<SidebarMenuItem key={link.title}>
<SidebarMenuButton
asChild
size="sm"
tooltip={isSidebarCollapsed ? link.title : undefined}
{/* Наш канал */}
<SidebarMenuItem>
<SidebarMenuButton
asChild
size="sm"
tooltip={isSidebarCollapsed ? channelLink.title : undefined}
>
<a
href={channelLink.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
{link.external ? (
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<link.icon className="size-4" />
<span>{link.title}</span>
</a>
) : (
<Link href={link.href} className="flex items-center gap-2">
<link.icon className="size-4" />
<span>{link.title}</span>
</Link>
)}
</SidebarMenuButton>
</SidebarMenuItem>
))}
<channelLink.icon className="size-4 text-muted-foreground/90" />
<span className="text-muted-foreground/90">{channelLink.title}</span>
<ArrowUpRight className="ml-auto size-3.5 text-muted-foreground/60" />
</a>
</SidebarMenuButton>
</SidebarMenuItem>
{/* Обучение */}
<SidebarMenuItem>
<SidebarMenuButton
size="sm"
tooltip={isSidebarCollapsed ? "Обучение" : undefined}
onClick={openTraining}
className="cursor-pointer"
>
<MonitorPlay className="size-4 text-muted-foreground/90" />
<span className="text-muted-foreground/90">Обучение</span>
</SidebarMenuButton>
</SidebarMenuItem>
{/* Помощь с подменю */}
{isSidebarCollapsed ? (
<SidebarMenuItem>
<DropdownMenu>
@@ -712,10 +710,10 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
className="justify-between"
>
<div className="flex items-center gap-2">
<LifeBuoy className="size-4" />
<span>Помощь</span>
<LifeBuoy className="size-4 text-muted-foreground/90" />
<span className="text-muted-foreground/90">Помощь</span>
</div>
<ChevronRight className="size-4 opacity-60" />
<ChevronRight className="size-4 text-muted-foreground/60" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
@@ -723,9 +721,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
align="start"
className="min-w-[280px] space-y-1"
>
{extraHelpItems.map((item) => (
{helpSubItems.map((item) => (
<DropdownMenuItem key={item.title} asChild>
{renderExtraHelpItem(item)}
{renderHelpSubItem(item)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
@@ -740,18 +738,18 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
className="justify-between"
>
<div className="flex items-center gap-2">
<LifeBuoy className="size-4" />
<span>Помощь</span>
<LifeBuoy className="size-4 text-muted-foreground/90" />
<span className="text-muted-foreground/90">Помощь</span>
</div>
<ChevronRight className="size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
<ChevronRight className="size-4 text-muted-foreground/60 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{extraHelpItems.map((item) => (
{helpSubItems.map((item) => (
<SidebarMenuSubItem key={item.title}>
<SidebarMenuSubButton asChild className="">
{renderExtraHelpItem(item)}
{renderHelpSubItem(item)}
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}

View File

@@ -64,6 +64,9 @@ export function NavMain({
const hasActiveSubmenu =
hasSubmenu && item.items!.some((sub) => pathname === sub.url);
const shouldForceOpen = guideState?.expandedItems?.includes(item.title);
const shouldHighlightButton = item.title === "Аналитика"
? false
: isActive;
const isGuideHighlight = highlightedItem === item.title;
const highlightedButtonClass = isGuideHighlight
? "ring-2 ring-primary/70"
@@ -77,19 +80,27 @@ export function NavMain({
<DropdownMenuTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={isActive || hasActiveSubmenu}
isActive={shouldHighlightButton}
className={cn(highlightedButtonClass)}
>
{item.icon && (
<item.icon
className={cn(
"h-4 w-4",
// (isActive || hasActiveSubmenu) &&
// "text-[#2B1D49] dark:text-[#5F31F4]"
"h-4 w-4 transition-colors duration-200",
shouldHighlightButton
? "text-[hsl(262_83%_65%)] drop-shadow-[0_0_8px_rgba(147_51_234_0.25)]"
: "text-muted-foreground/90"
)}
/>
)}
<span>{item.title}</span>
<span
className={cn(
"transition-colors duration-200",
shouldHighlightButton
? "text-foreground font-medium"
: "text-muted-foreground/90"
)}
>{item.title}</span>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
@@ -104,23 +115,28 @@ export function NavMain({
<Link
href={subItem.url}
className={cn(
"flex items-center gap-2",
"flex items-center gap-2 transition-colors duration-200",
isSubActive
? "bg-accent text-accent-foreground"
: ""
: "text-muted-foreground hover:text-foreground"
)}
>
{subItem.icon && (
<subItem.icon
className={cn(
"h-4 w-4",
// isSubActive &&
// "text-[#2B1D49] dark:text-[#5F31F4]"
)}
/>
)}
<span>{subItem.title}</span>
</Link>
{subItem.icon && (
<subItem.icon
className={cn(
"h-4 w-4 transition-colors duration-200",
isSubActive
? "!text-[hsl(262_83%_65%)] drop-shadow-[0_0_8px_rgba(147_51_234_0.25)]"
: "!text-muted-foreground/90"
)}
/>
)}
<span className={cn(
isSubActive
? "text-foreground font-medium"
: "text-muted-foreground/90"
)}>{subItem.title}</span>
</Link>
</DropdownMenuItem>
);
})}
@@ -144,47 +160,71 @@ export function NavMain({
{hasSubmenu ? (
<>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={isActive || hasActiveSubmenu}
className={cn(highlightedButtonClass)}
>
{item.icon && (
<item.icon
className={cn(
"h-4 w-4",
// (isActive || hasActiveSubmenu) &&
// "text-[#2B1D49] dark:text-[#5F31F4]"
)}
/>
<SidebarMenuButton
tooltip={item.title}
isActive={shouldHighlightButton}
className={cn(highlightedButtonClass)}
>
{item.icon && (
<item.icon
className={cn(
"h-4 w-4 transition-colors duration-200",
shouldHighlightButton
? "text-[hsl(262_83%_65%)] drop-shadow-[0_0_8px_rgba(147_51_234_0.25)]"
: "text-muted-foreground/90"
)}
/>
)}
<span
className={cn(
"transition-colors duration-200",
shouldHighlightButton
? "text-foreground font-medium"
: "text-muted-foreground/90"
)}
<span>{item.title}</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
>{item.title}</span>
<ChevronRight
className={cn(
"ml-auto transition-transform duration-200",
shouldHighlightButton
? "text-[hsl(262_83%_65%)]"
: "text-muted-foreground/60",
"group-data-[state=open]/collapsible:rotate-90"
)}
/>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items!.map((subItem) => {
const isSubActive = pathname === subItem.url;
return (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
isActive={isSubActive}
>
<Link href={subItem.url} className="flex items-center gap-2">
{subItem.icon && (
<subItem.icon
<SidebarMenuSubButton
asChild
isActive={isSubActive}
>
<Link href={subItem.url} className="flex items-center gap-2">
{subItem.icon && (
<subItem.icon
className={cn(
"h-4 w-4 transition-colors duration-200",
isSubActive
? "!text-[hsl(262_83%_65%)] drop-shadow-[0_0_8px_rgba(147_51_234_0.25)]"
: "!text-muted-foreground/90"
)}
/>
)}
<span
className={cn(
"h-4 w-4",
// isSubActive &&
// "text-[#2B1D49] dark:text-[#5F31F4]"
"transition-colors duration-200",
isSubActive
? "text-foreground font-medium"
: "text-muted-foreground/90"
)}
/>
)}
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
);
})}
@@ -195,20 +235,28 @@ export function NavMain({
<SidebarMenuButton
asChild
tooltip={item.title}
isActive={isActive}
isActive={shouldHighlightButton}
className={cn(highlightedButtonClass)}
>
<Link href={item.url} className="flex items-center gap-2">
{item.icon && (
<item.icon
className={cn(
"h-4 w-4",
// isActive &&
// "text-[#2B1D49] dark:text-[#5F31F4]"
"h-4 w-4 transition-colors duration-200",
shouldHighlightButton
? "text-[hsl(262_83%_65%)] drop-shadow-[0_0_8px_rgba(147_51_234_0.25)]"
: "text-muted-foreground/90"
)}
/>
)}
<span>{item.title}</span>
<span
className={cn(
"transition-colors duration-200",
shouldHighlightButton
? "text-foreground font-medium"
: "text-muted-foreground/90"
)}
>{item.title}</span>
</Link>
</SidebarMenuButton>
)}