From 058fa26e126cf8f6fe2d14f065fc44b860787b37 Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 21 May 2026 13:39:45 +0500 Subject: [PATCH] test commit --- client/package-lock.json | 223 +++++++++++++++++- client/package.json | 17 ++ .../src/entities/order/api/admin-order-api.ts | 2 +- .../product-review/ui/ProductReviewsList.tsx | 8 +- .../features/user/user-menu/ui/UserMenu.tsx | 14 +- client/src/pages/auth/ui/AuthPage.tsx | 9 +- .../src/pages/me/ui/sections/SettingsPage.tsx | 136 ++++++++++- client/src/shared/lib/avatar-styles.ts | 88 +++++++ client/src/shared/model/auth.ts | 10 +- client/src/shared/ui/UserAvatar.tsx | 30 +++ .../widgets/reviews-block/ui/ReviewsBlock.tsx | 18 +- .../migration.sql | 2 + .../migration.sql | 1 + server/prisma/prisma/dev.db | Bin 315392 -> 364544 bytes server/prisma/schema.prisma | 3 +- server/src/routes/api/admin-orders.js | 2 +- server/src/routes/auth.js | 43 +++- server/src/routes/user-payments.js | 2 +- 18 files changed, 563 insertions(+), 45 deletions(-) create mode 100644 client/src/shared/lib/avatar-styles.ts create mode 100644 client/src/shared/ui/UserAvatar.tsx create mode 100644 server/prisma/migrations/20260521100000_drop_phone_add_avatar_type/migration.sql create mode 100644 server/prisma/migrations/20260521103000_add_avatar_style/migration.sql diff --git a/client/package-lock.json b/client/package-lock.json index 375b76d..fcd74ee 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,23 @@ "name": "client", "version": "0.0.0", "dependencies": { + "@dicebear/adventurer": "^9.4.2", + "@dicebear/avataaars": "^9.4.2", + "@dicebear/big-ears": "^9.4.2", + "@dicebear/big-smile": "^9.4.2", + "@dicebear/bottts": "^9.4.2", + "@dicebear/core": "^9.4.2", + "@dicebear/croodles": "^9.4.2", + "@dicebear/fun-emoji": "^9.4.2", + "@dicebear/identicon": "^9.4.2", + "@dicebear/initials": "^9.4.2", + "@dicebear/lorelei": "^9.4.2", + "@dicebear/micah": "^9.4.2", + "@dicebear/notionists": "^9.4.2", + "@dicebear/pixel-art": "^9.4.2", + "@dicebear/rings": "^9.4.2", + "@dicebear/shapes": "^9.4.2", + "@dicebear/thumbs": "^9.4.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^9.0.0", @@ -465,6 +482,211 @@ "node": ">=18" } }, + "node_modules/@dicebear/adventurer": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.4.2.tgz", + "integrity": "sha512-jqYp834ZmGDA9HBBDQAdgF1O2UTCwHF4vVrktXWa2Dppp1JczPL5HnVOWsjtrLmXNn61Wd6OLmBb2e6rhzp3ig==", + "license": "(MIT AND CC-BY-4.0)", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/avataaars": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-9.4.2.tgz", + "integrity": "sha512-3x9jKFkOkFSPmpTbt9xvhiU2E1GX7beCSsX0tXRUShj8x6+5Ks9yBRT1VlkySbnXrZ/GglADGg7vJ/D2uIx1Yw==", + "license": "See LICENSE file", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/big-ears": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-9.4.2.tgz", + "integrity": "sha512-mNfz3ppNA7UBq0IO3nXCiV5pFPG7c1DfzRB0foNU2Wo1XXT8FIcSY2BvDlYqorZTOUOz7dHb0vx06hqvG0HP5w==", + "license": "(MIT AND CC-BY-4.0)", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/big-smile": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-9.4.2.tgz", + "integrity": "sha512-hmT5i7rcPPhStjZyg28pbIhdTnnMBzK3RObI0vKCpY30EFrzaPkkdDL6Ck5fAFBdvDIW1EpOJkenyR0XPmhgbQ==", + "license": "(MIT AND CC-BY-4.0)", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/bottts": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-9.4.2.tgz", + "integrity": "sha512-tsx+dII7EFUCVA8URj66G1GqORCCVduCAx4dY2prEY2IeFianVpkntXuFsWZ9BBGx1NZFndvDith5oTwKMQPbQ==", + "license": "See LICENSE file", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/core": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.4.2.tgz", + "integrity": "sha512-MF0042+Z3s8PGZKZLySfhft28bUa3B1iq0e5NSjCvY8gfMi5aIH/iRJGRJa1N9Jz1BNkxYb4yvJ/N9KO8Z6Y+w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@dicebear/croodles": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-9.4.2.tgz", + "integrity": "sha512-6VoO0JviIf7dKKMBTL/SMXxWhnXHaZuzufX90G0nXxS77ELG1YkGNMaZzawizN4C09Gbya2gJkozqrWiJN/aGw==", + "license": "(MIT AND CC-BY-4.0)", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/fun-emoji": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-9.4.2.tgz", + "integrity": "sha512-kqB6LPkdYCdEU/mwbyz34xLzoNUKL6ARcoo3fr5ASq9D6ZE07qIKybC3xv5+CPz7VmspJ1Q3c/VVWVMDRP7Twg==", + "license": "(MIT AND CC-BY-4.0)", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/identicon": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-9.4.2.tgz", + "integrity": "sha512-JVDSmZsv11mSWqwAktK5x9Bslht2xY3TFUn8xzu6slAYe1Z7hEXZ76eb+UJ6F4qEzdwZ7xPWzAS6Nb0Y3A0pww==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/initials": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-9.4.2.tgz", + "integrity": "sha512-yePuIUasmwtl9IrtB6rEzE/zb5fImKP/neW0CdcTC2MwLgMuP1GLHEGRgg1zI8exIh+PMv1YdLGyyUuRTE2Qpw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/lorelei": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-9.4.2.tgz", + "integrity": "sha512-YMv6vnriW6VLFDsreKuOnUFFno6SRe7+7X7R7zPY0rZ+MaHX9V3jcioIG+1PSjIHEDfOLUHpr5vd1JBWv8y7UA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/micah": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-9.4.2.tgz", + "integrity": "sha512-e4D3W/OlChSsLo7Llwsy0J18vk0azJqF/uFoY+EKACCNHBc1HGNsqVvu2CTf+OWOA8wTyAK6UkjBN5p01r7D+g==", + "license": "(MIT AND CC-BY-4.0)", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/notionists": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/notionists/-/notionists-9.4.2.tgz", + "integrity": "sha512-ZCySq+nxcD/x4xyYgytcj2N9uY3gxrL+qpnmOdp2BdA221KacVrxlsUPpIgEMqxS2rMmBQXfxg129Pzn4ycIpA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/pixel-art": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-9.4.2.tgz", + "integrity": "sha512-peHf7oKICDgBZ8dUyj+txPnS7VZEWgvKE+xW4mNQqBt6dYZIjmva2shOVHn0b1JU+FDxMx3uIkWVixKdUq4WGg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/rings": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/rings/-/rings-9.4.2.tgz", + "integrity": "sha512-Pc3ymWrRDQPJFNrbbLt7RJrzGvUuuxUiDkrfLhoVE+B6mZWEL1PC78DPbS1yUWYLErJOpJuM2GSwXmTbVjWf+g==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/shapes": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-9.4.2.tgz", + "integrity": "sha512-AFL6jAaiLztvcqyq+ds+lWZu6Vbp3PlGWhJeJRm842jxtiluJpl6r4f6nUXP2fdMz7MNpDzXfLooQK9E04NbUQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/thumbs": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-9.4.2.tgz", + "integrity": "sha512-ccWvDBqbkWS5uzHbsg5L6uML6vBfX7jT3J3jHCQksvz8haHItxTK02w+6e1UavZUsvza4lG5X/XY3eji3siJ4Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -3050,7 +3272,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { diff --git a/client/package.json b/client/package.json index 54fb1cc..d031569 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,23 @@ "test:watch": "vitest" }, "dependencies": { + "@dicebear/adventurer": "^9.4.2", + "@dicebear/avataaars": "^9.4.2", + "@dicebear/big-ears": "^9.4.2", + "@dicebear/big-smile": "^9.4.2", + "@dicebear/bottts": "^9.4.2", + "@dicebear/core": "^9.4.2", + "@dicebear/croodles": "^9.4.2", + "@dicebear/fun-emoji": "^9.4.2", + "@dicebear/identicon": "^9.4.2", + "@dicebear/initials": "^9.4.2", + "@dicebear/lorelei": "^9.4.2", + "@dicebear/micah": "^9.4.2", + "@dicebear/notionists": "^9.4.2", + "@dicebear/pixel-art": "^9.4.2", + "@dicebear/rings": "^9.4.2", + "@dicebear/shapes": "^9.4.2", + "@dicebear/thumbs": "^9.4.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^9.0.0", diff --git a/client/src/entities/order/api/admin-order-api.ts b/client/src/entities/order/api/admin-order-api.ts index 43df1d6..2c38e16 100644 --- a/client/src/entities/order/api/admin-order-api.ts +++ b/client/src/entities/order/api/admin-order-api.ts @@ -37,7 +37,7 @@ export type AdminOrderDetailResponse = { comment: string | null createdAt: string updatedAt: string - user: { id: string; email: string; displayName: string | null; phone: string | null } + user: { id: string; email: string; displayName: string | null } items: Array<{ id: string productId: string diff --git a/client/src/features/product-review/ui/ProductReviewsList.tsx b/client/src/features/product-review/ui/ProductReviewsList.tsx index fd922bb..8a0ac36 100644 --- a/client/src/features/product-review/ui/ProductReviewsList.tsx +++ b/client/src/features/product-review/ui/ProductReviewsList.tsx @@ -11,14 +11,18 @@ import type { PublicProductReviewItem } from '@/entities/review/api/reviews-api' import { reviewsCountRu } from '@/shared/lib/reviews-count-ru' import { OptimizedImage } from '@/shared/ui/OptimizedImage' import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent' +import { UserAvatar } from '@/shared/ui/UserAvatar' function ReviewItem({ rv }: { rv: PublicProductReviewItem }) { const body = typeof rv.text === 'string' && rv.text.trim() ? rv.text.trim() : null return ( - - {rv.authorDisplay} + + + + {rv.authorDisplay} + {new Date(rv.createdAt).toLocaleString('ru-RU')} diff --git a/client/src/features/user/user-menu/ui/UserMenu.tsx b/client/src/features/user/user-menu/ui/UserMenu.tsx index ed93c19..fe72a6a 100644 --- a/client/src/features/user/user-menu/ui/UserMenu.tsx +++ b/client/src/features/user/user-menu/ui/UserMenu.tsx @@ -4,8 +4,8 @@ import IconButton from '@mui/material/IconButton' import ListItemText from '@mui/material/ListItemText' import Menu from '@mui/material/Menu' import MenuItem from '@mui/material/MenuItem' -import { User } from 'lucide-react' import type { AuthUser } from '@/shared/model/auth' +import { UserAvatar } from '@/shared/ui/UserAvatar' type Props = { user: AuthUser | null @@ -40,7 +40,17 @@ export function UserMenu({ user, onNavigate, onLogout }: Props) { invisible={!user} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} > - + {user ? ( + + ) : ( + + )} diff --git a/client/src/pages/auth/ui/AuthPage.tsx b/client/src/pages/auth/ui/AuthPage.tsx index 49edb29..6105a6c 100644 --- a/client/src/pages/auth/ui/AuthPage.tsx +++ b/client/src/pages/auth/ui/AuthPage.tsx @@ -16,7 +16,14 @@ import { $user, tokenSet } from '@/shared/model/auth' type AuthResponse = { token: string - user: { id: string; email: string; displayName?: string | null; phone?: string | null } + user: { + id: string + email: string + displayName?: string | null + avatar?: string | null + avatarType?: string | null + avatarStyle?: string | null + } } function getApiErrorMessage(err: unknown): string | null { diff --git a/client/src/pages/me/ui/sections/SettingsPage.tsx b/client/src/pages/me/ui/sections/SettingsPage.tsx index 338ba1f..2b0e403 100644 --- a/client/src/pages/me/ui/sections/SettingsPage.tsx +++ b/client/src/pages/me/ui/sections/SettingsPage.tsx @@ -1,12 +1,19 @@ +import { useState } from 'react' import Alert from '@mui/material/Alert' import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Divider from '@mui/material/Divider' +import FormControl from '@mui/material/FormControl' +import InputLabel from '@mui/material/InputLabel' +import MenuItem from '@mui/material/MenuItem' +import Select from '@mui/material/Select' import Stack from '@mui/material/Stack' import TextField from '@mui/material/TextField' import Typography from '@mui/material/Typography' +import { createAvatar } from '@dicebear/core' import { useUnit } from 'effector-react' import { useForm } from 'react-hook-form' +import { AVATAR_STYLES, DEFAULT_STYLE_ID, getStyleById } from '@/shared/lib/avatar-styles' import { $requestEmailChangeCodeError, $updateProfileError, @@ -16,6 +23,7 @@ import { updateProfileFx, verifyEmailChangeFx, } from '@/shared/model/auth' +import { UserAvatar } from '@/shared/ui/UserAvatar' import type { AxiosError } from 'axios' function getApiErrorMessage(error: unknown): string | null { @@ -38,10 +46,9 @@ export function SettingsPage() { mode: 'onChange', }) - const profileForm = useForm<{ displayName: string; phone: string }>({ + const profileForm = useForm<{ displayName: string }>({ defaultValues: { displayName: user?.displayName ? String(user.displayName) : '', - phone: user?.phone ? String(user.phone) : '', }, mode: 'onChange', }) @@ -49,6 +56,16 @@ export function SettingsPage() { const emailErrorMsg = getApiErrorMessage(errorEmailReq) ?? getApiErrorMessage(errorEmailVerify) const profileErrorMsg = getApiErrorMessage(errorProfile) + const hasOAuthAvatar = Boolean(user?.avatar && user.avatarType !== 'generated') + const useOAuth = user?.avatarType === 'oauth' + const useGenerated = user?.avatarType === 'generated' + + const [selectedStyle, setSelectedStyle] = useState(user?.avatarStyle || DEFAULT_STYLE_ID) + const [previewSrc, setPreviewSrc] = useState(null) + const [previewStyle, setPreviewStyle] = useState(DEFAULT_STYLE_ID) + + const hasUnsavedPreview = previewSrc !== null + if (!user) { return Нужно войти. Перейдите на страницу «Вход». } @@ -85,20 +102,13 @@ export function SettingsPage() { slotProps={{ htmlInput: { maxLength: 40 } }} {...profileForm.register('displayName')} /> - + + + {hasUnsavedPreview && ( + + + + + )} + + {hasOAuthAvatar && !hasUnsavedPreview && ( + + )} + + + + Смена почты diff --git a/client/src/shared/lib/avatar-styles.ts b/client/src/shared/lib/avatar-styles.ts new file mode 100644 index 0000000..a3e3bec --- /dev/null +++ b/client/src/shared/lib/avatar-styles.ts @@ -0,0 +1,88 @@ +import { create as adventurerCreate, meta as adventurerMeta, schema as adventurerSchema } from '@dicebear/adventurer' +import { create as avataaarsCreate, meta as avataaarsMeta, schema as avataaarsSchema } from '@dicebear/avataaars' +import { create as bigEarsCreate, meta as bigEarsMeta, schema as bigEarsSchema } from '@dicebear/big-ears' +import { create as bigSmileCreate, meta as bigSmileMeta, schema as bigSmileSchema } from '@dicebear/big-smile' +import { create as botttsCreate, meta as botttsMeta, schema as botttsSchema } from '@dicebear/bottts' +import { create as croodlesCreate, meta as croodlesMeta, schema as croodlesSchema } from '@dicebear/croodles' +import { create as funEmojiCreate, meta as funEmojiMeta, schema as funEmojiSchema } from '@dicebear/fun-emoji' +import { create as identiconCreate, meta as identiconMeta, schema as identiconSchema } from '@dicebear/identicon' +import { create as initialsCreate, meta as initialsMeta, schema as initialsSchema } from '@dicebear/initials' +import { create as loreleiCreate, meta as loreleiMeta, schema as loreleiSchema } from '@dicebear/lorelei' +import { create as micahCreate, meta as micahMeta, schema as micahSchema } from '@dicebear/micah' +import { create as notionistsCreate, meta as notionistsMeta, schema as notionistsSchema } from '@dicebear/notionists' +import { create as pixelArtCreate, meta as pixelArtMeta, schema as pixelArtSchema } from '@dicebear/pixel-art' +import { create as ringsCreate, meta as ringsMeta, schema as ringsSchema } from '@dicebear/rings' +import { create as shapesCreate, meta as shapesMeta, schema as shapesSchema } from '@dicebear/shapes' +import { create as thumbsCreate, meta as thumbsMeta, schema as thumbsSchema } from '@dicebear/thumbs' +import type { Style } from '@dicebear/core' + +type StyleDef = { + id: string + label: string + style: Style +} + +export const AVATAR_STYLES: StyleDef[] = [ + { id: 'bottts', label: 'Роботы', style: { create: botttsCreate, meta: botttsMeta, schema: botttsSchema } }, + { + id: 'identicon', + label: 'Узоры', + style: { create: identiconCreate, meta: identiconMeta, schema: identiconSchema }, + }, + { + id: 'avataaars', + label: 'Персонажи', + style: { create: avataaarsCreate, meta: avataaarsMeta, schema: avataaarsSchema }, + }, + { + id: 'notionists', + label: 'Notion', + style: { create: notionistsCreate, meta: notionistsMeta, schema: notionistsSchema }, + }, + { id: 'thumbs', label: 'Thumbs', style: { create: thumbsCreate, meta: thumbsMeta, schema: thumbsSchema } }, + { id: 'lorelei', label: 'Lorelei', style: { create: loreleiCreate, meta: loreleiMeta, schema: loreleiSchema } }, + { id: 'micah', label: 'Micah', style: { create: micahCreate, meta: micahMeta, schema: micahSchema } }, + { + id: 'pixel-art', + label: 'Пиксели', + style: { create: pixelArtCreate, meta: pixelArtMeta, schema: pixelArtSchema }, + }, + { id: 'rings', label: 'Кольца', style: { create: ringsCreate, meta: ringsMeta, schema: ringsSchema } }, + { id: 'shapes', label: 'Фигуры', style: { create: shapesCreate, meta: shapesMeta, schema: shapesSchema } }, + { + id: 'initials', + label: 'Инициалы', + style: { create: initialsCreate, meta: initialsMeta, schema: initialsSchema }, + }, + { + id: 'adventurer', + label: 'Adventurer', + style: { create: adventurerCreate, meta: adventurerMeta, schema: adventurerSchema }, + }, + { + id: 'big-ears', + label: 'Big Ears', + style: { create: bigEarsCreate, meta: bigEarsMeta, schema: bigEarsSchema }, + }, + { + id: 'big-smile', + label: 'Big Smile', + style: { create: bigSmileCreate, meta: bigSmileMeta, schema: bigSmileSchema }, + }, + { + id: 'croodles', + label: 'Croodles', + style: { create: croodlesCreate, meta: croodlesMeta, schema: croodlesSchema }, + }, + { + id: 'fun-emoji', + label: 'Fun Emoji', + style: { create: funEmojiCreate, meta: funEmojiMeta, schema: funEmojiSchema }, + }, +] + +export const DEFAULT_STYLE_ID = 'bottts' + +export function getStyleById(id: string | null | undefined): StyleDef { + return AVATAR_STYLES.find((s) => s.id === id) ?? AVATAR_STYLES[0] +} diff --git a/client/src/shared/model/auth.ts b/client/src/shared/model/auth.ts index 3c4540f..6761582 100644 --- a/client/src/shared/model/auth.ts +++ b/client/src/shared/model/auth.ts @@ -11,7 +11,8 @@ export type AuthUser = { lastName?: string | null gender?: string | null avatar?: string | null - phone?: string | null + avatarType?: string | null + avatarStyle?: string | null isAdmin?: boolean } @@ -68,7 +69,12 @@ export const verifyEmailChangeFx = createEffect(async (params: { newEmail: strin // ----- Profile update ----- -export type UpdateProfileParams = { displayName: string | null; phone?: string | null } +export type UpdateProfileParams = { + displayName: string | null + avatar?: string | null + avatarType?: string | null + avatarStyle?: string | null +} export const updateProfileFx = createEffect(async (params: UpdateProfileParams) => { const { data } = await apiClient.patch<{ user: AuthUser }>('me/profile', params) diff --git a/client/src/shared/ui/UserAvatar.tsx b/client/src/shared/ui/UserAvatar.tsx new file mode 100644 index 0000000..bc3eecf --- /dev/null +++ b/client/src/shared/ui/UserAvatar.tsx @@ -0,0 +1,30 @@ +import { useMemo } from 'react' +import Avatar from '@mui/material/Avatar' +import type { SxProps, Theme } from '@mui/material/styles' +import { createAvatar } from '@dicebear/core' +import { DEFAULT_STYLE_ID, getStyleById } from '@/shared/lib/avatar-styles' + +type UserAvatarProps = { + userId: string + avatarUrl?: string | null + avatarType?: string | null + avatarStyle?: string | null + size?: number + sx?: SxProps +} + +export function UserAvatar({ userId, avatarUrl, avatarType, avatarStyle, size = 40, sx }: UserAvatarProps) { + const generatedSrc = useMemo(() => { + const styleDef = getStyleById(avatarStyle || DEFAULT_STYLE_ID) + const avatar = createAvatar(styleDef.style, { seed: userId }) + return avatar.toDataUri() + }, [userId, avatarStyle]) + + const src = avatarType && avatarUrl ? avatarUrl : generatedSrc + + return ( + + ? + + ) +} diff --git a/client/src/widgets/reviews-block/ui/ReviewsBlock.tsx b/client/src/widgets/reviews-block/ui/ReviewsBlock.tsx index d5a463b..f392849 100644 --- a/client/src/widgets/reviews-block/ui/ReviewsBlock.tsx +++ b/client/src/widgets/reviews-block/ui/ReviewsBlock.tsx @@ -1,6 +1,5 @@ import StarRoundedIcon from '@mui/icons-material/StarRounded' import Alert from '@mui/material/Alert' -import Avatar from '@mui/material/Avatar' import Box from '@mui/material/Box' import Paper from '@mui/material/Paper' import Rating from '@mui/material/Rating' @@ -12,12 +11,7 @@ import { Link as RouterLink } from 'react-router-dom' import { fetchLatestApprovedReviews } from '@/entities/review/api/reviews-api' import { OptimizedImage } from '@/shared/ui/OptimizedImage' import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent' - -function initials(display: string) { - const s = display.trim() - if (!s) return '?' - return s.slice(0, 1).toUpperCase() -} +import { UserAvatar } from '@/shared/ui/UserAvatar' function formatReviewDate(iso: string): string { try { @@ -107,9 +101,13 @@ export function ReviewsBlock() { )} - - {initials(r.authorDisplay)} - + {r.authorDisplay} diff --git a/server/prisma/migrations/20260521100000_drop_phone_add_avatar_type/migration.sql b/server/prisma/migrations/20260521100000_drop_phone_add_avatar_type/migration.sql new file mode 100644 index 0000000..384c607 --- /dev/null +++ b/server/prisma/migrations/20260521100000_drop_phone_add_avatar_type/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE User DROP COLUMN phone; +ALTER TABLE User ADD COLUMN "avatarType" TEXT; diff --git a/server/prisma/migrations/20260521103000_add_avatar_style/migration.sql b/server/prisma/migrations/20260521103000_add_avatar_style/migration.sql new file mode 100644 index 0000000..acccfeb --- /dev/null +++ b/server/prisma/migrations/20260521103000_add_avatar_style/migration.sql @@ -0,0 +1 @@ +ALTER TABLE User ADD COLUMN "avatarStyle" TEXT; diff --git a/server/prisma/prisma/dev.db b/server/prisma/prisma/dev.db index c72d8385c25bef9469f566ac93628e51a0d7cea5..8a10718afbb537e2244aae9e4785d6db96cef25c 100644 GIT binary patch literal 364544 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 T|NkFm$zPr^A$ImO^Z)-J!72XB delta 1124 zcmaJ>U1%It6ux)nPO`HgS zHAP?w-6w%h-K4`c_YE4-9hI*qgb;!(6IjNCt^i*?EbPUD83Hrt#(~egEM!Cqx2^i5 za1Uw$D#8bqVEjbtbsO|m8}-y1>1;F?EAbEi61wH%^OunYN}IR(kuEUDaobiN7nj!I zzC5huC&Y-K|C$g~i|OK@VXsX`fPDlOVIS5!2FlxlHxcXDxhse_lH?%qWKw+Yhy#7e zkxLyEjnKeel9VZwPi$GNLOC_k9*@TKp+YK`e>_tvp!>1ByeTj0EB{yN&8Dv6=wJ<3 zz*>D@+#Pj`icH|3GN;Ix?p>cHuZVYdU};YiIE^P+cx2o|{fQP?C8{`1Pz^e&OKeDB zstJCrhpCPBK$LTtk(1?8F`p|A(z%au_dfsZTl|N^tf^kb3~^fisk0YbMR4oLw+_>} z0sMAF*kLh0`zCzGkXobElaO6RmSxw!^=QL3#_Tqx)2*fWV5Z&S4V%(#&^DZ~`-~QJ z#3?sXK;KLPm{F(Sb`<_t@-ew2Sypk%nrL!mhvkQp#;$CA@E{w4{ z?a>Y5tNL`$PWR99W3Kn4Irk^>@63?D6aSz;!Zq4UHb}90MC+6*#ro|B9-`yB_~ls^ z_U8sp8L2l5gT>c+I(obI_wnCmS+j_zT>XqSTgEI)iSBp$hw8TngLERyf0}03-Puxc kFrxK!9qp@X(x~ 40) return reply.code(400).send({ error: 'Имя/ник максимум 40 символов' }) - if (phone !== null) { - const compact = phone.replace(/[\s()-]/g, '') - if (compact.length > 20) return reply.code(400).send({ error: 'Телефон слишком длинный' }) - if (compact.length && !/^\+?\d{7,20}$/.test(compact)) { - return reply.code(400).send({ error: 'Некорректный телефон' }) - } + if (avatarType !== undefined && avatarType !== 'oauth' && avatarType !== 'generated' && avatarType !== '') { + return reply.code(400).send({ error: 'avatarType должен быть oauth | generated | пустая строка' }) + } + if (avatar !== undefined && avatar.length > 200000) return reply.code(400).send({ error: 'Аватар слишком большой' }) + if (avatarStyle !== undefined && avatarStyle !== '' && avatarStyle.length > 30) { + return reply.code(400).send({ error: 'Стиль аватара слишком длинный' }) } + const data = { + displayName: displayName && displayName.length ? displayName : null, + } + + if (avatarType !== undefined) { + data.avatarType = avatarType === '' ? null : avatarType + } + if (avatar !== undefined) { + data.avatar = avatar === '' ? null : avatar + } + if (avatarStyle !== undefined) { + data.avatarStyle = avatarStyle === '' ? null : avatarStyle + } const updated = await prisma.user.update({ where: { id: userId }, - data: { - displayName: displayName && displayName.length ? displayName : null, - phone: phone && phone.length ? phone : null, - }, + data, }) return { user: mapUserForClient(updated) } }) diff --git a/server/src/routes/user-payments.js b/server/src/routes/user-payments.js index 8b8e99b..9d89bc4 100644 --- a/server/src/routes/user-payments.js +++ b/server/src/routes/user-payments.js @@ -60,7 +60,7 @@ export async function registerUserPaymentRoutes(fastify) { const receipt = buildReceipt({ orderItems: order.items, deliveryFeeCents: order.deliveryFeeCents, - userEmail: userEmail, + userEmail: userEmail, }) let result