refactor(profile): simplify profile page structure and enhance loading states

- Removed unnecessary data fetching and hydration logic from the main profile page.
- Updated the rendering of components to improve clarity and performance.
- Enhanced loading states in various profile components for better user experience.
- Refactored service list handling to utilize telegramId instead of masterId for consistency.
This commit is contained in:
vchikalkin 2025-09-18 14:02:10 +03:00
parent a669e1846e
commit 106fdc0da5
6 changed files with 61 additions and 74 deletions

View File

@ -1,10 +1,7 @@
import { getCustomer } from '@/actions/api/customers';
import { getSessionUser } from '@/actions/session';
import { Container } from '@/components/layout';
import { PageHeader } from '@/components/navigation';
import { ContactDataCard, PersonCard, ProfileOrdersList } from '@/components/profile';
import { ReadonlyServicesList } from '@/components/profile/services';
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
// Тип параметров страницы
type Props = { params: Promise<{ telegramId: string }> };
@ -12,33 +9,16 @@ type Props = { params: Promise<{ telegramId: string }> };
export default async function ProfilePage(props: Readonly<Props>) {
const { telegramId } = await props.params;
const contactTelegramId = Number(telegramId);
const queryClient = new QueryClient();
// Получаем профиль контакта
const { customer: profile } = await queryClient.fetchQuery({
queryFn: () => getCustomer({ telegramId: contactTelegramId }),
queryKey: ['customer', contactTelegramId],
});
// Получаем текущего пользователя
const sessionUser = await getSessionUser();
const { customer: currentUser } = await queryClient.fetchQuery({
queryFn: () => getCustomer({ telegramId: sessionUser.telegramId }),
queryKey: ['customer', sessionUser.telegramId],
});
// Проверка наличия данных
if (!profile || !currentUser) return null;
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<>
<PageHeader title="Профиль контакта" />
<Container className="px-0">
<PersonCard telegramId={contactTelegramId} />
<ContactDataCard telegramId={contactTelegramId} />
<ReadonlyServicesList masterId={profile.documentId} />
<ReadonlyServicesList telegramId={contactTelegramId} />
<ProfileOrdersList telegramId={contactTelegramId} />
</Container>
</HydrationBoundary>
</>
);
}

View File

@ -1,34 +1,13 @@
import { getCustomer } from '@/actions/api/customers';
import { getSubscriptionSettings } from '@/actions/api/subscriptions';
import { getSessionUser } from '@/actions/session';
import { Container } from '@/components/layout';
import { LinksCard, PersonCard, ProfileDataCard, SubscriptionInfoBar } from '@/components/profile';
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
export default async function ProfilePage() {
const queryClient = new QueryClient();
const { telegramId } = await getSessionUser();
await queryClient.prefetchQuery({
queryFn: () => getCustomer({ telegramId }),
queryKey: ['customer', telegramId],
});
const { subscriptionSetting } = await queryClient.fetchQuery({
queryFn: getSubscriptionSettings,
queryKey: ['subscriptionSetting'],
});
const proEnabled = subscriptionSetting?.proEnabled;
export default function ProfilePage() {
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Container className="px-0">
<PersonCard />
{proEnabled && <SubscriptionInfoBar />}
<ProfileDataCard />
<LinksCard />
</Container>
</HydrationBoundary>
<Container className="px-0">
<PersonCard />
<SubscriptionInfoBar />
<ProfileDataCard />
<LinksCard />
</Container>
);
}

View File

