From 34179b3f30987f599fb668ce5f42dc194b71a582 Mon Sep 17 00:00:00 2001 From: "Vaka.pro" Date: Sun, 26 Apr 2026 23:29:20 +0300 Subject: [PATCH] feat: add friend wishlist link --- .../src/modules/profile/profile.routes.ts | 11 +++++++ .../frontend/src/components/Layout/Header.tsx | 30 ++++++++++++++++++- apps/frontend/src/i18n/i18n.tsx | 2 ++ packages/shared/src/profile.schema.ts | 8 +++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/modules/profile/profile.routes.ts b/apps/backend/src/modules/profile/profile.routes.ts index 9ed433e..c28deb0 100644 --- a/apps/backend/src/modules/profile/profile.routes.ts +++ b/apps/backend/src/modules/profile/profile.routes.ts @@ -3,6 +3,7 @@ import { updateProfileSchema } from '@family-wishlist/shared'; import { ConflictError, NotFoundError, ValidationError } from '../../utils/errors.js'; import { Prisma } from '@prisma/client'; import { deleteLocalImageIfAny, saveUploadedAvatar } from '../images/storage.service.js'; +import { usersRegistry } from '../../auth/users.registry.js'; const MAX_AVATAR_BYTES = 2 * 1024 * 1024; @@ -15,6 +16,16 @@ export default async function profileRoutes(app: FastifyInstance) { return profile; }); + app.get('/friend', async (request) => { + const friend = usersRegistry.all().find((u) => u.id !== request.user.id); + if (!friend) return null; + + return app.prisma.user.findUnique({ + where: { id: friend.id }, + select: { slug: true, displayName: true, avatarUrl: true }, + }); + }); + app.patch('/', async (request) => { const body = updateProfileSchema.parse(request.body); try { diff --git a/apps/frontend/src/components/Layout/Header.tsx b/apps/frontend/src/components/Layout/Header.tsx index 84fe882..fefc8e5 100644 --- a/apps/frontend/src/components/Layout/Header.tsx +++ b/apps/frontend/src/components/Layout/Header.tsx @@ -1,14 +1,22 @@ import { Link, NavLink, useNavigate } from 'react-router-dom'; -import { Archive, Gift, LogOut, Sparkles, Trash2, UserCog, CheckCircle2 } from 'lucide-react'; +import { Archive, CheckCircle2, Gift, LogOut, Sparkles, Trash2, UserCog, Users } from 'lucide-react'; import type { ComponentType } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { Button } from '../ui/Button'; import { useAuthStore } from '@/features/auth/authStore'; import { cn } from '@/lib/cn'; import { useI18n, type TranslationKey } from '@/i18n/i18n'; import { LanguageSwitcher } from '../LanguageSwitcher'; +import { api } from '@/lib/api'; type NavIcon = ComponentType<{ className?: string }>; +interface FriendProfile { + slug: string; + displayName: string; + avatarUrl: string | null; +} + const links = [ { to: '/', label: 'header.active', icon: Sparkles, end: true }, { to: '/completed', label: 'header.fulfilled', icon: CheckCircle2 }, @@ -26,6 +34,12 @@ export function Header() { const logout = useAuthStore((s) => s.logout); const navigate = useNavigate(); const { t } = useI18n(); + const friend = useQuery({ + queryKey: ['profile-friend'], + queryFn: () => api.get('/api/profile/friend'), + staleTime: 10 * 60 * 1000, + enabled: user != null, + }); if (!user) return null; @@ -61,6 +75,20 @@ export function Header() { {t(l.label)} ))} + {friend.data && ( + + cn( + 'inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors', + isActive ? 'bg-primary text-primary-foreground shadow-card' : 'text-ink hover:bg-ink/5', + ) + } + > + + {t('header.friendWishes', { name: friend.data.displayName })} + + )}
diff --git a/apps/frontend/src/i18n/i18n.tsx b/apps/frontend/src/i18n/i18n.tsx index 6c01857..4d259f2 100644 --- a/apps/frontend/src/i18n/i18n.tsx +++ b/apps/frontend/src/i18n/i18n.tsx @@ -36,6 +36,7 @@ const translations = { 'header.signedInAs': 'вошли как {name}', 'header.profileSettings': 'Настройки профиля', 'header.profile': 'Профиль', + 'header.friendWishes': 'Желания {name}', 'header.logout': 'Выйти', 'login.title': 'С возвращением', 'login.description': 'Войдите, чтобы управлять списком желаний. Доступы задаются на сервере.', @@ -151,6 +152,7 @@ const translations = { 'header.signedInAs': 'signed in as {name}', 'header.profileSettings': 'Profile settings', 'header.profile': 'Profile', + 'header.friendWishes': "{name}'s wishes", 'header.logout': 'Log out', 'login.title': 'Welcome back', 'login.description': 'Sign in to manage your wishlist. Credentials are set up via the server environment.', diff --git a/packages/shared/src/profile.schema.ts b/packages/shared/src/profile.schema.ts index fb4033d..8f9fe12 100644 --- a/packages/shared/src/profile.schema.ts +++ b/packages/shared/src/profile.schema.ts @@ -15,6 +15,14 @@ export const profileSchema = z.object({ export type Profile = z.infer; +export const friendProfileSchema = z.object({ + slug: z.string(), + displayName: z.string(), + avatarUrl: z.string().nullable(), +}); + +export type FriendProfile = z.infer; + export const updateProfileSchema = z.object({ slug: z .string()