diff --git a/src/entities/user/model/schemas.ts b/src/entities/user/model/schemas.ts
index 82a91b5..f97a513 100644
--- a/src/entities/user/model/schemas.ts
+++ b/src/entities/user/model/schemas.ts
@@ -91,14 +91,11 @@ export const ProfileUpdateBody = z.object({
headline: z.string().max(100, 'Должность слишком длинная').nullable().optional(),
location: z.string().max(100, 'Локация слишком длинная').nullable().optional(),
phone: z.string().max(20, 'Номер телефона слишком длинный').nullable().optional(),
- gender: z
- .enum(['none', 'male', 'female', 'non_binary', 'other', 'prefer_not_to_say'])
- .default('none')
- .optional(),
+ gender: z.enum(['none', 'male', 'female', 'non_binary', 'other', 'prefer_not_to_say']).optional(),
vacationStart: z.string().nullable().optional(),
vacationEnd: z.string().nullable().optional(),
vacationMessage: z.string().max(500, 'Сообщение слишком длинное').nullable().optional(),
- pronouns: z.enum(['he_him', 'she_her', 'they_them', 'other', 'none']).default('none').optional(),
+ pronouns: z.enum(['he_him', 'she_her', 'they_them', 'other', 'none']).optional(),
pronounsCustom: z.string().max(50, 'Максимальная длина 50 символов').nullable().optional(),
bio: z.string().max(1000, 'О себе не более 1000 символов').nullable().optional(),
timezone: z.string().max(50).optional(),
diff --git a/src/pages/profile/api/useConnectedAccounts.ts b/src/pages/profile/api/useConnectedAccounts.ts
deleted file mode 100644
index 8518a9d..0000000
--- a/src/pages/profile/api/useConnectedAccounts.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-'use client';
-import { useQuery } from '@tanstack/react-query';
-import { AuthQueries } from 'entities/auth';
-import { useMemo } from 'react';
-
-export function useConnectedAccounts() {
- const available = useQuery(AuthQueries.getOAuthProviders());
- const connected = useQuery(AuthQueries.getConnectedOAuthProviders());
-
- const providers = useMemo(() => {
- if (!available.data) return [];
-
- const connectedSet = new Set(connected.data?.map((v) => v.provider));
-
- return available.data.map((item) => ({
- ...item,
- isConnected: connectedSet.has(item.value),
- }));
- }, [available.data, connected.data]);
-
- return { providers };
-}
diff --git a/src/pages/profile/config/profile.ts b/src/pages/profile/config/profile.ts
new file mode 100644
index 0000000..2e9755b
--- /dev/null
+++ b/src/pages/profile/config/profile.ts
@@ -0,0 +1,7 @@
+import { OAuthConnectionStatus } from '../model/profile';
+
+export const OAUTH_BADGE_LABELS = {
+ connected: 'Подключен',
+ disconnected: 'Не подключен',
+ unknown: 'Не удалось проверить',
+} as const satisfies Record
;
diff --git a/src/pages/profile/config/tabs.ts b/src/pages/profile/config/tabs.ts
deleted file mode 100644
index 1ae5728..0000000
--- a/src/pages/profile/config/tabs.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { routes } from 'shared/config';
-
-export const profileTabs = [
- { key: routes.user.profile(), label: 'Мой профиль' },
- { key: routes.user.security(), label: 'Безопасность' },
- { key: routes.user.notifications(), label: 'Уведомления' },
-];
diff --git a/src/pages/profile/index.ts b/src/pages/profile/index.ts
index 8922138..0f32dee 100644
--- a/src/pages/profile/index.ts
+++ b/src/pages/profile/index.ts
@@ -1,4 +1,3 @@
-export { profileTabs } from './config/tabs';
export { MePage } from './ui/me-page/MePage';
export { NotificationsPage } from './ui/notifications-page/NotificationsPage';
export { SecurityPage } from './ui/security-page/SecurityPage';
diff --git a/src/pages/profile/model/profile.ts b/src/pages/profile/model/profile.ts
index 91d5ebe..f0fb522 100644
--- a/src/pages/profile/model/profile.ts
+++ b/src/pages/profile/model/profile.ts
@@ -18,3 +18,5 @@ export const ProfileForm = z.object({
});
export type ProfileFormValues = z.infer;
+
+export type OAuthConnectionStatus = 'connected' | 'disconnected' | 'unknown';
diff --git a/src/pages/profile/model/useMePage.ts b/src/pages/profile/model/useMePage.ts
index a9fb11d..005afd8 100644
--- a/src/pages/profile/model/useMePage.ts
+++ b/src/pages/profile/model/useMePage.ts
@@ -1,7 +1,7 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
-import { useQuery } from '@tanstack/react-query';
+import { useSuspenseQuery } from '@tanstack/react-query';
import { type TUser, UserQueries } from 'entities/user';
import { useEffect } from 'react';
import { useForm, useFormState } from 'react-hook-form';
@@ -9,7 +9,7 @@ import { useUpdateProfile } from '../api/useUpdateProfile';
import { ProfileForm as ProfileFormSchema, type ProfileFormValues } from './profile';
export function useMePage() {
- const query = useQuery(UserQueries.getMe());
+ const query = useSuspenseQuery(UserQueries.getMe());
const profile = query.data?.profile;
const email = query.data?.email;
diff --git a/src/pages/profile/model/useOAuthManage.ts b/src/pages/profile/model/useOAuthManage.ts
new file mode 100644
index 0000000..bc61116
--- /dev/null
+++ b/src/pages/profile/model/useOAuthManage.ts
@@ -0,0 +1,37 @@
+import { useCallback } from 'react';
+import { useConnectOAuthProvider } from '../api/useConnectOauthProvider';
+import { useDisconnectOAuthProvider } from '../api/useDisconnectOauthProvider';
+import { authFabricKeys, TAuth } from 'entities/auth';
+import { toast } from 'sonner';
+import { OAuthConnectionStatus } from './profile';
+import { env } from 'shared/config';
+
+export function useOAuthManage(provider: TAuth.OAuthProvider, status: OAuthConnectionStatus) {
+ const connect = useConnectOAuthProvider();
+ const disconnect = useDisconnectOAuthProvider();
+
+ const isPending = connect.isPending || disconnect.isPending;
+
+ const handleToggleConnect = useCallback(() => {
+ if (status === 'connected') {
+ disconnect.mutate(provider, {
+ onSuccess: (data, _v, _m, context) => {
+ context.client.invalidateQueries({ queryKey: authFabricKeys.connectedProviders() });
+ toast.success(data.message);
+ },
+ });
+ } else if (status === 'disconnected') {
+ connect.mutate(provider, {
+ onSuccess: (data) => {
+ localStorage.setItem('test', JSON.stringify(data));
+ const url = data.url.startsWith('http')
+ ? data.url
+ : new URL(data.url, env.NEXT_PUBLIC_API_BASE_URL).toString();
+ window.location.href = url;
+ },
+ });
+ }
+ }, [connect, disconnect, status, provider]);
+
+ return { handleToggleConnect, isPending };
+}
diff --git a/src/pages/profile/ui/me-page/IdentityItem.tsx b/src/pages/profile/ui/me-page/IdentityItem.tsx
deleted file mode 100644
index e7475c0..0000000
--- a/src/pages/profile/ui/me-page/IdentityItem.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-'use client';
-
-import { Item, ItemActions, ItemContent, ItemMedia } from 'shared/ui';
-import { UploadAvatar } from 'features/upload-avatar';
-import { SignOut } from 'features/auth/sign-out';
-import { type TUser, UserAvatar } from 'entities/user';
-
-type AccountIdentityItemProps = {
- profile: TUser.UserResponse['profile'];
- email: string;
-};
-
-function IdentityItem({ profile, email }: AccountIdentityItemProps) {
- const fullName = `${profile.firstName} ${profile.lastName}`;
-
- return (
- -
-
-
- }
- />
-
-
-
-
{fullName}
-
{email}
-
- {profile.bio?.trim() || 'Добавьте информацию о себе в профиле'}
-
-
-
-
-
-
-
- );
-}
-
-export { IdentityItem };
diff --git a/src/pages/profile/ui/me-page/MePage.tsx b/src/pages/profile/ui/me-page/MePage.tsx
index 97952ff..9ff1ab3 100644
--- a/src/pages/profile/ui/me-page/MePage.tsx
+++ b/src/pages/profile/ui/me-page/MePage.tsx
@@ -1,61 +1,14 @@
'use client';
+import dynamic from 'next/dynamic';
+import { MePageFallback } from './MePageFallback';
-import {
- Card,
- CardDescription,
- CardHeader,
- CardSection,
- CardTitle,
- FloatingSaveBar,
- Separator,
-} from 'shared/ui';
-import { IdentityItem } from './IdentityItem';
-import { ProfileForm } from './ProfileForm';
-import { useMePage } from '../../model/useMePage';
-import { AccountSection } from './account-section/AccountsSection';
-import { Suspense } from 'react';
-import { QueryParamsHandler } from 'features/handle-query-params';
+const MePageContent = dynamic(() => import('./MePageContent').then((mod) => mod.MePage), {
+ ssr: false,
+ loading: () => ,
+});
function MePage() {
- const { form, profile, email, isDirty, isPending, onSubmit, onDiscard } = useMePage();
-
- if (!profile || !email) {
- return (
-
-
- Профиль
- Данные профиля пока недоступны.
-
-
- );
- }
-
- return (
- <>
-
-
-
-
- >
- );
+ return ;
}
export { MePage };
diff --git a/src/pages/profile/ui/me-page/MePageContent.tsx b/src/pages/profile/ui/me-page/MePageContent.tsx
new file mode 100644
index 0000000..d552be5
--- /dev/null
+++ b/src/pages/profile/ui/me-page/MePageContent.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+import { type PropsWithChildren, Suspense } from 'react';
+import { QueryParamsHandler } from 'features/handle-query-params';
+import { ProfileSection } from './profile-section/ProfileSection';
+import { AccountSection } from './account-section/AccountSection';
+import { ProfileSectionFallback } from './profile-section/ProfileSectionFallback';
+
+function MePage() {
+ return (
+ <>
+
+
+
+
+ }>
+
+
+
+
+ >
+ );
+}
+
+function MePageLayout({ children }: PropsWithChildren) {
+ return {children}
;
+}
+
+export { MePage, MePageLayout };
diff --git a/src/pages/profile/ui/me-page/MePageFallback.tsx b/src/pages/profile/ui/me-page/MePageFallback.tsx
new file mode 100644
index 0000000..11e0fc6
--- /dev/null
+++ b/src/pages/profile/ui/me-page/MePageFallback.tsx
@@ -0,0 +1,12 @@
+import { AccountSectionFallback } from './account-section/AccountSectionFallback';
+import { MePageLayout } from './MePageContent';
+import { ProfileSectionFallback } from './profile-section/ProfileSectionFallback';
+
+export function MePageFallback() {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/pages/profile/ui/me-page/account-section/AccountSection.tsx b/src/pages/profile/ui/me-page/account-section/AccountSection.tsx
new file mode 100644
index 0000000..f247561
--- /dev/null
+++ b/src/pages/profile/ui/me-page/account-section/AccountSection.tsx
@@ -0,0 +1,41 @@
+import { CardSection } from 'shared/ui';
+import { OAuthManageButton } from './OAuthManageButton';
+import { AuthQueries } from 'entities/auth';
+import { useQuery } from '@tanstack/react-query';
+import { useMemo } from 'react';
+import { AccountSectionFallback } from './AccountSectionFallback';
+import { AccountSectionErrorFallback } from './AccountSectionErrorFallback';
+
+export function AccountSection() {
+ const providers = useQuery(AuthQueries.getOAuthProviders());
+ const connected = useQuery(AuthQueries.getConnectedOAuthProviders());
+
+ const connectedSet = useMemo(
+ () => new Set(connected.data?.map((v) => v.provider)),
+ [connected.data]
+ );
+
+ if (providers.error) return ;
+ if (providers.isLoading) return ;
+
+ return (
+
+ {providers.data?.map((provider) => {
+ const isConnected = connectedSet.has(provider.value);
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/src/pages/profile/ui/me-page/account-section/AccountSectionErrorFallback.tsx b/src/pages/profile/ui/me-page/account-section/AccountSectionErrorFallback.tsx
new file mode 100644
index 0000000..f2edc25
--- /dev/null
+++ b/src/pages/profile/ui/me-page/account-section/AccountSectionErrorFallback.tsx
@@ -0,0 +1,14 @@
+import { CardSection } from 'shared/ui';
+import { ErrorState } from 'widgets/error-state';
+
+export function AccountSectionErrorFallback() {
+ return (
+
+
+
+ );
+}
diff --git a/src/pages/profile/ui/me-page/account-section/AccountSectionFallback.tsx b/src/pages/profile/ui/me-page/account-section/AccountSectionFallback.tsx
new file mode 100644
index 0000000..7a9c74e
--- /dev/null
+++ b/src/pages/profile/ui/me-page/account-section/AccountSectionFallback.tsx
@@ -0,0 +1,30 @@
+import { CardSection, Skeleton } from 'shared/ui';
+
+export function AccountSectionFallback() {
+ return (
+
+ {Array.from({ length: 3 }).map((_, index) => (
+
+ ))}
+
+ );
+}
diff --git a/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx b/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx
deleted file mode 100644
index 1744497..0000000
--- a/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { OAuthManageButton } from './OAuthManageButton';
-import { useConnectedAccounts } from '../../../api/useConnectedAccounts';
-import { CardSection } from 'shared/ui';
-
-export function AccountSection() {
- const { providers } = useConnectedAccounts();
-
- return (
-
- {providers?.map((provider) => {
- return (
-
- );
- })}
-
- );
-}
diff --git a/src/pages/profile/ui/me-page/account-section/OAuthBadge.tsx b/src/pages/profile/ui/me-page/account-section/OAuthBadge.tsx
new file mode 100644
index 0000000..f5311e1
--- /dev/null
+++ b/src/pages/profile/ui/me-page/account-section/OAuthBadge.tsx
@@ -0,0 +1,28 @@
+import { Badge } from 'shared/ui';
+import { type OAuthConnectionStatus } from '../../../model/profile';
+import { classNames } from 'shared/lib/utils';
+import { OAUTH_BADGE_LABELS } from 'pages/profile/config/profile';
+
+export function OAuthBadge({
+ status,
+ className = '',
+}: {
+ status: OAuthConnectionStatus;
+ className?: string;
+}) {
+ return (
+
+ {OAUTH_BADGE_LABELS[status]}
+
+ );
+}
diff --git a/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx b/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx
index d2a3d4f..c4944d9 100644
--- a/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx
+++ b/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx
@@ -1,63 +1,72 @@
'use client';
-import { authFabricKeys, OAUTH_PROVIDERS, type TAuth } from 'entities/auth';
-import { type ComponentProps, useCallback } from 'react';
-import { Button } from 'shared/ui';
-import { useConnectOAuthProvider } from '../../../api/useConnectOauthProvider';
-import { useDisconnectOAuthProvider } from '../../../api/useDisconnectOauthProvider';
-import { env } from 'shared/config';
-import { toast } from 'sonner';
+import { OAUTH_PROVIDERS, type TAuth } from 'entities/auth';
+import { type ComponentProps } from 'react';
+import { Button, Spinner } from 'shared/ui';
import Image from 'next/image';
+import { classNames } from 'shared/lib/utils';
+import { type OAuthConnectionStatus } from '../../../model/profile';
+import { useOAuthManage } from 'pages/profile/model/useOAuthManage';
+import { OAuthBadge } from './OAuthBadge';
type OAuthManageButtonProps = ComponentProps & {
+ wrap?: Omit, 'children'>;
provider: TAuth.OAuthProvider;
label: string;
- isLinked: boolean;
+ isLoading?: boolean;
+ status: OAuthConnectionStatus;
};
-export function OAuthManageButton({ provider, label, isLinked, ...props }: OAuthManageButtonProps) {
- const connect = useConnectOAuthProvider();
- const disconnect = useDisconnectOAuthProvider();
-
- const isLoading = connect.isPending || disconnect.isPending;
-
- const handleToggleConnect = useCallback(() => {
- if (isLinked) {
- disconnect.mutate(provider, {
- onSuccess: (data, _v, _m, context) => {
- context.client.invalidateQueries({ queryKey: authFabricKeys.connectedProviders() });
- toast.success(data.message);
- },
- });
- } else {
- connect.mutate(provider, {
- onSuccess: (data) => {
- const url = data.url.startsWith('http')
- ? data.url
- : new URL(data.url, env.NEXT_PUBLIC_API_BASE_URL).toString();
- window.location.href = url;
- },
- });
- }
- }, [connect, disconnect, isLinked, provider]);
-
+export function OAuthManageButton({
+ provider,
+ label,
+ disabled,
+ status,
+ isLoading = false,
+ wrap = {},
+ ...props
+}: OAuthManageButtonProps) {
+ const { isPending, handleToggleConnect } = useOAuthManage(provider, status);
+ const isConnected = status === 'connected';
+ const isDisabled = disabled || isPending || isLoading || status === 'unknown';
const meta = OAUTH_PROVIDERS[provider];
return (
-