feat(frontend): add react spa with wishlist flows and public profile
This commit is contained in:
110
apps/frontend/src/pages/PublicProfilePage.tsx
Normal file
110
apps/frontend/src/pages/PublicProfilePage.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
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';
|
||||
|
||||
export function PublicProfilePage() {
|
||||
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">
|
||||
<main className="container-page flex-1 py-10">
|
||||
{profile.isLoading && <div className="text-muted">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">Profile not found</h1>
|
||||
<p className="mt-2 text-sm text-muted">
|
||||
Check the link and try again. Slugs are case-sensitive.
|
||||
</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">{profile.data.displayName}'s wishlist</h1>
|
||||
{profile.data.bio && (
|
||||
<p className="max-w-xl text-muted">{profile.data.bio}</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{wishes.isLoading && <div className="text-muted">Loading wishes...</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">No wishes yet</h2>
|
||||
<p className="mt-1 text-sm text-muted">Check back later!</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user