Files
family_wishlist/apps/frontend/src/pages/PublicProfilePage.tsx
2026-04-26 22:16:59 +03:00

119 lines
4.2 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import type {
PublicProfile,
Wish,
} from '@family-wishlist/shared';
import { api } from '@/lib/api';
import { WishCard } from '@/components/WishCard/WishCard';
import { Footer } from '@/components/Layout/Footer';
import { Gift } from 'lucide-react';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import { useI18n } from '@/i18n/i18n';
export function PublicProfilePage() {
const { t } = useI18n();
const { slug = '' } = useParams<{ slug: string }>();
const queryClient = useQueryClient();
const profile = useQuery({
queryKey: ['public-profile', slug],
queryFn: () => api.get<PublicProfile>(`/api/public/${encodeURIComponent(slug)}`),
retry: false,
enabled: slug.length > 0,
});
const wishes = useQuery({
queryKey: ['public-wishes', slug],
queryFn: () => api.get<Wish[]>(`/api/public/${encodeURIComponent(slug)}/wishes`),
enabled: slug.length > 0 && profile.isSuccess,
});
const markSeen = useMutation({
mutationFn: (wishIds: string[]) =>
api.post(`/api/public/${encodeURIComponent(slug)}/views`, { wishIds }),
});
const marked = useRef(false);
useEffect(() => {
if (!wishes.data || marked.current) return;
const newIds = wishes.data.filter((w) => w.isNewForGuest).map((w) => w.id);
if (newIds.length === 0) {
marked.current = true;
return;
}
const t = window.setTimeout(() => {
markSeen.mutate(newIds, {
onSuccess: () => {
marked.current = true;
void queryClient.invalidateQueries({ queryKey: ['public-wishes', slug] });
},
});
}, 1500);
return () => window.clearTimeout(t);
}, [wishes.data, markSeen, queryClient, slug]);
return (
<div className="flex min-h-screen flex-col">
<div className="container-page flex justify-end pt-6">
<LanguageSwitcher />
</div>
<main className="container-page flex-1 py-10">
{profile.isLoading && <div className="text-muted">{t('common.loading')}</div>}
{profile.isError && (
<div className="mx-auto max-w-lg rounded-xl border border-border bg-surface p-8 text-center shadow-card">
<h1 className="font-display text-2xl">{t('public.notFoundTitle')}</h1>
<p className="mt-2 text-sm text-muted">
{t('public.notFoundText')}
</p>
</div>
)}
{profile.data && (
<>
<section className="mb-10 flex flex-col items-center gap-3 text-center">
<span className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-card">
{profile.data.avatarUrl ? (
<img
src={profile.data.avatarUrl}
alt=""
className="h-14 w-14 rounded-full object-cover"
/>
) : (
<Gift className="h-6 w-6" />
)}
</span>
<h1 className="font-display text-4xl">
{t('public.wishlistTitle', { name: profile.data.displayName })}
</h1>
{profile.data.bio && (
<p className="max-w-xl text-muted">{profile.data.bio}</p>
)}
</section>
{wishes.isLoading && <div className="text-muted">{t('public.loadingWishes')}</div>}
{wishes.data && wishes.data.length === 0 && (
<div className="mx-auto max-w-lg rounded-xl border border-border bg-surface/80 p-10 text-center shadow-card">
<h2 className="text-xl font-semibold">{t('public.emptyTitle')}</h2>
<p className="mt-1 text-sm text-muted">{t('public.emptyText')}</p>
</div>
)}
{wishes.data && wishes.data.length > 0 && (
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{wishes.data.map((wish) => (
<WishCard key={wish.id} wish={wish} view="guest" />
))}
</div>
)}
</>
)}
</main>
<Footer />
</div>
);
}