diff --git a/app/(protected)/team/(team)/layout.tsx b/app/(protected)/team/(team)/layout.tsx index 3ca574e..b1514db 100644 --- a/app/(protected)/team/(team)/layout.tsx +++ b/app/(protected)/team/(team)/layout.tsx @@ -1,5 +1,4 @@ import { PageLayout } from 'app/layouts/PageLayout'; -import { teamTabs } from 'pages/team'; import { Badge } from 'shared/ui'; export default function TeamLayout({ children }: { children: React.ReactNode }) { @@ -8,7 +7,6 @@ export default function TeamLayout({ children }: { children: React.ReactNode }) title="Управление командой" description="Управляйте участниками команды, ожидающими приглашениями, ролями и правами доступа." badge={8 участников} - tabs={teamTabs} > {children} diff --git a/app/(protected)/team/(team)/roles/page.tsx b/app/(protected)/team/(team)/roles/page.tsx index f81d7e5..3be4e15 100644 --- a/app/(protected)/team/(team)/roles/page.tsx +++ b/app/(protected)/team/(team)/roles/page.tsx @@ -1 +1,8 @@ -export { RolesPage as default } from 'pages/team'; +import { notFound } from 'next/navigation'; + +// export { RolesPage as default } from 'pages/team'; +export default function Page() { + notFound(); +} + +// TODO: временно убрал страницу. Вернуть, когда появится функциональность прав и ролей diff --git a/app/(protected)/user/(user)/layout.tsx b/app/(protected)/user/(user)/layout.tsx index 5ee9d2d..7b4319a 100644 --- a/app/(protected)/user/(user)/layout.tsx +++ b/app/(protected)/user/(user)/layout.tsx @@ -1,14 +1,23 @@ import { PageLayout } from 'app/layouts/PageLayout'; -import { profileTabs } from 'pages/profile'; +import { Bell, Settings } from 'lucide-react'; +import { routes } from 'shared/config'; +import { VerticalTabsNav, type TabNavItem } from 'widgets/tabs-nav'; + +export const tabs: TabNavItem[] = [ + { key: routes.user.profile(), label: 'Основные настройки', icon: }, + { key: routes.user.notifications(), label: 'Уведомления', icon: }, +]; export default function ProfileLayout({ children }: { children: React.ReactNode }) { return ( - {children} +
+ + {children} +
); } diff --git a/src/entities/user/model/schemas.ts b/src/entities/user/model/schemas.ts index 82a91b5..f97a513 100644 --- a/src/entities/user/model/schemas.ts +++ b/src/entities/user/model/schemas.ts @@ -91,14 +91,11 @@ export const ProfileUpdateBody = z.object({ headline: z.string().max(100, 'Должность слишком длинная').nullable().optional(), location: z.string().max(100, 'Локация слишком длинная').nullable().optional(), phone: z.string().max(20, 'Номер телефона слишком длинный').nullable().optional(), - gender: z - .enum(['none', 'male', 'female', 'non_binary', 'other', 'prefer_not_to_say']) - .default('none') - .optional(), + gender: z.enum(['none', 'male', 'female', 'non_binary', 'other', 'prefer_not_to_say']).optional(), vacationStart: z.string().nullable().optional(), vacationEnd: z.string().nullable().optional(), vacationMessage: z.string().max(500, 'Сообщение слишком длинное').nullable().optional(), - pronouns: z.enum(['he_him', 'she_her', 'they_them', 'other', 'none']).default('none').optional(), + pronouns: z.enum(['he_him', 'she_her', 'they_them', 'other', 'none']).optional(), pronounsCustom: z.string().max(50, 'Максимальная длина 50 символов').nullable().optional(), bio: z.string().max(1000, 'О себе не более 1000 символов').nullable().optional(), timezone: z.string().max(50).optional(), diff --git a/src/pages/profile/config/tabs.ts b/src/pages/profile/config/tabs.ts deleted file mode 100644 index 1ae5728..0000000 --- a/src/pages/profile/config/tabs.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { routes } from 'shared/config'; - -export const profileTabs = [ - { key: routes.user.profile(), label: 'Мой профиль' }, - { key: routes.user.security(), label: 'Безопасность' }, - { key: routes.user.notifications(), label: 'Уведомления' }, -]; diff --git a/src/pages/profile/index.ts b/src/pages/profile/index.ts index 8922138..0f32dee 100644 --- a/src/pages/profile/index.ts +++ b/src/pages/profile/index.ts @@ -1,4 +1,3 @@ -export { profileTabs } from './config/tabs'; export { MePage } from './ui/me-page/MePage'; export { NotificationsPage } from './ui/notifications-page/NotificationsPage'; export { SecurityPage } from './ui/security-page/SecurityPage'; diff --git a/src/pages/profile/ui/me-page/IdentityItem.tsx b/src/pages/profile/ui/me-page/IdentityItem.tsx index e7475c0..43fc64b 100644 --- a/src/pages/profile/ui/me-page/IdentityItem.tsx +++ b/src/pages/profile/ui/me-page/IdentityItem.tsx @@ -1,26 +1,25 @@ 'use client'; -import { Item, ItemActions, ItemContent, ItemMedia } from 'shared/ui'; +import { Item, ItemActions, ItemMedia } from 'shared/ui'; import { UploadAvatar } from 'features/upload-avatar'; import { SignOut } from 'features/auth/sign-out'; import { type TUser, UserAvatar } from 'entities/user'; type AccountIdentityItemProps = { profile: TUser.UserResponse['profile']; - email: string; }; -function IdentityItem({ profile, email }: AccountIdentityItemProps) { +function IdentityItem({ profile }: AccountIdentityItemProps) { const fullName = `${profile.firstName} ${profile.lastName}`; return ( - - + + - -
-

{fullName}

-

{email}

-

- {profile.bio?.trim() || 'Добавьте информацию о себе в профиле'} -

-
-
- +
); diff --git a/src/pages/profile/ui/me-page/MePage.tsx b/src/pages/profile/ui/me-page/MePage.tsx index 97952ff..24db62e 100644 --- a/src/pages/profile/ui/me-page/MePage.tsx +++ b/src/pages/profile/ui/me-page/MePage.tsx @@ -7,7 +7,6 @@ import { CardSection, CardTitle, FloatingSaveBar, - Separator, } from 'shared/ui'; import { IdentityItem } from './IdentityItem'; import { ProfileForm } from './ProfileForm'; @@ -35,16 +34,16 @@ function MePage() { -
+
- - + + Имя Фамилия diff --git a/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx b/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx index d2a3d4f..3f2a811 100644 --- a/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx +++ b/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx @@ -2,20 +2,27 @@ import { authFabricKeys, OAUTH_PROVIDERS, type TAuth } from 'entities/auth'; import { type ComponentProps, useCallback } from 'react'; -import { Button } from 'shared/ui'; +import { Badge, Button } from 'shared/ui'; import { useConnectOAuthProvider } from '../../../api/useConnectOauthProvider'; import { useDisconnectOAuthProvider } from '../../../api/useDisconnectOauthProvider'; import { env } from 'shared/config'; import { toast } from 'sonner'; import Image from 'next/image'; +import { classNames } from 'shared/lib/utils'; -type OAuthManageButtonProps = ComponentProps & { +type OAuthManageButtonProps = ComponentProps<'div'> & { provider: TAuth.OAuthProvider; label: string; isLinked: boolean; }; -export function OAuthManageButton({ provider, label, isLinked, ...props }: OAuthManageButtonProps) { +export function OAuthManageButton({ + provider, + className = '', + label, + isLinked, + ...props +}: OAuthManageButtonProps) { const connect = useConnectOAuthProvider(); const disconnect = useDisconnectOAuthProvider(); @@ -32,6 +39,8 @@ export function OAuthManageButton({ provider, label, isLinked, ...props }: OAuth } else { connect.mutate(provider, { onSuccess: (data) => { + console.log(data); + localStorage.setItem('test', JSON.stringify(data)); const url = data.url.startsWith('http') ? data.url : new URL(data.url, env.NEXT_PUBLIC_API_BASE_URL).toString(); @@ -44,20 +53,56 @@ export function OAuthManageButton({ provider, label, isLinked, ...props }: OAuth const meta = OAUTH_PROVIDERS[provider]; return ( - +
+ + +
+
+ ); +} + +function OAuthBadge({ isLinked, className = '' }: { isLinked: boolean; className?: string }) { + return ( + + {isLinked ? 'Подключен' : 'Не подключен'} + ); } diff --git a/src/pages/profile/ui/notifications-page/NotificationsPageFallback.tsx b/src/pages/profile/ui/notifications-page/NotificationsPageFallback.tsx index 339c750..e45f3c7 100644 --- a/src/pages/profile/ui/notifications-page/NotificationsPageFallback.tsx +++ b/src/pages/profile/ui/notifications-page/NotificationsPageFallback.tsx @@ -1,4 +1,4 @@ -import { CardSection, OptionGroup, Skeleton } from 'shared/ui'; +import { CardSection, Skeleton } from 'shared/ui'; export function NotificationsPageFallback() { return ( diff --git a/src/pages/team/index.ts b/src/pages/team/index.ts index 3b6cf02..f579de1 100644 --- a/src/pages/team/index.ts +++ b/src/pages/team/index.ts @@ -1,4 +1,3 @@ -export { teamTabs } from './config/tabs'; export { InvitationsPage } from './ui/invitations/InvitationsPage'; export { MembersPage } from './ui/members/MembersPage'; export { RolesPage } from './ui/roles/RolesPage'; diff --git a/src/pages/team/ui/members/MemberCard.tsx b/src/pages/team/ui/members/MemberCard.tsx index ce311df..0ff058d 100644 --- a/src/pages/team/ui/members/MemberCard.tsx +++ b/src/pages/team/ui/members/MemberCard.tsx @@ -1,4 +1,4 @@ -import { type TTeam } from 'entities/team'; +import { ROLE_LABELS, type TTeam } from 'entities/team'; import { X } from 'lucide-react'; import { ComponentProps } from 'react'; import { classNames } from 'shared/lib/utils'; @@ -6,31 +6,25 @@ import { Avatar, AvatarFallback, AvatarImage, - Badge, Button, Item, ItemActions, ItemContent, - ItemFooter, ItemGroup, ItemHeader, - Progress, + OwnerWrap, } from 'shared/ui'; import { memberCardConfig as cfg } from '../../config/member'; import { MemberRoleSelect } from './MemberRoleSelect'; import { MemberStatusSelect } from './MemberStatusSelect'; import { RemoveMemberDialog } from './RemoveMemberDialog'; -const workload = 61; //todo: mock -const skills = ['Design System', 'Sprint Plan']; //todo: mock -const backOn = '2026-05-10'; //todo: mock - interface MemberCardProps extends Omit, 'children'> { member: TTeam.TeamMemberResponse; } export function MemberCard({ className, member, ...props }: MemberCardProps) { - const wl = cfg.workloadLabel(workload); + const isOwner = member.role === 'owner'; return ( - - - - + + + + + +

{member.fullName}

+ {member.role !== 'owner' && ( + {ROLE_LABELS[member.role]} + )}
- {member.role !== 'owner' && ( + {!isOwner && ( @@ -25,14 +25,6 @@ export function MembersPage() {

Показано {filtered.length} из {total}

-
- - -
diff --git a/src/pages/team/ui/settings/SettingsPage.tsx b/src/pages/team/ui/settings/SettingsPage.tsx index 94c4045..1700871 100644 --- a/src/pages/team/ui/settings/SettingsPage.tsx +++ b/src/pages/team/ui/settings/SettingsPage.tsx @@ -5,13 +5,9 @@ import { FormProvider, useForm } from 'react-hook-form'; import { useQueryTeam } from '../../api/useQueryTeam'; import { TeamSettingsFormSchema, type TeamSettingsFormValues } from '../../model/settings'; import { DangerZone } from './DangerZone'; -import { DefaultSettings } from './DefaultSettings'; -import { InvitationSecurity } from './InvitationSecurity'; import { SaveBar } from './SaveBar'; import { TeamIdentity } from './TeamIdentity'; import { DangerZoneSkeleton } from './skeletons/DangerZone.skeleton'; -import { DefaultSettingsSkeleton } from './skeletons/DefaultSettings.skeleton'; -import { InvitationSecuritySkeleton } from './skeletons/InvitationSecurity.skeleton'; import { TeamIdentitySkeleton } from './skeletons/TeamIdentity.skeleton'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -52,8 +48,6 @@ export function Settings() {
{team ? : } - {team ? : } - {team ? : } {team ? : } {team ? : null} diff --git a/src/shared/ui/card-section/CardSection.tsx b/src/shared/ui/card-section/CardSection.tsx index 3f6e778..081faed 100644 --- a/src/shared/ui/card-section/CardSection.tsx +++ b/src/shared/ui/card-section/CardSection.tsx @@ -12,7 +12,7 @@ function CardSection({ title, description, ...props }: ICardSectionProps) { {title} - {description} + {description} diff --git a/src/widgets/app-sidebar/config/sidebar.ts b/src/widgets/app-sidebar/config/sidebar.ts index 7652ac1..8c425c0 100644 --- a/src/widgets/app-sidebar/config/sidebar.ts +++ b/src/widgets/app-sidebar/config/sidebar.ts @@ -1,4 +1,4 @@ -import { Mail, Settings, ShieldUser, UsersRound } from 'lucide-react'; +import { BriefcaseBusiness, Mail, Settings, UsersRound } from 'lucide-react'; import { routes } from 'shared/config'; export const team = [ @@ -7,7 +7,7 @@ export const team = [ title: 'Участники', icon: UsersRound, }, + { url: routes.team.projects.all(), title: 'Проекты', icon: BriefcaseBusiness }, { url: routes.team.invitations(), title: 'Приглашения', icon: Mail }, - { url: routes.team.roles(), title: 'Роли', icon: ShieldUser }, { url: routes.team.settings(), title: 'Настройки', icon: Settings }, ] as const; diff --git a/src/widgets/app-sidebar/ui/projects/ProjectsContent.tsx b/src/widgets/app-sidebar/ui/projects/ProjectsContent.tsx index 53339f0..ae7b453 100644 --- a/src/widgets/app-sidebar/ui/projects/ProjectsContent.tsx +++ b/src/widgets/app-sidebar/ui/projects/ProjectsContent.tsx @@ -67,7 +67,8 @@ export function ProjectsContent() { asChild > - {projectIconCodeToEmoji(project.icon)} {project.name} + {projectIconCodeToEmoji(project.icon)} + {project.name} @@ -80,15 +81,15 @@ export function ProjectsContent() { - Новый проект + Новый проект {totalProjects > 0 ? ( - + - Все проекты ({totalProjects}) + Все проекты ({totalProjects}) diff --git a/src/widgets/app-sidebar/ui/teams/TeamContent.tsx b/src/widgets/app-sidebar/ui/teams/TeamContent.tsx index 974a57e..7eef692 100644 --- a/src/widgets/app-sidebar/ui/teams/TeamContent.tsx +++ b/src/widgets/app-sidebar/ui/teams/TeamContent.tsx @@ -60,7 +60,7 @@ export function TeamContent() { - Добавить участника + Добавить участника diff --git a/src/widgets/tabs-nav/index.ts b/src/widgets/tabs-nav/index.ts index 408ab4f..ac3dc31 100644 --- a/src/widgets/tabs-nav/index.ts +++ b/src/widgets/tabs-nav/index.ts @@ -1,2 +1,3 @@ export { type TabNavItem } from './model/types'; export { TabsNav } from './ui/TabsNav'; +export { VerticalTabsNav } from './ui/VerticalTabsNav'; diff --git a/src/widgets/tabs-nav/model/types.ts b/src/widgets/tabs-nav/model/types.ts index 6404556..853d56e 100644 --- a/src/widgets/tabs-nav/model/types.ts +++ b/src/widgets/tabs-nav/model/types.ts @@ -7,4 +7,5 @@ export type TabNavItem = { label: string; matchPrefix?: boolean; badge?: { value: string | ReactNode; variant: ComponentProps['variant'] }; + icon?: ReactNode; }; diff --git a/src/widgets/tabs-nav/ui/VerticalTabsNav.tsx b/src/widgets/tabs-nav/ui/VerticalTabsNav.tsx new file mode 100644 index 0000000..854a03a --- /dev/null +++ b/src/widgets/tabs-nav/ui/VerticalTabsNav.tsx @@ -0,0 +1,54 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { ComponentProps } from 'react'; +import { classNames } from 'shared/lib/utils'; +import { Badge, Button } from 'shared/ui'; +import { TabNavItem } from '../model/types'; + +interface TabsNavProps extends Omit, 'children'> { + tabs: TabNavItem[]; +} + +export function VerticalTabsNav({ className, tabs, ...props }: TabsNavProps) { + const pathname = usePathname(); + + if (tabs.length === 0) { + return null; + } + + return ( +
+ {tabs.map((tab) => { + const active = tab.matchPrefix + ? (pathname ?? '').startsWith(tab.key) + : pathname === tab.key; + + return ( + + ); + })} +
+ ); +}