diff --git a/client/public/robots.txt b/client/public/robots.txt
new file mode 100644
index 0000000..bcb400c
--- /dev/null
+++ b/client/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://любимыйкреатив.рф/sitemap.xml
diff --git a/client/public/sitemap.xml b/client/public/sitemap.xml
new file mode 100644
index 0000000..43a2a44
--- /dev/null
+++ b/client/public/sitemap.xml
@@ -0,0 +1,28 @@
+
+
+
+ https://любимыйкреатив.рф/
+ 1.0
+ daily
+
+
+ https://любимыйкреатив.рф/info
+ 0.8
+ monthly
+
+
+ https://любимыйкреатив.рф/about
+ 0.7
+ monthly
+
+
+ https://любимыйкреатив.рф/privacy
+ 0.5
+ yearly
+
+
+ https://любимыйкреатив.рф/terms
+ 0.5
+ yearly
+
+
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/info/ui/sections/PaymentSection.tsx b/client/src/pages/info/ui/sections/PaymentSection.tsx
index 0e43938..e9f118f 100644
--- a/client/src/pages/info/ui/sections/PaymentSection.tsx
+++ b/client/src/pages/info/ui/sections/PaymentSection.tsx
@@ -7,7 +7,8 @@ const methods = [
{
icon: ,
primary: 'Онлайн-оплата через ЮKassa',
- secondary: 'Карты, СБП. Оплата после подтверждения заказа админом. Перенаправление на защищённую платёжную страницу.',
+ secondary:
+ 'Карты, СБП. Оплата после подтверждения заказа админом. Перенаправление на защищённую платёжную страницу.',
},
{
icon: ,
@@ -39,7 +40,8 @@ export function PaymentSection() {
maxWidth: '56ch',
}}
>
- Оплата происходит после подтверждения заказа админом. Вы получите уведомление, когда заказ будет подтверждён и готов к оплате.
+ Оплата происходит после подтверждения заказа админом. Вы получите уведомление, когда заказ будет подтверждён и
+ готов к оплате.
diff --git a/client/src/pages/info/ui/sections/ReturnsSection.tsx b/client/src/pages/info/ui/sections/ReturnsSection.tsx
index 63fc641..5561115 100644
--- a/client/src/pages/info/ui/sections/ReturnsSection.tsx
+++ b/client/src/pages/info/ui/sections/ReturnsSection.tsx
@@ -36,7 +36,9 @@ export function ReturnsSection() {
lineHeight: 1.65,
}}
>
- Если товар не соответствует описанию или имеет производственный дефект, свяжитесь с нами в течение 7 дней после получения. Мы заменим изделие на аналогичное или вернём деньги. Возврат товара надлежащего качества возможен в течение 14 дней, если изделие не было в употреблении и сохранён его товарный вид.
+ Если товар не соответствует описанию или имеет производственный дефект, свяжитесь с нами в течение 7 дней
+ после получения. Мы заменим изделие на аналогичное или вернём деньги. Возврат товара надлежащего качества
+ возможен в течение 14 дней, если изделие не было в употреблении и сохранён его товарный вид.
@@ -58,7 +60,9 @@ export function ReturnsSection() {
lineHeight: 1.65,
}}
>
- Мы отвечаем за качество каждого изделия ручной работы. Все дефекты, возникшие не по вине покупателя, устраняются или компенсируются заменой изделия. Если у вас возникли вопросы по качеству — напишите нам, и мы решим проблему в кратчайшие сроки.
+ Мы отвечаем за качество каждого изделия ручной работы. Все дефекты, возникшие не по вине покупателя,
+ устраняются или компенсируются заменой изделия. Если у вас возникли вопросы по качеству — напишите нам, и мы
+ решим проблему в кратчайшие сроки.
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 (
= [
label: 'Отправлен',
iconName: 'package',
color: 'info',
- description: 'Заказ передан в службу доставки. Трек-номер для отслеживания(при наличии) будет указан в сообщении админа.',
+ description:
+ 'Заказ передан в службу доставки. Трек-номер для отслеживания(при наличии) будет указан в сообщении админа.',
},
{
code: 'READY_FOR_PICKUP',
diff --git a/client/src/shared/lib/use-page-title.ts b/client/src/shared/lib/use-page-title.ts
new file mode 100644
index 0000000..e968072
--- /dev/null
+++ b/client/src/shared/lib/use-page-title.ts
@@ -0,0 +1,22 @@
+import { useEffect } from 'react'
+import { useLocation } from 'react-router-dom'
+
+const BASE_TITLE = 'Любимый Креатив — Изделия ручной работы'
+
+let currentTitle: string = BASE_TITLE
+
+export function usePageTitle(title: string | null) {
+ useEffect(() => {
+ 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/client/src/shared/ui/OrderStatusChip.tsx b/client/src/shared/ui/OrderStatusChip.tsx
index 5e50236..35bc79b 100644
--- a/client/src/shared/ui/OrderStatusChip.tsx
+++ b/client/src/shared/ui/OrderStatusChip.tsx
@@ -6,20 +6,47 @@ import { getOrderStatusData, type StatusIconName } from '@/shared/lib/order-stat
const iconMap: Record = {
banknote: (
-