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

@@ -0,0 +1,5 @@
import { redirect } from "next/navigation"
export default function AnalyticsPage({ params }: { params: { workspaceId: string } }) {
redirect(`/dashboard/${params.workspaceId}/analytics/projects`)
}

View File

@@ -127,7 +127,7 @@ const getCreativeWord = (count: number): string => {
export default function CreativesPage() { export default function CreativesPage() {
const params = useParams(); const params = useParams();
const workspaceId = params?.workspaceId as string; const workspaceId = params?.workspaceId as string;
const { selectedProjects, getProjectFilterId, allProjectsSelected, hasPermission } = useWorkspace(); const { selectedProjects, getProjectFilterId, allProjectsSelected, hasPermission, currentProject } = useWorkspace();
const projectFilterId = getProjectFilterId(); const projectFilterId = getProjectFilterId();
const [creatives, setCreatives] = useState<Creative[]>([]); const [creatives, setCreatives] = useState<Creative[]>([]);
@@ -622,7 +622,10 @@ export default function CreativesPage() {
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
window.open(`https://t.me/${BOT_USERNAME}`, "_blank"); const botUrl = currentProject
? `https://t.me/${BOT_USERNAME}?start=project_${currentProject.id}_createcreative`
: `https://t.me/${BOT_USERNAME}`;
window.open(botUrl, "_blank");
setShowCreateDialog(false); setShowCreateDialog(false);
}} }}
> >

View File

@@ -342,8 +342,8 @@ export default function PurchasePlanDetailPage() {
<DashboardHeader <DashboardHeader
breadcrumbs={[ breadcrumbs={[
{ label: "Главная", href: `/dashboard/${workspaceId}` }, { label: "Главная", href: `/dashboard/${workspaceId}` },
{ label: "Планы закупок", href: `/dashboard/${workspaceId}/purchase-plans` }, { label: "Планы закупов", href: `/dashboard/${workspaceId}/purchase-plans` },
{ label: project?.title || "План закупок" }, { label: project?.title || "План закупов" },
]} ]}
/> />
@@ -360,7 +360,7 @@ export default function PurchasePlanDetailPage() {
</Button> </Button>
<div> <div>
<h1 className="text-2xl font-bold tracking-tight"> <h1 className="text-2xl font-bold tracking-tight">
План закупок: {project?.title} План закупов: {project?.title}
</h1> </h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
{stats.totalChannels} каналов Планируемый бюджет:{" "} {stats.totalChannels} каналов Планируемый бюджет:{" "}
@@ -577,7 +577,7 @@ export default function PurchasePlanDetailPage() {
<DialogHeader> <DialogHeader>
<DialogTitle>Добавить канал в план</DialogTitle> <DialogTitle>Добавить канал в план</DialogTitle>
<DialogDescription> <DialogDescription>
Выберите канал из каталога для добавления в план закупок Выберите канал из каталога для добавления в план закупов
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -723,7 +723,7 @@ export default function PurchasePlanDetailPage() {
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Удалить канал из плана?</AlertDialogTitle> <AlertDialogTitle>Удалить канал из плана?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Канал "{deletingChannel?.channel.title}" будет удалён из плана закупок. Канал "{deletingChannel?.channel.title}" будет удалён из плана закупов.
Это действие нельзя отменить. Это действие нельзя отменить.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>

View File

@@ -116,16 +116,16 @@ export default function PurchasePlansPage() {
<DashboardHeader <DashboardHeader
breadcrumbs={[ breadcrumbs={[
{ label: "Главная", href: `/dashboard/${workspaceId}` }, { label: "Главная", href: `/dashboard/${workspaceId}` },
{ label: "Планы закупок" }, { label: "Планы закупов" },
]} ]}
/> />
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4"> <div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold tracking-tight">Планы закупок</h1> <h1 className="text-2xl font-bold tracking-tight">Планы закупов</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Управление планами закупок по проектам Управление планами закупов по проектам
</p> </p>
</div> </div>
</div> </div>
@@ -140,7 +140,7 @@ export default function PurchasePlansPage() {
<ClipboardList className="h-12 w-12 text-muted-foreground mb-4" /> <ClipboardList className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">Нет проектов</h3> <h3 className="text-lg font-semibold mb-2">Нет проектов</h3>
<p className="text-muted-foreground text-center mb-4"> <p className="text-muted-foreground text-center mb-4">
Добавьте проект через Telegram бота для создания плана закупок Добавьте проект через Telegram бота для создания плана закупов
</p> </p>
<Button asChild> <Button asChild>
<Link href={`/dashboard/${workspaceId}/projects`}> <Link href={`/dashboard/${workspaceId}/projects`}>

View File

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

View File

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

View File

@@ -67,7 +67,7 @@ export const buildRoute = {
`/dashboard/${workspaceId}/placements/${placementId}`, `/dashboard/${workspaceId}/placements/${placementId}`,
// Analytics // Analytics
analytics: (workspaceId: string) => `/dashboard/${workspaceId}/analytics/projects`, analytics: (workspaceId: string) => `/dashboard/${workspaceId}/analytics`,
analyticsProjects: (workspaceId: string) => analyticsProjects: (workspaceId: string) =>
`/dashboard/${workspaceId}/analytics/projects`, `/dashboard/${workspaceId}/analytics/projects`,
analyticsCreatives: (workspaceId: string) => analyticsCreatives: (workspaceId: string) =>

View File

@@ -228,4 +228,4 @@ CPM — Cost Per Mile — метрика отражающая стоимость
* Отображать средний CPF в списках закупов, креативов и внешних каналов * Отображать средний CPF в списках закупов, креативов и внешних каналов
* Отображать средний CPM в списках закупов, креативов и внешних каналов * Отображать средний CPM в списках закупов, креативов и внешних каналов
* Отображать столбчатую диаграмму с объемом закупок с группировкой по дате (день, неделя, месяц, квартал, год) и возможностью фильтрации по целевым каналам на отдельном экране “Аналитика” → “Затраты” * Отображать столбчатую диаграмму с объемом закупов с группировкой по дате (день, неделя, месяц, квартал, год) и возможностью фильтрации по целевым каналам на отдельном экране “Аналитика” → “Затраты”