Compare commits
2 Commits
feature/i1
...
feature/fr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34179b3f30 | ||
| f8fcda0d13 |
@@ -3,6 +3,7 @@ import { updateProfileSchema } from '@family-wishlist/shared';
|
|||||||
import { ConflictError, NotFoundError, ValidationError } from '../../utils/errors.js';
|
import { ConflictError, NotFoundError, ValidationError } from '../../utils/errors.js';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { deleteLocalImageIfAny, saveUploadedAvatar } from '../images/storage.service.js';
|
import { deleteLocalImageIfAny, saveUploadedAvatar } from '../images/storage.service.js';
|
||||||
|
import { usersRegistry } from '../../auth/users.registry.js';
|
||||||
|
|
||||||
const MAX_AVATAR_BYTES = 2 * 1024 * 1024;
|
const MAX_AVATAR_BYTES = 2 * 1024 * 1024;
|
||||||
|
|
||||||
@@ -15,6 +16,16 @@ export default async function profileRoutes(app: FastifyInstance) {
|
|||||||
return profile;
|
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) => {
|
app.patch('/', async (request) => {
|
||||||
const body = updateProfileSchema.parse(request.body);
|
const body = updateProfileSchema.parse(request.body);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import { Link, NavLink, useNavigate } from 'react-router-dom';
|
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 type { ComponentType } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Button } from '../ui/Button';
|
import { Button } from '../ui/Button';
|
||||||
import { useAuthStore } from '@/features/auth/authStore';
|
import { useAuthStore } from '@/features/auth/authStore';
|
||||||
import { cn } from '@/lib/cn';
|
import { cn } from '@/lib/cn';
|
||||||
import { useI18n, type TranslationKey } from '@/i18n/i18n';
|
import { useI18n, type TranslationKey } from '@/i18n/i18n';
|
||||||
import { LanguageSwitcher } from '../LanguageSwitcher';
|
import { LanguageSwitcher } from '../LanguageSwitcher';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
|
||||||
type NavIcon = ComponentType<{ className?: string }>;
|
type NavIcon = ComponentType<{ className?: string }>;
|
||||||
|
|
||||||
|
interface FriendProfile {
|
||||||
|
slug: string;
|
||||||
|
displayName: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ to: '/', label: 'header.active', icon: Sparkles, end: true },
|
{ to: '/', label: 'header.active', icon: Sparkles, end: true },
|
||||||
{ to: '/completed', label: 'header.fulfilled', icon: CheckCircle2 },
|
{ to: '/completed', label: 'header.fulfilled', icon: CheckCircle2 },
|
||||||
@@ -26,6 +34,12 @@ export function Header() {
|
|||||||
const logout = useAuthStore((s) => s.logout);
|
const logout = useAuthStore((s) => s.logout);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const friend = useQuery({
|
||||||
|
queryKey: ['profile-friend'],
|
||||||
|
queryFn: () => api.get<FriendProfile | null>('/api/profile/friend'),
|
||||||
|
staleTime: 10 * 60 * 1000,
|
||||||
|
enabled: user != null,
|
||||||
|
});
|
||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
@@ -61,6 +75,20 @@ export function Header() {
|
|||||||
{t(l.label)}
|
{t(l.label)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
|
{friend.data && (
|
||||||
|
<NavLink
|
||||||
|
to={`/u/${friend.data.slug}`}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
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',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Users className="h-4 w-4" />
|
||||||
|
{t('header.friendWishes', { name: friend.data.displayName })}
|
||||||
|
</NavLink>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const translations = {
|
|||||||
'header.signedInAs': 'вошли как {name}',
|
'header.signedInAs': 'вошли как {name}',
|
||||||
'header.profileSettings': 'Настройки профиля',
|
'header.profileSettings': 'Настройки профиля',
|
||||||
'header.profile': 'Профиль',
|
'header.profile': 'Профиль',
|
||||||
|
'header.friendWishes': 'Желания {name}',
|
||||||
'header.logout': 'Выйти',
|
'header.logout': 'Выйти',
|
||||||
'login.title': 'С возвращением',
|
'login.title': 'С возвращением',
|
||||||
'login.description': 'Войдите, чтобы управлять списком желаний. Доступы задаются на сервере.',
|
'login.description': 'Войдите, чтобы управлять списком желаний. Доступы задаются на сервере.',
|
||||||
@@ -151,6 +152,7 @@ const translations = {
|
|||||||
'header.signedInAs': 'signed in as {name}',
|
'header.signedInAs': 'signed in as {name}',
|
||||||
'header.profileSettings': 'Profile settings',
|
'header.profileSettings': 'Profile settings',
|
||||||
'header.profile': 'Profile',
|
'header.profile': 'Profile',
|
||||||
|
'header.friendWishes': "{name}'s wishes",
|
||||||
'header.logout': 'Log out',
|
'header.logout': 'Log out',
|
||||||
'login.title': 'Welcome back',
|
'login.title': 'Welcome back',
|
||||||
'login.description': 'Sign in to manage your wishlist. Credentials are set up via the server environment.',
|
'login.description': 'Sign in to manage your wishlist. Credentials are set up via the server environment.',
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ export const profileSchema = z.object({
|
|||||||
|
|
||||||
export type Profile = z.infer<typeof profileSchema>;
|
export type Profile = z.infer<typeof profileSchema>;
|
||||||
|
|
||||||
|
export const friendProfileSchema = z.object({
|
||||||
|
slug: z.string(),
|
||||||
|
displayName: z.string(),
|
||||||
|
avatarUrl: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FriendProfile = z.infer<typeof friendProfileSchema>;
|
||||||
|
|
||||||
export const updateProfileSchema = z.object({
|
export const updateProfileSchema = z.object({
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
Reference in New Issue
Block a user