Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/app/styles/animations.scss
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,31 @@
}
}
}

.collapsible-content {
overflow: hidden;
}
.collapsible-content[data-state='open'] {
animation: slideDown 300ms ease-out;
}
.collapsible-content[data-state='closed'] {
animation: slideUp 300ms ease-out;
}

@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--radix-collapsible-content-height);
}
}

@keyframes slideUp {
from {
height: var(--radix-collapsible-content-height);
}
to {
height: 0;
}
}
2 changes: 1 addition & 1 deletion src/shared/ui/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ function SidebarMenuSubButton({
data-size={size}
data-active={isActive}
className={cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden group-data-[collapsible=icon]:hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden group-data-[collapsible=icon]:hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
className
)}
{...props}
Expand Down
13 changes: 13 additions & 0 deletions src/widgets/app-sidebar/config/sidebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Mail, Settings, ShieldUser, UsersRound } from 'lucide-react';
import { routes } from 'shared/config';

export const team = [
{
url: routes.team.members(),
title: 'Участники',
icon: UsersRound,
},
{ url: routes.team.invitations(), title: 'Приглашения', icon: Mail },
{ url: routes.team.roles(), title: 'Роли', icon: ShieldUser },
{ url: routes.team.settings(), title: 'Настройки', icon: Settings },
] as const;
74 changes: 4 additions & 70 deletions src/widgets/app-sidebar/ui/AppSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,16 @@
'use client';

import { InviteTeamMemberDialog } from 'features/teams/invite';
import { ChevronRight, SquarePlusIcon, UserRound, UsersRound } from 'lucide-react';
import Link from 'next/link';
import { routes } from 'shared/config';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
Separator,
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarHeader,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarRail,
} from 'shared/ui';
import { NavUser } from './NavUser';
import { TeamsDropdown } from './teams/TeamsDropdown';
import { Projects } from './Projects';

const team = [
{
url: routes.team.members(),
title: 'Участники',
action: (
<InviteTeamMemberDialog asChild>
<SquarePlusIcon />
</InviteTeamMemberDialog>
),
},
{ url: routes.team.invitations(), title: 'Приглашения', action: null },
{ url: routes.team.roles(), title: 'Роли', action: null },
{ url: routes.team.settings(), title: 'Настройки', action: null },
];
import { MyTeams } from './MyTeams';
import { Team } from './Team';