@ -12,7 +12,18 @@ import Link from 'next/link';
import { useState } from 'react';
export function ContactDataCard({ telegramId }: Readonly<ProfileProps>) {
const { data: { customer } = {} } = useCustomerQuery({ telegramId });
const { data: { customer } = {}, isLoading } = useCustomerQuery({ telegramId });
if (isLoading)
return (
<Card className="p-4">
<div className="flex animate-pulse flex-col gap-4">
<div className="h-4 w-16 rounded bg-muted" />
<div className="h-6 w-32 rounded bg-muted" />
<div className="h-10 w-full rounded bg-muted" />
</div>
</Card>
);
if (!customer) return null;
@ -33,10 +44,24 @@ export function ContactDataCard({ telegramId }: Readonly<ProfileProps>) {
}
export function ProfileDataCard() {
const { data: { customer } = {} } = useCustomerQuery();
const { data: { customer } = {}, isLoading } = useCustomerQuery();
const { cancelChanges, hasChanges, isPending, resetTrigger, saveChanges, updateField } =
useProfileEdit();
if (isLoading)
return (
<Card className="p-4">
<div className="flex animate-pulse flex-col gap-4">
<div className="h-6 w-32 rounded bg-muted" />
<div className="h-4 w-16 rounded bg-muted" />
<div className="h-10 w-full rounded bg-muted" />
<div className="h-4 w-16 rounded bg-muted" />
<div className="h-10 w-full rounded bg-muted" />
<div className="h-5 w-60 rounded bg-muted" />
</div>
</Card>
);
if (!customer) return null;
return (

View File

@ -4,16 +4,18 @@ import { type ProfileProps } from './types';
import { UserAvatar } from '@/components/shared/user-avatar';
import { useCustomerQuery } from '@/hooks/api/customers';
import { Card } from '@repo/ui/components/ui/card';
import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
export function PersonCard({ telegramId }: Readonly<ProfileProps>) {
const { data: { customer } = {}, isLoading } = useCustomerQuery({ telegramId });
if (isLoading)
return (
<div className="p-4">
<LoadingSpinner />
</div>
<Card className="bg-transparent p-4 shadow-none">
<div className="flex animate-pulse flex-col items-center space-y-2">
<div className="size-32 rounded-full bg-muted" />
<div className="h-6 w-40 rounded bg-muted" />
</div>
</Card>
);
if (!customer) return null;

View File

@ -1,5 +1,6 @@
'use client';
import { type ProfileProps } from '../types';
import { DataNotFound } from '@/components/shared/alert';
import { ServiceCard } from '@/components/shared/service-card';
import { useCustomerQuery } from '@/hooks/api/customers';
@ -7,13 +8,9 @@ import { useServicesQuery } from '@/hooks/api/services';
import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
import Link from 'next/link';
type MasterServicesListProps = {
masterId: string;
};
// Компонент для отображения услуг мастера (без ссылок, только просмотр)
export function ReadonlyServicesList({ masterId }: Readonly<MasterServicesListProps>) {
const { isLoading, services } = useServices(masterId);
export function ReadonlyServicesList({ telegramId }: Readonly<ProfileProps>) {
const { isLoading, services } = useServices(telegramId);
return (
<div className="space-y-2 px-4">
@ -54,17 +51,17 @@ export function ServicesList() {
);
}
function useServices(masterId?: string) {
function useServices(telegramId?: Readonly<ProfileProps>['telegramId']) {
const { data: { customer } = {}, isLoading: isLoadingCustomer } = useCustomerQuery();
// Используем переданный masterId или текущего пользователя
const targetMasterId = masterId || customer?.documentId;
const targetTelegramId = telegramId || customer?.telegramId;
const { data: { services } = {}, isLoading: isLoadingServices } = useServicesQuery({
filters: {
master: {
documentId: {
eq: targetMasterId,
telegramId: {
eq: targetTelegramId,
},
},
},

View File

@ -2,26 +2,26 @@
'use client';
import { useCustomerQuery } from '@/hooks/api/customers';
import { useSubscriptionQuery } from '@/hooks/api/subscriptions';
import { useSubscriptionQuery, useSubscriptionSettingQuery } from '@/hooks/api/subscriptions';
import { Enum_Customer_Role } from '@repo/graphql/types';
import { cn } from '@repo/ui/lib/utils';
import { ChevronRight } from 'lucide-react';
import Link from 'next/link';
export function SubscriptionInfoBar() {
const { data, error, isLoading } = useSubscriptionQuery();
const { data: { subscriptionSetting } = {}, isLoading: isLoadingSubscriptionSetting } =
useSubscriptionSettingQuery();
const { data, isLoading: isLoadingSubscription } = useSubscriptionQuery();
const { data: { customer } = {} } = useCustomerQuery();
const { data: { customer } = {}, isLoading: isLoadingCustomer } = useCustomerQuery();
const isLoading = isLoadingCustomer || isLoadingSubscription || isLoadingSubscriptionSetting;
const isActive = data?.hasActiveSubscription;
const remainingOrdersCount = data?.remainingOrdersCount;
const remainingDays = data?.remainingDays;
const maxOrdersPerMonth = data?.maxOrdersPerMonth;
if (customer?.role === Enum_Customer_Role.Client) return null;
if (error) return null;
const title = isActive ? 'Pro доступ активен' : 'Pro доступ неактивен';
let description = 'Попробуйте бесплатно';
@ -34,6 +34,10 @@ export function SubscriptionInfoBar() {
description = `Осталось ${remainingDays} дней`;
}
if (!subscriptionSetting?.proEnabled) return null;
if (customer?.role === Enum_Customer_Role.Client) return null;
return (
<Link href="/pro" rel="noopener noreferrer">
<div className={cn('px-4', isLoading && 'animate-pulse')}>