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..f0a2a26 100644 --- a/app/(protected)/user/(user)/layout.tsx +++ b/app/(protected)/user/(user)/layout.tsx @@ -1,14 +1,25 @@ -import { PageLayout } from 'app/layouts/PageLayout'; -import { profileTabs } from 'pages/profile'; +import { Bell, Settings } from 'lucide-react'; +import { routes } from 'shared/config'; +import { PageWrapper } from 'widgets/page-wrapper'; +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/app/(protected)/user/(user)/notifications/error.tsx b/app/(protected)/user/(user)/notifications/error.tsx new file mode 100644 index 0000000..7b38cf4 --- /dev/null +++ b/app/(protected)/user/(user)/notifications/error.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { ErrorState } from 'widgets/error-state'; + +export default function Error({ + unstable_retry, +}: { + error: Error & { digest?: string }; + unstable_retry: () => void; +}) { + return ( + unstable_retry()} + className="border" + /> + ); +} diff --git a/app/(protected)/user/(user)/profile/error.tsx b/app/(protected)/user/(user)/profile/error.tsx new file mode 100644 index 0000000..3575c16 --- /dev/null +++ b/app/(protected)/user/(user)/profile/error.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { ErrorState } from 'widgets/error-state'; + +export default function Error({ + unstable_retry, +}: { + error: Error & { digest?: string }; + unstable_retry: () => void; +}) { + return ( + unstable_retry()} + className="border" + /> + ); +} diff --git a/src/app/layouts/SidebarLayout.tsx b/src/app/layouts/SidebarLayout.tsx index 81104ae..533598a 100644 --- a/src/app/layouts/SidebarLayout.tsx +++ b/src/app/layouts/SidebarLayout.tsx @@ -12,7 +12,7 @@ export function SidebarLayout({ children, ...props }: ComponentProps - +
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/api/useConnectedAccounts.ts b/src/pages/profile/api/useConnectedAccounts.ts deleted file mode 100644 index 8518a9d..0000000 --- a/src/pages/profile/api/useConnectedAccounts.ts +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; -import { useQuery } from '@tanstack/react-query'; -import { AuthQueries } from 'entities/auth'; -import { useMemo } from 'react'; - -export function useConnectedAccounts() { - const available = useQuery(AuthQueries.getOAuthProviders()); - const connected = useQuery(AuthQueries.getConnectedOAuthProviders()); - - const providers = useMemo(() => { - if (!available.data) return []; - - const connectedSet = new Set(connected.data?.map((v) => v.provider)); - - return available.data.map((item) => ({ - ...item, - isConnected: connectedSet.has(item.value), - })); - }, [available.data, connected.data]); - - return { providers }; -} diff --git a/src/pages/profile/config/profile.ts b/src/pages/profile/config/profile.ts new file mode 100644 index 0000000..2e9755b --- /dev/null +++ b/src/pages/profile/config/profile.ts @@ -0,0 +1,7 @@ +import { OAuthConnectionStatus } from '../model/profile'; + +export const OAUTH_BADGE_LABELS = { + connected: 'Подключен', + disconnected: 'Не подключен', + unknown: 'Не удалось проверить', +} as const satisfies Record; 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/model/profile.ts b/src/pages/profile/model/profile.ts index 91d5ebe..f0fb522 100644 --- a/src/pages/profile/model/profile.ts +++ b/src/pages/profile/model/profile.ts @@ -18,3 +18,5 @@ export const ProfileForm = z.object({ }); export type ProfileFormValues = z.infer; + +export type OAuthConnectionStatus = 'connected' | 'disconnected' | 'unknown'; diff --git a/src/pages/profile/model/useMePage.ts b/src/pages/profile/model/useMePage.ts index a9fb11d..005afd8 100644 --- a/src/pages/profile/model/useMePage.ts +++ b/src/pages/profile/model/useMePage.ts @@ -1,7 +1,7 @@ 'use client'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useQuery } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { type TUser, UserQueries } from 'entities/user'; import { useEffect } from 'react'; import { useForm, useFormState } from 'react-hook-form'; @@ -9,7 +9,7 @@ import { useUpdateProfile } from '../api/useUpdateProfile'; import { ProfileForm as ProfileFormSchema, type ProfileFormValues } from './profile'; export function useMePage() { - const query = useQuery(UserQueries.getMe()); + const query = useSuspenseQuery(UserQueries.getMe()); const profile = query.data?.profile; const email = query.data?.email; diff --git a/src/pages/profile/model/useOAuthManage.ts b/src/pages/profile/model/useOAuthManage.ts new file mode 100644 index 0000000..bc61116 --- /dev/null +++ b/src/pages/profile/model/useOAuthManage.ts @@ -0,0 +1,37 @@ +import { useCallback } from 'react'; +import { useConnectOAuthProvider } from '../api/useConnectOauthProvider'; +import { useDisconnectOAuthProvider } from '../api/useDisconnectOauthProvider'; +import { authFabricKeys, TAuth } from 'entities/auth'; +import { toast } from 'sonner'; +import { OAuthConnectionStatus } from './profile'; +import { env } from 'shared/config'; + +export function useOAuthManage(provider: TAuth.OAuthProvider, status: OAuthConnectionStatus) { + const connect = useConnectOAuthProvider(); + const disconnect = useDisconnectOAuthProvider(); + + const isPending = connect.isPending || disconnect.isPending; + + const handleToggleConnect = useCallback(() => { + if (status === 'connected') { + disconnect.mutate(provider, { + onSuccess: (data, _v, _m, context) => { + context.client.invalidateQueries({ queryKey: authFabricKeys.connectedProviders() }); + toast.success(data.message); + }, + }); + } else if (status === 'disconnected') { + connect.mutate(provider, { + onSuccess: (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(); + window.location.href = url; + }, + }); + } + }, [connect, disconnect, status, provider]); + + return { handleToggleConnect, isPending }; +} diff --git a/src/pages/profile/ui/me-page/IdentityItem.tsx b/src/pages/profile/ui/me-page/IdentityItem.tsx deleted file mode 100644 index e7475c0..0000000 --- a/src/pages/profile/ui/me-page/IdentityItem.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; - -import { Item, ItemActions, ItemContent, 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) { - const fullName = `${profile.firstName} ${profile.lastName}`; - - return ( - - - - } - /> - - -
-

{fullName}

-

{email}

-

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

-
-
- - - -
- ); -} - -export { IdentityItem }; diff --git a/src/pages/profile/ui/me-page/MePage.tsx b/src/pages/profile/ui/me-page/MePage.tsx index 97952ff..9ff1ab3 100644 --- a/src/pages/profile/ui/me-page/MePage.tsx +++ b/src/pages/profile/ui/me-page/MePage.tsx @@ -1,61 +1,14 @@ 'use client'; +import dynamic from 'next/dynamic'; +import { MePageFallback } from './MePageFallback'; -import { - Card, - CardDescription, - CardHeader, - CardSection, - CardTitle, - FloatingSaveBar, - Separator, -} from 'shared/ui'; -import { IdentityItem } from './IdentityItem'; -import { ProfileForm } from './ProfileForm'; -import { useMePage } from '../../model/useMePage'; -import { AccountSection } from './account-section/AccountsSection'; -import { Suspense } from 'react'; -import { QueryParamsHandler } from 'features/handle-query-params'; +const MePageContent = dynamic(() => import('./MePageContent').then((mod) => mod.MePage), { + ssr: false, + loading: () => , +}); function MePage() { - const { form, profile, email, isDirty, isPending, onSubmit, onDiscard } = useMePage(); - - if (!profile || !email) { - return ( - - - Профиль - Данные профиля пока недоступны. - - - ); - } - - return ( - <> - - - -
- - - - - - - -
- - ); + return ; } export { MePage }; diff --git a/src/pages/profile/ui/me-page/MePageContent.tsx b/src/pages/profile/ui/me-page/MePageContent.tsx new file mode 100644 index 0000000..d552be5 --- /dev/null +++ b/src/pages/profile/ui/me-page/MePageContent.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { type PropsWithChildren, Suspense } from 'react'; +import { QueryParamsHandler } from 'features/handle-query-params'; +import { ProfileSection } from './profile-section/ProfileSection'; +import { AccountSection } from './account-section/AccountSection'; +import { ProfileSectionFallback } from './profile-section/ProfileSectionFallback'; + +function MePage() { + return ( + <> + + + + + }> + + + + + + ); +} + +function MePageLayout({ children }: PropsWithChildren) { + return
{children}
; +} + +export { MePage, MePageLayout }; diff --git a/src/pages/profile/ui/me-page/MePageFallback.tsx b/src/pages/profile/ui/me-page/MePageFallback.tsx new file mode 100644 index 0000000..11e0fc6 --- /dev/null +++ b/src/pages/profile/ui/me-page/MePageFallback.tsx @@ -0,0 +1,12 @@ +import { AccountSectionFallback } from './account-section/AccountSectionFallback'; +import { MePageLayout } from './MePageContent'; +import { ProfileSectionFallback } from './profile-section/ProfileSectionFallback'; + +export function MePageFallback() { + return ( + + + + + ); +} diff --git a/src/pages/profile/ui/me-page/account-section/AccountSection.tsx b/src/pages/profile/ui/me-page/account-section/AccountSection.tsx new file mode 100644 index 0000000..f247561 --- /dev/null +++ b/src/pages/profile/ui/me-page/account-section/AccountSection.tsx @@ -0,0 +1,41 @@ +import { CardSection } from 'shared/ui'; +import { OAuthManageButton } from './OAuthManageButton'; +import { AuthQueries } from 'entities/auth'; +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import { AccountSectionFallback } from './AccountSectionFallback'; +import { AccountSectionErrorFallback } from './AccountSectionErrorFallback'; + +export function AccountSection() { + const providers = useQuery(AuthQueries.getOAuthProviders()); + const connected = useQuery(AuthQueries.getConnectedOAuthProviders()); + + const connectedSet = useMemo( + () => new Set(connected.data?.map((v) => v.provider)), + [connected.data] + ); + + if (providers.error) return ; + if (providers.isLoading) return ; + + return ( + + {providers.data?.map((provider) => { + const isConnected = connectedSet.has(provider.value); + return ( + + ); + })} + + ); +} diff --git a/src/pages/profile/ui/me-page/account-section/AccountSectionErrorFallback.tsx b/src/pages/profile/ui/me-page/account-section/AccountSectionErrorFallback.tsx new file mode 100644 index 0000000..f2edc25 --- /dev/null +++ b/src/pages/profile/ui/me-page/account-section/AccountSectionErrorFallback.tsx @@ -0,0 +1,14 @@ +import { CardSection } from 'shared/ui'; +import { ErrorState } from 'widgets/error-state'; + +export function AccountSectionErrorFallback() { + return ( + + + + ); +} diff --git a/src/pages/profile/ui/me-page/account-section/AccountSectionFallback.tsx b/src/pages/profile/ui/me-page/account-section/AccountSectionFallback.tsx new file mode 100644 index 0000000..7a9c74e --- /dev/null +++ b/src/pages/profile/ui/me-page/account-section/AccountSectionFallback.tsx @@ -0,0 +1,30 @@ +import { CardSection, Skeleton } from 'shared/ui'; + +export function AccountSectionFallback() { + return ( + + {Array.from({ length: 3 }).map((_, index) => ( +
+
+ +
+ + +
+
+
+ + +
+
+ ))} +
+ ); +} diff --git a/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx b/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx deleted file mode 100644 index 1744497..0000000 --- a/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { OAuthManageButton } from './OAuthManageButton'; -import { useConnectedAccounts } from '../../../api/useConnectedAccounts'; -import { CardSection } from 'shared/ui'; - -export function AccountSection() { - const { providers } = useConnectedAccounts(); - - return ( - - {providers?.map((provider) => { - return ( - - ); - })} - - ); -} diff --git a/src/pages/profile/ui/me-page/account-section/OAuthBadge.tsx b/src/pages/profile/ui/me-page/account-section/OAuthBadge.tsx new file mode 100644 index 0000000..f5311e1 --- /dev/null +++ b/src/pages/profile/ui/me-page/account-section/OAuthBadge.tsx @@ -0,0 +1,28 @@ +import { Badge } from 'shared/ui'; +import { type OAuthConnectionStatus } from '../../../model/profile'; +import { classNames } from 'shared/lib/utils'; +import { OAUTH_BADGE_LABELS } from 'pages/profile/config/profile'; + +export function OAuthBadge({ + status, + className = '', +}: { + status: OAuthConnectionStatus; + className?: string; +}) { + return ( + + {OAUTH_BADGE_LABELS[status]} + + ); +} 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..c4944d9 100644 --- a/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx +++ b/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx @@ -1,63 +1,72 @@ 'use client'; -import { authFabricKeys, OAUTH_PROVIDERS, type TAuth } from 'entities/auth'; -import { type ComponentProps, useCallback } from 'react'; -import { Button } from 'shared/ui'; -import { useConnectOAuthProvider } from '../../../api/useConnectOauthProvider'; -import { useDisconnectOAuthProvider } from '../../../api/useDisconnectOauthProvider'; -import { env } from 'shared/config'; -import { toast } from 'sonner'; +import { OAUTH_PROVIDERS, type TAuth } from 'entities/auth'; +import { type ComponentProps } from 'react'; +import { Button, Spinner } from 'shared/ui'; import Image from 'next/image'; +import { classNames } from 'shared/lib/utils'; +import { type OAuthConnectionStatus } from '../../../model/profile'; +import { useOAuthManage } from 'pages/profile/model/useOAuthManage'; +import { OAuthBadge } from './OAuthBadge'; type OAuthManageButtonProps = ComponentProps & { + wrap?: Omit, 'children'>; provider: TAuth.OAuthProvider; label: string; - isLinked: boolean; + isLoading?: boolean; + status: OAuthConnectionStatus; }; -export function OAuthManageButton({ provider, label, isLinked, ...props }: OAuthManageButtonProps) { - const connect = useConnectOAuthProvider(); - const disconnect = useDisconnectOAuthProvider(); - - const isLoading = connect.isPending || disconnect.isPending; - - const handleToggleConnect = useCallback(() => { - if (isLinked) { - disconnect.mutate(provider, { - onSuccess: (data, _v, _m, context) => { - context.client.invalidateQueries({ queryKey: authFabricKeys.connectedProviders() }); - toast.success(data.message); - }, - }); - } else { - connect.mutate(provider, { - onSuccess: (data) => { - const url = data.url.startsWith('http') - ? data.url - : new URL(data.url, env.NEXT_PUBLIC_API_BASE_URL).toString(); - window.location.href = url; - }, - }); - } - }, [connect, disconnect, isLinked, provider]); - +export function OAuthManageButton({ + provider, + label, + disabled, + status, + isLoading = false, + wrap = {}, + ...props +}: OAuthManageButtonProps) { + const { isPending, handleToggleConnect } = useOAuthManage(provider, status); + const isConnected = status === 'connected'; + const isDisabled = disabled || isPending || isLoading || status === 'unknown'; const meta = OAUTH_PROVIDERS[provider]; return ( -
- - {isLinked ? 'Отвязать' : 'Привязать'} {label} аккаунт - - + ); } diff --git a/src/pages/profile/ui/me-page/profile-section/IdentityItem.tsx b/src/pages/profile/ui/me-page/profile-section/IdentityItem.tsx new file mode 100644 index 0000000..43fc64b --- /dev/null +++ b/src/pages/profile/ui/me-page/profile-section/IdentityItem.tsx @@ -0,0 +1,37 @@ +'use client'; + +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']; +}; + +function IdentityItem({ profile }: AccountIdentityItemProps) { + const fullName = `${profile.firstName} ${profile.lastName}`; + + return ( + + + + } + /> + + + + + + ); +} + +export { IdentityItem }; diff --git a/src/pages/profile/ui/me-page/ProfileForm.tsx b/src/pages/profile/ui/me-page/profile-section/ProfileForm.tsx similarity index 95% rename from src/pages/profile/ui/me-page/ProfileForm.tsx rename to src/pages/profile/ui/me-page/profile-section/ProfileForm.tsx index c02b809..08c368e 100644 --- a/src/pages/profile/ui/me-page/ProfileForm.tsx +++ b/src/pages/profile/ui/me-page/profile-section/ProfileForm.tsx @@ -1,6 +1,6 @@ import { Controller, UseFormReturn } from 'react-hook-form'; import { Field, FieldError, FieldGroup, FieldLabel, Input, Textarea } from 'shared/ui'; -import type { ProfileFormValues } from '../../model/profile'; +import type { ProfileFormValues } from '../../../model/profile'; interface ProfileFormProps { form: UseFormReturn; @@ -18,6 +18,7 @@ function ProfileForm({ form, onSubmit }: ProfileFormProps) { Имя Фамилия + + + + + + + ); +} diff --git a/src/pages/profile/ui/me-page/profile-section/ProfileSectionFallback.tsx b/src/pages/profile/ui/me-page/profile-section/ProfileSectionFallback.tsx new file mode 100644 index 0000000..d08fd03 --- /dev/null +++ b/src/pages/profile/ui/me-page/profile-section/ProfileSectionFallback.tsx @@ -0,0 +1,36 @@ +import { CardSection, Skeleton } from 'shared/ui'; + +export function ProfileSectionFallback() { + return ( + <> + +
+
+ +
+ +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + ); +} diff --git a/src/pages/profile/ui/notifications-page/NotificationsPageFallback.tsx b/src/pages/profile/ui/notifications-page/NotificationsPageFallback.tsx index 339c750..0658884 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 ( @@ -11,6 +11,7 @@ export function NotificationsPageFallback() { ); } + function OptionGroupSkeleton({ items = 3 }: { items?: number }) { 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/page-wrapper/ui/PageWrapper.tsx b/src/widgets/page-wrapper/ui/PageWrapper.tsx index fcce9e6..649991b 100644 --- a/src/widgets/page-wrapper/ui/PageWrapper.tsx +++ b/src/widgets/page-wrapper/ui/PageWrapper.tsx @@ -1,14 +1,26 @@ +import { classNames } from 'shared/lib/utils'; import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from 'shared/ui'; interface PageWrapperProps extends React.ComponentProps { + wrap?: Omit, 'children'>; title?: string; description?: React.ReactNode; action?: React.ReactNode; } -export function PageWrapper({ title, description, action, children, ...props }: PageWrapperProps) { +export function PageWrapper({ + title, + description, + action, + children, + wrap: { className, ...wrapProps } = {}, + ...props +}: PageWrapperProps) { return ( - + {(title || description || action) && ( {title && {title}} 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 ( + + ); + })} +
+ ); +}