From 984de493ad572654d1729a78e0d827c3d889a7e2 Mon Sep 17 00:00:00 2001 From: Alexandr Nelyubov Date: Sun, 28 Jun 2026 20:34:04 +0300 Subject: [PATCH 01/11] refactor(profile): fix profile schemas, add avatar upload button, profile html layout restructuring --- src/entities/user/model/schemas.ts | 7 +-- src/features/upload-avatar/index.ts | 1 + .../upload-avatar/model/useAvatarFileInput.ts | 31 ++++++++++++ .../upload-avatar/ui/UploadAvatar.tsx | 28 +++-------- .../upload-avatar/ui/UploadAvatarButton.tsx | 49 +++++++++++++++++++ src/pages/profile/ui/me-page/IdentityItem.tsx | 42 ++++++---------- src/pages/profile/ui/me-page/MePage.tsx | 9 ++-- src/pages/profile/ui/me-page/ProfileForm.tsx | 2 + .../account-section/AccountsSection.tsx | 2 +- .../account-section/OAuthManageButton.tsx | 2 +- src/shared/ui/card-section/CardSection.tsx | 2 +- 11 files changed, 116 insertions(+), 59 deletions(-) create mode 100644 src/features/upload-avatar/model/useAvatarFileInput.ts create mode 100644 src/features/upload-avatar/ui/UploadAvatarButton.tsx 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/features/upload-avatar/index.ts b/src/features/upload-avatar/index.ts index ebb79b0..b5659d6 100644 --- a/src/features/upload-avatar/index.ts +++ b/src/features/upload-avatar/index.ts @@ -1 +1,2 @@ export { UploadAvatar } from './ui/UploadAvatar'; +export { UploadAvatarButton } from './ui/UploadAvatarButton'; diff --git a/src/features/upload-avatar/model/useAvatarFileInput.ts b/src/features/upload-avatar/model/useAvatarFileInput.ts new file mode 100644 index 0000000..6265b4e --- /dev/null +++ b/src/features/upload-avatar/model/useAvatarFileInput.ts @@ -0,0 +1,31 @@ +import { type ChangeEvent, useRef } from 'react'; +import { type TAsset } from 'entities/asset'; +import { useUploadAvatar, type UseUploadFileOptions } from './useUploadAvatar'; + +function useAvatarFileInput( + context: TAsset.UploadAssetData['context'], + mutationOptions?: UseUploadFileOptions +) { + const fileInputRef = useRef(null); + const uploadAvatarMutation = useUploadAvatar(mutationOptions); + + const handleAvatarPick = () => { + fileInputRef.current?.click(); + }; + + const handleAvatarChange = (event: ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + uploadAvatarMutation.mutate({ file, context }); + event.target.value = ''; + }; + + return { + fileInputRef, + handleAvatarPick, + handleAvatarChange, + isPending: uploadAvatarMutation.isPending, + }; +} + +export { useAvatarFileInput }; diff --git a/src/features/upload-avatar/ui/UploadAvatar.tsx b/src/features/upload-avatar/ui/UploadAvatar.tsx index e030787..88d4921 100644 --- a/src/features/upload-avatar/ui/UploadAvatar.tsx +++ b/src/features/upload-avatar/ui/UploadAvatar.tsx @@ -1,9 +1,10 @@ import { type TAsset } from 'entities/asset'; import { Pencil } from 'lucide-react'; -import { type ChangeEvent, type ComponentProps, type ReactElement, useRef } from 'react'; +import { type ComponentProps, type ReactElement } from 'react'; import { classNames } from 'shared/lib/utils'; import { type Avatar, Button } from 'shared/ui'; -import { useUploadAvatar, UseUploadFileOptions } from '../model/useUploadAvatar'; +import { type UseUploadFileOptions } from '../model/useUploadAvatar'; +import { useAvatarFileInput } from '../model/useAvatarFileInput'; interface UploadAvatarProps { className?: string; @@ -13,23 +14,10 @@ interface UploadAvatarProps { } function UploadAvatar({ className, avatar, context, mutationOptions }: UploadAvatarProps) { - const fileInputRef = useRef(null); - - const uploadAvatarMutation = useUploadAvatar(mutationOptions); - - const handleAvatarPick = () => { - fileInputRef.current?.click(); - }; - - const handleAvatarChange = (event: ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file) { - return; - } - - uploadAvatarMutation.mutate({ file, context }); - event.target.value = ''; - }; + const { handleAvatarChange, handleAvatarPick, isPending, fileInputRef } = useAvatarFileInput( + context, + mutationOptions + ); return (
@@ -40,7 +28,7 @@ function UploadAvatar({ className, avatar, context, mutationOptions }: UploadAva variant="outline" className="absolute right-0 bottom-0 rounded-full" onClick={handleAvatarPick} - disabled={uploadAvatarMutation.isPending} + disabled={isPending} aria-label="Загрузить новый аватар" > diff --git a/src/features/upload-avatar/ui/UploadAvatarButton.tsx b/src/features/upload-avatar/ui/UploadAvatarButton.tsx new file mode 100644 index 0000000..7189f2a --- /dev/null +++ b/src/features/upload-avatar/ui/UploadAvatarButton.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { type TAsset } from 'entities/asset'; +import { Button } from 'shared/ui'; +import { type UseUploadFileOptions } from '../model/useUploadAvatar'; +import { type ComponentProps } from 'react'; +import { useAvatarFileInput } from '../model/useAvatarFileInput'; + +interface UploadAvatarButtonProps extends ComponentProps { + className?: string; + context: TAsset.UploadAssetData['context']; + mutationOptions?: UseUploadFileOptions; +} +function UploadAvatarButton({ + context, + mutationOptions, + children, + asChild = false, + variant = 'default', + size = 'lg', +}: UploadAvatarButtonProps) { + const { handleAvatarChange, handleAvatarPick, isPending, fileInputRef } = useAvatarFileInput( + context, + mutationOptions + ); + return ( + <> + + + + ); +} + +export { UploadAvatarButton }; diff --git a/src/pages/profile/ui/me-page/IdentityItem.tsx b/src/pages/profile/ui/me-page/IdentityItem.tsx index e7475c0..2f66bb5 100644 --- a/src/pages/profile/ui/me-page/IdentityItem.tsx +++ b/src/pages/profile/ui/me-page/IdentityItem.tsx @@ -1,44 +1,34 @@ 'use client'; -import { Item, ItemActions, ItemContent, ItemMedia } from 'shared/ui'; -import { UploadAvatar } from 'features/upload-avatar'; +import { Item, ItemActions, ItemMedia } from 'shared/ui'; +import { UploadAvatarButton } 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..1968a68 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..ff3e4c2 100644 --- a/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx +++ b/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx @@ -56,7 +56,7 @@ export function OAuthManageButton({ provider, label, isLinked, ...props }: OAuth {label}
- {isLinked ? 'Отвязать' : 'Привязать'} {label} аккаунт + {isLinked ? 'Отвязать' : 'Привязать'} {label} ); 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} From 7de57981fd496cadc1c1742884c8f019dbc183ba Mon Sep 17 00:00:00 2001 From: Alexandr Nelyubov Date: Tue, 30 Jun 2026 15:21:41 +0300 Subject: [PATCH 02/11] refactor(profile): restructure profile layout, remove unused tabs, and enhance member card display --- app/(protected)/team/(team)/layout.tsx | 2 - app/(protected)/team/(team)/roles/page.tsx | 9 ++- app/(protected)/user/(user)/layout.tsx | 15 +++- src/pages/profile/config/tabs.ts | 7 -- src/pages/profile/index.ts | 1 - src/pages/profile/ui/me-page/MePage.tsx | 2 +- .../account-section/AccountsSection.tsx | 2 +- .../account-section/OAuthManageButton.tsx | 75 +++++++++++++++---- .../NotificationsPageFallback.tsx | 2 +- src/pages/team/index.ts | 1 - src/pages/team/ui/members/MemberCard.tsx | 75 +++++-------------- src/pages/team/ui/members/MembersPage.tsx | 12 +-- src/pages/team/ui/settings/SettingsPage.tsx | 6 -- src/widgets/app-sidebar/config/sidebar.ts | 4 +- .../ui/projects/ProjectsContent.tsx | 9 ++- .../app-sidebar/ui/teams/TeamContent.tsx | 2 +- src/widgets/tabs-nav/index.ts | 1 + src/widgets/tabs-nav/model/types.ts | 1 + src/widgets/tabs-nav/ui/VerticalTabsNav.tsx | 54 +++++++++++++ 19 files changed, 167 insertions(+), 113 deletions(-) delete mode 100644 src/pages/profile/config/tabs.ts create mode 100644 src/widgets/tabs-nav/ui/VerticalTabsNav.tsx 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/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/MePage.tsx b/src/pages/profile/ui/me-page/MePage.tsx index 1968a68..24db62e 100644 --- a/src/pages/profile/ui/me-page/MePage.tsx +++ b/src/pages/profile/ui/me-page/MePage.tsx @@ -37,7 +37,7 @@ function MePage() {
diff --git a/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx b/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx index 2c3b8b3..debf171 100644 --- a/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx +++ b/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx @@ -7,7 +7,7 @@ export function AccountSection() { return ( 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 ff3e4c2..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/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 ( + + ); + })} +
+ ); +} From f0969402ad9c1b01f6298499b2bbc0d061a225ad Mon Sep 17 00:00:00 2001 From: Alexandr Nelyubov Date: Fri, 3 Jul 2026 13:42:41 +0300 Subject: [PATCH 03/11] refactor(profile): update profile layout, replace PageLayout with PageWrapper, and enhance loading states with suspense components --- app/(protected)/user/(user)/layout.tsx | 6 +- .../user/(user)/notifications/error.tsx | 20 +++++ app/(protected)/user/(user)/profile/error.tsx | 20 +++++ src/pages/profile/api/useConnectedAccounts.ts | 22 ----- src/pages/profile/config/profile.ts | 7 ++ src/pages/profile/model/profile.ts | 2 + src/pages/profile/model/useMePage.ts | 4 +- src/pages/profile/model/useOAuthManage.ts | 37 ++++++++ src/pages/profile/ui/me-page/MePage.tsx | 60 ++----------- .../profile/ui/me-page/MePageContent.tsx | 29 ++++++ .../profile/ui/me-page/MePageFallback.tsx | 12 +++ .../account-section/AccountSection.tsx | 41 +++++++++ .../AccountSectionErrorFallback.tsx | 14 +++ .../AccountSectionFallback.tsx | 30 +++++++ .../account-section/AccountsSection.tsx | 26 ------ .../ui/me-page/account-section/OAuthBadge.tsx | 28 ++++++ .../account-section/OAuthManageButton.tsx | 90 ++++++------------- .../{ => profile-section}/IdentityItem.tsx | 0 .../{ => profile-section}/ProfileForm.tsx | 2 +- .../profile-section/ProfileSection.tsx | 29 ++++++ .../ProfileSectionFallback.tsx | 36 ++++++++ .../NotificationsPageFallback.tsx | 1 + 22 files changed, 346 insertions(+), 170 deletions(-) create mode 100644 app/(protected)/user/(user)/notifications/error.tsx create mode 100644 app/(protected)/user/(user)/profile/error.tsx delete mode 100644 src/pages/profile/api/useConnectedAccounts.ts create mode 100644 src/pages/profile/config/profile.ts create mode 100644 src/pages/profile/model/useOAuthManage.ts create mode 100644 src/pages/profile/ui/me-page/MePageContent.tsx create mode 100644 src/pages/profile/ui/me-page/MePageFallback.tsx create mode 100644 src/pages/profile/ui/me-page/account-section/AccountSection.tsx create mode 100644 src/pages/profile/ui/me-page/account-section/AccountSectionErrorFallback.tsx create mode 100644 src/pages/profile/ui/me-page/account-section/AccountSectionFallback.tsx delete mode 100644 src/pages/profile/ui/me-page/account-section/AccountsSection.tsx create mode 100644 src/pages/profile/ui/me-page/account-section/OAuthBadge.tsx rename src/pages/profile/ui/me-page/{ => profile-section}/IdentityItem.tsx (100%) rename src/pages/profile/ui/me-page/{ => profile-section}/ProfileForm.tsx (97%) create mode 100644 src/pages/profile/ui/me-page/profile-section/ProfileSection.tsx create mode 100644 src/pages/profile/ui/me-page/profile-section/ProfileSectionFallback.tsx diff --git a/app/(protected)/user/(user)/layout.tsx b/app/(protected)/user/(user)/layout.tsx index 7b4319a..f8c299f 100644 --- a/app/(protected)/user/(user)/layout.tsx +++ b/app/(protected)/user/(user)/layout.tsx @@ -1,6 +1,6 @@ -import { PageLayout } from 'app/layouts/PageLayout'; 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[] = [ @@ -10,7 +10,7 @@ export const tabs: TabNavItem[] = [ export default function ProfileLayout({ children }: { children: React.ReactNode }) { return ( - @@ -18,6 +18,6 @@ export default function ProfileLayout({ children }: { children: React.ReactNode {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/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/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/MePage.tsx b/src/pages/profile/ui/me-page/MePage.tsx index 24db62e..9ff1ab3 100644 --- a/src/pages/profile/ui/me-page/MePage.tsx +++ b/src/pages/profile/ui/me-page/MePage.tsx @@ -1,60 +1,14 @@ 'use client'; +import dynamic from 'next/dynamic'; +import { MePageFallback } from './MePageFallback'; -import { - Card, - CardDescription, - CardHeader, - CardSection, - CardTitle, - FloatingSaveBar, -} 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..63ed338 --- /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 debf171..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 3f2a811..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,65 +1,44 @@ 'use client'; -import { authFabricKeys, OAUTH_PROVIDERS, type TAuth } from 'entities/auth'; -import { type ComponentProps, useCallback } from 'react'; -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 { 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<'div'> & { +type OAuthManageButtonProps = ComponentProps & { + wrap?: Omit, 'children'>; provider: TAuth.OAuthProvider; label: string; - isLinked: boolean; + isLoading?: boolean; + status: OAuthConnectionStatus; }; export function OAuthManageButton({ provider, - className = '', label, - isLinked, + disabled, + status, + isLoading = false, + wrap = {}, ...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) => { - 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(); - window.location.href = url; - }, - }); - } - }, [connect, disconnect, isLinked, provider]); - + const { isPending, handleToggleConnect } = useOAuthManage(provider, status); + const isConnected = status === 'connected'; + const isDisabled = disabled || isPending || isLoading || status === 'unknown'; const meta = OAUTH_PROVIDERS[provider]; return (
{label} - +
-
- +
+
); } - -function OAuthBadge({ isLinked, className = '' }: { isLinked: boolean; className?: string }) { - return ( - - {isLinked ? 'Подключен' : 'Не подключен'} - - ); -} diff --git a/src/pages/profile/ui/me-page/IdentityItem.tsx b/src/pages/profile/ui/me-page/profile-section/IdentityItem.tsx similarity index 100% rename from src/pages/profile/ui/me-page/IdentityItem.tsx rename to src/pages/profile/ui/me-page/profile-section/IdentityItem.tsx diff --git a/src/pages/profile/ui/me-page/ProfileForm.tsx b/src/pages/profile/ui/me-page/profile-section/ProfileForm.tsx similarity index 97% rename from src/pages/profile/ui/me-page/ProfileForm.tsx rename to src/pages/profile/ui/me-page/profile-section/ProfileForm.tsx index 75b481b..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; diff --git a/src/pages/profile/ui/me-page/profile-section/ProfileSection.tsx b/src/pages/profile/ui/me-page/profile-section/ProfileSection.tsx new file mode 100644 index 0000000..d686556 --- /dev/null +++ b/src/pages/profile/ui/me-page/profile-section/ProfileSection.tsx @@ -0,0 +1,29 @@ +import { CardSection, FloatingSaveBar } from 'shared/ui'; +import { IdentityItem } from './IdentityItem'; +import { ProfileForm } from './ProfileForm'; +import { useMePage } from '../../../model/useMePage'; +import { ProfileSectionFallback } from './ProfileSectionFallback'; + +export function ProfileSection() { + const { form, profile, isDirty, isPending, onSubmit, onDiscard } = useMePage(); + + return ( + <> + + + + + + + ); +} 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..a8988f5 --- /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 e45f3c7..0658884 100644 --- a/src/pages/profile/ui/notifications-page/NotificationsPageFallback.tsx +++ b/src/pages/profile/ui/notifications-page/NotificationsPageFallback.tsx @@ -11,6 +11,7 @@ export function NotificationsPageFallback() { ); } + function OptionGroupSkeleton({ items = 3 }: { items?: number }) { return (
From ba46e715bafd7da7c87798623f7db69e7e3ea975 Mon Sep 17 00:00:00 2001 From: Alexandr Nelyubov Date: Fri, 3 Jul 2026 18:08:48 +0300 Subject: [PATCH 04/11] refactor(profile): imorove profile layout with styling and structure, add scroll on page content --- app/(protected)/user/(user)/layout.tsx | 4 +++- src/app/layouts/SidebarLayout.tsx | 2 +- src/pages/profile/ui/me-page/MePageContent.tsx | 2 +- .../me-page/profile-section/ProfileSection.tsx | 1 - .../profile-section/ProfileSectionFallback.tsx | 10 +++++----- src/widgets/page-wrapper/ui/PageWrapper.tsx | 16 ++++++++++++++-- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/(protected)/user/(user)/layout.tsx b/app/(protected)/user/(user)/layout.tsx index f8c299f..f0a2a26 100644 --- a/app/(protected)/user/(user)/layout.tsx +++ b/app/(protected)/user/(user)/layout.tsx @@ -13,8 +13,10 @@ export default function ProfileLayout({ children }: { children: React.ReactNode -
+
{children}
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/pages/profile/ui/me-page/MePageContent.tsx b/src/pages/profile/ui/me-page/MePageContent.tsx index 63ed338..d552be5 100644 --- a/src/pages/profile/ui/me-page/MePageContent.tsx +++ b/src/pages/profile/ui/me-page/MePageContent.tsx @@ -23,7 +23,7 @@ function MePage() { } function MePageLayout({ children }: PropsWithChildren) { - return
{children}
; + return
{children}
; } export { MePage, MePageLayout }; diff --git a/src/pages/profile/ui/me-page/profile-section/ProfileSection.tsx b/src/pages/profile/ui/me-page/profile-section/ProfileSection.tsx index d686556..5fdd11f 100644 --- a/src/pages/profile/ui/me-page/profile-section/ProfileSection.tsx +++ b/src/pages/profile/ui/me-page/profile-section/ProfileSection.tsx @@ -2,7 +2,6 @@ import { CardSection, FloatingSaveBar } from 'shared/ui'; import { IdentityItem } from './IdentityItem'; import { ProfileForm } from './ProfileForm'; import { useMePage } from '../../../model/useMePage'; -import { ProfileSectionFallback } from './ProfileSectionFallback'; export function ProfileSection() { const { form, profile, isDirty, isPending, onSubmit, onDiscard } = useMePage(); diff --git a/src/pages/profile/ui/me-page/profile-section/ProfileSectionFallback.tsx b/src/pages/profile/ui/me-page/profile-section/ProfileSectionFallback.tsx index a8988f5..d08fd03 100644 --- a/src/pages/profile/ui/me-page/profile-section/ProfileSectionFallback.tsx +++ b/src/pages/profile/ui/me-page/profile-section/ProfileSectionFallback.tsx @@ -8,7 +8,7 @@ export function ProfileSectionFallback() { title="Идентификация профиля" description="Публичная информация о вас." > -
+
@@ -16,18 +16,18 @@ export function ProfileSectionFallback() {
-
+
-
+
-
+
- +
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}} From 349f53352983bf4cccb14fcf01de0ef089d314da Mon Sep 17 00:00:00 2001 From: Alexandr Nelyubov Date: Mon, 6 Jul 2026 17:26:48 +0300 Subject: [PATCH 05/11] refactor: domain-based folder structure replacing pages/ui/ nesting --- app/(protected)/user/(user)/notifications/page.tsx | 2 +- app/(protected)/user/(user)/profile/page.tsx | 2 +- app/(protected)/user/(user)/security/page.tsx | 2 +- src/pages/profile/index.ts | 3 --- src/pages/profile/{ => me}/api/useConnectOauthProvider.ts | 0 .../profile/{ => me}/api/useDisconnectOauthProvider.ts | 0 src/pages/profile/{ => me}/api/useUpdateProfile.ts | 0 .../{config/profile.ts => me/config/oauth-status.ts} | 2 +- src/pages/profile/me/index.ts | 1 + src/pages/profile/{ => me}/model/profile.ts | 0 src/pages/profile/{ => me}/model/useMePage.ts | 0 src/pages/profile/{ => me}/model/useOAuthManage.ts | 0 src/pages/profile/{ui/me-page => me/ui}/MePage.tsx | 0 src/pages/profile/{ui/me-page => me/ui}/MePageContent.tsx | 0 src/pages/profile/{ui/me-page => me/ui}/MePageFallback.tsx | 0 .../me-page => me/ui}/account-section/AccountSection.tsx | 0 .../ui}/account-section/AccountSectionErrorFallback.tsx | 0 .../ui}/account-section/AccountSectionFallback.tsx | 0 .../{ui/me-page => me/ui}/account-section/OAuthBadge.tsx | 6 +++--- .../me-page => me/ui}/account-section/OAuthManageButton.tsx | 4 ++-- .../{ui/me-page => me/ui}/profile-section/IdentityItem.tsx | 0 .../{ui/me-page => me/ui}/profile-section/ProfileForm.tsx | 2 +- .../me-page => me/ui}/profile-section/ProfileSection.tsx | 2 +- .../ui}/profile-section/ProfileSectionFallback.tsx | 0 .../{ => notifications}/api/useUpdateNotifications.ts | 0 .../profile/{ => notifications}/config/notifications.ts | 0 src/pages/profile/notifications/index.ts | 1 + .../profile/{ => notifications}/model/notifications.ts | 0 .../ui}/NotificationsPage.tsx | 0 .../ui}/NotificationsPageContent.tsx | 6 +++--- .../ui}/NotificationsPageFallback.tsx | 0 src/pages/profile/security/index.ts | 1 + .../{ui/security-page => security/ui}/SecurityPage.tsx | 0 src/pages/profile/teams/index.ts | 0 src/pages/profile/{ui/teams-page => teams/ui}/TeamList.tsx | 0 35 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 src/pages/profile/index.ts rename src/pages/profile/{ => me}/api/useConnectOauthProvider.ts (100%) rename src/pages/profile/{ => me}/api/useDisconnectOauthProvider.ts (100%) rename src/pages/profile/{ => me}/api/useUpdateProfile.ts (100%) rename src/pages/profile/{config/profile.ts => me/config/oauth-status.ts} (87%) create mode 100644 src/pages/profile/me/index.ts rename src/pages/profile/{ => me}/model/profile.ts (100%) rename src/pages/profile/{ => me}/model/useMePage.ts (100%) rename src/pages/profile/{ => me}/model/useOAuthManage.ts (100%) rename src/pages/profile/{ui/me-page => me/ui}/MePage.tsx (100%) rename src/pages/profile/{ui/me-page => me/ui}/MePageContent.tsx (100%) rename src/pages/profile/{ui/me-page => me/ui}/MePageFallback.tsx (100%) rename src/pages/profile/{ui/me-page => me/ui}/account-section/AccountSection.tsx (100%) rename src/pages/profile/{ui/me-page => me/ui}/account-section/AccountSectionErrorFallback.tsx (100%) rename src/pages/profile/{ui/me-page => me/ui}/account-section/AccountSectionFallback.tsx (100%) rename src/pages/profile/{ui/me-page => me/ui}/account-section/OAuthBadge.tsx (78%) rename src/pages/profile/{ui/me-page => me/ui}/account-section/OAuthManageButton.tsx (94%) rename src/pages/profile/{ui/me-page => me/ui}/profile-section/IdentityItem.tsx (100%) rename src/pages/profile/{ui/me-page => me/ui}/profile-section/ProfileForm.tsx (97%) rename src/pages/profile/{ui/me-page => me/ui}/profile-section/ProfileSection.tsx (94%) rename src/pages/profile/{ui/me-page => me/ui}/profile-section/ProfileSectionFallback.tsx (100%) rename src/pages/profile/{ => notifications}/api/useUpdateNotifications.ts (100%) rename src/pages/profile/{ => notifications}/config/notifications.ts (100%) create mode 100644 src/pages/profile/notifications/index.ts rename src/pages/profile/{ => notifications}/model/notifications.ts (100%) rename src/pages/profile/{ui/notifications-page => notifications/ui}/NotificationsPage.tsx (100%) rename src/pages/profile/{ui/notifications-page => notifications/ui}/NotificationsPageContent.tsx (93%) rename src/pages/profile/{ui/notifications-page => notifications/ui}/NotificationsPageFallback.tsx (100%) create mode 100644 src/pages/profile/security/index.ts rename src/pages/profile/{ui/security-page => security/ui}/SecurityPage.tsx (100%) create mode 100644 src/pages/profile/teams/index.ts rename src/pages/profile/{ui/teams-page => teams/ui}/TeamList.tsx (100%) diff --git a/app/(protected)/user/(user)/notifications/page.tsx b/app/(protected)/user/(user)/notifications/page.tsx index 4344000..c606279 100644 --- a/app/(protected)/user/(user)/notifications/page.tsx +++ b/app/(protected)/user/(user)/notifications/page.tsx @@ -1 +1 @@ -export { NotificationsPage as default } from 'pages/profile'; +export { NotificationsPage as default } from 'pages/profile/notifications'; diff --git a/app/(protected)/user/(user)/profile/page.tsx b/app/(protected)/user/(user)/profile/page.tsx index 284e44f..1855560 100644 --- a/app/(protected)/user/(user)/profile/page.tsx +++ b/app/(protected)/user/(user)/profile/page.tsx @@ -1 +1 @@ -export { MePage as default } from 'pages/profile'; +export { MePage as default } from 'pages/profile/me'; diff --git a/app/(protected)/user/(user)/security/page.tsx b/app/(protected)/user/(user)/security/page.tsx index dc4ccd9..bb3be46 100644 --- a/app/(protected)/user/(user)/security/page.tsx +++ b/app/(protected)/user/(user)/security/page.tsx @@ -1 +1 @@ -export { SecurityPage as default } from 'pages/profile'; +export { SecurityPage as default } from 'pages/profile/security'; diff --git a/src/pages/profile/index.ts b/src/pages/profile/index.ts deleted file mode 100644 index 0f32dee..0000000 --- a/src/pages/profile/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -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/api/useConnectOauthProvider.ts b/src/pages/profile/me/api/useConnectOauthProvider.ts similarity index 100% rename from src/pages/profile/api/useConnectOauthProvider.ts rename to src/pages/profile/me/api/useConnectOauthProvider.ts diff --git a/src/pages/profile/api/useDisconnectOauthProvider.ts b/src/pages/profile/me/api/useDisconnectOauthProvider.ts similarity index 100% rename from src/pages/profile/api/useDisconnectOauthProvider.ts rename to src/pages/profile/me/api/useDisconnectOauthProvider.ts diff --git a/src/pages/profile/api/useUpdateProfile.ts b/src/pages/profile/me/api/useUpdateProfile.ts similarity index 100% rename from src/pages/profile/api/useUpdateProfile.ts rename to src/pages/profile/me/api/useUpdateProfile.ts diff --git a/src/pages/profile/config/profile.ts b/src/pages/profile/me/config/oauth-status.ts similarity index 87% rename from src/pages/profile/config/profile.ts rename to src/pages/profile/me/config/oauth-status.ts index 2e9755b..c1c80b2 100644 --- a/src/pages/profile/config/profile.ts +++ b/src/pages/profile/me/config/oauth-status.ts @@ -1,6 +1,6 @@ import { OAuthConnectionStatus } from '../model/profile'; -export const OAUTH_BADGE_LABELS = { +export const OAUTH_STATUS_LABELS = { connected: 'Подключен', disconnected: 'Не подключен', unknown: 'Не удалось проверить', diff --git a/src/pages/profile/me/index.ts b/src/pages/profile/me/index.ts new file mode 100644 index 0000000..16bf398 --- /dev/null +++ b/src/pages/profile/me/index.ts @@ -0,0 +1 @@ +export { MePage } from './ui/MePage'; diff --git a/src/pages/profile/model/profile.ts b/src/pages/profile/me/model/profile.ts similarity index 100% rename from src/pages/profile/model/profile.ts rename to src/pages/profile/me/model/profile.ts diff --git a/src/pages/profile/model/useMePage.ts b/src/pages/profile/me/model/useMePage.ts similarity index 100% rename from src/pages/profile/model/useMePage.ts rename to src/pages/profile/me/model/useMePage.ts diff --git a/src/pages/profile/model/useOAuthManage.ts b/src/pages/profile/me/model/useOAuthManage.ts similarity index 100% rename from src/pages/profile/model/useOAuthManage.ts rename to src/pages/profile/me/model/useOAuthManage.ts diff --git a/src/pages/profile/ui/me-page/MePage.tsx b/src/pages/profile/me/ui/MePage.tsx similarity index 100% rename from src/pages/profile/ui/me-page/MePage.tsx rename to src/pages/profile/me/ui/MePage.tsx diff --git a/src/pages/profile/ui/me-page/MePageContent.tsx b/src/pages/profile/me/ui/MePageContent.tsx similarity index 100% rename from src/pages/profile/ui/me-page/MePageContent.tsx rename to src/pages/profile/me/ui/MePageContent.tsx diff --git a/src/pages/profile/ui/me-page/MePageFallback.tsx b/src/pages/profile/me/ui/MePageFallback.tsx similarity index 100% rename from src/pages/profile/ui/me-page/MePageFallback.tsx rename to src/pages/profile/me/ui/MePageFallback.tsx diff --git a/src/pages/profile/ui/me-page/account-section/AccountSection.tsx b/src/pages/profile/me/ui/account-section/AccountSection.tsx similarity index 100% rename from src/pages/profile/ui/me-page/account-section/AccountSection.tsx rename to src/pages/profile/me/ui/account-section/AccountSection.tsx diff --git a/src/pages/profile/ui/me-page/account-section/AccountSectionErrorFallback.tsx b/src/pages/profile/me/ui/account-section/AccountSectionErrorFallback.tsx similarity index 100% rename from src/pages/profile/ui/me-page/account-section/AccountSectionErrorFallback.tsx rename to src/pages/profile/me/ui/account-section/AccountSectionErrorFallback.tsx diff --git a/src/pages/profile/ui/me-page/account-section/AccountSectionFallback.tsx b/src/pages/profile/me/ui/account-section/AccountSectionFallback.tsx similarity index 100% rename from src/pages/profile/ui/me-page/account-section/AccountSectionFallback.tsx rename to src/pages/profile/me/ui/account-section/AccountSectionFallback.tsx diff --git a/src/pages/profile/ui/me-page/account-section/OAuthBadge.tsx b/src/pages/profile/me/ui/account-section/OAuthBadge.tsx similarity index 78% rename from src/pages/profile/ui/me-page/account-section/OAuthBadge.tsx rename to src/pages/profile/me/ui/account-section/OAuthBadge.tsx index f5311e1..aae302c 100644 --- a/src/pages/profile/ui/me-page/account-section/OAuthBadge.tsx +++ b/src/pages/profile/me/ui/account-section/OAuthBadge.tsx @@ -1,7 +1,7 @@ import { Badge } from 'shared/ui'; -import { type OAuthConnectionStatus } from '../../../model/profile'; +import { type OAuthConnectionStatus } from '../../model/profile'; import { classNames } from 'shared/lib/utils'; -import { OAUTH_BADGE_LABELS } from 'pages/profile/config/profile'; +import { OAUTH_STATUS_LABELS } from '../../config/oauth-status'; export function OAuthBadge({ status, @@ -22,7 +22,7 @@ export function OAuthBadge({ [className] )} > - {OAUTH_BADGE_LABELS[status]} + {OAUTH_STATUS_LABELS[status]} ); } diff --git a/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx b/src/pages/profile/me/ui/account-section/OAuthManageButton.tsx similarity index 94% rename from src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx rename to src/pages/profile/me/ui/account-section/OAuthManageButton.tsx index c4944d9..ffe5e73 100644 --- a/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx +++ b/src/pages/profile/me/ui/account-section/OAuthManageButton.tsx @@ -5,8 +5,8 @@ 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 { type OAuthConnectionStatus } from '../../model/profile'; +import { useOAuthManage } from 'pages/profile/me/model/useOAuthManage'; import { OAuthBadge } from './OAuthBadge'; type OAuthManageButtonProps = ComponentProps & { diff --git a/src/pages/profile/ui/me-page/profile-section/IdentityItem.tsx b/src/pages/profile/me/ui/profile-section/IdentityItem.tsx similarity index 100% rename from src/pages/profile/ui/me-page/profile-section/IdentityItem.tsx rename to src/pages/profile/me/ui/profile-section/IdentityItem.tsx diff --git a/src/pages/profile/ui/me-page/profile-section/ProfileForm.tsx b/src/pages/profile/me/ui/profile-section/ProfileForm.tsx similarity index 97% rename from src/pages/profile/ui/me-page/profile-section/ProfileForm.tsx rename to src/pages/profile/me/ui/profile-section/ProfileForm.tsx index 08c368e..75b481b 100644 --- a/src/pages/profile/ui/me-page/profile-section/ProfileForm.tsx +++ b/src/pages/profile/me/ui/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; diff --git a/src/pages/profile/ui/me-page/profile-section/ProfileSection.tsx b/src/pages/profile/me/ui/profile-section/ProfileSection.tsx similarity index 94% rename from src/pages/profile/ui/me-page/profile-section/ProfileSection.tsx rename to src/pages/profile/me/ui/profile-section/ProfileSection.tsx index 5fdd11f..00ec08e 100644 --- a/src/pages/profile/ui/me-page/profile-section/ProfileSection.tsx +++ b/src/pages/profile/me/ui/profile-section/ProfileSection.tsx @@ -1,7 +1,7 @@ import { CardSection, FloatingSaveBar } from 'shared/ui'; import { IdentityItem } from './IdentityItem'; import { ProfileForm } from './ProfileForm'; -import { useMePage } from '../../../model/useMePage'; +import { useMePage } from '../../model/useMePage'; export function ProfileSection() { const { form, profile, isDirty, isPending, onSubmit, onDiscard } = useMePage(); diff --git a/src/pages/profile/ui/me-page/profile-section/ProfileSectionFallback.tsx b/src/pages/profile/me/ui/profile-section/ProfileSectionFallback.tsx similarity index 100% rename from src/pages/profile/ui/me-page/profile-section/ProfileSectionFallback.tsx rename to src/pages/profile/me/ui/profile-section/ProfileSectionFallback.tsx diff --git a/src/pages/profile/api/useUpdateNotifications.ts b/src/pages/profile/notifications/api/useUpdateNotifications.ts similarity index 100% rename from src/pages/profile/api/useUpdateNotifications.ts rename to src/pages/profile/notifications/api/useUpdateNotifications.ts diff --git a/src/pages/profile/config/notifications.ts b/src/pages/profile/notifications/config/notifications.ts similarity index 100% rename from src/pages/profile/config/notifications.ts rename to src/pages/profile/notifications/config/notifications.ts diff --git a/src/pages/profile/notifications/index.ts b/src/pages/profile/notifications/index.ts new file mode 100644 index 0000000..cf6f382 --- /dev/null +++ b/src/pages/profile/notifications/index.ts @@ -0,0 +1 @@ +export { NotificationsPage } from './ui/NotificationsPage'; diff --git a/src/pages/profile/model/notifications.ts b/src/pages/profile/notifications/model/notifications.ts similarity index 100% rename from src/pages/profile/model/notifications.ts rename to src/pages/profile/notifications/model/notifications.ts diff --git a/src/pages/profile/ui/notifications-page/NotificationsPage.tsx b/src/pages/profile/notifications/ui/NotificationsPage.tsx similarity index 100% rename from src/pages/profile/ui/notifications-page/NotificationsPage.tsx rename to src/pages/profile/notifications/ui/NotificationsPage.tsx diff --git a/src/pages/profile/ui/notifications-page/NotificationsPageContent.tsx b/src/pages/profile/notifications/ui/NotificationsPageContent.tsx similarity index 93% rename from src/pages/profile/ui/notifications-page/NotificationsPageContent.tsx rename to src/pages/profile/notifications/ui/NotificationsPageContent.tsx index 05f18da..0690019 100644 --- a/src/pages/profile/ui/notifications-page/NotificationsPageContent.tsx +++ b/src/pages/profile/notifications/ui/NotificationsPageContent.tsx @@ -3,10 +3,10 @@ import { useReducer } from 'react'; import { CardSection, FloatingSaveBar, OptionGroup, Switch } from 'shared/ui'; import { UserQueries } from 'entities/user'; -import { useUpdateNotifications } from '../../api/useUpdateNotifications'; +import { useUpdateNotifications } from '../api/useUpdateNotifications'; import { useSuspenseQuery } from '@tanstack/react-query'; -import { notificationItems } from '../../config/notifications'; -import { NotificationChannel, type Notifications } from '../../model/notifications'; +import { notificationItems } from '../config/notifications'; +import { NotificationChannel, type Notifications } from '../model/notifications'; type NotificationsState = Notifications | null; diff --git a/src/pages/profile/ui/notifications-page/NotificationsPageFallback.tsx b/src/pages/profile/notifications/ui/NotificationsPageFallback.tsx similarity index 100% rename from src/pages/profile/ui/notifications-page/NotificationsPageFallback.tsx rename to src/pages/profile/notifications/ui/NotificationsPageFallback.tsx diff --git a/src/pages/profile/security/index.ts b/src/pages/profile/security/index.ts new file mode 100644 index 0000000..ed2c465 --- /dev/null +++ b/src/pages/profile/security/index.ts @@ -0,0 +1 @@ +export { SecurityPage } from './ui/SecurityPage'; diff --git a/src/pages/profile/ui/security-page/SecurityPage.tsx b/src/pages/profile/security/ui/SecurityPage.tsx similarity index 100% rename from src/pages/profile/ui/security-page/SecurityPage.tsx rename to src/pages/profile/security/ui/SecurityPage.tsx diff --git a/src/pages/profile/teams/index.ts b/src/pages/profile/teams/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/profile/ui/teams-page/TeamList.tsx b/src/pages/profile/teams/ui/TeamList.tsx similarity index 100% rename from src/pages/profile/ui/teams-page/TeamList.tsx rename to src/pages/profile/teams/ui/TeamList.tsx From d8809c4669f91ba074acb2cbd916633624220f78 Mon Sep 17 00:00:00 2001 From: kapitulin24 Date: Mon, 6 Jul 2026 19:59:46 +0300 Subject: [PATCH 06/11] feat(team): add new team pages and remove deprecated tabs configuration - Introduced TeamPage with a redirect to team members. - Created separate pages for team invitations, members, and settings. - Removed the obsolete tabs configuration file to streamline the team structure. --- app/(protected)/team/(team)/layout.tsx | 14 ----- app/(protected)/team/(team)/roles/page.tsx | 8 --- .../team/{(team) => }/invitations/page.tsx | 0 .../team/{(team) => }/members/page.tsx | 0 app/(protected)/team/{(team) => }/page.tsx | 0 .../team/{(team) => }/settings/page.tsx | 0 next-env.d.ts | 2 +- src/app/layouts/PageLayout.tsx | 15 ----- src/pages/team/config/tabs.ts | 18 ------ src/pages/team/index.ts | 1 - src/pages/team/ui/roles/Permissions.tsx | 56 ------------------ src/pages/team/ui/roles/RolesList.tsx | 52 ----------------- src/pages/team/ui/roles/RolesPage.tsx | 30 ---------- src/shared/config/routes.ts | 1 - src/widgets/page-layout/index.ts | 1 - src/widgets/page-layout/ui/PageLayout.tsx | 39 ------------- .../config/route-definitions.ts | 1 - src/widgets/tabs-nav/index.ts | 1 - src/widgets/tabs-nav/ui/TabsNav.tsx | 57 ------------------- steiger.config.ts | 1 + 20 files changed, 2 insertions(+), 295 deletions(-) delete mode 100644 app/(protected)/team/(team)/layout.tsx delete mode 100644 app/(protected)/team/(team)/roles/page.tsx rename app/(protected)/team/{(team) => }/invitations/page.tsx (100%) rename app/(protected)/team/{(team) => }/members/page.tsx (100%) rename app/(protected)/team/{(team) => }/page.tsx (100%) rename app/(protected)/team/{(team) => }/settings/page.tsx (100%) delete mode 100644 src/app/layouts/PageLayout.tsx delete mode 100644 src/pages/team/config/tabs.ts delete mode 100644 src/pages/team/ui/roles/Permissions.tsx delete mode 100644 src/pages/team/ui/roles/RolesList.tsx delete mode 100644 src/pages/team/ui/roles/RolesPage.tsx delete mode 100644 src/widgets/page-layout/index.ts delete mode 100644 src/widgets/page-layout/ui/PageLayout.tsx delete mode 100644 src/widgets/tabs-nav/ui/TabsNav.tsx diff --git a/app/(protected)/team/(team)/layout.tsx b/app/(protected)/team/(team)/layout.tsx deleted file mode 100644 index b1514db..0000000 --- a/app/(protected)/team/(team)/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { PageLayout } from 'app/layouts/PageLayout'; -import { Badge } from 'shared/ui'; - -export default function TeamLayout({ children }: { children: React.ReactNode }) { - return ( - 8 участников} - > - {children} - - ); -} diff --git a/app/(protected)/team/(team)/roles/page.tsx b/app/(protected)/team/(team)/roles/page.tsx deleted file mode 100644 index 3be4e15..0000000 --- a/app/(protected)/team/(team)/roles/page.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { notFound } from 'next/navigation'; - -// export { RolesPage as default } from 'pages/team'; -export default function Page() { - notFound(); -} - -// TODO: временно убрал страницу. Вернуть, когда появится функциональность прав и ролей diff --git a/app/(protected)/team/(team)/invitations/page.tsx b/app/(protected)/team/invitations/page.tsx similarity index 100% rename from app/(protected)/team/(team)/invitations/page.tsx rename to app/(protected)/team/invitations/page.tsx diff --git a/app/(protected)/team/(team)/members/page.tsx b/app/(protected)/team/members/page.tsx similarity index 100% rename from app/(protected)/team/(team)/members/page.tsx rename to app/(protected)/team/members/page.tsx diff --git a/app/(protected)/team/(team)/page.tsx b/app/(protected)/team/page.tsx similarity index 100% rename from app/(protected)/team/(team)/page.tsx rename to app/(protected)/team/page.tsx diff --git a/app/(protected)/team/(team)/settings/page.tsx b/app/(protected)/team/settings/page.tsx similarity index 100% rename from app/(protected)/team/(team)/settings/page.tsx rename to app/(protected)/team/settings/page.tsx diff --git a/next-env.d.ts b/next-env.d.ts index c05d9f7..cdb6b7b 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,7 +1,7 @@ /// /// /// -import './.next/types/routes.d.ts'; +import './.next/dev/types/routes.d.ts'; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/layouts/PageLayout.tsx b/src/app/layouts/PageLayout.tsx deleted file mode 100644 index cf44ab8..0000000 --- a/src/app/layouts/PageLayout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ComponentProps } from 'react'; -import { PageLayout as PageLayoutParent } from 'widgets/page-layout'; -import { TabsNav } from 'widgets/tabs-nav'; - -interface PageLayoutProps extends Omit, 'nav'> { - tabs?: ComponentProps['tabs']; -} - -export function PageLayout({ children, tabs, ...props }: PageLayoutProps) { - return ( - : undefined} {...props}> - {children} - - ); -} diff --git a/src/pages/team/config/tabs.ts b/src/pages/team/config/tabs.ts deleted file mode 100644 index 192faf3..0000000 --- a/src/pages/team/config/tabs.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { routes } from 'shared/config'; -import { TabNavItem } from 'widgets/tabs-nav'; - -export const teamTabs: TabNavItem[] = [ - { key: routes.team.members(), label: 'Участники', badge: { value: '8', variant: 'default' } }, - { key: routes.team.projects.all(), label: 'Проекты' }, - { - key: routes.team.invitations(), - label: 'Приглашения', - badge: { value: '3', variant: 'default' }, - }, - { - key: routes.team.roles(), - label: 'Роли и права', - badge: { value: 'Не реализовано', variant: 'destructive' }, - }, - { key: routes.team.settings(), label: 'Настройки' }, -]; diff --git a/src/pages/team/index.ts b/src/pages/team/index.ts index f579de1..06bb280 100644 --- a/src/pages/team/index.ts +++ b/src/pages/team/index.ts @@ -1,4 +1,3 @@ export { InvitationsPage } from './ui/invitations/InvitationsPage'; export { MembersPage } from './ui/members/MembersPage'; -export { RolesPage } from './ui/roles/RolesPage'; export { Settings } from './ui/settings/SettingsPage'; diff --git a/src/pages/team/ui/roles/Permissions.tsx b/src/pages/team/ui/roles/Permissions.tsx deleted file mode 100644 index 564c85c..0000000 --- a/src/pages/team/ui/roles/Permissions.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { Badge, CardSection, OptionGroup, Switch } from 'shared/ui'; -import { DEFAULTS, PERMISSION_GROUPS, PermissionKey, RoleKey, ROLES } from '../../model/roles-mock'; - -interface PermissionsProps { - className?: string; - active: RoleKey; -} - -export function Permissions({ className, active }: PermissionsProps) { - const [matrix, setMatrix] = useState(DEFAULTS); - - const update = (role: RoleKey, key: PermissionKey, v: boolean) => { - setMatrix((m) => ({ ...m, [role]: { ...m[role], [key]: v } })); - }; - - return ( - - Права роли {active} - {ROLES.find((r) => r.key === active)?.locked && ( - Системная роль - )} - - } - description={`Настройте действия, разрешённые для роли ${active}.`} - > - {PERMISSION_GROUPS.map((group) => ( - { - const lockedPerm = p.key === 'billing.view' && active !== 'Admin'; - return { - key: p.key, - label: p.label, - hint: lockedPerm ? 'Зарезервировано для роли Admin' : undefined, - input: (props) => ( - update(active, p.key, v)} - disabled={lockedPerm} - {...props} - /> - ), - }; - })} - /> - ))} - - ); -} diff --git a/src/pages/team/ui/roles/RolesList.tsx b/src/pages/team/ui/roles/RolesList.tsx deleted file mode 100644 index cc95c58..0000000 --- a/src/pages/team/ui/roles/RolesList.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client'; - -import { Plus, Shield } from 'lucide-react'; -import { Dispatch, SetStateAction } from 'react'; -import { - Button, - Field, - FieldContent, - FieldDescription, - FieldLabel, - FieldTitle, - RadioGroup, - RadioGroupItem, -} from 'shared/ui'; -import { RoleKey, ROLES } from '../../model/roles-mock'; - -interface RolesListProps { - className?: string; - active: RoleKey; - setActive: Dispatch>; -} - -export function RolesList({ className, active, setActive }: RolesListProps) { - return ( -
- setActive(v)}> - {ROLES.map(({ key, description, members }) => ( - - - - - ))} - - -
- ); -} diff --git a/src/pages/team/ui/roles/RolesPage.tsx b/src/pages/team/ui/roles/RolesPage.tsx deleted file mode 100644 index 7fbbeab..0000000 --- a/src/pages/team/ui/roles/RolesPage.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { FloatingSaveBar } from 'shared/ui'; -import { DEFAULTS, RoleKey } from '../../model/roles-mock'; -import { RolesList } from './RolesList'; -import { Permissions } from './Permissions'; - -export function RolesPage() { - const [matrix, setMatrix] = useState(DEFAULTS); - const [saved, setSaved] = useState(DEFAULTS); - const [active, setActive] = useState('Member'); - - const dirty = JSON.stringify(matrix) !== JSON.stringify(saved); - - return ( - <> -
- - -
- - setSaved(matrix)} - onDiscard={() => setMatrix(saved)} - /> - - ); -} diff --git a/src/shared/config/routes.ts b/src/shared/config/routes.ts index ac0c9c1..59ca0f3 100644 --- a/src/shared/config/routes.ts +++ b/src/shared/config/routes.ts @@ -13,7 +13,6 @@ export const routes = { root: (): Route => '/team', members: (): Route => '/team/members', invitations: (): Route => '/team/invitations', - roles: (): Route => '/team/roles', settings: (): Route => '/team/settings', projects: { all: (): Route => '/team/projects', diff --git a/src/widgets/page-layout/index.ts b/src/widgets/page-layout/index.ts deleted file mode 100644 index 32121a9..0000000 --- a/src/widgets/page-layout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PageLayout } from './ui/PageLayout'; diff --git a/src/widgets/page-layout/ui/PageLayout.tsx b/src/widgets/page-layout/ui/PageLayout.tsx deleted file mode 100644 index f7d5533..0000000 --- a/src/widgets/page-layout/ui/PageLayout.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { ReactNode } from 'react'; - -interface PageLayoutProps { - title: string | ReactNode; - description?: string; - badge?: ReactNode; - headerSlot?: ReactNode; - nav?: ReactNode; - children: ReactNode; -} - -export function PageLayout({ - title, - description, - badge, - headerSlot, - nav, - children, -}: PageLayoutProps) { - return ( -
-
-
-
-

{title}

- {badge} -
- {description &&

{description}

} -
- - {headerSlot &&
{headerSlot}
} - - {nav &&
{nav}
} - -
{children}
-
-
- ); -} diff --git a/src/widgets/sidebar-header-title/config/route-definitions.ts b/src/widgets/sidebar-header-title/config/route-definitions.ts index 9a6d2bd..05cae79 100644 --- a/src/widgets/sidebar-header-title/config/route-definitions.ts +++ b/src/widgets/sidebar-header-title/config/route-definitions.ts @@ -22,7 +22,6 @@ export const routeDefinitions = [ ['team.root', (pathname) => pathname === routes.team.root(), 'Команда'], ['team.members', (pathname) => pathname === routes.team.members(), 'Участники'], ['team.invitations', (pathname) => pathname === routes.team.invitations(), 'Приглашения'], - ['team.roles', (pathname) => pathname === routes.team.roles(), 'Роли и права'], ['team.settings', (pathname) => pathname === routes.team.settings(), 'Настройки'], ['team.projects.all', (pathname) => pathname === routes.team.projects.all(), 'Проекты'], ['auth.signin', (pathname) => pathname === routes.auth.signin(), 'Вход'], diff --git a/src/widgets/tabs-nav/index.ts b/src/widgets/tabs-nav/index.ts index ac3dc31..9b60f71 100644 --- a/src/widgets/tabs-nav/index.ts +++ b/src/widgets/tabs-nav/index.ts @@ -1,3 +1,2 @@ export { type TabNavItem } from './model/types'; -export { TabsNav } from './ui/TabsNav'; export { VerticalTabsNav } from './ui/VerticalTabsNav'; diff --git a/src/widgets/tabs-nav/ui/TabsNav.tsx b/src/widgets/tabs-nav/ui/TabsNav.tsx deleted file mode 100644 index cb0739f..0000000 --- a/src/widgets/tabs-nav/ui/TabsNav.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { ComponentProps } from 'react'; -import { classNames } from 'shared/lib/utils'; -import { Badge } from 'shared/ui'; -import { TabNavItem } from '../model/types'; - -interface TabsNavProps extends Omit, 'children'> { - tabs: TabNavItem[]; -} - -export function TabsNav({ 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 ( - - {tab.label} - {tab.badge && ( - - {tab.badge.value} - - )} - {active && } - - ); - })} -
- ); -} diff --git a/steiger.config.ts b/steiger.config.ts index 26e9bd6..8bc1425 100644 --- a/steiger.config.ts +++ b/steiger.config.ts @@ -21,6 +21,7 @@ export default defineConfig([ './src/features/projects/remove/**', './src/features/projects/share/**', './src/widgets/task/**', + './src/widgets/tabs-nav/**', ], rules: { 'fsd/insignificant-slice': 'off', From 76c31cba851f06da95d15c5a98275a354bc9bca0 Mon Sep 17 00:00:00 2001 From: kapitulin24 Date: Mon, 6 Jul 2026 20:04:25 +0300 Subject: [PATCH 07/11] refactor(routes): remove security route and associated components - Deleted the security page component and its route definition from the user structure. - Updated route definitions and sidebar header to reflect the removal of the security section. --- app/(protected)/user/(user)/security/page.tsx | 1 - src/shared/config/routes.ts | 1 - src/widgets/sidebar-header-title/config/route-definitions.ts | 1 - 3 files changed, 3 deletions(-) delete mode 100644 app/(protected)/user/(user)/security/page.tsx diff --git a/app/(protected)/user/(user)/security/page.tsx b/app/(protected)/user/(user)/security/page.tsx deleted file mode 100644 index bb3be46..0000000 --- a/app/(protected)/user/(user)/security/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { SecurityPage as default } from 'pages/profile/security'; diff --git a/src/shared/config/routes.ts b/src/shared/config/routes.ts index 59ca0f3..dfca8e1 100644 --- a/src/shared/config/routes.ts +++ b/src/shared/config/routes.ts @@ -5,7 +5,6 @@ export const routes = { user: { root: (): Route => '/user', profile: (): Route => '/user/profile', - security: (): Route => '/user/security', notifications: (): Route => '/user/notifications', teams: (): Route => '/user/teams', }, diff --git a/src/widgets/sidebar-header-title/config/route-definitions.ts b/src/widgets/sidebar-header-title/config/route-definitions.ts index 05cae79..5285f97 100644 --- a/src/widgets/sidebar-header-title/config/route-definitions.ts +++ b/src/widgets/sidebar-header-title/config/route-definitions.ts @@ -17,7 +17,6 @@ export const routeDefinitions = [ ['home', (pathname) => pathname === routes.home(), 'Главная'], ['user.root', (pathname) => pathname === routes.user.root(), 'Профиль'], ['user.profile', (pathname) => pathname === routes.user.profile(), 'Мой профиль'], - ['user.security', (pathname) => pathname === routes.user.security(), 'Безопасность'], ['user.notifications', (pathname) => pathname === routes.user.notifications(), 'Уведомления'], ['team.root', (pathname) => pathname === routes.team.root(), 'Команда'], ['team.members', (pathname) => pathname === routes.team.members(), 'Участники'], From 0b7524fd87accb8d48e9143de2cb51ff16f6beee Mon Sep 17 00:00:00 2001 From: kapitulin24 Date: Mon, 6 Jul 2026 21:09:28 +0300 Subject: [PATCH 08/11] refactor(profile): enhance layout and component structure - Updated the profile layout to improve styling and responsiveness. - Refactored the PageWrapper component for better content overflow handling. - Adjusted the VerticalTabsNav component to use a sticky position and modified its class names for improved visual consistency. - Changed the import statements for better type handling in user-related files. --- app/(protected)/user/(user)/layout.tsx | 10 ++++------ src/entities/user/api/http.ts | 2 +- src/entities/user/model/schemas.ts | 1 - src/widgets/page-wrapper/ui/PageWrapper.tsx | 16 ++-------------- src/widgets/tabs-nav/model/types.ts | 4 +--- src/widgets/tabs-nav/ui/VerticalTabsNav.tsx | 20 +++++--------------- 6 files changed, 13 insertions(+), 40 deletions(-) diff --git a/app/(protected)/user/(user)/layout.tsx b/app/(protected)/user/(user)/layout.tsx index f0a2a26..042a775 100644 --- a/app/(protected)/user/(user)/layout.tsx +++ b/app/(protected)/user/(user)/layout.tsx @@ -1,9 +1,9 @@ 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'; +import { type TabNavItem, VerticalTabsNav } from 'widgets/tabs-nav'; -export const tabs: TabNavItem[] = [ +const tabs: TabNavItem[] = [ { key: routes.user.profile(), label: 'Основные настройки', icon: }, { key: routes.user.notifications(), label: 'Уведомления', icon: }, ]; @@ -13,11 +13,9 @@ export default function ProfileLayout({ children }: { children: React.ReactNode -
- +
+ {children}
diff --git a/src/entities/user/api/http.ts b/src/entities/user/api/http.ts index b2dc8ab..3e07f1c 100644 --- a/src/entities/user/api/http.ts +++ b/src/entities/user/api/http.ts @@ -1,6 +1,6 @@ import { api } from 'shared/api'; -import * as TUser from '../model/types'; import * as SUser from '../model/schemas'; +import type * as TUser from '../model/types'; export class UserHttp { static getUser(signal?: AbortSignal) { diff --git a/src/entities/user/model/schemas.ts b/src/entities/user/model/schemas.ts index fc1e427..facb05d 100644 --- a/src/entities/user/model/schemas.ts +++ b/src/entities/user/model/schemas.ts @@ -1,5 +1,4 @@ import { DateTimeString, GlobalSuccess } from 'shared/api'; -import { PaginatedResponseSchema } from 'shared/api/'; import { z } from 'zod/v4'; export const UserAvatarSchema = z diff --git a/src/widgets/page-wrapper/ui/PageWrapper.tsx b/src/widgets/page-wrapper/ui/PageWrapper.tsx index 649991b..d6f4c0d 100644 --- a/src/widgets/page-wrapper/ui/PageWrapper.tsx +++ b/src/widgets/page-wrapper/ui/PageWrapper.tsx @@ -1,26 +1,14 @@ -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, - wrap: { className, ...wrapProps } = {}, - ...props -}: PageWrapperProps) { +export function PageWrapper({ title, description, action, children, ...props }: PageWrapperProps) { return ( - + {(title || description || action) && ( {title && {title}} diff --git a/src/widgets/tabs-nav/model/types.ts b/src/widgets/tabs-nav/model/types.ts index 853d56e..693be45 100644 --- a/src/widgets/tabs-nav/model/types.ts +++ b/src/widgets/tabs-nav/model/types.ts @@ -1,11 +1,9 @@ import type { Route } from 'next'; -import { ComponentProps, ReactNode } from 'react'; -import { Badge } from 'shared/ui'; +import { ReactNode } from 'react'; export type TabNavItem = { key: Route; 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 index 854a03a..2e25e07 100644 --- a/src/widgets/tabs-nav/ui/VerticalTabsNav.tsx +++ b/src/widgets/tabs-nav/ui/VerticalTabsNav.tsx @@ -4,7 +4,7 @@ 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 { Button } from 'shared/ui'; import { TabNavItem } from '../model/types'; interface TabsNavProps extends Omit, 'children'> { @@ -19,10 +19,7 @@ export function VerticalTabsNav({ className, tabs, ...props }: TabsNavProps) { } return ( -
+
{tabs.map((tab) => { const active = tab.matchPrefix ? (pathname ?? '').startsWith(tab.key) @@ -30,21 +27,14 @@ export function VerticalTabsNav({ className, tabs, ...props }: TabsNavProps) { return ( ); From 6f04effa30d790096bd8e39728684f9d1c50e431 Mon Sep 17 00:00:00 2001 From: kapitulin24 Date: Tue, 7 Jul 2026 13:31:05 +0300 Subject: [PATCH 09/11] feat(profile): implement OAuth section for managing connected accounts - Added OAuthItem component to display individual OAuth provider status and connection options. - Introduced OAuthSection and OAuthSectionContent components for managing and displaying connected accounts. - Created OAuthSectionFallback for loading states while fetching OAuth providers. - Implemented OAuthStatusBadge to visually represent connection status of OAuth providers. --- .../[projectSlug]/[boardSlug]/error.tsx | 7 +- .../[boardSlug]/settings/error.tsx | 7 +- .../projects/[projectSlug]/settings/error.tsx | 7 +- app/(protected)/team/projects/error.tsx | 7 +- .../user/(user)/notifications/error.tsx | 7 +- app/(protected)/user/(user)/profile/error.tsx | 7 +- .../user/teams/@invitations/error.tsx | 7 +- app/(protected)/user/teams/error.tsx | 7 +- .../[projectSlug]/[boardSlug]/error.tsx | 1 - src/entities/auth/api/queries.ts | 11 ++- src/entities/auth/model/schemas.ts | 2 +- .../ui/OAuthLoginButtonsContent.tsx | 2 +- src/pages/profile/me/config/oauth-status.ts | 8 ++- src/pages/profile/me/model/profile.ts | 2 +- src/pages/profile/me/ui/MePage.tsx | 17 +++-- src/pages/profile/me/ui/MePageContent.tsx | 29 -------- src/pages/profile/me/ui/MePageFallback.tsx | 12 ---- .../me/ui/account-section/AccountSection.tsx | 41 ----------- .../AccountSectionErrorFallback.tsx | 14 ---- .../AccountSectionFallback.tsx | 30 -------- .../me/ui/account-section/OAuthBadge.tsx | 28 -------- .../ui/account-section/OAuthManageButton.tsx | 72 ------------------- .../profile/me/ui/oauth-section/OAuthItem.tsx | 64 +++++++++++++++++ .../me/ui/oauth-section/OAuthSection.tsx | 21 ++++++ .../ui/oauth-section/OAuthSectionContent.tsx | 40 +++++++++++ .../ui/oauth-section/OAuthSectionFallback.tsx | 35 +++++++++ .../me/ui/oauth-section/OAuthStatusBadge.tsx | 19 +++++ .../me/ui/profile-section/IdentityItem.tsx | 2 +- .../me/ui/profile-section/ProfileSection.tsx | 39 +++++----- .../profile-section/ProfileSectionContent.tsx | 30 ++++++++ .../ProfileSectionFallback.tsx | 4 +- .../boards/ui/column/ColumnHeaderActions.tsx | 2 +- src/pages/team/ui/members/MembersPage.tsx | 2 +- src/shared/ui/Badge.tsx | 1 + src/widgets/app-sidebar/lib/useTeamHotkeys.ts | 2 +- src/widgets/error-state/index.ts | 1 + src/widgets/error-state/ui/ErrorFallback.tsx | 11 +++ 37 files changed, 280 insertions(+), 318 deletions(-) delete mode 100644 src/pages/profile/me/ui/MePageContent.tsx delete mode 100644 src/pages/profile/me/ui/MePageFallback.tsx delete mode 100644 src/pages/profile/me/ui/account-section/AccountSection.tsx delete mode 100644 src/pages/profile/me/ui/account-section/AccountSectionErrorFallback.tsx delete mode 100644 src/pages/profile/me/ui/account-section/AccountSectionFallback.tsx delete mode 100644 src/pages/profile/me/ui/account-section/OAuthBadge.tsx delete mode 100644 src/pages/profile/me/ui/account-section/OAuthManageButton.tsx create mode 100644 src/pages/profile/me/ui/oauth-section/OAuthItem.tsx create mode 100644 src/pages/profile/me/ui/oauth-section/OAuthSection.tsx create mode 100644 src/pages/profile/me/ui/oauth-section/OAuthSectionContent.tsx create mode 100644 src/pages/profile/me/ui/oauth-section/OAuthSectionFallback.tsx create mode 100644 src/pages/profile/me/ui/oauth-section/OAuthStatusBadge.tsx create mode 100644 src/pages/profile/me/ui/profile-section/ProfileSectionContent.tsx create mode 100644 src/widgets/error-state/ui/ErrorFallback.tsx diff --git a/app/(protected)/team/projects/[projectSlug]/[boardSlug]/error.tsx b/app/(protected)/team/projects/[projectSlug]/[boardSlug]/error.tsx index 3865873..756c188 100644 --- a/app/(protected)/team/projects/[projectSlug]/[boardSlug]/error.tsx +++ b/app/(protected)/team/projects/[projectSlug]/[boardSlug]/error.tsx @@ -2,12 +2,7 @@ import { ErrorState } from 'widgets/error-state'; -export default function Error({ - unstable_retry, -}: { - error: Error & { digest?: string }; - unstable_retry: () => void; -}) { +export default function Error({ unstable_retry }: { unstable_retry: () => void }) { return ( void; -}) { +export default function Error({ unstable_retry }: { unstable_retry: () => void }) { return ( void; -}) { +export default function Error({ unstable_retry }: { unstable_retry: () => void }) { return ( void; -}) { +export default function Error({ unstable_retry }: { unstable_retry: () => void }) { return ( void; -}) { +export default function Error({ unstable_retry }: { unstable_retry: () => void }) { return ( void; -}) { +export default function Error({ unstable_retry }: { unstable_retry: () => void }) { return ( void; -}) { +export default function Error({ unstable_retry }: { unstable_retry: () => void }) { return ( void; -}) { +export default function Error({ unstable_retry }: { unstable_retry: () => void }) { return ( void; }) { return ( diff --git a/src/entities/auth/api/queries.ts b/src/entities/auth/api/queries.ts index fc28d30..5f8ba53 100644 --- a/src/entities/auth/api/queries.ts +++ b/src/entities/auth/api/queries.ts @@ -1,6 +1,7 @@ import { queryOptions } from '@tanstack/react-query'; -import { AuthHttp } from './http'; +import type { ConnectedOAuthProvidersResponse, OAuthProvider } from '../model/types'; import { authFabricKeys } from '../model/const'; +import { AuthHttp } from './http'; export class AuthQueries { static getOAuthProviders() { @@ -15,6 +16,14 @@ export class AuthQueries { queryKey: authFabricKeys.connectedProviders(), queryFn: async ({ signal }) => AuthHttp.connectedOAuthProviders(signal), staleTime: 60_000 * 360 * 24, + select: (data) => + data.reduce( + (acc, v) => { + acc[v.provider] = v; + return acc; + }, + {} as Record + ), }); } } diff --git a/src/entities/auth/model/schemas.ts b/src/entities/auth/model/schemas.ts index e8f58ab..d98d381 100644 --- a/src/entities/auth/model/schemas.ts +++ b/src/entities/auth/model/schemas.ts @@ -90,7 +90,7 @@ export const ConnectedOAuthProvidersResponse = z .object({ email: Email, avatarUrl: z.string().nullable(), - provider: z.string(), + provider: OAuthProvider, connectedAt: z.string(), }) .array(); diff --git a/src/features/auth/oauth-login/ui/OAuthLoginButtonsContent.tsx b/src/features/auth/oauth-login/ui/OAuthLoginButtonsContent.tsx index d0d1e38..d21d0d5 100644 --- a/src/features/auth/oauth-login/ui/OAuthLoginButtonsContent.tsx +++ b/src/features/auth/oauth-login/ui/OAuthLoginButtonsContent.tsx @@ -34,7 +34,7 @@ export function OAuthLoginButtonsContent({ className }: OAuthLoginButtonsContent type="button" className={data.className} key={value} - variant={'outline'} + variant="outline" size="icon" onClick={() => startOAuth(value)} > diff --git a/src/pages/profile/me/config/oauth-status.ts b/src/pages/profile/me/config/oauth-status.ts index c1c80b2..3587eb8 100644 --- a/src/pages/profile/me/config/oauth-status.ts +++ b/src/pages/profile/me/config/oauth-status.ts @@ -1,7 +1,13 @@ +import { ComponentProps } from 'react'; +import { Badge } from 'shared/ui'; import { OAuthConnectionStatus } from '../model/profile'; export const OAUTH_STATUS_LABELS = { connected: 'Подключен', disconnected: 'Не подключен', - unknown: 'Не удалось проверить', } as const satisfies Record; + +export const OAUTH_STATUS_BADGE_VARIANT = { + connected: 'success', + disconnected: 'destructive', +} as const satisfies Record['variant']>; diff --git a/src/pages/profile/me/model/profile.ts b/src/pages/profile/me/model/profile.ts index f0fb522..e05616a 100644 --- a/src/pages/profile/me/model/profile.ts +++ b/src/pages/profile/me/model/profile.ts @@ -19,4 +19,4 @@ export const ProfileForm = z.object({ export type ProfileFormValues = z.infer; -export type OAuthConnectionStatus = 'connected' | 'disconnected' | 'unknown'; +export type OAuthConnectionStatus = 'connected' | 'disconnected'; diff --git a/src/pages/profile/me/ui/MePage.tsx b/src/pages/profile/me/ui/MePage.tsx index 9ff1ab3..b9b73c4 100644 --- a/src/pages/profile/me/ui/MePage.tsx +++ b/src/pages/profile/me/ui/MePage.tsx @@ -1,14 +1,13 @@ -'use client'; -import dynamic from 'next/dynamic'; -import { MePageFallback } from './MePageFallback'; - -const MePageContent = dynamic(() => import('./MePageContent').then((mod) => mod.MePage), { - ssr: false, - loading: () => , -}); +import { OAuthSection } from './oauth-section/OAuthSection'; +import { ProfileSection } from './profile-section/ProfileSection'; function MePage() { - return ; + return ( +
+ + +
+ ); } export { MePage }; diff --git a/src/pages/profile/me/ui/MePageContent.tsx b/src/pages/profile/me/ui/MePageContent.tsx deleted file mode 100644 index d552be5..0000000 --- a/src/pages/profile/me/ui/MePageContent.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'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/me/ui/MePageFallback.tsx b/src/pages/profile/me/ui/MePageFallback.tsx deleted file mode 100644 index 11e0fc6..0000000 --- a/src/pages/profile/me/ui/MePageFallback.tsx +++ /dev/null @@ -1,12 +0,0 @@ -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/me/ui/account-section/AccountSection.tsx b/src/pages/profile/me/ui/account-section/AccountSection.tsx deleted file mode 100644 index f247561..0000000 --- a/src/pages/profile/me/ui/account-section/AccountSection.tsx +++ /dev/null @@ -1,41 +0,0 @@ -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/me/ui/account-section/AccountSectionErrorFallback.tsx b/src/pages/profile/me/ui/account-section/AccountSectionErrorFallback.tsx deleted file mode 100644 index f2edc25..0000000 --- a/src/pages/profile/me/ui/account-section/AccountSectionErrorFallback.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { CardSection } from 'shared/ui'; -import { ErrorState } from 'widgets/error-state'; - -export function AccountSectionErrorFallback() { - return ( - - - - ); -} diff --git a/src/pages/profile/me/ui/account-section/AccountSectionFallback.tsx b/src/pages/profile/me/ui/account-section/AccountSectionFallback.tsx deleted file mode 100644 index 7a9c74e..0000000 --- a/src/pages/profile/me/ui/account-section/AccountSectionFallback.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { CardSection, Skeleton } from 'shared/ui'; - -export function AccountSectionFallback() { - return ( - - {Array.from({ length: 3 }).map((_, index) => ( -
-
- -
- - -
-
-
- - -
-
- ))} -
- ); -} diff --git a/src/pages/profile/me/ui/account-section/OAuthBadge.tsx b/src/pages/profile/me/ui/account-section/OAuthBadge.tsx deleted file mode 100644 index aae302c..0000000 --- a/src/pages/profile/me/ui/account-section/OAuthBadge.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Badge } from 'shared/ui'; -import { type OAuthConnectionStatus } from '../../model/profile'; -import { classNames } from 'shared/lib/utils'; -import { OAUTH_STATUS_LABELS } from '../../config/oauth-status'; - -export function OAuthBadge({ - status, - className = '', -}: { - status: OAuthConnectionStatus; - className?: string; -}) { - return ( - - {OAUTH_STATUS_LABELS[status]} - - ); -} diff --git a/src/pages/profile/me/ui/account-section/OAuthManageButton.tsx b/src/pages/profile/me/ui/account-section/OAuthManageButton.tsx deleted file mode 100644 index ffe5e73..0000000 --- a/src/pages/profile/me/ui/account-section/OAuthManageButton.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client'; - -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/me/model/useOAuthManage'; -import { OAuthBadge } from './OAuthBadge'; - -type OAuthManageButtonProps = ComponentProps & { - wrap?: Omit, 'children'>; - provider: TAuth.OAuthProvider; - label: string; - isLoading?: boolean; - status: OAuthConnectionStatus; -}; - -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 ( -
-
- {label} -
- {label} - -
-
-
- - -
-
- ); -} diff --git a/src/pages/profile/me/ui/oauth-section/OAuthItem.tsx b/src/pages/profile/me/ui/oauth-section/OAuthItem.tsx new file mode 100644 index 0000000..dcae1ad --- /dev/null +++ b/src/pages/profile/me/ui/oauth-section/OAuthItem.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { OAUTH_PROVIDERS, type TAuth } from 'entities/auth'; +import Image from 'next/image'; +import { useOAuthManage } from 'pages/profile/me/model/useOAuthManage'; +import { cn } from 'shared/lib/utils'; +import { + Button, + Item, + ItemActions, + ItemContent, + ItemDescription, + ItemMedia, + ItemTitle, + Spinner, +} from 'shared/ui'; +import { type OAuthConnectionStatus } from '../../model/profile'; +import { OAuthStatusBadge } from './OAuthStatusBadge'; + +interface OAuthItemProps { + provider: TAuth.OAuthProvider; + label: string; + status: OAuthConnectionStatus; +} + +export function OAuthItem({ provider, label, status }: OAuthItemProps) { + const { isPending, handleToggleConnect } = useOAuthManage(provider, status); + const isConnected = status === 'connected'; + const meta = OAUTH_PROVIDERS[provider]; + + return ( + + + {provider} + + + {label} + + + + + + + + + + ); +} diff --git a/src/pages/profile/me/ui/oauth-section/OAuthSection.tsx b/src/pages/profile/me/ui/oauth-section/OAuthSection.tsx new file mode 100644 index 0000000..8e030bb --- /dev/null +++ b/src/pages/profile/me/ui/oauth-section/OAuthSection.tsx @@ -0,0 +1,21 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import { ErrorFallback } from 'widgets/error-state'; +import { OAuthSectionFallback } from './OAuthSectionFallback'; + +const OAuthSectionContent = dynamic( + () => import('./OAuthSectionContent').then((mod) => mod.OAuthSectionContent), + { + ssr: false, + loading: () => , + } +); + +export function OAuthSection() { + return ( + + + + ); +} diff --git a/src/pages/profile/me/ui/oauth-section/OAuthSectionContent.tsx b/src/pages/profile/me/ui/oauth-section/OAuthSectionContent.tsx new file mode 100644 index 0000000..6f7e447 --- /dev/null +++ b/src/pages/profile/me/ui/oauth-section/OAuthSectionContent.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useSuspenseQuery } from '@tanstack/react-query'; +import { AuthQueries } from 'entities/auth'; +import { QueryParamsHandler } from 'features/handle-query-params'; +import { Suspense } from 'react'; +import { CardSection } from 'shared/ui'; +import { OAuthItem } from './OAuthItem'; + +export function OAuthSectionContent() { + const providers = useSuspenseQuery(AuthQueries.getOAuthProviders()); + const connected = useSuspenseQuery(AuthQueries.getConnectedOAuthProviders()); + + return ( + <> + + + + +
    + {providers.data?.map((provider) => { + const isConnected = connected.data[provider.value]; + return ( +
  • + +
  • + ); + })} +
+
+ + ); +} diff --git a/src/pages/profile/me/ui/oauth-section/OAuthSectionFallback.tsx b/src/pages/profile/me/ui/oauth-section/OAuthSectionFallback.tsx new file mode 100644 index 0000000..4169c8c --- /dev/null +++ b/src/pages/profile/me/ui/oauth-section/OAuthSectionFallback.tsx @@ -0,0 +1,35 @@ +import { CardSection, Item, ItemActions, ItemContent, ItemMedia, Skeleton } from 'shared/ui'; + +export function OAuthSectionFallback() { + return ( + +
    + {Array.from({ length: 3 }).map((_, index) => ( +
  • + + + + + + + + + + + + + +
  • + ))} +
+
+ ); +} diff --git a/src/pages/profile/me/ui/oauth-section/OAuthStatusBadge.tsx b/src/pages/profile/me/ui/oauth-section/OAuthStatusBadge.tsx new file mode 100644 index 0000000..138eb28 --- /dev/null +++ b/src/pages/profile/me/ui/oauth-section/OAuthStatusBadge.tsx @@ -0,0 +1,19 @@ +import { type ComponentProps } from 'react'; +import { Badge } from 'shared/ui'; +import { OAUTH_STATUS_BADGE_VARIANT, OAUTH_STATUS_LABELS } from '../../config/oauth-status'; +import { type OAuthConnectionStatus } from '../../model/profile'; + +export interface OAuthStatusBadgeProps extends Omit< + ComponentProps, + 'variant' | 'children' +> { + status: OAuthConnectionStatus; +} + +export function OAuthStatusBadge({ status, ...props }: OAuthStatusBadgeProps) { + return ( + + {OAUTH_STATUS_LABELS[status]} + + ); +} diff --git a/src/pages/profile/me/ui/profile-section/IdentityItem.tsx b/src/pages/profile/me/ui/profile-section/IdentityItem.tsx index 43fc64b..0ed10a2 100644 --- a/src/pages/profile/me/ui/profile-section/IdentityItem.tsx +++ b/src/pages/profile/me/ui/profile-section/IdentityItem.tsx @@ -28,7 +28,7 @@ function IdentityItem({ profile }: AccountIdentityItemProps) { /> - + ); diff --git a/src/pages/profile/me/ui/profile-section/ProfileSection.tsx b/src/pages/profile/me/ui/profile-section/ProfileSection.tsx index 00ec08e..045380f 100644 --- a/src/pages/profile/me/ui/profile-section/ProfileSection.tsx +++ b/src/pages/profile/me/ui/profile-section/ProfileSection.tsx @@ -1,28 +1,21 @@ -import { CardSection, FloatingSaveBar } from 'shared/ui'; -import { IdentityItem } from './IdentityItem'; -import { ProfileForm } from './ProfileForm'; -import { useMePage } from '../../model/useMePage'; +'use client'; -export function ProfileSection() { - const { form, profile, isDirty, isPending, onSubmit, onDiscard } = useMePage(); +import dynamic from 'next/dynamic'; +import { ErrorFallback } from 'widgets/error-state'; +import { ProfileSectionFallback } from './ProfileSectionFallback'; + +const ProfileSectionContent = dynamic( + () => import('./ProfileSectionContent').then((mod) => mod.ProfileSectionContent), + { + ssr: false, + loading: () => , + } +); +export function ProfileSection() { return ( - <> - - - - - - + + + ); } diff --git a/src/pages/profile/me/ui/profile-section/ProfileSectionContent.tsx b/src/pages/profile/me/ui/profile-section/ProfileSectionContent.tsx new file mode 100644 index 0000000..d047384 --- /dev/null +++ b/src/pages/profile/me/ui/profile-section/ProfileSectionContent.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { CardSection, FloatingSaveBar } from 'shared/ui'; +import { useMePage } from '../../model/useMePage'; +import { IdentityItem } from './IdentityItem'; +import { ProfileForm } from './ProfileForm'; + +export function ProfileSectionContent() { + const { form, profile, isDirty, isPending, onSubmit, onDiscard } = useMePage(); + + return ( + <> + + + + + + + ); +} diff --git a/src/pages/profile/me/ui/profile-section/ProfileSectionFallback.tsx b/src/pages/profile/me/ui/profile-section/ProfileSectionFallback.tsx index d08fd03..15e7b81 100644 --- a/src/pages/profile/me/ui/profile-section/ProfileSectionFallback.tsx +++ b/src/pages/profile/me/ui/profile-section/ProfileSectionFallback.tsx @@ -8,11 +8,11 @@ export function ProfileSectionFallback() { title="Идентификация профиля" description="Публичная информация о вас." > -
+
- +
diff --git a/src/pages/project/boards/ui/column/ColumnHeaderActions.tsx b/src/pages/project/boards/ui/column/ColumnHeaderActions.tsx index e080aef..aa15462 100644 --- a/src/pages/project/boards/ui/column/ColumnHeaderActions.tsx +++ b/src/pages/project/boards/ui/column/ColumnHeaderActions.tsx @@ -30,7 +30,7 @@ export function ColumnHeaderActions({ return ( - diff --git a/src/pages/team/ui/members/MembersPage.tsx b/src/pages/team/ui/members/MembersPage.tsx index 0b725ef..bfc0e26 100644 --- a/src/pages/team/ui/members/MembersPage.tsx +++ b/src/pages/team/ui/members/MembersPage.tsx @@ -15,7 +15,7 @@ export function MembersPage() {
- diff --git a/src/shared/ui/Badge.tsx b/src/shared/ui/Badge.tsx index c77e528..d682d37 100644 --- a/src/shared/ui/Badge.tsx +++ b/src/shared/ui/Badge.tsx @@ -10,6 +10,7 @@ const badgeVariants = cva( variants: { variant: { default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', + success: 'bg-green-600/30 text-foreground [a]:hover:bg-green-600/50', secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80', destructive: 'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20', diff --git a/src/widgets/app-sidebar/lib/useTeamHotkeys.ts b/src/widgets/app-sidebar/lib/useTeamHotkeys.ts index 1a0f251..7fa4721 100644 --- a/src/widgets/app-sidebar/lib/useTeamHotkeys.ts +++ b/src/widgets/app-sidebar/lib/useTeamHotkeys.ts @@ -12,7 +12,7 @@ export function useTeamHotkeys( const slot = parseInt(e.key, 10); - if (slot < 1 || slot > MAX_VISIBLE_TEAMS) return; + if (slot < 1 || slot > MAX_VISIBLE_TEAMS || Number.isNaN(slot)) return; e.preventDefault(); diff --git a/src/widgets/error-state/index.ts b/src/widgets/error-state/index.ts index 0ac7db5..d1b01b6 100644 --- a/src/widgets/error-state/index.ts +++ b/src/widgets/error-state/index.ts @@ -1 +1,2 @@ +export { default as ErrorFallback } from './ui/ErrorFallback'; export { ErrorState } from './ui/ErrorState'; diff --git a/src/widgets/error-state/ui/ErrorFallback.tsx b/src/widgets/error-state/ui/ErrorFallback.tsx new file mode 100644 index 0000000..841b5a2 --- /dev/null +++ b/src/widgets/error-state/ui/ErrorFallback.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { unstable_catchError as catchError, type ErrorInfo } from 'next/error'; +import { ComponentProps } from 'react'; +import { ErrorState } from './ErrorState'; + +function ErrorFallback(props: ComponentProps, { unstable_retry }: ErrorInfo) { + return unstable_retry()} {...props} />; +} + +export default catchError(ErrorFallback); From 92d89b098722177d11a4e564fbaa65d150ab7683 Mon Sep 17 00:00:00 2001 From: kapitulin24 Date: Tue, 7 Jul 2026 18:13:11 +0300 Subject: [PATCH 10/11] feat(team): add error handling components for invitations, members, and settings - Introduced Error components for handling loading failures in team invitations, members, and settings views. - Each component provides user-friendly messages and retry options to enhance user experience. - Implemented localized titles and descriptions for better accessibility. - Added new API hooks for managing invitations and members, including update and remove functionalities. - Created UI components for displaying invitations and members, including cards and dialogs for actions. --- app/(protected)/team/invitations/error.tsx | 15 +++ app/(protected)/team/invitations/page.tsx | 2 +- app/(protected)/team/members/error.tsx | 15 +++ app/(protected)/team/members/page.tsx | 2 +- app/(protected)/team/projects/page.tsx | 2 +- app/(protected)/team/settings/error.tsx | 15 +++ app/(protected)/team/settings/page.tsx | 2 +- .../user/(user)/notifications/page.tsx | 2 +- app/(protected)/user/(user)/profile/page.tsx | 2 +- .../user/teams/@invitations/page.tsx | 2 +- app/(protected)/user/teams/page.tsx | 2 +- src/pages/profile/me/index.ts | 1 - src/pages/profile/security/index.ts | 1 - .../profile/security/ui/SecurityPage.tsx | 39 ------ src/pages/profile/teams/index.ts | 0 src/pages/profile/teams/ui/TeamList.tsx | 80 ------------ src/pages/project/boards/ui/task/TaskCard.tsx | 1 - src/pages/team/api/useQueryInvitations.ts | 11 -- src/pages/team/api/useQueryTeam.ts | 11 -- src/pages/team/config/member.ts | 49 -------- src/pages/team/index.ts | 3 - .../api/useRemoveInvitation.ts} | 5 +- .../api/useUpdateInvitation.ts | 0 src/pages/{ => team}/invitations/index.ts | 0 .../lib/get-pending-members-count-text.ts | 14 +++ .../team/invitations/ui/InvitationCard.tsx | 43 +++++++ .../invitations/ui/InvitationCardActions.tsx | 88 +++++++++++++ .../ui}/InvitationsEmpty.tsx | 0 .../invitations/ui/InvitationsPage.tsx | 0 .../invitations/ui/InvitationsPageContent.tsx | 42 +++++++ .../ui/InvitationsPageFallback.tsx | 45 +++++++ .../ui}/RemoveInvitationDialog.tsx | 34 +++-- .../team/{ => members}/api/useRemoveMember.ts | 0 .../team/{ => members}/api/useUpdateMember.ts | 0 src/pages/team/members/config/member.ts | 27 ++++ src/pages/team/members/index.ts | 1 + .../{ => members}/model/useMembersPage.ts | 9 +- src/pages/team/members/ui/MemberCard.tsx | 59 +++++++++ .../team/members/ui/MemberCardActions.tsx | 118 ++++++++++++++++++ src/pages/team/members/ui/MembersPage.tsx | 16 +++ .../team/members/ui/MembersPageContent.tsx | 40 ++++++ .../team/members/ui/MembersPageFallback.tsx | 49 ++++++++ .../ui}/RemoveMemberDialog.tsx | 29 ++++- src/pages/team/model/roles-mock.ts | 81 ------------ src/pages/team/model/team-identity.ts | 5 - .../projects/config/project-status-map.ts | 0 src/pages/{ => team}/projects/index.ts | 0 .../projects/lib/get-projects-count-text.ts | 0 .../{ => team}/projects/ui/ProjectCard.tsx | 0 .../{ => team}/projects/ui/ProjectsPage.tsx | 0 .../projects/ui/ProjectsPageContent.tsx | 0 .../projects/ui/ProjectsPageFallback.tsx | 0 src/pages/team/settings/api/useQueryTeam.ts | 8 ++ .../team/{ => settings}/api/useUpdateTeam.ts | 0 .../team/{ => settings}/api/useUploadCover.ts | 0 src/pages/team/settings/index.ts | 1 + .../settings.ts => settings/model/types.ts} | 0 .../settings => settings/ui}/DangerZone.tsx | 0 .../{ui/settings => settings/ui}/SaveBar.tsx | 4 +- src/pages/team/settings/ui/SettingsPage.tsx | 16 +++ .../team/settings/ui/SettingsPageContent.tsx | 48 +++++++ .../team/settings/ui/SettingsPageFallback.tsx | 60 +++++++++ .../settings => settings/ui}/TeamCover.tsx | 2 +- .../settings => settings/ui}/TeamIdentity.tsx | 0 .../ui}/TeamIdentityForm.tsx | 0 .../invitations/InvitationCard.skeleton.tsx | 27 ---- .../team/ui/invitations/InvitationCard.tsx | 78 ------------ .../ui/invitations/InvitationRoleSelect.tsx | 36 ------ .../team/ui/invitations/InvitationsPage.tsx | 22 ---- .../team/ui/members/MemberCard.skeleton.tsx | 24 ---- src/pages/team/ui/members/MemberCard.tsx | 75 ----------- .../team/ui/members/MemberRoleSelect.tsx | 36 ------ .../team/ui/members/MemberStatusSelect.tsx | 36 ------ src/pages/team/ui/members/MembersPage.tsx | 37 ------ .../team/ui/settings/DefaultSettings.tsx | 59 --------- .../team/ui/settings/InvitationSecurity.tsx | 53 -------- src/pages/team/ui/settings/SettingsPage.tsx | 57 --------- .../skeletons/DangerZone.skeleton.tsx | 18 --- .../skeletons/DefaultSettings.skeleton.tsx | 27 ---- .../skeletons/InvitationSecurity.skeleton.tsx | 23 ---- .../skeletons/TeamIdentity.skeleton.tsx | 19 --- .../api/useAcceptTeamInvitation.ts | 0 src/pages/user/invitations/index.ts | 1 + .../lib/get-invitations-count-text.ts | 0 .../ui/InvitationCard.skeleton.tsx | 0 .../invitations/ui/InvitationCard.tsx | 0 .../user/invitations/ui/InvitationsPage.tsx | 16 +++ .../invitations/ui/InvitationsPageContent.tsx | 0 .../ui/InvitationsPageFallback.tsx | 0 .../api/useUpdateNotifications.ts | 0 .../notifications/config/notifications.ts | 0 .../{profile => user}/notifications/index.ts | 0 .../notifications/model/notifications.ts | 0 .../notifications/ui/NotificationsPage.tsx | 0 .../ui/NotificationsPageContent.tsx | 0 .../ui/NotificationsPageFallback.tsx | 0 .../profile}/api/useConnectOauthProvider.ts | 0 .../api/useDisconnectOauthProvider.ts | 0 .../profile}/api/useUpdateProfile.ts | 0 .../profile}/config/oauth-status.ts | 0 src/pages/user/profile/index.ts | 1 + .../me => user/profile}/model/profile.ts | 0 .../me => user/profile}/model/useMePage.ts | 4 +- .../profile}/model/useOAuthManage.ts | 0 .../profile/ui/UserPage.tsx} | 4 +- .../profile}/ui/oauth-section/OAuthItem.tsx | 2 +- .../ui/oauth-section/OAuthSection.tsx | 0 .../ui/oauth-section/OAuthSectionContent.tsx | 0 .../ui/oauth-section/OAuthSectionFallback.tsx | 0 .../ui/oauth-section/OAuthStatusBadge.tsx | 0 .../ui/profile-section/IdentityItem.tsx | 0 .../ui/profile-section/ProfileForm.tsx | 0 .../ui/profile-section/ProfileSection.tsx | 0 .../profile-section/ProfileSectionContent.tsx | 0 .../ProfileSectionFallback.tsx | 0 src/pages/{ => user}/teams/index.ts | 0 .../teams/lib/get-teams-count-text.ts | 0 .../{ => user}/teams/ui/TeamCard.skeleton.tsx | 0 src/pages/{ => user}/teams/ui/TeamCard.tsx | 0 .../{ => user}/teams/ui/TeamCardActions.tsx | 0 src/pages/{ => user}/teams/ui/TeamsPage.tsx | 0 .../{ => user}/teams/ui/TeamsPageContent.tsx | 0 .../{ => user}/teams/ui/TeamsPageFallback.tsx | 0 src/widgets/app-sidebar/config/sidebar.ts | 6 +- .../app-sidebar/ui/teams/TeamContent.tsx | 2 +- src/widgets/error-state/ui/ErrorState.tsx | 3 +- 126 files changed, 815 insertions(+), 952 deletions(-) create mode 100644 app/(protected)/team/invitations/error.tsx create mode 100644 app/(protected)/team/members/error.tsx create mode 100644 app/(protected)/team/settings/error.tsx delete mode 100644 src/pages/profile/me/index.ts delete mode 100644 src/pages/profile/security/index.ts delete mode 100644 src/pages/profile/security/ui/SecurityPage.tsx delete mode 100644 src/pages/profile/teams/index.ts delete mode 100644 src/pages/profile/teams/ui/TeamList.tsx delete mode 100644 src/pages/team/api/useQueryInvitations.ts delete mode 100644 src/pages/team/api/useQueryTeam.ts delete mode 100644 src/pages/team/config/member.ts delete mode 100644 src/pages/team/index.ts rename src/pages/team/{api/useRemoveMemberInvitation.ts => invitations/api/useRemoveInvitation.ts} (89%) rename src/pages/team/{ => invitations}/api/useUpdateInvitation.ts (100%) rename src/pages/{ => team}/invitations/index.ts (100%) create mode 100644 src/pages/team/invitations/lib/get-pending-members-count-text.ts create mode 100644 src/pages/team/invitations/ui/InvitationCard.tsx create mode 100644 src/pages/team/invitations/ui/InvitationCardActions.tsx rename src/pages/team/{ui/invitations => invitations/ui}/InvitationsEmpty.tsx (100%) rename src/pages/{ => team}/invitations/ui/InvitationsPage.tsx (100%) create mode 100644 src/pages/team/invitations/ui/InvitationsPageContent.tsx create mode 100644 src/pages/team/invitations/ui/InvitationsPageFallback.tsx rename src/pages/team/{ui/invitations => invitations/ui}/RemoveInvitationDialog.tsx (58%) rename src/pages/team/{ => members}/api/useRemoveMember.ts (100%) rename src/pages/team/{ => members}/api/useUpdateMember.ts (100%) create mode 100644 src/pages/team/members/config/member.ts create mode 100644 src/pages/team/members/index.ts rename src/pages/team/{ => members}/model/useMembersPage.ts (84%) create mode 100644 src/pages/team/members/ui/MemberCard.tsx create mode 100644 src/pages/team/members/ui/MemberCardActions.tsx create mode 100644 src/pages/team/members/ui/MembersPage.tsx create mode 100644 src/pages/team/members/ui/MembersPageContent.tsx create mode 100644 src/pages/team/members/ui/MembersPageFallback.tsx rename src/pages/team/{ui/members => members/ui}/RemoveMemberDialog.tsx (68%) delete mode 100644 src/pages/team/model/roles-mock.ts delete mode 100644 src/pages/team/model/team-identity.ts rename src/pages/{ => team}/projects/config/project-status-map.ts (100%) rename src/pages/{ => team}/projects/index.ts (100%) rename src/pages/{ => team}/projects/lib/get-projects-count-text.ts (100%) rename src/pages/{ => team}/projects/ui/ProjectCard.tsx (100%) rename src/pages/{ => team}/projects/ui/ProjectsPage.tsx (100%) rename src/pages/{ => team}/projects/ui/ProjectsPageContent.tsx (100%) rename src/pages/{ => team}/projects/ui/ProjectsPageFallback.tsx (100%) create mode 100644 src/pages/team/settings/api/useQueryTeam.ts rename src/pages/team/{ => settings}/api/useUpdateTeam.ts (100%) rename src/pages/team/{ => settings}/api/useUploadCover.ts (100%) create mode 100644 src/pages/team/settings/index.ts rename src/pages/team/{model/settings.ts => settings/model/types.ts} (100%) rename src/pages/team/{ui/settings => settings/ui}/DangerZone.tsx (100%) rename src/pages/team/{ui/settings => settings/ui}/SaveBar.tsx (88%) create mode 100644 src/pages/team/settings/ui/SettingsPage.tsx create mode 100644 src/pages/team/settings/ui/SettingsPageContent.tsx create mode 100644 src/pages/team/settings/ui/SettingsPageFallback.tsx rename src/pages/team/{ui/settings => settings/ui}/TeamCover.tsx (95%) rename src/pages/team/{ui/settings => settings/ui}/TeamIdentity.tsx (100%) rename src/pages/team/{ui/settings => settings/ui}/TeamIdentityForm.tsx (100%) delete mode 100644 src/pages/team/ui/invitations/InvitationCard.skeleton.tsx delete mode 100644 src/pages/team/ui/invitations/InvitationCard.tsx delete mode 100644 src/pages/team/ui/invitations/InvitationRoleSelect.tsx delete mode 100644 src/pages/team/ui/invitations/InvitationsPage.tsx delete mode 100644 src/pages/team/ui/members/MemberCard.skeleton.tsx delete mode 100644 src/pages/team/ui/members/MemberCard.tsx delete mode 100644 src/pages/team/ui/members/MemberRoleSelect.tsx delete mode 100644 src/pages/team/ui/members/MemberStatusSelect.tsx delete mode 100644 src/pages/team/ui/members/MembersPage.tsx delete mode 100644 src/pages/team/ui/settings/DefaultSettings.tsx delete mode 100644 src/pages/team/ui/settings/InvitationSecurity.tsx delete mode 100644 src/pages/team/ui/settings/SettingsPage.tsx delete mode 100644 src/pages/team/ui/settings/skeletons/DangerZone.skeleton.tsx delete mode 100644 src/pages/team/ui/settings/skeletons/DefaultSettings.skeleton.tsx delete mode 100644 src/pages/team/ui/settings/skeletons/InvitationSecurity.skeleton.tsx delete mode 100644 src/pages/team/ui/settings/skeletons/TeamIdentity.skeleton.tsx rename src/pages/{ => user}/invitations/api/useAcceptTeamInvitation.ts (100%) create mode 100644 src/pages/user/invitations/index.ts rename src/pages/{ => user}/invitations/lib/get-invitations-count-text.ts (100%) rename src/pages/{ => user}/invitations/ui/InvitationCard.skeleton.tsx (100%) rename src/pages/{ => user}/invitations/ui/InvitationCard.tsx (100%) create mode 100644 src/pages/user/invitations/ui/InvitationsPage.tsx rename src/pages/{ => user}/invitations/ui/InvitationsPageContent.tsx (100%) rename src/pages/{ => user}/invitations/ui/InvitationsPageFallback.tsx (100%) rename src/pages/{profile => user}/notifications/api/useUpdateNotifications.ts (100%) rename src/pages/{profile => user}/notifications/config/notifications.ts (100%) rename src/pages/{profile => user}/notifications/index.ts (100%) rename src/pages/{profile => user}/notifications/model/notifications.ts (100%) rename src/pages/{profile => user}/notifications/ui/NotificationsPage.tsx (100%) rename src/pages/{profile => user}/notifications/ui/NotificationsPageContent.tsx (100%) rename src/pages/{profile => user}/notifications/ui/NotificationsPageFallback.tsx (100%) rename src/pages/{profile/me => user/profile}/api/useConnectOauthProvider.ts (100%) rename src/pages/{profile/me => user/profile}/api/useDisconnectOauthProvider.ts (100%) rename src/pages/{profile/me => user/profile}/api/useUpdateProfile.ts (100%) rename src/pages/{profile/me => user/profile}/config/oauth-status.ts (100%) create mode 100644 src/pages/user/profile/index.ts rename src/pages/{profile/me => user/profile}/model/profile.ts (100%) rename src/pages/{profile/me => user/profile}/model/useMePage.ts (96%) rename src/pages/{profile/me => user/profile}/model/useOAuthManage.ts (100%) rename src/pages/{profile/me/ui/MePage.tsx => user/profile/ui/UserPage.tsx} (86%) rename src/pages/{profile/me => user/profile}/ui/oauth-section/OAuthItem.tsx (96%) rename src/pages/{profile/me => user/profile}/ui/oauth-section/OAuthSection.tsx (100%) rename src/pages/{profile/me => user/profile}/ui/oauth-section/OAuthSectionContent.tsx (100%) rename src/pages/{profile/me => user/profile}/ui/oauth-section/OAuthSectionFallback.tsx (100%) rename src/pages/{profile/me => user/profile}/ui/oauth-section/OAuthStatusBadge.tsx (100%) rename src/pages/{profile/me => user/profile}/ui/profile-section/IdentityItem.tsx (100%) rename src/pages/{profile/me => user/profile}/ui/profile-section/ProfileForm.tsx (100%) rename src/pages/{profile/me => user/profile}/ui/profile-section/ProfileSection.tsx (100%) rename src/pages/{profile/me => user/profile}/ui/profile-section/ProfileSectionContent.tsx (100%) rename src/pages/{profile/me => user/profile}/ui/profile-section/ProfileSectionFallback.tsx (100%) rename src/pages/{ => user}/teams/index.ts (100%) rename src/pages/{ => user}/teams/lib/get-teams-count-text.ts (100%) rename src/pages/{ => user}/teams/ui/TeamCard.skeleton.tsx (100%) rename src/pages/{ => user}/teams/ui/TeamCard.tsx (100%) rename src/pages/{ => user}/teams/ui/TeamCardActions.tsx (100%) rename src/pages/{ => user}/teams/ui/TeamsPage.tsx (100%) rename src/pages/{ => user}/teams/ui/TeamsPageContent.tsx (100%) rename src/pages/{ => user}/teams/ui/TeamsPageFallback.tsx (100%) diff --git a/app/(protected)/team/invitations/error.tsx b/app/(protected)/team/invitations/error.tsx new file mode 100644 index 0000000..4aa4c7d --- /dev/null +++ b/app/(protected)/team/invitations/error.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { ErrorState } from 'widgets/error-state'; + +export default function Error({ unstable_retry }: { unstable_retry: () => void }) { + return ( + unstable_retry()} + className="border" + /> + ); +} diff --git a/app/(protected)/team/invitations/page.tsx b/app/(protected)/team/invitations/page.tsx index 9f9f851..2b72773 100644 --- a/app/(protected)/team/invitations/page.tsx +++ b/app/(protected)/team/invitations/page.tsx @@ -1 +1 @@ -export { InvitationsPage as default } from 'pages/team'; +export { InvitationsPage as default } from 'pages/team/invitations'; diff --git a/app/(protected)/team/members/error.tsx b/app/(protected)/team/members/error.tsx new file mode 100644 index 0000000..3e37ec9 --- /dev/null +++ b/app/(protected)/team/members/error.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { ErrorState } from 'widgets/error-state'; + +export default function Error({ unstable_retry }: { unstable_retry: () => void }) { + return ( + unstable_retry()} + className="border" + /> + ); +} diff --git a/app/(protected)/team/members/page.tsx b/app/(protected)/team/members/page.tsx index 74be9f9..52a3f9d 100644 --- a/app/(protected)/team/members/page.tsx +++ b/app/(protected)/team/members/page.tsx @@ -1 +1 @@ -export { MembersPage as default } from 'pages/team'; +export { MembersPage as default } from 'pages/team/members'; diff --git a/app/(protected)/team/projects/page.tsx b/app/(protected)/team/projects/page.tsx index e50f0df..9544323 100644 --- a/app/(protected)/team/projects/page.tsx +++ b/app/(protected)/team/projects/page.tsx @@ -1 +1 @@ -export { ProjectsPage as default } from 'pages/projects'; +export { ProjectsPage as default } from 'pages/team/projects'; diff --git a/app/(protected)/team/settings/error.tsx b/app/(protected)/team/settings/error.tsx new file mode 100644 index 0000000..bab7c90 --- /dev/null +++ b/app/(protected)/team/settings/error.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { ErrorState } from 'widgets/error-state'; + +export default function Error({ unstable_retry }: { unstable_retry: () => void }) { + return ( + unstable_retry()} + className="border" + /> + ); +} diff --git a/app/(protected)/team/settings/page.tsx b/app/(protected)/team/settings/page.tsx index 420eb16..e85e395 100644 --- a/app/(protected)/team/settings/page.tsx +++ b/app/(protected)/team/settings/page.tsx @@ -1 +1 @@ -export { Settings as default } from 'pages/team'; +export { SettingsPage as default } from 'pages/team/settings'; diff --git a/app/(protected)/user/(user)/notifications/page.tsx b/app/(protected)/user/(user)/notifications/page.tsx index c606279..2f8866c 100644 --- a/app/(protected)/user/(user)/notifications/page.tsx +++ b/app/(protected)/user/(user)/notifications/page.tsx @@ -1 +1 @@ -export { NotificationsPage as default } from 'pages/profile/notifications'; +export { NotificationsPage as default } from 'pages/user/notifications'; diff --git a/app/(protected)/user/(user)/profile/page.tsx b/app/(protected)/user/(user)/profile/page.tsx index 1855560..cc2114f 100644 --- a/app/(protected)/user/(user)/profile/page.tsx +++ b/app/(protected)/user/(user)/profile/page.tsx @@ -1 +1 @@ -export { MePage as default } from 'pages/profile/me'; +export { UserPage as default } from 'pages/user/profile'; diff --git a/app/(protected)/user/teams/@invitations/page.tsx b/app/(protected)/user/teams/@invitations/page.tsx index 3ceee30..550c850 100644 --- a/app/(protected)/user/teams/@invitations/page.tsx +++ b/app/(protected)/user/teams/@invitations/page.tsx @@ -1 +1 @@ -export { InvitationsPage as default } from 'pages/invitations'; +export { InvitationsPage as default } from 'pages/user/invitations'; diff --git a/app/(protected)/user/teams/page.tsx b/app/(protected)/user/teams/page.tsx index 3050d1f..f3b17fc 100644 --- a/app/(protected)/user/teams/page.tsx +++ b/app/(protected)/user/teams/page.tsx @@ -1 +1 @@ -export { TeamsPage as default } from 'pages/teams'; +export { TeamsPage as default } from 'pages/user/teams'; diff --git a/src/pages/profile/me/index.ts b/src/pages/profile/me/index.ts deleted file mode 100644 index 16bf398..0000000 --- a/src/pages/profile/me/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { MePage } from './ui/MePage'; diff --git a/src/pages/profile/security/index.ts b/src/pages/profile/security/index.ts deleted file mode 100644 index ed2c465..0000000 --- a/src/pages/profile/security/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SecurityPage } from './ui/SecurityPage'; diff --git a/src/pages/profile/security/ui/SecurityPage.tsx b/src/pages/profile/security/ui/SecurityPage.tsx deleted file mode 100644 index bb2f9f5..0000000 --- a/src/pages/profile/security/ui/SecurityPage.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'use client'; - -import { CardSection, OptionItem, Separator, Switch } from 'shared/ui'; -import { formatDate } from 'shared/lib/utils'; -import { useQuery } from '@tanstack/react-query'; -import { UserQueries } from 'entities/user'; - -function SecurityPage() { - const query = useQuery(UserQueries.getMe()); - const is2faEnabled = query.data?.security.is2faEnabled ?? false; - const lastPasswordChange = formatDate(query.data?.security.lastPasswordChange ?? ''); - - return ( - - ( - - )} - /> - -
- Последняя смена пароля - {lastPasswordChange} -
-
- ); -} - -export { SecurityPage }; diff --git a/src/pages/profile/teams/index.ts b/src/pages/profile/teams/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/profile/teams/ui/TeamList.tsx b/src/pages/profile/teams/ui/TeamList.tsx deleted file mode 100644 index fbf5b00..0000000 --- a/src/pages/profile/teams/ui/TeamList.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { TeamAvatar, useTeamStore } from 'entities/team'; -import { UserQueries } from 'entities/user'; -import { RemoveTeamDialog } from 'features/teams/remove'; -import { Trash2Icon } from 'lucide-react'; -import { - Badge, - Button, - Item, - ItemActions, - ItemContent, - ItemDescription, - ItemMedia, - ItemTitle, -} from 'shared/ui'; -import { useSwitchTeam } from 'features/teams/active-team'; - -export function TeamsList() { - const teamsQuery = useQuery(UserQueries.getMyTeams()); - const teamId = useTeamStore.use.teamId(); - - const { switchTeam } = useSwitchTeam({ - teams: teamsQuery.data, - defaultOptions: { redirect: true }, - }); - - if (teamsQuery.isError) { - return ( -

- {teamsQuery.error.message} -

- ); - } - - const teams = teamsQuery.data ?? []; - - return ( -
    - {teams.map((team) => ( -
  • - - {team.permissions.isOwner ? ( - - Owner - - ) : null} - - - - - {team.name} - {team.description} - - -
    - - - - -
    -
    -
    -
  • - ))} -
- ); -} diff --git a/src/pages/project/boards/ui/task/TaskCard.tsx b/src/pages/project/boards/ui/task/TaskCard.tsx index 8036629..7479004 100644 --- a/src/pages/project/boards/ui/task/TaskCard.tsx +++ b/src/pages/project/boards/ui/task/TaskCard.tsx @@ -40,7 +40,6 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
-
{task.position}
{task.assignee && ( diff --git a/src/pages/team/api/useQueryInvitations.ts b/src/pages/team/api/useQueryInvitations.ts deleted file mode 100644 index ee1d4ea..0000000 --- a/src/pages/team/api/useQueryInvitations.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { TeamQueries, useTeamStore } from 'entities/team'; - -export function useQueryInvitations() { - const teamId = useTeamStore.use.teamId(); - - return useQuery({ - ...TeamQueries.getInvitations(teamId!), - enabled: Boolean(teamId), - }); -} diff --git a/src/pages/team/api/useQueryTeam.ts b/src/pages/team/api/useQueryTeam.ts deleted file mode 100644 index 596f758..0000000 --- a/src/pages/team/api/useQueryTeam.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { TeamQueries, useTeamStore } from 'entities/team'; - -export function useQueryTeam() { - const teamId = useTeamStore.use.teamId(); - - return useQuery({ - ...TeamQueries.getTeam(teamId!), - enabled: Boolean(teamId), - }); -} diff --git a/src/pages/team/config/member.ts b/src/pages/team/config/member.ts deleted file mode 100644 index 457a9f5..0000000 --- a/src/pages/team/config/member.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { type TTeam } from 'entities/team'; -import { ComponentProps } from 'react'; -import { Badge } from 'shared/ui'; - -interface IMemberCardConfig { - ringColor: Record; - bgColor: Record; - workloadColor: (w: number) => string; - statusBadgeVariant: (s: TTeam.MemberStatus) => ComponentProps['variant']; - workloadLabel: (w: number) => { text: string; color: string }; -} - -type EnsureAllStatuses = { - [K in TTeam.MemberStatus]: T; -}; - -export const memberCardConfig: IMemberCardConfig = { - ringColor: { - active: 'ring-primary', - banned: 'ring-destructive', - blocked: 'ring-destructive', - inactive: 'ring-muted', - pending: 'ring-muted', - } satisfies EnsureAllStatuses, - bgColor: { - active: 'bg-card', - banned: 'bg-destructive/10', - blocked: 'bg-destructive/10', - inactive: 'bg-muted/90', - pending: 'bg-muted/90', - } satisfies EnsureAllStatuses, - workloadColor: (w) => { - if (w === 0) return 'bg-muted/90'; - if (w <= 60) return 'bg-primary/40'; - if (w <= 80) return 'bg-primary'; - return 'bg-orange-500'; - }, - statusBadgeVariant: (s) => { - if (s === 'banned') return 'destructive'; - if (s === 'active') return 'default'; - if (s === 'inactive') return 'outline'; - }, - workloadLabel: (w) => { - if (w === 0) return { text: 'Не загружен', color: 'text-muted-foreground' }; - if (w <= 60) return { text: 'Низкая', color: 'text-muted-foreground' }; - if (w <= 80) return { text: 'Оптимальная', color: 'text-primary' }; - return { text: 'Перегружен', color: 'text-orange-600' }; - }, -}; diff --git a/src/pages/team/index.ts b/src/pages/team/index.ts deleted file mode 100644 index 06bb280..0000000 --- a/src/pages/team/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { InvitationsPage } from './ui/invitations/InvitationsPage'; -export { MembersPage } from './ui/members/MembersPage'; -export { Settings } from './ui/settings/SettingsPage'; diff --git a/src/pages/team/api/useRemoveMemberInvitation.ts b/src/pages/team/invitations/api/useRemoveInvitation.ts similarity index 89% rename from src/pages/team/api/useRemoveMemberInvitation.ts rename to src/pages/team/invitations/api/useRemoveInvitation.ts index 64ce1ec..fb08bf3 100644 --- a/src/pages/team/api/useRemoveMemberInvitation.ts +++ b/src/pages/team/invitations/api/useRemoveInvitation.ts @@ -7,10 +7,7 @@ export type UseRemoveMemberInvitationOptions = Omit< 'mutationFn' >; -export function useRemoveMemberInvitation({ - onSuccess, - ...rest -}: UseRemoveMemberInvitationOptions = {}) { +export function useRemoveInvitation({ onSuccess, ...rest }: UseRemoveMemberInvitationOptions = {}) { const teamId = useTeamStore.use.teamId(); return useMutation({ diff --git a/src/pages/team/api/useUpdateInvitation.ts b/src/pages/team/invitations/api/useUpdateInvitation.ts similarity index 100% rename from src/pages/team/api/useUpdateInvitation.ts rename to src/pages/team/invitations/api/useUpdateInvitation.ts diff --git a/src/pages/invitations/index.ts b/src/pages/team/invitations/index.ts similarity index 100% rename from src/pages/invitations/index.ts rename to src/pages/team/invitations/index.ts diff --git a/src/pages/team/invitations/lib/get-pending-members-count-text.ts b/src/pages/team/invitations/lib/get-pending-members-count-text.ts new file mode 100644 index 0000000..29e0a65 --- /dev/null +++ b/src/pages/team/invitations/lib/get-pending-members-count-text.ts @@ -0,0 +1,14 @@ +import { getPluralForm, type PluralForms } from 'shared/lib/utils'; + +const pendingMembersCountForms: PluralForms = { + one: 'участника', + few: 'участника', + two: 'участника', + many: 'участников', + zero: 'участников', + other: 'участников', +}; + +export function getPendingMembersCountText(count: number) { + return `${count} ${getPluralForm(count, pendingMembersCountForms)}`; +} diff --git a/src/pages/team/invitations/ui/InvitationCard.tsx b/src/pages/team/invitations/ui/InvitationCard.tsx new file mode 100644 index 0000000..27281b7 --- /dev/null +++ b/src/pages/team/invitations/ui/InvitationCard.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { ROLE_LABELS, type TTeam } from 'entities/team'; +import { formatDate } from 'shared/lib/utils'; +import { + Avatar, + AvatarFallback, + Item, + ItemActions, + ItemContent, + ItemDescription, + ItemMedia, + ItemTitle, +} from 'shared/ui'; +import { InvitationCardActions } from './InvitationCardActions'; + +interface InvitationCardProps { + invitation: TTeam.TeamInvitationResponse; +} + +export function InvitationCard({ invitation }: InvitationCardProps) { + const emailInitial = invitation.email.charAt(0).toUpperCase(); + + return ( + + + + {emailInitial} + + + + {invitation.email} + + {ROLE_LABELS[invitation.role as Exclude]} · Истекает{' '} + {formatDate(invitation.expiresAt)} + + + + + + + ); +} diff --git a/src/pages/team/invitations/ui/InvitationCardActions.tsx b/src/pages/team/invitations/ui/InvitationCardActions.tsx new file mode 100644 index 0000000..009d011 --- /dev/null +++ b/src/pages/team/invitations/ui/InvitationCardActions.tsx @@ -0,0 +1,88 @@ +'use client'; + +import { INVITATION_ROLES, ROLE_LABELS, type TTeam } from 'entities/team'; +import { Check, MoreHorizontal, Shield, Trash2 } from 'lucide-react'; +import { useState } from 'react'; +import { classNames } from 'shared/lib/utils'; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from 'shared/ui'; +import { useUpdateInvitation } from '../api/useUpdateInvitation'; +import { RemoveInvitationDialog } from './RemoveInvitationDialog'; + +interface InvitationCardActionsProps { + invitation: TTeam.TeamInvitationResponse; +} + +export function InvitationCardActions({ invitation }: InvitationCardActionsProps) { + const [removeDialogOpen, setRemoveDialogOpen] = useState(false); + const { mutate: updateInvitation, isPending } = useUpdateInvitation(); + + return ( +
{ + event.stopPropagation(); + }} + > + + + + + + + + + Роль + + + {INVITATION_ROLES.map((role) => ( + updateInvitation({ code: invitation.code, role })} + > + + {ROLE_LABELS[role]} + + ))} + + + + setRemoveDialogOpen(true)}> + + Отозвать + + + + + +
+ ); +} diff --git a/src/pages/team/ui/invitations/InvitationsEmpty.tsx b/src/pages/team/invitations/ui/InvitationsEmpty.tsx similarity index 100% rename from src/pages/team/ui/invitations/InvitationsEmpty.tsx rename to src/pages/team/invitations/ui/InvitationsEmpty.tsx diff --git a/src/pages/invitations/ui/InvitationsPage.tsx b/src/pages/team/invitations/ui/InvitationsPage.tsx similarity index 100% rename from src/pages/invitations/ui/InvitationsPage.tsx rename to src/pages/team/invitations/ui/InvitationsPage.tsx diff --git a/src/pages/team/invitations/ui/InvitationsPageContent.tsx b/src/pages/team/invitations/ui/InvitationsPageContent.tsx new file mode 100644 index 0000000..c523d46 --- /dev/null +++ b/src/pages/team/invitations/ui/InvitationsPageContent.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { useSuspenseQuery } from '@tanstack/react-query'; +import { TeamQueries, useTeamStore } from 'entities/team'; +import { InviteTeamMemberDialog } from 'features/teams/invite'; +import { Plus } from 'lucide-react'; +import { Button } from 'shared/ui'; +import { PageWrapper } from 'widgets/page-wrapper'; +import { getPendingMembersCountText } from '../lib/get-pending-members-count-text'; +import { InvitationCard } from './InvitationCard'; +import { InvitationsEmpty } from './InvitationsEmpty'; + +export function InvitationsPageContent() { + const teamId = useTeamStore.use.teamId() ?? ''; + const { data } = useSuspenseQuery(TeamQueries.getInvitations(teamId)); + const invitations = data ?? []; + const total = invitations.length; + + return ( + + + + } + > + {total === 0 ? ( + + ) : ( +
+ {invitations.map((invitation) => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/pages/team/invitations/ui/InvitationsPageFallback.tsx b/src/pages/team/invitations/ui/InvitationsPageFallback.tsx new file mode 100644 index 0000000..c4fdb09 --- /dev/null +++ b/src/pages/team/invitations/ui/InvitationsPageFallback.tsx @@ -0,0 +1,45 @@ +import { + Item, + ItemActions, + ItemContent, + ItemDescription, + ItemMedia, + ItemTitle, + Skeleton, +} from 'shared/ui'; +import { PageWrapper } from 'widgets/page-wrapper'; + +export function InvitationsPageFallback() { + return ( + } + action={} + > +
+ {Array.from({ length: 4 }).map((_, index) => ( + + + + + + + + + + + + + + + + + ))} +
+
+ ); +} diff --git a/src/pages/team/ui/invitations/RemoveInvitationDialog.tsx b/src/pages/team/invitations/ui/RemoveInvitationDialog.tsx similarity index 58% rename from src/pages/team/ui/invitations/RemoveInvitationDialog.tsx rename to src/pages/team/invitations/ui/RemoveInvitationDialog.tsx index 8879af1..e0be53d 100644 --- a/src/pages/team/ui/invitations/RemoveInvitationDialog.tsx +++ b/src/pages/team/invitations/ui/RemoveInvitationDialog.tsx @@ -1,5 +1,8 @@ +'use client'; + import { MailX } from 'lucide-react'; -import { PropsWithChildren } from 'react'; +import { ComponentProps } from 'react'; +import { useControllableState } from 'shared/lib/hooks'; import { AlertDialog, AlertDialogAction, @@ -12,18 +15,35 @@ import { AlertDialogTitle, AlertDialogTrigger, } from 'shared/ui'; -import { useRemoveMemberInvitation } from '../../api/useRemoveMemberInvitation'; +import { useRemoveInvitation } from '../api/useRemoveInvitation'; -interface RemoveInvitationDialogProps extends PropsWithChildren { +interface RemoveInvitationDialogProps extends ComponentProps { code: string; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; + open?: boolean; } -export function RemoveInvitationDialog({ code, children }: RemoveInvitationDialogProps) { - const { mutate, isPending } = useRemoveMemberInvitation(); +export function RemoveInvitationDialog({ + code, + defaultOpen, + onOpenChange, + open, + ...props +}: RemoveInvitationDialogProps) { + const [dialogOpen, setDialogOpen] = useControllableState({ + defaultValue: defaultOpen, + value: open, + onChange: onOpenChange, + }); + + const { mutate, isPending } = useRemoveInvitation({ + onSuccess: () => setDialogOpen(false), + }); return ( - - {children} + + {props.children ? : null} diff --git a/src/pages/team/api/useRemoveMember.ts b/src/pages/team/members/api/useRemoveMember.ts similarity index 100% rename from src/pages/team/api/useRemoveMember.ts rename to src/pages/team/members/api/useRemoveMember.ts diff --git a/src/pages/team/api/useUpdateMember.ts b/src/pages/team/members/api/useUpdateMember.ts similarity index 100% rename from src/pages/team/api/useUpdateMember.ts rename to src/pages/team/members/api/useUpdateMember.ts diff --git a/src/pages/team/members/config/member.ts b/src/pages/team/members/config/member.ts new file mode 100644 index 0000000..82d99b3 --- /dev/null +++ b/src/pages/team/members/config/member.ts @@ -0,0 +1,27 @@ +import { type TTeam } from 'entities/team'; + +interface IMemberCardConfig { + ringColor: Record; + bgColor: Record; +} + +type EnsureAllStatuses = { + [K in TTeam.MemberStatus]: T; +}; + +export const memberCardConfig: IMemberCardConfig = { + ringColor: { + active: 'ring-primary', + banned: 'ring-destructive', + blocked: 'ring-destructive', + inactive: 'ring-muted', + pending: 'ring-muted', + } satisfies EnsureAllStatuses, + bgColor: { + active: 'bg-card', + banned: 'bg-destructive/10', + blocked: 'bg-destructive/10', + inactive: 'bg-muted/90', + pending: 'bg-muted/90', + } satisfies EnsureAllStatuses, +}; diff --git a/src/pages/team/members/index.ts b/src/pages/team/members/index.ts new file mode 100644 index 0000000..f9509aa --- /dev/null +++ b/src/pages/team/members/index.ts @@ -0,0 +1 @@ +export { MembersPage } from './ui/MembersPage'; diff --git a/src/pages/team/model/useMembersPage.ts b/src/pages/team/members/model/useMembersPage.ts similarity index 84% rename from src/pages/team/model/useMembersPage.ts rename to src/pages/team/members/model/useMembersPage.ts index 1e472ee..eb10d14 100644 --- a/src/pages/team/model/useMembersPage.ts +++ b/src/pages/team/members/model/useMembersPage.ts @@ -1,13 +1,13 @@ 'use client'; -import { useQuery } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { TeamQueries, type TTeam, useTeamStore } from 'entities/team'; import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'; import { debounce } from 'shared/lib/utils'; export function useMembersPage() { - const teamId = useTeamStore.use.teamId(); - const { data, isPending } = useQuery(TeamQueries.getMembers(teamId!)); + const teamId = useTeamStore.use.teamId() ?? ''; + const { data } = useSuspenseQuery(TeamQueries.getMembers(teamId)); const [search, setSearch] = useState(''); const [filtered, setFiltered] = useState([]); @@ -45,7 +45,6 @@ export function useMembersPage() { search, onChange, filtered, - total: data?.items?.length ?? 0, - isPending, + total: data.items.length, }; } diff --git a/src/pages/team/members/ui/MemberCard.tsx b/src/pages/team/members/ui/MemberCard.tsx new file mode 100644 index 0000000..c8ba95e --- /dev/null +++ b/src/pages/team/members/ui/MemberCard.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { ROLE_LABELS, STATUS_LABELS, type TTeam } from 'entities/team'; +import { classNames } from 'shared/lib/utils'; +import { + Avatar, + AvatarFallback, + AvatarImage, + Item, + ItemActions, + ItemContent, + ItemDescription, + ItemMedia, + ItemTitle, + OwnerWrap, +} from 'shared/ui'; +import { memberCardConfig as cfg } from '../config/member'; +import { MemberCardActions } from './MemberCardActions'; + +interface MemberCardProps { + member: TTeam.TeamMemberResponse; +} + +export function MemberCard({ member }: MemberCardProps) { + const isOwner = member.role === 'owner'; + + return ( + + + + + + + + + + + {member.fullName} + + {isOwner + ? 'Владелец' + : `${ROLE_LABELS[member.role as Exclude]} · ${STATUS_LABELS[member.status]}`} + + + + + + + ); +} diff --git a/src/pages/team/members/ui/MemberCardActions.tsx b/src/pages/team/members/ui/MemberCardActions.tsx new file mode 100644 index 0000000..1ae8f7d --- /dev/null +++ b/src/pages/team/members/ui/MemberCardActions.tsx @@ -0,0 +1,118 @@ +'use client'; + +import { + INVITATION_ROLES, + MEMBER_STATUSES, + ROLE_LABELS, + STATUS_LABELS, + type TTeam, +} from 'entities/team'; +import { Check, CircleDot, MoreHorizontal, Shield, Trash2 } from 'lucide-react'; +import { useState } from 'react'; +import { classNames } from 'shared/lib/utils'; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from 'shared/ui'; +import { useUpdateMember } from '../api/useUpdateMember'; +import { RemoveMemberDialog } from './RemoveMemberDialog'; + +interface MemberCardActionsProps { + member: TTeam.TeamMemberResponse; +} + +export function MemberCardActions({ member }: MemberCardActionsProps) { + const [removeDialogOpen, setRemoveDialogOpen] = useState(false); + const { mutate: updateMember, isPending } = useUpdateMember(); + + return ( +
{ + event.stopPropagation(); + }} + > + + + + + + + + + Роль + + + {INVITATION_ROLES.map((role) => ( + updateMember({ userId: member.id, role })} + > + + {ROLE_LABELS[role]} + + ))} + + + + + + + Статус + + + {MEMBER_STATUSES.map((status) => ( + updateMember({ userId: member.id, status })} + > + + {STATUS_LABELS[status]} + + ))} + + + + setRemoveDialogOpen(true)}> + + Удалить + + + + + +
+ ); +} diff --git a/src/pages/team/members/ui/MembersPage.tsx b/src/pages/team/members/ui/MembersPage.tsx new file mode 100644 index 0000000..2cc1faa --- /dev/null +++ b/src/pages/team/members/ui/MembersPage.tsx @@ -0,0 +1,16 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import { MembersPageFallback } from './MembersPageFallback'; + +const MembersPageContent = dynamic( + () => import('./MembersPageContent').then((mod) => mod.MembersPageContent), + { + ssr: false, + loading: () => , + } +); + +export function MembersPage() { + return ; +} diff --git a/src/pages/team/members/ui/MembersPageContent.tsx b/src/pages/team/members/ui/MembersPageContent.tsx new file mode 100644 index 0000000..cd8c517 --- /dev/null +++ b/src/pages/team/members/ui/MembersPageContent.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { InviteTeamMemberDialog } from 'features/teams/invite'; +import { Plus } from 'lucide-react'; +import { Button, Search } from 'shared/ui'; +import { PageWrapper } from 'widgets/page-wrapper'; +import { useMembersPage } from '../model/useMembersPage'; +import { MemberCard } from './MemberCard'; + +export function MembersPageContent() { + const { search, onChange, filtered, total } = useMembersPage(); + + return ( + + Показано {filtered.length} из {total} + + } + action={ + + + + } + > +
+ +
+ +
+ {filtered.map((member) => ( + + ))} +
+
+ ); +} diff --git a/src/pages/team/members/ui/MembersPageFallback.tsx b/src/pages/team/members/ui/MembersPageFallback.tsx new file mode 100644 index 0000000..57b9c19 --- /dev/null +++ b/src/pages/team/members/ui/MembersPageFallback.tsx @@ -0,0 +1,49 @@ +import { + Item, + ItemActions, + ItemContent, + ItemDescription, + ItemMedia, + ItemTitle, + Skeleton, +} from 'shared/ui'; +import { PageWrapper } from 'widgets/page-wrapper'; + +export function MembersPageFallback() { + return ( + } + action={} + > +
+ +
+ +
+ {Array.from({ length: 8 }).map((_, index) => ( + + + + + + + + + + + + + + + + + ))} +
+
+ ); +} diff --git a/src/pages/team/ui/members/RemoveMemberDialog.tsx b/src/pages/team/members/ui/RemoveMemberDialog.tsx similarity index 68% rename from src/pages/team/ui/members/RemoveMemberDialog.tsx rename to src/pages/team/members/ui/RemoveMemberDialog.tsx index 192990c..3485856 100644 --- a/src/pages/team/ui/members/RemoveMemberDialog.tsx +++ b/src/pages/team/members/ui/RemoveMemberDialog.tsx @@ -1,5 +1,8 @@ +'use client'; + import { UserX } from 'lucide-react'; import { ComponentProps } from 'react'; +import { useControllableState } from 'shared/lib/hooks'; import { AlertDialog, AlertDialogAction, @@ -12,18 +15,36 @@ import { AlertDialogTitle, AlertDialogTrigger, } from 'shared/ui'; -import { useRemoveMember } from '../../api/useRemoveMember'; +import { useRemoveMember } from '../api/useRemoveMember'; interface RemoveMemberDialogProps extends ComponentProps { userId: string; name: string; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; + open?: boolean; } -export function RemoveMemberDialog({ userId, name, ...props }: RemoveMemberDialogProps) { - const { mutate, isPending } = useRemoveMember(); +export function RemoveMemberDialog({ + userId, + name, + defaultOpen, + onOpenChange, + open, + ...props +}: RemoveMemberDialogProps) { + const [dialogOpen, setDialogOpen] = useControllableState({ + defaultValue: defaultOpen, + value: open, + onChange: onOpenChange, + }); + + const { mutate, isPending } = useRemoveMember({ + onSuccess: () => setDialogOpen(false), + }); return ( - + {props.children ? : null} diff --git a/src/pages/team/model/roles-mock.ts b/src/pages/team/model/roles-mock.ts deleted file mode 100644 index ff0a70d..0000000 --- a/src/pages/team/model/roles-mock.ts +++ /dev/null @@ -1,81 +0,0 @@ -export type RoleKey = 'Admin' | 'Member' | 'Guest' | 'Viewer'; - -export type PermissionKey = - | 'task.create' - | 'task.edit' - | 'task.delete' - | 'project.invite' - | 'project.create' - | 'billing.view'; - -export const PERMISSION_GROUPS: { - label: string; - items: { key: PermissionKey; label: string }[]; -}[] = [ - { - label: 'Управление задачами', - items: [ - { key: 'task.create', label: 'Создавать задачи' }, - { key: 'task.edit', label: 'Редактировать задачи' }, - { key: 'task.delete', label: 'Удалять задачи' }, - ], - }, - { - label: 'Доступ к проектам', - items: [ - { key: 'project.invite', label: 'Приглашать участников' }, - { key: 'project.create', label: 'Создавать проекты' }, - ], - }, - { - label: 'Биллинг', - items: [{ key: 'billing.view', label: 'Просмотр счетов' }], - }, -]; - -export const ROLES: { key: RoleKey; description: string; members: number; locked?: boolean }[] = [ - { - key: 'Admin', - description: 'Полный контроль команды и биллинг.', - members: 2, - locked: true, - }, - { key: 'Member', description: 'Стандартный доступ участника.', members: 12 }, - { key: 'Guest', description: 'Внешний, ограниченный доступ к проектам.', members: 4 }, - { key: 'Viewer', description: 'Доступ к проектам только для чтения.', members: 7 }, -]; - -export const DEFAULTS: Record> = { - Admin: { - 'task.create': true, - 'task.edit': true, - 'task.delete': true, - 'project.invite': true, - 'project.create': true, - 'billing.view': true, - }, - Member: { - 'task.create': true, - 'task.edit': true, - 'task.delete': false, - 'project.invite': false, - 'project.create': true, - 'billing.view': false, - }, - Guest: { - 'task.create': true, - 'task.edit': false, - 'task.delete': false, - 'project.invite': false, - 'project.create': false, - 'billing.view': false, - }, - Viewer: { - 'task.create': false, - 'task.edit': false, - 'task.delete': false, - 'project.invite': false, - 'project.create': false, - 'billing.view': false, - }, -}; diff --git a/src/pages/team/model/team-identity.ts b/src/pages/team/model/team-identity.ts deleted file mode 100644 index 82f78e5..0000000 --- a/src/pages/team/model/team-identity.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function getTeamPathPrefix() { - const origin = window?.location.origin; - - return origin ? `${origin}/team/` : '/team/'; -} diff --git a/src/pages/projects/config/project-status-map.ts b/src/pages/team/projects/config/project-status-map.ts similarity index 100% rename from src/pages/projects/config/project-status-map.ts rename to src/pages/team/projects/config/project-status-map.ts diff --git a/src/pages/projects/index.ts b/src/pages/team/projects/index.ts similarity index 100% rename from src/pages/projects/index.ts rename to src/pages/team/projects/index.ts diff --git a/src/pages/projects/lib/get-projects-count-text.ts b/src/pages/team/projects/lib/get-projects-count-text.ts similarity index 100% rename from src/pages/projects/lib/get-projects-count-text.ts rename to src/pages/team/projects/lib/get-projects-count-text.ts diff --git a/src/pages/projects/ui/ProjectCard.tsx b/src/pages/team/projects/ui/ProjectCard.tsx similarity index 100% rename from src/pages/projects/ui/ProjectCard.tsx rename to src/pages/team/projects/ui/ProjectCard.tsx diff --git a/src/pages/projects/ui/ProjectsPage.tsx b/src/pages/team/projects/ui/ProjectsPage.tsx similarity index 100% rename from src/pages/projects/ui/ProjectsPage.tsx rename to src/pages/team/projects/ui/ProjectsPage.tsx diff --git a/src/pages/projects/ui/ProjectsPageContent.tsx b/src/pages/team/projects/ui/ProjectsPageContent.tsx similarity index 100% rename from src/pages/projects/ui/ProjectsPageContent.tsx rename to src/pages/team/projects/ui/ProjectsPageContent.tsx diff --git a/src/pages/projects/ui/ProjectsPageFallback.tsx b/src/pages/team/projects/ui/ProjectsPageFallback.tsx similarity index 100% rename from src/pages/projects/ui/ProjectsPageFallback.tsx rename to src/pages/team/projects/ui/ProjectsPageFallback.tsx diff --git a/src/pages/team/settings/api/useQueryTeam.ts b/src/pages/team/settings/api/useQueryTeam.ts new file mode 100644 index 0000000..3370e20 --- /dev/null +++ b/src/pages/team/settings/api/useQueryTeam.ts @@ -0,0 +1,8 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; +import { TeamQueries, useTeamStore } from 'entities/team'; + +export function useQueryTeam() { + const teamId = useTeamStore.use.teamId() ?? ''; + + return useSuspenseQuery(TeamQueries.getTeam(teamId)); +} diff --git a/src/pages/team/api/useUpdateTeam.ts b/src/pages/team/settings/api/useUpdateTeam.ts similarity index 100% rename from src/pages/team/api/useUpdateTeam.ts rename to src/pages/team/settings/api/useUpdateTeam.ts diff --git a/src/pages/team/api/useUploadCover.ts b/src/pages/team/settings/api/useUploadCover.ts similarity index 100% rename from src/pages/team/api/useUploadCover.ts rename to src/pages/team/settings/api/useUploadCover.ts diff --git a/src/pages/team/settings/index.ts b/src/pages/team/settings/index.ts new file mode 100644 index 0000000..c730dcd --- /dev/null +++ b/src/pages/team/settings/index.ts @@ -0,0 +1 @@ +export { SettingsPage } from './ui/SettingsPage'; diff --git a/src/pages/team/model/settings.ts b/src/pages/team/settings/model/types.ts similarity index 100% rename from src/pages/team/model/settings.ts rename to src/pages/team/settings/model/types.ts diff --git a/src/pages/team/ui/settings/DangerZone.tsx b/src/pages/team/settings/ui/DangerZone.tsx similarity index 100% rename from src/pages/team/ui/settings/DangerZone.tsx rename to src/pages/team/settings/ui/DangerZone.tsx diff --git a/src/pages/team/ui/settings/SaveBar.tsx b/src/pages/team/settings/ui/SaveBar.tsx similarity index 88% rename from src/pages/team/ui/settings/SaveBar.tsx rename to src/pages/team/settings/ui/SaveBar.tsx index 0992d04..e0d36cd 100644 --- a/src/pages/team/ui/settings/SaveBar.tsx +++ b/src/pages/team/settings/ui/SaveBar.tsx @@ -1,8 +1,8 @@ import { type TTeam } from 'entities/team'; import { useFormContext, useFormState } from 'react-hook-form'; import { FloatingSaveBar } from 'shared/ui'; -import { useUpdateTeam } from '../../api/useUpdateTeam'; -import { type TeamSettingsFormValues } from '../../model/settings'; +import { useUpdateTeam } from '../api/useUpdateTeam'; +import { type TeamSettingsFormValues } from '../model/types'; export function SaveBar({ team }: { team: TTeam.TeamDetailsResponse }) { const form = useFormContext(); diff --git a/src/pages/team/settings/ui/SettingsPage.tsx b/src/pages/team/settings/ui/SettingsPage.tsx new file mode 100644 index 0000000..ccc8a4c --- /dev/null +++ b/src/pages/team/settings/ui/SettingsPage.tsx @@ -0,0 +1,16 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import { SettingsPageFallback } from './SettingsPageFallback'; + +const SettingsPageContent = dynamic( + () => import('./SettingsPageContent').then((mod) => mod.SettingsPageContent), + { + ssr: false, + loading: () => , + } +); + +export function SettingsPage() { + return ; +} diff --git a/src/pages/team/settings/ui/SettingsPageContent.tsx b/src/pages/team/settings/ui/SettingsPageContent.tsx new file mode 100644 index 0000000..c439f29 --- /dev/null +++ b/src/pages/team/settings/ui/SettingsPageContent.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useLayoutEffect } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { PageWrapper } from 'widgets/page-wrapper'; +import { useQueryTeam } from '../api/useQueryTeam'; +import { TeamSettingsFormSchema, type TeamSettingsFormValues } from '../model/types'; +import { DangerZone } from './DangerZone'; +import { SaveBar } from './SaveBar'; +import { TeamIdentity } from './TeamIdentity'; + +export function SettingsPageContent() { + const { data: team } = useQueryTeam(); + + const form = useForm({ + resolver: zodResolver(TeamSettingsFormSchema), + mode: 'onChange', + defaultValues: { + name: '', + description: '', + }, + }); + + const { reset } = form; + + useLayoutEffect(() => { + reset({ + name: team.name, + description: team.description || '', + }); + }, [reset, team]); + + return ( + + +
+ + + + +
+
+ ); +} diff --git a/src/pages/team/settings/ui/SettingsPageFallback.tsx b/src/pages/team/settings/ui/SettingsPageFallback.tsx new file mode 100644 index 0000000..9662412 --- /dev/null +++ b/src/pages/team/settings/ui/SettingsPageFallback.tsx @@ -0,0 +1,60 @@ +import { ComponentProps } from 'react'; +import { classNames } from 'shared/lib/utils'; +import { CardSection, Item, ItemActions, ItemContent, ItemMedia, Skeleton } from 'shared/ui'; +import { PageWrapper } from 'widgets/page-wrapper'; + +export function SettingsPageFallback() { + return ( + +
+ +
+ + +
+ +
+ + +
+
+ + + +
+
+ + + + + + + + + + + + + +
+
+ ); +} + +function FieldSkeleton({ className, ...props }: ComponentProps) { + return ( +
+ + +
+ ); +} diff --git a/src/pages/team/ui/settings/TeamCover.tsx b/src/pages/team/settings/ui/TeamCover.tsx similarity index 95% rename from src/pages/team/ui/settings/TeamCover.tsx rename to src/pages/team/settings/ui/TeamCover.tsx index da0627b..6dc9f36 100644 --- a/src/pages/team/ui/settings/TeamCover.tsx +++ b/src/pages/team/settings/ui/TeamCover.tsx @@ -2,7 +2,7 @@ import { ImagePlus } from 'lucide-react'; import Image from 'next/image'; import { type ChangeEvent, useRef } from 'react'; import { Button } from 'shared/ui'; -import { useUploadCover, type UseUploadFileOptions } from '../../api/useUploadCover'; +import { useUploadCover, type UseUploadFileOptions } from '../api/useUploadCover'; interface TeamCoverProps { mutationOptions?: UseUploadFileOptions; diff --git a/src/pages/team/ui/settings/TeamIdentity.tsx b/src/pages/team/settings/ui/TeamIdentity.tsx similarity index 100% rename from src/pages/team/ui/settings/TeamIdentity.tsx rename to src/pages/team/settings/ui/TeamIdentity.tsx diff --git a/src/pages/team/ui/settings/TeamIdentityForm.tsx b/src/pages/team/settings/ui/TeamIdentityForm.tsx similarity index 100% rename from src/pages/team/ui/settings/TeamIdentityForm.tsx rename to src/pages/team/settings/ui/TeamIdentityForm.tsx diff --git a/src/pages/team/ui/invitations/InvitationCard.skeleton.tsx b/src/pages/team/ui/invitations/InvitationCard.skeleton.tsx deleted file mode 100644 index 0eed609..0000000 --- a/src/pages/team/ui/invitations/InvitationCard.skeleton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Skeleton } from 'shared/ui'; - -export function InvitationCardSkeleton() { - return ( -
-
-
- -
- - -
-
-
- -
- - -
- -
- - -
-
- ); -} diff --git a/src/pages/team/ui/invitations/InvitationCard.tsx b/src/pages/team/ui/invitations/InvitationCard.tsx deleted file mode 100644 index 72b1e2a..0000000 --- a/src/pages/team/ui/invitations/InvitationCard.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { type TTeam } from 'entities/team'; -import { Clock, Copy, MailIcon, RotateCw, X } from 'lucide-react'; -import { ComponentProps } from 'react'; -import { classNames, formatDate } from 'shared/lib/utils'; -import { - Badge, - Button, - Item, - ItemActions, - ItemContent, - ItemFooter, - ItemGroup, - ItemHeader, -} from 'shared/ui'; -import { InvitationRoleSelect } from './InvitationRoleSelect'; -import { RemoveInvitationDialog } from './RemoveInvitationDialog'; - -interface InvitationCardProps extends Omit, 'children'> { - inv: TTeam.TeamInvitationResponse; -} - -export function InvitationCard({ className, inv, ...props }: InvitationCardProps) { - return ( - - - -
-
{inv.email}
-
-
- Роль: - -
-
- - - Отправлено {formatDate(inv.createdAt)} - -
-
-
- - - - - -
- -
- - - Ссылка истекает{' '} - {formatDate(inv.expiresAt)} - -
-
- - - - -
-
- ); -} diff --git a/src/pages/team/ui/invitations/InvitationRoleSelect.tsx b/src/pages/team/ui/invitations/InvitationRoleSelect.tsx deleted file mode 100644 index 2803897..0000000 --- a/src/pages/team/ui/invitations/InvitationRoleSelect.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { INVITATION_ROLES, ROLE_LABELS, type TTeam } from 'entities/team'; -import { ComponentProps } from 'react'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from 'shared/ui'; -import { useUpdateInvitation } from '../../api/useUpdateInvitation'; - -interface InvitationRoleSelectProps extends Omit< - ComponentProps, - 'children' | 'value' | 'onValueChange' | 'disabled' -> { - code: string; - role: TTeam.TeamRole; -} - -export function InvitationRoleSelect({ code, role, ...props }: InvitationRoleSelectProps) { - const { mutate: updateInvitation, isPending } = useUpdateInvitation(); - - return ( - - ); -} diff --git a/src/pages/team/ui/invitations/InvitationsPage.tsx b/src/pages/team/ui/invitations/InvitationsPage.tsx deleted file mode 100644 index 77016d6..0000000 --- a/src/pages/team/ui/invitations/InvitationsPage.tsx +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; - -import { useQueryInvitations } from '../../api/useQueryInvitations'; -import { InvitationCard } from './InvitationCard'; -import { InvitationCardSkeleton } from './InvitationCard.skeleton'; -import { InvitationsEmpty } from './InvitationsEmpty'; - -export function InvitationsPage() { - const { data, isPending } = useQueryInvitations(); - - if (!isPending && !data?.length) { - return ; - } - - return ( -
- {isPending - ? Array.from({ length: 6 }).map((_, i) => ) - : data?.map((inv) => )} -
- ); -} diff --git a/src/pages/team/ui/members/MemberCard.skeleton.tsx b/src/pages/team/ui/members/MemberCard.skeleton.tsx deleted file mode 100644 index 342f751..0000000 --- a/src/pages/team/ui/members/MemberCard.skeleton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Skeleton } from 'shared/ui'; - -export function MemberCardSkeleton() { - return ( -
-
- -
- - -
-
-
- - -
-
- - -
- -
- ); -} diff --git a/src/pages/team/ui/members/MemberCard.tsx b/src/pages/team/ui/members/MemberCard.tsx deleted file mode 100644 index 0ff058d..0000000 --- a/src/pages/team/ui/members/MemberCard.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { ROLE_LABELS, type TTeam } from 'entities/team'; -import { X } from 'lucide-react'; -import { ComponentProps } from 'react'; -import { classNames } from 'shared/lib/utils'; -import { - Avatar, - AvatarFallback, - AvatarImage, - Button, - Item, - ItemActions, - ItemContent, - ItemGroup, - ItemHeader, - OwnerWrap, -} from 'shared/ui'; -import { memberCardConfig as cfg } from '../../config/member'; -import { MemberRoleSelect } from './MemberRoleSelect'; -import { MemberStatusSelect } from './MemberStatusSelect'; -import { RemoveMemberDialog } from './RemoveMemberDialog'; - -interface MemberCardProps extends Omit, 'children'> { - member: TTeam.TeamMemberResponse; -} - -export function MemberCard({ className, member, ...props }: MemberCardProps) { - const isOwner = member.role === 'owner'; - - return ( - - - - - - - - - -
-

{member.fullName}

- {member.role !== 'owner' && ( - {ROLE_LABELS[member.role]} - )} -
- {!isOwner && ( - - - - - - )} -
- {!isOwner && ( - -
- - -
-
- )} -
-
- ); -} diff --git a/src/pages/team/ui/members/MemberRoleSelect.tsx b/src/pages/team/ui/members/MemberRoleSelect.tsx deleted file mode 100644 index 65e007a..0000000 --- a/src/pages/team/ui/members/MemberRoleSelect.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { INVITATION_ROLES, ROLE_LABELS, type TTeam } from 'entities/team'; -import { ComponentProps } from 'react'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from 'shared/ui'; -import { useUpdateMember } from '../../api/useUpdateMember'; - -interface MemberRoleSelectProps extends Omit< - ComponentProps, - 'children' | 'value' | 'onValueChange' | 'disabled' -> { - userId: string; - role: TTeam.TeamRole; -} - -export function MemberRoleSelect({ userId, role, ...props }: MemberRoleSelectProps) { - const { mutate: updateMember, isPending } = useUpdateMember(); - - return ( - - ); -} diff --git a/src/pages/team/ui/members/MemberStatusSelect.tsx b/src/pages/team/ui/members/MemberStatusSelect.tsx deleted file mode 100644 index 597a5c3..0000000 --- a/src/pages/team/ui/members/MemberStatusSelect.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { MEMBER_STATUSES, STATUS_LABELS, type TTeam } from 'entities/team'; -import { ComponentProps } from 'react'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from 'shared/ui'; -import { useUpdateMember } from '../../api/useUpdateMember'; - -interface MemberStatusSelectProps extends Omit< - ComponentProps, - 'children' | 'value' | 'onValueChange' | 'disabled' -> { - userId: string; - status: TTeam.MemberStatus; -} - -export function MemberStatusSelect({ userId, status, ...props }: MemberStatusSelectProps) { - const { mutate: updateMember, isPending } = useUpdateMember(); - - return ( - - ); -} diff --git a/src/pages/team/ui/members/MembersPage.tsx b/src/pages/team/ui/members/MembersPage.tsx deleted file mode 100644 index bfc0e26..0000000 --- a/src/pages/team/ui/members/MembersPage.tsx +++ /dev/null @@ -1,37 +0,0 @@ -'use client'; - -import { Plus } from 'lucide-react'; -import { InviteTeamMemberDialog } from 'features/teams/invite'; -import { Button, Search } from 'shared/ui'; -import { MemberCard } from './MemberCard'; -import { MemberCardSkeleton } from './MemberCard.skeleton'; -import { useMembersPage } from '../../model/useMembersPage'; - -export function MembersPage() { - const { search, onChange, filtered, total, isPending } = useMembersPage(); - - return ( - <> -
- - - - -
- -
-

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

-
- -
- {isPending - ? Array.from({ length: 8 }).map((_, i) => ) - : filtered.map((m) => )} -
- - ); -} diff --git a/src/pages/team/ui/settings/DefaultSettings.tsx b/src/pages/team/ui/settings/DefaultSettings.tsx deleted file mode 100644 index dd3c8b3..0000000 --- a/src/pages/team/ui/settings/DefaultSettings.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useId } from 'react'; -import { - Badge, - CardSection, - Field, - FieldLabel, - Input, - OptionItem, - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, - Switch, -} from 'shared/ui'; - -export function DefaultSettings() { - const roleId = useId(); - const autoJoinId = useId(); - - return ( - - Настройки по умолчанию - Не реализовано - - } - description="Применяется к новым участникам." - > - - Роль по умолчанию для новых участников - - - - Домен для автоматического входа - - - } - /> - - ); -} diff --git a/src/pages/team/ui/settings/InvitationSecurity.tsx b/src/pages/team/ui/settings/InvitationSecurity.tsx deleted file mode 100644 index 89e590c..0000000 --- a/src/pages/team/ui/settings/InvitationSecurity.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useId } from 'react'; -import { - Badge, - CardSection, - Field, - FieldLabel, - OptionItem, - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, - Switch, -} from 'shared/ui'; - -export function InvitationSecurity() { - const id = useId(); - - return ( - - Безопасность приглашений - Не реализовано - - } - description="Настройте поведение приглашений." - > - - Срок действия ссылки приглашения - - - } - /> - - ); -} diff --git a/src/pages/team/ui/settings/SettingsPage.tsx b/src/pages/team/ui/settings/SettingsPage.tsx deleted file mode 100644 index 1700871..0000000 --- a/src/pages/team/ui/settings/SettingsPage.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; -import { useQueryTeam } from '../../api/useQueryTeam'; -import { TeamSettingsFormSchema, type TeamSettingsFormValues } from '../../model/settings'; -import { DangerZone } from './DangerZone'; -import { SaveBar } from './SaveBar'; -import { TeamIdentity } from './TeamIdentity'; -import { DangerZoneSkeleton } from './skeletons/DangerZone.skeleton'; -import { TeamIdentitySkeleton } from './skeletons/TeamIdentity.skeleton'; -import { zodResolver } from '@hookform/resolvers/zod'; - -export function Settings() { - const teamQuery = useQueryTeam(); - const team = teamQuery.data; - - const form = useForm({ - resolver: zodResolver(TeamSettingsFormSchema), - mode: 'onChange', - defaultValues: { - name: '', - description: '', - }, - }); - - const { reset } = form; - - useEffect(() => { - if (team) { - reset({ - name: team.name, - description: team.description || '', - }); - } - }, [reset, team]); - - if (teamQuery.isError) { - return ( -

- Не удалось загрузить данные команды. Попробуйте обновить страницу. -

- ); - } - - return ( - <> - -
- {team ? : } - {team ? : } - - {team ? : null} -
- - ); -} diff --git a/src/pages/team/ui/settings/skeletons/DangerZone.skeleton.tsx b/src/pages/team/ui/settings/skeletons/DangerZone.skeleton.tsx deleted file mode 100644 index b3f97b9..0000000 --- a/src/pages/team/ui/settings/skeletons/DangerZone.skeleton.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Item, ItemActions, ItemContent, ItemMedia, Skeleton } from 'shared/ui'; - -export function DangerZoneSkeleton() { - return ( - - - - - - - - - - - - - ); -} diff --git a/src/pages/team/ui/settings/skeletons/DefaultSettings.skeleton.tsx b/src/pages/team/ui/settings/skeletons/DefaultSettings.skeleton.tsx deleted file mode 100644 index 8f74eee..0000000 --- a/src/pages/team/ui/settings/skeletons/DefaultSettings.skeleton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { CardSection, Skeleton } from 'shared/ui'; - -export function DefaultSettingsSkeleton() { - return ( - -
- - -
-
- - -
-
-
- - -
- -
-
- ); -} diff --git a/src/pages/team/ui/settings/skeletons/InvitationSecurity.skeleton.tsx b/src/pages/team/ui/settings/skeletons/InvitationSecurity.skeleton.tsx deleted file mode 100644 index 689d413..0000000 --- a/src/pages/team/ui/settings/skeletons/InvitationSecurity.skeleton.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { CardSection, Skeleton } from 'shared/ui'; - -export function InvitationSecuritySkeleton() { - return ( - -
- - -
-
-
- - -
- -
-
- ); -} diff --git a/src/pages/team/ui/settings/skeletons/TeamIdentity.skeleton.tsx b/src/pages/team/ui/settings/skeletons/TeamIdentity.skeleton.tsx deleted file mode 100644 index 616c01f..0000000 --- a/src/pages/team/ui/settings/skeletons/TeamIdentity.skeleton.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { CardSection, Skeleton } from 'shared/ui'; - -export function TeamIdentitySkeleton() { - return ( - -
- -
- -
- - -
-
- -
-
- ); -} diff --git a/src/pages/invitations/api/useAcceptTeamInvitation.ts b/src/pages/user/invitations/api/useAcceptTeamInvitation.ts similarity index 100% rename from src/pages/invitations/api/useAcceptTeamInvitation.ts rename to src/pages/user/invitations/api/useAcceptTeamInvitation.ts diff --git a/src/pages/user/invitations/index.ts b/src/pages/user/invitations/index.ts new file mode 100644 index 0000000..f384caa --- /dev/null +++ b/src/pages/user/invitations/index.ts @@ -0,0 +1 @@ +export { InvitationsPage } from './ui/InvitationsPage'; diff --git a/src/pages/invitations/lib/get-invitations-count-text.ts b/src/pages/user/invitations/lib/get-invitations-count-text.ts similarity index 100% rename from src/pages/invitations/lib/get-invitations-count-text.ts rename to src/pages/user/invitations/lib/get-invitations-count-text.ts diff --git a/src/pages/invitations/ui/InvitationCard.skeleton.tsx b/src/pages/user/invitations/ui/InvitationCard.skeleton.tsx similarity index 100% rename from src/pages/invitations/ui/InvitationCard.skeleton.tsx rename to src/pages/user/invitations/ui/InvitationCard.skeleton.tsx diff --git a/src/pages/invitations/ui/InvitationCard.tsx b/src/pages/user/invitations/ui/InvitationCard.tsx similarity index 100% rename from src/pages/invitations/ui/InvitationCard.tsx rename to src/pages/user/invitations/ui/InvitationCard.tsx diff --git a/src/pages/user/invitations/ui/InvitationsPage.tsx b/src/pages/user/invitations/ui/InvitationsPage.tsx new file mode 100644 index 0000000..ac9cd20 --- /dev/null +++ b/src/pages/user/invitations/ui/InvitationsPage.tsx @@ -0,0 +1,16 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import { InvitationsPageFallback } from './InvitationsPageFallback'; + +const InvitationsPageContent = dynamic( + () => import('./InvitationsPageContent').then((mod) => mod.InvitationsPageContent), + { + ssr: false, + loading: () => , + } +); + +export function InvitationsPage() { + return ; +} diff --git a/src/pages/invitations/ui/InvitationsPageContent.tsx b/src/pages/user/invitations/ui/InvitationsPageContent.tsx similarity index 100% rename from src/pages/invitations/ui/InvitationsPageContent.tsx rename to src/pages/user/invitations/ui/InvitationsPageContent.tsx diff --git a/src/pages/invitations/ui/InvitationsPageFallback.tsx b/src/pages/user/invitations/ui/InvitationsPageFallback.tsx similarity index 100% rename from src/pages/invitations/ui/InvitationsPageFallback.tsx rename to src/pages/user/invitations/ui/InvitationsPageFallback.tsx diff --git a/src/pages/profile/notifications/api/useUpdateNotifications.ts b/src/pages/user/notifications/api/useUpdateNotifications.ts similarity index 100% rename from src/pages/profile/notifications/api/useUpdateNotifications.ts rename to src/pages/user/notifications/api/useUpdateNotifications.ts diff --git a/src/pages/profile/notifications/config/notifications.ts b/src/pages/user/notifications/config/notifications.ts similarity index 100% rename from src/pages/profile/notifications/config/notifications.ts rename to src/pages/user/notifications/config/notifications.ts diff --git a/src/pages/profile/notifications/index.ts b/src/pages/user/notifications/index.ts similarity index 100% rename from src/pages/profile/notifications/index.ts rename to src/pages/user/notifications/index.ts diff --git a/src/pages/profile/notifications/model/notifications.ts b/src/pages/user/notifications/model/notifications.ts similarity index 100% rename from src/pages/profile/notifications/model/notifications.ts rename to src/pages/user/notifications/model/notifications.ts diff --git a/src/pages/profile/notifications/ui/NotificationsPage.tsx b/src/pages/user/notifications/ui/NotificationsPage.tsx similarity index 100% rename from src/pages/profile/notifications/ui/NotificationsPage.tsx rename to src/pages/user/notifications/ui/NotificationsPage.tsx diff --git a/src/pages/profile/notifications/ui/NotificationsPageContent.tsx b/src/pages/user/notifications/ui/NotificationsPageContent.tsx similarity index 100% rename from src/pages/profile/notifications/ui/NotificationsPageContent.tsx rename to src/pages/user/notifications/ui/NotificationsPageContent.tsx diff --git a/src/pages/profile/notifications/ui/NotificationsPageFallback.tsx b/src/pages/user/notifications/ui/NotificationsPageFallback.tsx similarity index 100% rename from src/pages/profile/notifications/ui/NotificationsPageFallback.tsx rename to src/pages/user/notifications/ui/NotificationsPageFallback.tsx diff --git a/src/pages/profile/me/api/useConnectOauthProvider.ts b/src/pages/user/profile/api/useConnectOauthProvider.ts similarity index 100% rename from src/pages/profile/me/api/useConnectOauthProvider.ts rename to src/pages/user/profile/api/useConnectOauthProvider.ts diff --git a/src/pages/profile/me/api/useDisconnectOauthProvider.ts b/src/pages/user/profile/api/useDisconnectOauthProvider.ts similarity index 100% rename from src/pages/profile/me/api/useDisconnectOauthProvider.ts rename to src/pages/user/profile/api/useDisconnectOauthProvider.ts diff --git a/src/pages/profile/me/api/useUpdateProfile.ts b/src/pages/user/profile/api/useUpdateProfile.ts similarity index 100% rename from src/pages/profile/me/api/useUpdateProfile.ts rename to src/pages/user/profile/api/useUpdateProfile.ts diff --git a/src/pages/profile/me/config/oauth-status.ts b/src/pages/user/profile/config/oauth-status.ts similarity index 100% rename from src/pages/profile/me/config/oauth-status.ts rename to src/pages/user/profile/config/oauth-status.ts diff --git a/src/pages/user/profile/index.ts b/src/pages/user/profile/index.ts new file mode 100644 index 0000000..8bf014f --- /dev/null +++ b/src/pages/user/profile/index.ts @@ -0,0 +1 @@ +export { UserPage } from './ui/UserPage'; diff --git a/src/pages/profile/me/model/profile.ts b/src/pages/user/profile/model/profile.ts similarity index 100% rename from src/pages/profile/me/model/profile.ts rename to src/pages/user/profile/model/profile.ts diff --git a/src/pages/profile/me/model/useMePage.ts b/src/pages/user/profile/model/useMePage.ts similarity index 96% rename from src/pages/profile/me/model/useMePage.ts rename to src/pages/user/profile/model/useMePage.ts index 005afd8..d698fd7 100644 --- a/src/pages/profile/me/model/useMePage.ts +++ b/src/pages/user/profile/model/useMePage.ts @@ -3,7 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useSuspenseQuery } from '@tanstack/react-query'; import { type TUser, UserQueries } from 'entities/user'; -import { useEffect } from 'react'; +import { useLayoutEffect } from 'react'; import { useForm, useFormState } from 'react-hook-form'; import { useUpdateProfile } from '../api/useUpdateProfile'; import { ProfileForm as ProfileFormSchema, type ProfileFormValues } from './profile'; @@ -26,7 +26,7 @@ export function useMePage() { const { dirtyFields, isDirty } = useFormState({ control: form.control }); const updateProfileMutation = useUpdateProfile(); - useEffect(() => { + useLayoutEffect(() => { if (!profile) return; form.reset({ diff --git a/src/pages/profile/me/model/useOAuthManage.ts b/src/pages/user/profile/model/useOAuthManage.ts similarity index 100% rename from src/pages/profile/me/model/useOAuthManage.ts rename to src/pages/user/profile/model/useOAuthManage.ts diff --git a/src/pages/profile/me/ui/MePage.tsx b/src/pages/user/profile/ui/UserPage.tsx similarity index 86% rename from src/pages/profile/me/ui/MePage.tsx rename to src/pages/user/profile/ui/UserPage.tsx index b9b73c4..730ce18 100644 --- a/src/pages/profile/me/ui/MePage.tsx +++ b/src/pages/user/profile/ui/UserPage.tsx @@ -1,7 +1,7 @@ import { OAuthSection } from './oauth-section/OAuthSection'; import { ProfileSection } from './profile-section/ProfileSection'; -function MePage() { +function UserPage() { return (
@@ -10,4 +10,4 @@ function MePage() { ); } -export { MePage }; +export { UserPage }; diff --git a/src/pages/profile/me/ui/oauth-section/OAuthItem.tsx b/src/pages/user/profile/ui/oauth-section/OAuthItem.tsx similarity index 96% rename from src/pages/profile/me/ui/oauth-section/OAuthItem.tsx rename to src/pages/user/profile/ui/oauth-section/OAuthItem.tsx index dcae1ad..2e72153 100644 --- a/src/pages/profile/me/ui/oauth-section/OAuthItem.tsx +++ b/src/pages/user/profile/ui/oauth-section/OAuthItem.tsx @@ -2,7 +2,7 @@ import { OAUTH_PROVIDERS, type TAuth } from 'entities/auth'; import Image from 'next/image'; -import { useOAuthManage } from 'pages/profile/me/model/useOAuthManage'; +import { useOAuthManage } from '../../model/useOAuthManage'; import { cn } from 'shared/lib/utils'; import { Button, diff --git a/src/pages/profile/me/ui/oauth-section/OAuthSection.tsx b/src/pages/user/profile/ui/oauth-section/OAuthSection.tsx similarity index 100% rename from src/pages/profile/me/ui/oauth-section/OAuthSection.tsx rename to src/pages/user/profile/ui/oauth-section/OAuthSection.tsx diff --git a/src/pages/profile/me/ui/oauth-section/OAuthSectionContent.tsx b/src/pages/user/profile/ui/oauth-section/OAuthSectionContent.tsx similarity index 100% rename from src/pages/profile/me/ui/oauth-section/OAuthSectionContent.tsx rename to src/pages/user/profile/ui/oauth-section/OAuthSectionContent.tsx diff --git a/src/pages/profile/me/ui/oauth-section/OAuthSectionFallback.tsx b/src/pages/user/profile/ui/oauth-section/OAuthSectionFallback.tsx similarity index 100% rename from src/pages/profile/me/ui/oauth-section/OAuthSectionFallback.tsx rename to src/pages/user/profile/ui/oauth-section/OAuthSectionFallback.tsx diff --git a/src/pages/profile/me/ui/oauth-section/OAuthStatusBadge.tsx b/src/pages/user/profile/ui/oauth-section/OAuthStatusBadge.tsx similarity index 100% rename from src/pages/profile/me/ui/oauth-section/OAuthStatusBadge.tsx rename to src/pages/user/profile/ui/oauth-section/OAuthStatusBadge.tsx diff --git a/src/pages/profile/me/ui/profile-section/IdentityItem.tsx b/src/pages/user/profile/ui/profile-section/IdentityItem.tsx similarity index 100% rename from src/pages/profile/me/ui/profile-section/IdentityItem.tsx rename to src/pages/user/profile/ui/profile-section/IdentityItem.tsx diff --git a/src/pages/profile/me/ui/profile-section/ProfileForm.tsx b/src/pages/user/profile/ui/profile-section/ProfileForm.tsx similarity index 100% rename from src/pages/profile/me/ui/profile-section/ProfileForm.tsx rename to src/pages/user/profile/ui/profile-section/ProfileForm.tsx diff --git a/src/pages/profile/me/ui/profile-section/ProfileSection.tsx b/src/pages/user/profile/ui/profile-section/ProfileSection.tsx similarity index 100% rename from src/pages/profile/me/ui/profile-section/ProfileSection.tsx rename to src/pages/user/profile/ui/profile-section/ProfileSection.tsx diff --git a/src/pages/profile/me/ui/profile-section/ProfileSectionContent.tsx b/src/pages/user/profile/ui/profile-section/ProfileSectionContent.tsx similarity index 100% rename from src/pages/profile/me/ui/profile-section/ProfileSectionContent.tsx rename to src/pages/user/profile/ui/profile-section/ProfileSectionContent.tsx diff --git a/src/pages/profile/me/ui/profile-section/ProfileSectionFallback.tsx b/src/pages/user/profile/ui/profile-section/ProfileSectionFallback.tsx similarity index 100% rename from src/pages/profile/me/ui/profile-section/ProfileSectionFallback.tsx rename to src/pages/user/profile/ui/profile-section/ProfileSectionFallback.tsx diff --git a/src/pages/teams/index.ts b/src/pages/user/teams/index.ts similarity index 100% rename from src/pages/teams/index.ts rename to src/pages/user/teams/index.ts diff --git a/src/pages/teams/lib/get-teams-count-text.ts b/src/pages/user/teams/lib/get-teams-count-text.ts similarity index 100% rename from src/pages/teams/lib/get-teams-count-text.ts rename to src/pages/user/teams/lib/get-teams-count-text.ts diff --git a/src/pages/teams/ui/TeamCard.skeleton.tsx b/src/pages/user/teams/ui/TeamCard.skeleton.tsx similarity index 100% rename from src/pages/teams/ui/TeamCard.skeleton.tsx rename to src/pages/user/teams/ui/TeamCard.skeleton.tsx diff --git a/src/pages/teams/ui/TeamCard.tsx b/src/pages/user/teams/ui/TeamCard.tsx similarity index 100% rename from src/pages/teams/ui/TeamCard.tsx rename to src/pages/user/teams/ui/TeamCard.tsx diff --git a/src/pages/teams/ui/TeamCardActions.tsx b/src/pages/user/teams/ui/TeamCardActions.tsx similarity index 100% rename from src/pages/teams/ui/TeamCardActions.tsx rename to src/pages/user/teams/ui/TeamCardActions.tsx diff --git a/src/pages/teams/ui/TeamsPage.tsx b/src/pages/user/teams/ui/TeamsPage.tsx similarity index 100% rename from src/pages/teams/ui/TeamsPage.tsx rename to src/pages/user/teams/ui/TeamsPage.tsx diff --git a/src/pages/teams/ui/TeamsPageContent.tsx b/src/pages/user/teams/ui/TeamsPageContent.tsx similarity index 100% rename from src/pages/teams/ui/TeamsPageContent.tsx rename to src/pages/user/teams/ui/TeamsPageContent.tsx diff --git a/src/pages/teams/ui/TeamsPageFallback.tsx b/src/pages/user/teams/ui/TeamsPageFallback.tsx similarity index 100% rename from src/pages/teams/ui/TeamsPageFallback.tsx rename to src/pages/user/teams/ui/TeamsPageFallback.tsx diff --git a/src/widgets/app-sidebar/config/sidebar.ts b/src/widgets/app-sidebar/config/sidebar.ts index 8c425c0..9396e0a 100644 --- a/src/widgets/app-sidebar/config/sidebar.ts +++ b/src/widgets/app-sidebar/config/sidebar.ts @@ -2,11 +2,7 @@ import { BriefcaseBusiness, Mail, Settings, UsersRound } from 'lucide-react'; import { routes } from 'shared/config'; export const team = [ - { - url: routes.team.members(), - title: 'Участники', - icon: UsersRound, - }, + { url: routes.team.members(), title: 'Участники', icon: UsersRound }, { url: routes.team.projects.all(), title: 'Проекты', icon: BriefcaseBusiness }, { url: routes.team.invitations(), title: 'Приглашения', icon: Mail }, { url: routes.team.settings(), title: 'Настройки', icon: Settings }, diff --git a/src/widgets/app-sidebar/ui/teams/TeamContent.tsx b/src/widgets/app-sidebar/ui/teams/TeamContent.tsx index 7eef692..68573f8 100644 --- a/src/widgets/app-sidebar/ui/teams/TeamContent.tsx +++ b/src/widgets/app-sidebar/ui/teams/TeamContent.tsx @@ -49,7 +49,7 @@ export function TeamContent() { {team.map((subItem) => ( - + {subItem.title} diff --git a/src/widgets/error-state/ui/ErrorState.tsx b/src/widgets/error-state/ui/ErrorState.tsx index 8430015..746236e 100644 --- a/src/widgets/error-state/ui/ErrorState.tsx +++ b/src/widgets/error-state/ui/ErrorState.tsx @@ -1,4 +1,5 @@ import { TriangleAlert } from 'lucide-react'; +import { classNames } from 'shared/lib/utils'; import { Button, Empty, @@ -25,7 +26,7 @@ export function ErrorState({ className, }: ErrorStateProps) { return ( - + From 576222877b545f94e50b585c999e719e264953ba Mon Sep 17 00:00:00 2001 From: kapitulin24 Date: Tue, 7 Jul 2026 18:46:03 +0300 Subject: [PATCH 11/11] feat(landing): implement landing page components and structure - Added new components for the landing page, including LandingHeader, LandingHero, LandingFeatures, LandingTemplates, LandingSteps, LandingTestimonials, LandingCta, LandingProblem, and LandingFooter. - Introduced configuration for landing features, templates, industries, testimonials, and footer sections to enhance modularity and maintainability. - Implemented a BoardPreview component to visually represent task columns in templates. - Enhanced user experience with clear calls to action and informative sections tailored for various business needs. --- app/(protected)/user/(user)/layout.tsx | 2 +- src/pages/main/config/landing.ts | 223 ++++++++++++++++++++++ src/pages/main/ui/BoardPreview.tsx | 41 ++++ src/pages/main/ui/LandingCta.tsx | 33 ++++ src/pages/main/ui/LandingFeatures.tsx | 41 ++++ src/pages/main/ui/LandingFooter.tsx | 85 +++++++++ src/pages/main/ui/LandingHeader.tsx | 40 ++++ src/pages/main/ui/LandingHero.tsx | 71 +++++++ src/pages/main/ui/LandingProblem.tsx | 42 ++++ src/pages/main/ui/LandingSteps.tsx | 37 ++++ src/pages/main/ui/LandingTemplates.tsx | 50 +++++ src/pages/main/ui/LandingTestimonials.tsx | 37 ++++ src/pages/main/ui/MainPage.tsx | 96 +++------- 13 files changed, 727 insertions(+), 71 deletions(-) create mode 100644 src/pages/main/config/landing.ts create mode 100644 src/pages/main/ui/BoardPreview.tsx create mode 100644 src/pages/main/ui/LandingCta.tsx create mode 100644 src/pages/main/ui/LandingFeatures.tsx create mode 100644 src/pages/main/ui/LandingFooter.tsx create mode 100644 src/pages/main/ui/LandingHeader.tsx create mode 100644 src/pages/main/ui/LandingHero.tsx create mode 100644 src/pages/main/ui/LandingProblem.tsx create mode 100644 src/pages/main/ui/LandingSteps.tsx create mode 100644 src/pages/main/ui/LandingTemplates.tsx create mode 100644 src/pages/main/ui/LandingTestimonials.tsx diff --git a/app/(protected)/user/(user)/layout.tsx b/app/(protected)/user/(user)/layout.tsx index 042a775..34a3979 100644 --- a/app/(protected)/user/(user)/layout.tsx +++ b/app/(protected)/user/(user)/layout.tsx @@ -4,7 +4,7 @@ import { PageWrapper } from 'widgets/page-wrapper'; import { type TabNavItem, VerticalTabsNav } from 'widgets/tabs-nav'; const tabs: TabNavItem[] = [ - { key: routes.user.profile(), label: 'Основные настройки', icon: }, + { key: routes.user.profile(), label: 'Общие', icon: }, { key: routes.user.notifications(), label: 'Уведомления', icon: }, ]; diff --git a/src/pages/main/config/landing.ts b/src/pages/main/config/landing.ts new file mode 100644 index 0000000..276d851 --- /dev/null +++ b/src/pages/main/config/landing.ts @@ -0,0 +1,223 @@ +import type { LucideIcon } from 'lucide-react'; +import { + Building2, + GraduationCap, + HardHat, + Languages, + LayoutTemplate, + MousePointerClick, + Scissors, + Store, + Users, + UtensilsCrossed, +} from 'lucide-react'; +import type { Route } from 'next'; +import { routes } from 'shared/config'; + +interface LandingFeature { + icon: LucideIcon; + title: string; + description: string; +} + +interface LandingTemplate { + icon: LucideIcon; + title: string; + description: string; + columns: [string, string, string]; + accent: string; +} + +interface LandingStep { + title: string; + description: string; +} + +interface LandingTestimonial { + quote: string; + name: string; + role: string; + business: string; +} + +const landingIndustries = [ + 'Магазины и розница', + 'Салоны и сервисы', + 'Рестораны и кафе', + 'Офисы и администрация', + 'Строительство и ремонт', + 'Обучение и курсы', + 'Производство и склады', + 'Клиники и медцентры', + 'Логистика и доставка', + 'Агентства и студии', +] as const; + +const landingHeroAudience = + 'Для розницы, услуг, общепита, офисов, строительства, образования, медицины, логистики и любых команд, где важна простота.'; + +const landingFeatures: LandingFeature[] = [ + { + icon: LayoutTemplate, + title: 'Готовые доски под ваш бизнес', + description: + 'Не нужно придумывать структуру с нуля. Выберите шаблон под свою сферу — от магазина и салона до стройки и учебного центра — и сразу работайте.', + }, + { + icon: Languages, + title: 'Понятные слова', + description: + 'Никаких спринтов, бэклогов и канбан-досок. Только привычные формулировки: «новые заявки», «в работе», «готово».', + }, + { + icon: MousePointerClick, + title: 'Низкий порог входа', + description: + 'Разберётся любой сотрудник за несколько минут. Без обучения, вебинаров и длинных инструкций.', + }, + { + icon: Users, + title: 'Команда на одной волне', + description: + 'Пригласите коллег по ссылке. Все видят одни и те же задачи и статусы — без путаницы в чатах.', + }, +]; + +const landingTemplates: LandingTemplate[] = [ + { + icon: Store, + title: 'Магазин', + description: 'Заказы, поставки и выкладка товара', + columns: ['Новые заказы', 'Собираем', 'Выдано'], + accent: 'bg-blue-500/10 text-blue-700 dark:text-blue-300', + }, + { + icon: Scissors, + title: 'Салон красоты', + description: 'Записи, мастера и расходники', + columns: ['Записи', 'Клиент в кресле', 'Завершено'], + accent: 'bg-pink-500/10 text-pink-700 dark:text-pink-300', + }, + { + icon: UtensilsCrossed, + title: 'Ресторан', + description: 'Заказы, кухня и зал', + columns: ['Принято', 'Готовим', 'Подано'], + accent: 'bg-orange-500/10 text-orange-700 dark:text-orange-300', + }, + { + icon: Building2, + title: 'Офис', + description: 'Поручения, согласования, документы', + columns: ['Новые', 'В работе', 'Сделано'], + accent: 'bg-violet-500/10 text-violet-700 dark:text-violet-300', + }, + { + icon: HardHat, + title: 'Строительство', + description: 'Объекты, бригады и снабжение', + columns: ['План', 'На объекте', 'Сдано'], + accent: 'bg-amber-500/10 text-amber-700 dark:text-amber-300', + }, + { + icon: GraduationCap, + title: 'Обучение', + description: 'Группы, материалы и расписание', + columns: ['Записались', 'Идёт курс', 'Выпуск'], + accent: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300', + }, +]; + +const landingSteps: LandingStep[] = [ + { + title: 'Выберите шаблон', + description: 'Укажите сферу бизнеса — доска уже настроена под ваши процессы.', + }, + { + title: 'Пригласите команду', + description: 'Отправьте ссылку коллегам. Каждый сразу видит свои задачи.', + }, + { + title: 'Ведите дела каждый день', + description: 'Перетаскивайте карточки по этапам. Всё наглядно и без лишних слов.', + }, +]; + +const landingTestimonials: LandingTestimonial[] = [ + { + quote: + 'Раньше всё было в блокноте и WhatsApp. Теперь вся команда видит, что на ком висит — и никто не теряет заказы.', + name: 'Анна К.', + role: 'Владелица', + business: 'салон красоты', + }, + { + quote: + 'Нам не нужны были «спринты» и «бэклоги». Здесь всё по-человечески: приняли заказ, готовим, отдали.', + name: 'Игорь М.', + role: 'Управляющий', + business: 'кофейня', + }, + { + quote: + 'Подключили офис за один день. Бухгалтерия, закупки и администраторы — все работают в одной системе.', + name: 'Елена С.', + role: 'Директор', + business: 'торговая компания', + }, +]; + +interface LandingFooterLink { + label: string; + href: Route | `#${string}`; +} + +interface LandingFooterSection { + title: string; + links: LandingFooterLink[]; +} + +const landingFooterDescription = + 'Трекер задач для обычного бизнеса. Готовые доски, понятные слова и быстрый старт без обучения.'; + +const landingFooterSections: LandingFooterSection[] = [ + { + title: 'Продукт', + links: [ + { label: 'Возможности', href: '#features' }, + { label: 'Шаблоны досок', href: '#templates' }, + { label: 'Как начать', href: '#how-it-works' }, + ], + }, + { + title: 'Аккаунт', + links: [ + { label: 'Регистрация', href: routes.auth.signup() }, + { label: 'Вход', href: routes.auth.signin() }, + ], + }, +]; + +const landingFooterHighlights = [ + 'Бесплатный старт', + 'Без кредитной карты', + 'Настройка за 5 минут', +] as const; + +export { + landingFeatures, + landingFooterDescription, + landingFooterHighlights, + landingFooterSections, + landingHeroAudience, + landingIndustries, + landingSteps, + landingTemplates, + landingTestimonials, + type LandingFeature, + type LandingFooterLink, + type LandingFooterSection, + type LandingStep, + type LandingTemplate, + type LandingTestimonial, +}; diff --git a/src/pages/main/ui/BoardPreview.tsx b/src/pages/main/ui/BoardPreview.tsx new file mode 100644 index 0000000..296b0e5 --- /dev/null +++ b/src/pages/main/ui/BoardPreview.tsx @@ -0,0 +1,41 @@ +interface BoardPreviewProps { + columns: [string, string, string]; + className?: string; +} + +function BoardPreview({ columns, className }: BoardPreviewProps) { + const cards = [ + [2, 1], + [1, 2], + [3, 0], + ] as const; + + return ( +
+ {columns.map((title, columnIndex) => ( +
+

{title}

+
+ {Array.from({ length: cards[columnIndex][0] }).map((_, cardIndex) => ( +
+
+
+
+ ))} +
+
+ ))} +
+ ); +} + +export { BoardPreview }; diff --git a/src/pages/main/ui/LandingCta.tsx b/src/pages/main/ui/LandingCta.tsx new file mode 100644 index 0000000..76c55aa --- /dev/null +++ b/src/pages/main/ui/LandingCta.tsx @@ -0,0 +1,33 @@ +import { ArrowRight } from 'lucide-react'; +import NextLink from 'next/link'; +import { routes } from 'shared/config'; +import { Button } from 'shared/ui'; + +function LandingCta() { + return ( +
+
+

+ Попробуйте сегодня — без сложных настроек +

+

+ Создайте рабочее пространство, выберите шаблон под свой бизнес и пригласите команду. + Первые задачи можно добавить уже через пять минут. +

+
+ + +
+
+
+ ); +} + +export { LandingCta }; diff --git a/src/pages/main/ui/LandingFeatures.tsx b/src/pages/main/ui/LandingFeatures.tsx new file mode 100644 index 0000000..81fcaff --- /dev/null +++ b/src/pages/main/ui/LandingFeatures.tsx @@ -0,0 +1,41 @@ +import { landingFeatures } from '../config/landing'; + +function LandingFeatures() { + return ( +
+
+

Почему мы

+

+ Всё, что нужно бизнесу — и ничего лишнего +

+

+ Мы убрали сложность и оставили только то, что помогает команде работать слаженно. +

+
+ +
+ {landingFeatures.map((feature) => { + const Icon = feature.icon; + + return ( +
+
+ +
+

{feature.title}

+

{feature.description}

+
+ ); + })} +
+
+ ); +} + +export { LandingFeatures }; diff --git a/src/pages/main/ui/LandingFooter.tsx b/src/pages/main/ui/LandingFooter.tsx new file mode 100644 index 0000000..63fa19a --- /dev/null +++ b/src/pages/main/ui/LandingFooter.tsx @@ -0,0 +1,85 @@ +import { routes } from 'shared/config'; +import { AppCopyright, Link, Logo } from 'shared/ui'; +import { + landingFooterDescription, + landingFooterHighlights, + landingFooterSections, + landingIndustries, + type LandingFooterLink, +} from '../config/landing'; + +function isAnchorLink(href: LandingFooterLink['href']): href is `#${string}` { + return href.startsWith('#'); +} + +function LandingFooter() { + return ( +
+
+
+ + + +

+ {landingFooterDescription} +

+
    + {landingFooterHighlights.map((highlight) => ( +
  • + {highlight} +
  • + ))} +
+
+ + {landingFooterSections.map((section) => ( + + ))} + +
+

Для кого

+
    + {landingIndustries.map((industry) => ( +
  • {industry}
  • + ))} +
+
+
+ +
+ +

+ Сделано для бизнеса, где важна простота +

+
+
+ ); +} + +export { LandingFooter }; diff --git a/src/pages/main/ui/LandingHeader.tsx b/src/pages/main/ui/LandingHeader.tsx new file mode 100644 index 0000000..587548d --- /dev/null +++ b/src/pages/main/ui/LandingHeader.tsx @@ -0,0 +1,40 @@ +import NextLink from 'next/link'; +import { routes } from 'shared/config'; +import { Button, Link, Logo } from 'shared/ui'; + +const navItems = [ + { href: '#features', label: 'Возможности' }, + { href: '#templates', label: 'Шаблоны' }, + { href: '#how-it-works', label: 'Как начать' }, +] as const; + +function LandingHeader() { + return ( +
+
+ + + + + + +
+ + +
+
+
+ ); +} + +export { LandingHeader }; diff --git a/src/pages/main/ui/LandingHero.tsx b/src/pages/main/ui/LandingHero.tsx new file mode 100644 index 0000000..98fe094 --- /dev/null +++ b/src/pages/main/ui/LandingHero.tsx @@ -0,0 +1,71 @@ +import { ArrowRight } from 'lucide-react'; +import NextLink from 'next/link'; +import { routes } from 'shared/config'; +import { Badge, Button } from 'shared/ui'; +import { landingHeroAudience, landingIndustries } from '../config/landing'; +import { BoardPreview } from './BoardPreview'; + +function LandingHero() { + return ( +
+
+ + Для бизнеса, а не для IT-отделов + + +

+ Ведите дела команды без сложных терминов +

+
+ +
+
+
+

+ {landingHeroAudience} Готовые доски и понятные названия — начните работать в первый же + день. +

+
    + {landingIndustries.map((industry) => ( +
  • + {industry} +
  • + ))} +
+
+ +
+ + +
+ +

+ Бесплатный старт · Без кредитной карты · Настройка за 5 минут +

+
+ +
+
+
+

+ Пример доски «Магазин» +

+ +
+
+
+
+ ); +} + +export { LandingHero }; diff --git a/src/pages/main/ui/LandingProblem.tsx b/src/pages/main/ui/LandingProblem.tsx new file mode 100644 index 0000000..7a47c5b --- /dev/null +++ b/src/pages/main/ui/LandingProblem.tsx @@ -0,0 +1,42 @@ +import { XCircle } from 'lucide-react'; + +const painPoints = [ + 'Спринты, бэклоги и story points — непонятно большинству сотрудников', + 'Нужно проходить обучение, прежде чем можно начать работать', + 'Пустая доска: неясно, с чего начать и как назвать этапы', +] as const; + +function LandingProblem() { + return ( +
+
+
+
+

Знакомо?

+

+ Обычные трекеры созданы для разработчиков, а не для вашей команды +

+

+ Владельцы бизнеса и сотрудники хотят просто видеть, что нужно сделать сегодня. Без + переводчика с «айтишного» на человеческий. +

+
+ +
    + {painPoints.map((point) => ( +
  • + + {point} +
  • + ))} +
+
+
+
+ ); +} + +export { LandingProblem }; diff --git a/src/pages/main/ui/LandingSteps.tsx b/src/pages/main/ui/LandingSteps.tsx new file mode 100644 index 0000000..5dea150 --- /dev/null +++ b/src/pages/main/ui/LandingSteps.tsx @@ -0,0 +1,37 @@ +import { landingSteps } from '../config/landing'; + +function LandingSteps() { + return ( +
+
+

Как начать

+

+ Три шага — и команда уже в работе +

+
+ +
    + {landingSteps.map((step, index) => ( +
  1. + + {String(index + 1).padStart(2, '0')} + +

    {step.title}

    +

    {step.description}

    +
  2. + ))} +
+
+ ); +} + +export { LandingSteps }; diff --git a/src/pages/main/ui/LandingTemplates.tsx b/src/pages/main/ui/LandingTemplates.tsx new file mode 100644 index 0000000..83d1cbc --- /dev/null +++ b/src/pages/main/ui/LandingTemplates.tsx @@ -0,0 +1,50 @@ +import { landingTemplates } from '../config/landing'; +import { BoardPreview } from './BoardPreview'; + +function LandingTemplates() { + return ( +
+
+

Шаблоны

+

+ Готовые доски для разных сфер бизнеса +

+

+ Выберите шаблон под свою отрасль — этапы и формулировки уже подобраны. Останется только + добавить свои задачи. +

+
+ +
+ {landingTemplates.map((template) => { + const Icon = template.icon; + + return ( +
+
+
+ +
+
+

{template.title}

+

{template.description}

+
+
+ +
+ ); + })} +
+
+ ); +} + +export { LandingTemplates }; diff --git a/src/pages/main/ui/LandingTestimonials.tsx b/src/pages/main/ui/LandingTestimonials.tsx new file mode 100644 index 0000000..8a1fa64 --- /dev/null +++ b/src/pages/main/ui/LandingTestimonials.tsx @@ -0,0 +1,37 @@ +import { Quote } from 'lucide-react'; +import { landingTestimonials } from '../config/landing'; + +function LandingTestimonials() { + return ( +
+
+

Отзывы

+

+ Уже помогает реальным командам +

+
+ +
+ {landingTestimonials.map((testimonial) => ( +
+ +

“{testimonial.quote}”

+
+ + {testimonial.name} + + {testimonial.role}, {testimonial.business} + + +
+
+ ))} +
+
+ ); +} + +export { LandingTestimonials }; diff --git a/src/pages/main/ui/MainPage.tsx b/src/pages/main/ui/MainPage.tsx index 2e4317c..e5af2ec 100644 --- a/src/pages/main/ui/MainPage.tsx +++ b/src/pages/main/ui/MainPage.tsx @@ -1,5 +1,12 @@ -import { routes } from 'shared/config'; -import { Button, Link, Logo, Separator } from 'shared/ui'; +import { LandingCta } from './LandingCta'; +import { LandingFeatures } from './LandingFeatures'; +import { LandingFooter } from './LandingFooter'; +import { LandingHeader } from './LandingHeader'; +import { LandingHero } from './LandingHero'; +import { LandingProblem } from './LandingProblem'; +import { LandingSteps } from './LandingSteps'; +import { LandingTemplates } from './LandingTemplates'; +import { LandingTestimonials } from './LandingTestimonials'; interface MainPageProps { className?: string; @@ -7,77 +14,26 @@ interface MainPageProps { function MainPage({ className }: MainPageProps) { return ( -
-
+
+
-
-
-
+
+
-
-
-
-
- - Open Source Task Tracker -
- -
- - -
-
- -
-

- Планируйте спринты и держите фокус команды в одной команде -

-

- Один трекер для продукта, разработки и QA. Все статусы и приоритеты прозрачны в - реальном времени. -

-
-
- - - -
-
-

- С чего начать -

-

- Три шага до первого спринта -

-
- - -
-
- -
    -
  1. -

    01. Создайте проект

    -
  2. -
  3. -

    02. Добавьте команду

    -
  4. -
  5. -

    03. Запустите спринт

    -
  6. -
-
-
-
+ + +
+ + + + + + + + +
+
); }