export function AppSidebar({ ...props }: Omit<React.ComponentProps<typeof Sidebar>, 'children'>) {
return (
Expand All @@ -51,49 +21,13 @@ export function AppSidebar({ ...props }: Omit<React.ComponentProps<typeof Sideba
<SidebarContent className="gap-2">
<SidebarGroup>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href={routes.profile.root()}>
<UserRound />
<span>Мой профиль</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<Collapsible asChild defaultOpen className="group/collapsible">
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip="Управление командой">
<UsersRound />
<span>Команда</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{team.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
{subItem.action ? (
<SidebarMenuAction>{subItem.action}</SidebarMenuAction>
) : null}
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
<MyTeams />
<Team />
</SidebarMenu>
</SidebarGroup>
<Separator />
<Projects />
</SidebarContent>
<SidebarFooter>
<NavUser />
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
Expand Down
24 changes: 24 additions & 0 deletions src/widgets/app-sidebar/ui/MyTeams.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';
import { Network } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { routes } from 'shared/config';
import { SidebarMenuButton, SidebarMenuItem } from 'shared/ui';

export function MyTeams() {
const pathname = usePathname();
return (
<SidebarMenuItem>
<SidebarMenuButton
tooltip="Мои команды"
isActive={pathname === routes.profile.teams()}
asChild
>
<Link href={routes.profile.teams()}>
<Network />
<span>Мои команды</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
43 changes: 26 additions & 17 deletions src/widgets/app-sidebar/ui/Projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { useTeamStore } from 'entities/team';
import { ArchiveProjectDialog, RestoreProjectDialog } from 'features/projects/archive';
import { CreateProjectDialog } from 'features/projects/create';
import { ShareProjectDialog } from 'features/projects/share';
import { Archive, Link2, MoreHorizontal, Plus } from 'lucide-react';
import { Archive, BriefcaseBusiness, Link2, MoreHorizontal, Plus } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useState } from 'react';
import { routes } from 'shared/config';
import {
Expand All @@ -16,7 +17,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
SidebarGroup,
SidebarGroupAction,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
Expand All @@ -28,8 +28,11 @@ import {
export function Projects() {
const slug = useTeamStore.use.slug();
const { isMobile } = useSidebar();
const pathname = usePathname();
const [createProjectOpen, setCreateProjectOpen] = useState(false);
const projects = useQuery({ ...ProjectQueries.getProjects(slug!), enabled: !!slug });
const projectList = projects.data?.items.slice(0, 6) ?? [];
const totalProjects = projects.data?.items.length ?? 0;

if (!projects.data) {
return null;
Expand All @@ -39,20 +42,13 @@ export function Projects() {
<>
<SidebarGroup>
<SidebarGroupLabel>Проекты</SidebarGroupLabel>
<SidebarGroupAction
aria-label="Создать проект"
disabled={!slug}
onClick={() => setCreateProjectOpen(true)}
>
<Plus />
</SidebarGroupAction>
<SidebarMenu>
{projects.data.items.map((project) => {
{projectList.map((project) => {
const canManage = Boolean(slug && project.canEdit);

return (
<SidebarMenuItem key={project.id}>
<SidebarMenuButton asChild>
<SidebarMenuButton tooltip={project.name} asChild>
<Link href={routes.team.project.root(project.id)}>
<span>{projectIconCodeToEmoji(project.icon)}</span>
<span>{project.name}</span>
Expand Down Expand Up @@ -117,12 +113,25 @@ export function Projects() {
);
})}
<SidebarMenuItem>
<Link href={routes.team.projects()}>
<SidebarMenuButton>
<MoreHorizontal />
<span>Больше</span>
</SidebarMenuButton>
</Link>
<SidebarMenuButton
tooltip="Все проекты"
asChild
isActive={routes.team.projects() === pathname}
>
<Link href={routes.team.projects()}>
<BriefcaseBusiness />
<span>Все проекты {!!totalProjects && `(${totalProjects})`}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
onClick={() => setCreateProjectOpen(true)}
tooltip={'Добавить проект'}
>
<Plus />
<span>Добавить проект</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
Expand Down
72 changes: 72 additions & 0 deletions src/widgets/app-sidebar/ui/Team.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';
import { InviteTeamMemberDialog } from 'features/teams/invite';
import { UsersRound, ChevronRight, Plus } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { routes } from 'shared/config';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from 'shared/ui';
import { team } from '../config/sidebar';
import { useRouter } from 'next/navigation';

export function Team() {
const pathname = usePathname();
const router = useRouter();
const { open, isMobile } = useSidebar();

const isAllowedToHighlight = !open && !isMobile;

const handleClickTrigger = () => {
if (isAllowedToHighlight) {
router.push(routes.team.members());
}
};
return (
<Collapsible asChild className="group/collapsible">
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton
onClick={handleClickTrigger}
isActive={isAllowedToHighlight && pathname?.startsWith(routes.team.root())}
tooltip="Управление командой"
>
<UsersRound />
Команда
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent className="collapsible-content">
<SidebarMenuSub>
{team.map((subItem) => (
<SidebarMenuSubItem key={subItem.url}>
<SidebarMenuSubButton isActive={pathname?.startsWith(subItem.url)} asChild>
<Link href={subItem.url}>
<subItem.icon />
{subItem.title}
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}

<SidebarMenuSubItem>
<SidebarMenuSubButton asChild>
<InviteTeamMemberDialog>
<Plus /> Пригласить участника
</InviteTeamMemberDialog>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
);
}
12 changes: 6 additions & 6 deletions src/widgets/app-sidebar/ui/teams/TeamsDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';
import { CreateTeamDialog } from 'features/teams/create';
import { Plus } from 'lucide-react';
import Link from 'next/link';
import { useState } from 'react';
import { routes } from 'shared/config';
import {
DropdownMenu,
Expand All @@ -12,18 +11,19 @@ import {
DropdownMenuShortcut,
DropdownMenuTrigger,
SidebarMenuButton,
useSidebar,
} from 'shared/ui';
import { useTeamsDropdown } from '../../model/useTeamsDropdown';
import { TeamItem } from './TeamItem';
import { TeamTrigger } from './TeamTrigger';
import { useIsMobile } from 'shared/lib/hooks';
import { Plus } from 'lucide-react';
import { useState } from 'react';

export function TeamsDropdown() {
const { isMobile } = useSidebar();
const [createTeamOpen, setCreateTeamOpen] = useState(false);
const { open, setOpen, query, visibleTeams, teams, hasMoreTeams, switchTeam } =
useTeamsDropdown();

const isMobile = useIsMobile();
const [createTeamOpen, setCreateTeamOpen] = useState(false);
return (
<>
<DropdownMenu open={open} onOpenChange={setOpen}>
Expand Down
Loading