From 96f06c79b49fce8916b8798c2285c2bfd1d8feef Mon Sep 17 00:00:00 2001 From: Kirill Date: Sun, 24 May 2026 15:48:33 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/app/routes/index.tsx | 3 ++ .../src/entities/product/ui/ProductCard.tsx | 30 ++++++++++-------- client/src/pages/about/ui/AboutPage.tsx | 2 ++ client/src/pages/cart/ui/CartPage.tsx | 2 ++ client/src/pages/info/ui/InfoPage.tsx | 2 ++ .../pages/me/ui/sections/AddressesPage.tsx | 2 ++ .../src/pages/me/ui/sections/MessagesPage.tsx | 2 ++ .../me/ui/sections/NotificationsPage.tsx | 2 ++ .../src/pages/me/ui/sections/OrdersPage.tsx | 2 ++ .../src/pages/me/ui/sections/SettingsPage.tsx | 2 ++ .../privacy-policy/ui/PrivacyPolicyPage.tsx | 2 ++ client/src/pages/product/ui/ProductPage.tsx | 3 ++ client/src/pages/terms/ui/TermsPage.tsx | 2 ++ client/src/shared/lib/use-page-title.ts | 22 +++++++++++++ server/prisma/prisma/dev.db | Bin 364544 -> 364544 bytes 15 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 client/src/shared/lib/use-page-title.ts diff --git a/client/src/app/routes/index.tsx b/client/src/app/routes/index.tsx index 5e9177e..1f34896 100644 --- a/client/src/app/routes/index.tsx +++ b/client/src/app/routes/index.tsx @@ -11,12 +11,15 @@ import { NotFoundPage } from '@/pages/not-found' import { PrivacyPolicyPage } from '@/pages/privacy-policy' import { ProductPage } from '@/pages/product' import { TermsPage } from '@/pages/terms' +import { usePageTitleReset } from '@/shared/lib/use-page-title' import { SkeletonPage } from '@/shared/ui/SkeletonPage' const AdminLayoutPage = lazy(() => import('@/pages/admin-layout').then((m) => ({ default: m.AdminLayoutPage }))) const MeLayoutPage = lazy(() => import('@/pages/me').then((m) => ({ default: m.MeLayoutPage }))) export function AppRoutes() { + usePageTitleReset() + return ( diff --git a/client/src/entities/product/ui/ProductCard.tsx b/client/src/entities/product/ui/ProductCard.tsx index 3e1aa31..0322d3a 100644 --- a/client/src/entities/product/ui/ProductCard.tsx +++ b/client/src/entities/product/ui/ProductCard.tsx @@ -1,13 +1,14 @@ import type { ReactNode } from 'react' import { useCallback, useMemo, useRef } from 'react' +import { useMediaQuery } from '@mui/material' import Box from '@mui/material/Box' import Card from '@mui/material/Card' -import CardContent from '@mui/material/CardContent' import CardMedia from '@mui/material/CardMedia' import Chip from '@mui/material/Chip' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' import { useNavigate } from 'react-router-dom' +import { Autoplay } from 'swiper/modules' import { Swiper, SwiperSlide } from 'swiper/react' import 'swiper/css' import type { Product } from '@/entities/product/model/types' @@ -19,6 +20,7 @@ type Props = { product: Product; mediaHeight?: number; actions?: ReactNode } export function ProductCard({ product, mediaHeight = 200, actions }: Props) { const navigate = useNavigate() + const isMobile = useMediaQuery('(max-width:600px)') const swiperRef = useRef(null) const imageUrls = useMemo(() => { const fromImages = (product.images ?? []) @@ -76,12 +78,14 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) { > {imageUrls.length ? ( - + { swiperRef.current = s }} - allowTouchMove={false} + modules={isMobile ? [Autoplay] : undefined} + autoplay={isMobile ? { delay: 3000, disableOnInteraction: false, pauseOnMouseEnter: true } : undefined} + allowTouchMove={!isMobile} style={{ width: '100%', height: mediaHeight }} > {imageUrls.map((url) => ( @@ -150,8 +154,8 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) { )} - - + + {product.category && ( {product.shortDescription ?? 'Описание появится позже.'} - - - - {formatPriceRub(product.priceCents)} - - {actions} - - + + + + {formatPriceRub(product.priceCents)} + + {actions} + + ) } diff --git a/client/src/pages/about/ui/AboutPage.tsx b/client/src/pages/about/ui/AboutPage.tsx index bfeac6f..1e2e25b 100644 --- a/client/src/pages/about/ui/AboutPage.tsx +++ b/client/src/pages/about/ui/AboutPage.tsx @@ -7,6 +7,7 @@ import * as maplibregl from 'maplibre-gl' import Map, { Marker } from 'react-map-gl/maplibre' import { STORE_EMAIL, STORE_PHONE, VK_URL } from '@/shared/config' import { PICKUP_ADDRESS_FULL, PICKUP_COORDINATES } from '@/shared/constants/pickup-point' +import { usePageTitle } from '@/shared/lib/use-page-title' const rasterStyle = { version: 8 as const, @@ -22,6 +23,7 @@ const rasterStyle = { } export function AboutPage() { + usePageTitle('О нас') const { lat, lng } = PICKUP_COORDINATES return ( diff --git a/client/src/pages/cart/ui/CartPage.tsx b/client/src/pages/cart/ui/CartPage.tsx index b849e78..fe4ece2 100644 --- a/client/src/pages/cart/ui/CartPage.tsx +++ b/client/src/pages/cart/ui/CartPage.tsx @@ -12,9 +12,11 @@ import { Minus, Plus, Trash2 } from 'lucide-react' import { Link as RouterLink } from 'react-router-dom' import { fetchMyCart, removeCartItem, setCartQty } from '@/entities/cart/api/cart-api' import { formatPriceRub } from '@/shared/lib/format-price' +import { usePageTitle } from '@/shared/lib/use-page-title' import { $user } from '@/shared/model/auth' export function CartPage() { + usePageTitle('Корзина') const user = useUnit($user) const qc = useQueryClient() diff --git a/client/src/pages/info/ui/InfoPage.tsx b/client/src/pages/info/ui/InfoPage.tsx index 7f47023..2f66c95 100644 --- a/client/src/pages/info/ui/InfoPage.tsx +++ b/client/src/pages/info/ui/InfoPage.tsx @@ -3,6 +3,7 @@ import Container from '@mui/material/Container' import Divider from '@mui/material/Divider' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' +import { usePageTitle } from '@/shared/lib/use-page-title' import { DeliverySection } from './sections/DeliverySection' import { HowToOrderSection } from './sections/HowToOrderSection' import { OrderStatusesSection } from './sections/OrderStatusesSection' @@ -10,6 +11,7 @@ import { PaymentSection } from './sections/PaymentSection' import { ReturnsSection } from './sections/ReturnsSection' export function InfoPage() { + usePageTitle('Информация для покупателей') return ( {/* Hero */} diff --git a/client/src/pages/me/ui/sections/AddressesPage.tsx b/client/src/pages/me/ui/sections/AddressesPage.tsx index 8535e09..16a834f 100644 --- a/client/src/pages/me/ui/sections/AddressesPage.tsx +++ b/client/src/pages/me/ui/sections/AddressesPage.tsx @@ -16,6 +16,7 @@ import { } from '@/entities/user/api/address-api' import type { ShippingAddress } from '@/entities/user/model/types' import { AddressFormDialog, type AddressFormValues } from '@/features/address-form' +import { usePageTitle } from '@/shared/lib/use-page-title' const defaultAddressForm = (isDefault: boolean): AddressFormValues => ({ label: '', @@ -29,6 +30,7 @@ const defaultAddressForm = (isDefault: boolean): AddressFormValues => ({ }) export function AddressesPage() { + usePageTitle('Адреса доставки') const queryClient = useQueryClient() const [open, setOpen] = useState(false) const [editing, setEditing] = useState(null) diff --git a/client/src/pages/me/ui/sections/MessagesPage.tsx b/client/src/pages/me/ui/sections/MessagesPage.tsx index f225f78..b558264 100644 --- a/client/src/pages/me/ui/sections/MessagesPage.tsx +++ b/client/src/pages/me/ui/sections/MessagesPage.tsx @@ -16,6 +16,7 @@ import { fetchMyOrder, postOrderMessage } from '@/entities/order/api/order-api' import { fetchMyConversations, markOrderMessagesRead } from '@/entities/user/api/messages-api' import { fetchAdminAvatar } from '@/entities/user/api/user-api' import { orderStatusLabelRu } from '@/shared/lib/order-status-labels' +import { usePageTitle } from '@/shared/lib/use-page-title' import { $user } from '@/shared/model/auth' import { ChatMessageBubble } from '@/shared/ui/ChatMessageBubble' import { OrderMessageBody } from '@/shared/ui/OrderMessageBody' @@ -24,6 +25,7 @@ import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor' import { UserAvatar } from '@/shared/ui/UserAvatar' export function MessagesPage() { + usePageTitle('Сообщения') const qc = useQueryClient() const [selectedId, setSelectedId] = useState(null) const [text, setText] = useState('') diff --git a/client/src/pages/me/ui/sections/NotificationsPage.tsx b/client/src/pages/me/ui/sections/NotificationsPage.tsx index a9c5819..a255904 100644 --- a/client/src/pages/me/ui/sections/NotificationsPage.tsx +++ b/client/src/pages/me/ui/sections/NotificationsPage.tsx @@ -13,6 +13,7 @@ import { } from '@/entities/notification/api/notifications-api' import type { UserNotificationSettings } from '@/entities/notification/api/notifications-api' import { isSyntheticEmail } from '@/shared/lib/is-synthetic-email' +import { usePageTitle } from '@/shared/lib/use-page-title' import { $user } from '@/shared/model/auth' function isOrderStatusChangesOn(s: UserNotificationSettings): boolean { @@ -27,6 +28,7 @@ const orderStatusChangesPayload = (on: boolean) => ({ }) export function NotificationsPage() { + usePageTitle('Уведомления') const queryClient = useQueryClient() const [error, setError] = useState(null) const user = useUnit($user) diff --git a/client/src/pages/me/ui/sections/OrdersPage.tsx b/client/src/pages/me/ui/sections/OrdersPage.tsx index fc248af..c6f8351 100644 --- a/client/src/pages/me/ui/sections/OrdersPage.tsx +++ b/client/src/pages/me/ui/sections/OrdersPage.tsx @@ -12,8 +12,10 @@ import { ORDER_STATUSES } from '@/shared/constants/order' import { formatPriceRub } from '@/shared/lib/format-price' import { groupOrdersByStatus } from '@/shared/lib/group-orders-by-status' import { orderStatusLabelRu } from '@/shared/lib/order-status-labels' +import { usePageTitle } from '@/shared/lib/use-page-title' export function OrdersPage() { + usePageTitle('Заказы') const ordersQuery = useQuery({ queryKey: ['me', 'orders'], queryFn: fetchMyOrders, diff --git a/client/src/pages/me/ui/sections/SettingsPage.tsx b/client/src/pages/me/ui/sections/SettingsPage.tsx index 70b5ea5..79c4a2a 100644 --- a/client/src/pages/me/ui/sections/SettingsPage.tsx +++ b/client/src/pages/me/ui/sections/SettingsPage.tsx @@ -4,6 +4,7 @@ import Divider from '@mui/material/Divider' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' import { useUnit } from 'effector-react' +import { usePageTitle } from '@/shared/lib/use-page-title' import { $user } from '@/shared/model/auth' import { AuthMethodsSection } from './AuthMethodsSection' import { AvatarSection } from './AvatarSection' @@ -11,6 +12,7 @@ import { DeleteAccountSection } from './DeleteAccountSection' import { ProfileSection } from './ProfileSection' export function SettingsPage() { + usePageTitle('Настройки') const user = useUnit($user) if (!user) { diff --git a/client/src/pages/privacy-policy/ui/PrivacyPolicyPage.tsx b/client/src/pages/privacy-policy/ui/PrivacyPolicyPage.tsx index 4901f71..c418bab 100644 --- a/client/src/pages/privacy-policy/ui/PrivacyPolicyPage.tsx +++ b/client/src/pages/privacy-policy/ui/PrivacyPolicyPage.tsx @@ -9,6 +9,7 @@ import { STORE_OP_ADDR, STORE_PUBLIC_SITE_URL, } from '@/shared/config' +import { usePageTitle } from '@/shared/lib/use-page-title' const SITE_URL = STORE_PUBLIC_SITE_URL || (typeof window !== 'undefined' ? window.location.origin : '') @@ -90,6 +91,7 @@ const sections = [ ] export function PrivacyPolicyPage() { + usePageTitle('Политика конфиденциальности') return ( { const p = productQuery.data if (!p) return [] diff --git a/client/src/pages/terms/ui/TermsPage.tsx b/client/src/pages/terms/ui/TermsPage.tsx index be64128..2c40c61 100644 --- a/client/src/pages/terms/ui/TermsPage.tsx +++ b/client/src/pages/terms/ui/TermsPage.tsx @@ -10,6 +10,7 @@ import { STORE_OP_INN, STORE_OP_ADDR, } from '@/shared/config' +import { usePageTitle } from '@/shared/lib/use-page-title' const SITE_URL = STORE_PUBLIC_SITE_URL || (typeof window !== 'undefined' ? window.location.origin : '') @@ -147,6 +148,7 @@ const sections = [ ] export function TermsPage() { + usePageTitle('Пользовательское соглашение') return ( { + currentTitle = title ? `${title} — Любимый Креатив` : BASE_TITLE + document.title = currentTitle + }, [title]) +} + +export function usePageTitleReset() { + const location = useLocation() + + useEffect(() => { + document.title = BASE_TITLE + currentTitle = BASE_TITLE + }, [location.pathname]) +} diff --git a/server/prisma/prisma/dev.db b/server/prisma/prisma/dev.db index 72c18a15770083e6197ffa657151ccc335f757e9..58f210993dc6fcda2714094acfcb5472a4f0f7ed 100644 GIT binary patch delta 2882 zcmb7GeQX-l_VJN`O< z$Jx{*Op`hxN-B$)3a#xx>y~QL02iB&i4N8ZY1$ZKt3Z5oP$nj|e`I24{4*4u_v|Ef zV*cQydwt3CJiq68e$U6w#r?zKHbT45Wj8CiQKYtC6bT**khTGG=tpH5_I&EH%l#Ub+i^xUg&HZEsJ z$NgYH_6)3IeFhA&*uWO`d^u%@9fM&8ES{Krq_LD69rtau$Y6_Fm6xm>uQXqMjuwaM zyDZRFi!`=S>j_DW6lWW6jM8xdG|!D21z zUJ@y65tb8$M0ieVyposJ9sI~1@_8R2&=b#lbG)#gWD|aOWf zl4KbnF=mP($&kWAC=`oZx*0e4c+3(ayrs)#Scu7S#WE~e5RH%!=k-iJk;;+h=ck`v z{SP$bxsmU~`JF##iyl{hJlS~FD&cr=wS}^k;cI$oYdo?NQ58{<3|_Z*0X*@F5Rb$y zRaGKJ%#b6ZAWN1Qm-L7Z{|Q@$8qsxeT$NOXmttb57tr)mqX!_8xx>%FS^sCUb>`<) zW`~=*L7=~*>*z)F3`(O(^dRzcA8|Keu7Aqmu9OG!=3cM=pe$T1|Jxryfw=bw^gjAE zx{SVwZ1gA!bDwbUp&)?2&NYH7T;tRVN4gvK$}Z9cf8W{RMhyaejP9Vnq8sQ9bQN7e z%ji66fJF@j+%!u<@Ib7)_Xs(;TFa|n8|Mxa=x^v(=zHkv=rA%s--p;SMk7gB9>F1I z{c%6z-@|Cz@JJ>3nR*-wAS!yPT1h6%S}kD|J?pB&v~S~?LSW|VqHzua1}432o_q~O zbGn$404h{2=J@1HB^6896$o|dra*m~(O8$$fS^pNE zWFBB$Q%8<|VdCh+k57Gmdh*E8$9g(bWb4?@>}(}e1P~i{O(vw5Vwtc23a9UXsrjdQ zCfNDl0?GJB2CB7x03r{Cu6IlR40;iG`U=8jZWiArA36?cBy<2*%8{ZGQ?g1qUsdU~ zQs**R9!pg`K!w?AxT0G*Y8S0_vrE=zk{}haG*zhPEj68N|CYR+Z{F7E(GC-(-a^fr z^Gu-g!7*xd385zlit`Yh2MxpQ4?!rOct5o`OCs&y!NpLW^wWe(V_8gd^Mw_PC&!z= zT%or2|8S}lmJ}7b-T84e@bNh>UhGAR(}F>nAe;UbDoPGFkFQV%w!=FJ96k+hK(fip z31)ik!WY{gQVVD#2N`7`BME0@SF6biyqucfUZp1XJ^SQd;2ch;u7fhaLf`ox2yGvI z=RpU2D_v2V;oTG)AYA`o1L$Ojv+3_L^J&_psbdoa!Lq$RlJXE2pUVm*v7RWIu?f&P z3`uQh$PrKwutqskv?@v}3(K{9D6dz_$wRqHCUf`l#SQl7uXfMnBNXf@;&=4e8rb$h zb?+Q2(96t$FTIRNP~ZLg;dS*vY&6HJ^wXU;3-m#z^RrX*5t2O7UZDB;5j;2wL@MK% zm@UaU$rkd>pRCgRCt(_JL^p-C+1WB+1=6QI;Mz?Dw{4^E#ko5lAOML!C!mP|(#)n! zT}=uO}9<)^6Kg9%^r>`;xoXvt`)$wMj7mB$#t0?qsmMxiV5I!il_G`>A?;@e)zl`sW z??dKantOe>UGH!_`#i#asc{r#dI|;#rpmJzZ)&e{u{Z@ja+t^yxg8*KOS=f O8+#l!_FS^r@&5wXqKVJ| delta 2279 zcmZ`)ZEO=|9KYvYx?Z>4)5(T*FgC<7CbHdKU#<-ySIwr9I2ZvTiyG+5ZGBnWYkOQOS zu2c;MIc{WRUqlV?p#eY7^PO!X=KVQ1Ra6V}g70MP&rch;;K&m51`HZX_)W(eK@ zVu%h*@VPvWKq}-f5Xxx8s2;zZ8Hv_5&A`oveo8j2Yq20)DaRTqm8F++&hT>)jF zJM(GGc0a%g)nMj22zUj43y;A!VHkFx=DA>|b$^-uV?E98ozpQx9VLTeKNo2aGv7L= z74&@ToU*C;)>4&xu}pF~C9|^w&nd}-v;g1~0jJ=_Y{kev*w`2C4MxN31W9r^1j#D# zcBjp)^m`&|uhQa+MJTMG%&yUuHIFw_U96@gwRI6i3&i|MOv+A;)|#u0<--JwbCcZG z$}^SrilY?|mk*=vq{_3yU#944BdnU!IM@oSkog}DH!u6h`5VXsv=Hzr{0<(6Z$TAq zg$`&z5CAj3-{dk)w=N;X7y<9VY4``c2+zU^I1b0MRh0Rj zunkdp_}B_sHkLM_C~7C(-awC%PWSi{hI(2C0EyF5Ml%oYDf+=ZNg)?(0b?NRkT({? z-&W~7&4W|W9dUNY5@=ei=|CjL3#w)fhMnMWS89n$uA*fIZ$t=z)LsN~U{EpFH??it^vurAC%Zb@H*G6nIze{xvY;9W_8}(`LDYI8S~M;M zd|m`tcH*vnc|Wk^ua5%ST(c-F_Zc!(0C)v&!X(n}G1Q(HuyR>lI}g_DNtvlx#h{H| zU&=SwkBjOTT|(Sp?G5zWggPuM{oE+9+7XD{ zkkClLm(fBH1{CTpXG+9LPPSO=1go2Gk#2p@|YD1`l$t=Q)*nm(E9bQ_q1al=^mpFqO$d-O;p# zV362HKV}ujUq4b`EUBD_W-jk&=>LQ8(yJS8{2|DGLUTpOx6pt26Dd;!;PTPblHsDHXK)LqVx0a7S{F@f=2%_cp>$wX^yk|K5!X8@RYbcjx zw(w(^QvN9)3g)%)F3mCM2qb(_WyT;Og9{s^xJ|Y7OA-!U-BnySg88nHv@yehrR^2F z(^f%B7F?kt8Bd}p`SnYqz*K=99J*axb3?^!7n|y9d+2KYjS0GG3+hQi#V%s$l&VIB zP|6od%^vj-9x8ib)T5HaJDBzqM!gh%VP3hf=`x-r(R@6Qt{r*vZTfH7^x$yGb3F3_ zbfUNm4-xz=nK@#5MHJVeB@