пва
This commit is contained in:
@@ -11,12 +11,15 @@ import { NotFoundPage } from '@/pages/not-found'
|
|||||||
import { PrivacyPolicyPage } from '@/pages/privacy-policy'
|
import { PrivacyPolicyPage } from '@/pages/privacy-policy'
|
||||||
import { ProductPage } from '@/pages/product'
|
import { ProductPage } from '@/pages/product'
|
||||||
import { TermsPage } from '@/pages/terms'
|
import { TermsPage } from '@/pages/terms'
|
||||||
|
import { usePageTitleReset } from '@/shared/lib/use-page-title'
|
||||||
import { SkeletonPage } from '@/shared/ui/SkeletonPage'
|
import { SkeletonPage } from '@/shared/ui/SkeletonPage'
|
||||||
|
|
||||||
const AdminLayoutPage = lazy(() => import('@/pages/admin-layout').then((m) => ({ default: m.AdminLayoutPage })))
|
const AdminLayoutPage = lazy(() => import('@/pages/admin-layout').then((m) => ({ default: m.AdminLayoutPage })))
|
||||||
const MeLayoutPage = lazy(() => import('@/pages/me').then((m) => ({ default: m.MeLayoutPage })))
|
const MeLayoutPage = lazy(() => import('@/pages/me').then((m) => ({ default: m.MeLayoutPage })))
|
||||||
|
|
||||||
export function AppRoutes() {
|
export function AppRoutes() {
|
||||||
|
usePageTitleReset()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { useCallback, useMemo, useRef } from 'react'
|
import { useCallback, useMemo, useRef } from 'react'
|
||||||
|
import { useMediaQuery } from '@mui/material'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Card from '@mui/material/Card'
|
import Card from '@mui/material/Card'
|
||||||
import CardContent from '@mui/material/CardContent'
|
|
||||||
import CardMedia from '@mui/material/CardMedia'
|
import CardMedia from '@mui/material/CardMedia'
|
||||||
import Chip from '@mui/material/Chip'
|
import Chip from '@mui/material/Chip'
|
||||||
import Stack from '@mui/material/Stack'
|
import Stack from '@mui/material/Stack'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { Autoplay } from 'swiper/modules'
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||||
import 'swiper/css'
|
import 'swiper/css'
|
||||||
import type { Product } from '@/entities/product/model/types'
|
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) {
|
export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const isMobile = useMediaQuery('(max-width:600px)')
|
||||||
const swiperRef = useRef<SwiperType | null>(null)
|
const swiperRef = useRef<SwiperType | null>(null)
|
||||||
const imageUrls = useMemo(() => {
|
const imageUrls = useMemo(() => {
|
||||||
const fromImages = (product.images ?? [])
|
const fromImages = (product.images ?? [])
|
||||||
@@ -76,12 +78,14 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
|
|||||||
>
|
>
|
||||||
<Box sx={{ position: 'relative' }}>
|
<Box sx={{ position: 'relative' }}>
|
||||||
{imageUrls.length ? (
|
{imageUrls.length ? (
|
||||||
<Box onMouseMove={onMouseMove} sx={{ height: mediaHeight, overflow: 'hidden' }}>
|
<Box onMouseMove={!isMobile ? onMouseMove : undefined} sx={{ height: mediaHeight, overflow: 'hidden' }}>
|
||||||
<Swiper
|
<Swiper
|
||||||
onSwiper={(s) => {
|
onSwiper={(s) => {
|
||||||
swiperRef.current = s
|
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 }}
|
style={{ width: '100%', height: mediaHeight }}
|
||||||
>
|
>
|
||||||
{imageUrls.map((url) => (
|
{imageUrls.map((url) => (
|
||||||
@@ -150,8 +154,8 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<CardContent sx={{ flexGrow: 1, p: 2, '&:last-child': { pb: 2 } }}>
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', p: 2, pb: 2 }}>
|
||||||
<Stack spacing={1.25}>
|
<Stack spacing={1.25} sx={{ flexGrow: 1 }}>
|
||||||
{product.category && (
|
{product.category && (
|
||||||
<Chip
|
<Chip
|
||||||
label={product.category.name}
|
label={product.category.name}
|
||||||
@@ -231,15 +235,15 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
|
|||||||
>
|
>
|
||||||
{product.shortDescription ?? 'Описание появится позже.'}
|
{product.shortDescription ?? 'Описание появится позже.'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mt: 'auto', pt: 0.5 }}>
|
|
||||||
<Typography variant="h6" color="primary" sx={{ fontWeight: 700, fontSize: '1.1rem' }}>
|
|
||||||
{formatPriceRub(product.priceCents)}
|
|
||||||
</Typography>
|
|
||||||
{actions}
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</CardContent>
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', pt: 1.5 }}>
|
||||||
|
<Typography variant="h6" color="primary" sx={{ fontWeight: 700, fontSize: '1.1rem' }}>
|
||||||
|
{formatPriceRub(product.priceCents)}
|
||||||
|
</Typography>
|
||||||
|
{actions}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as maplibregl from 'maplibre-gl'
|
|||||||
import Map, { Marker } from 'react-map-gl/maplibre'
|
import Map, { Marker } from 'react-map-gl/maplibre'
|
||||||
import { STORE_EMAIL, STORE_PHONE, VK_URL } from '@/shared/config'
|
import { STORE_EMAIL, STORE_PHONE, VK_URL } from '@/shared/config'
|
||||||
import { PICKUP_ADDRESS_FULL, PICKUP_COORDINATES } from '@/shared/constants/pickup-point'
|
import { PICKUP_ADDRESS_FULL, PICKUP_COORDINATES } from '@/shared/constants/pickup-point'
|
||||||
|
import { usePageTitle } from '@/shared/lib/use-page-title'
|
||||||
|
|
||||||
const rasterStyle = {
|
const rasterStyle = {
|
||||||
version: 8 as const,
|
version: 8 as const,
|
||||||
@@ -22,6 +23,7 @@ const rasterStyle = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AboutPage() {
|
export function AboutPage() {
|
||||||
|
usePageTitle('О нас')
|
||||||
const { lat, lng } = PICKUP_COORDINATES
|
const { lat, lng } = PICKUP_COORDINATES
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import { Minus, Plus, Trash2 } from 'lucide-react'
|
|||||||
import { Link as RouterLink } from 'react-router-dom'
|
import { Link as RouterLink } from 'react-router-dom'
|
||||||
import { fetchMyCart, removeCartItem, setCartQty } from '@/entities/cart/api/cart-api'
|
import { fetchMyCart, removeCartItem, setCartQty } from '@/entities/cart/api/cart-api'
|
||||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||||
|
import { usePageTitle } from '@/shared/lib/use-page-title'
|
||||||
import { $user } from '@/shared/model/auth'
|
import { $user } from '@/shared/model/auth'
|
||||||
|
|
||||||
export function CartPage() {
|
export function CartPage() {
|
||||||
|
usePageTitle('Корзина')
|
||||||
const user = useUnit($user)
|
const user = useUnit($user)
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Container from '@mui/material/Container'
|
|||||||
import Divider from '@mui/material/Divider'
|
import Divider from '@mui/material/Divider'
|
||||||
import Stack from '@mui/material/Stack'
|
import Stack from '@mui/material/Stack'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
|
import { usePageTitle } from '@/shared/lib/use-page-title'
|
||||||
import { DeliverySection } from './sections/DeliverySection'
|
import { DeliverySection } from './sections/DeliverySection'
|
||||||
import { HowToOrderSection } from './sections/HowToOrderSection'
|
import { HowToOrderSection } from './sections/HowToOrderSection'
|
||||||
import { OrderStatusesSection } from './sections/OrderStatusesSection'
|
import { OrderStatusesSection } from './sections/OrderStatusesSection'
|
||||||
@@ -10,6 +11,7 @@ import { PaymentSection } from './sections/PaymentSection'
|
|||||||
import { ReturnsSection } from './sections/ReturnsSection'
|
import { ReturnsSection } from './sections/ReturnsSection'
|
||||||
|
|
||||||
export function InfoPage() {
|
export function InfoPage() {
|
||||||
|
usePageTitle('Информация для покупателей')
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="lg" sx={{ py: { xs: 4 } }}>
|
<Container maxWidth="lg" sx={{ py: { xs: 4 } }}>
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '@/entities/user/api/address-api'
|
} from '@/entities/user/api/address-api'
|
||||||
import type { ShippingAddress } from '@/entities/user/model/types'
|
import type { ShippingAddress } from '@/entities/user/model/types'
|
||||||
import { AddressFormDialog, type AddressFormValues } from '@/features/address-form'
|
import { AddressFormDialog, type AddressFormValues } from '@/features/address-form'
|
||||||
|
import { usePageTitle } from '@/shared/lib/use-page-title'
|
||||||
|
|
||||||
const defaultAddressForm = (isDefault: boolean): AddressFormValues => ({
|
const defaultAddressForm = (isDefault: boolean): AddressFormValues => ({
|
||||||
label: '',
|
label: '',
|
||||||
@@ -29,6 +30,7 @@ const defaultAddressForm = (isDefault: boolean): AddressFormValues => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export function AddressesPage() {
|
export function AddressesPage() {
|
||||||
|
usePageTitle('Адреса доставки')
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [editing, setEditing] = useState<ShippingAddress | null>(null)
|
const [editing, setEditing] = useState<ShippingAddress | null>(null)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { fetchMyOrder, postOrderMessage } from '@/entities/order/api/order-api'
|
|||||||
import { fetchMyConversations, markOrderMessagesRead } from '@/entities/user/api/messages-api'
|
import { fetchMyConversations, markOrderMessagesRead } from '@/entities/user/api/messages-api'
|
||||||
import { fetchAdminAvatar } from '@/entities/user/api/user-api'
|
import { fetchAdminAvatar } from '@/entities/user/api/user-api'
|
||||||
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
||||||
|
import { usePageTitle } from '@/shared/lib/use-page-title'
|
||||||
import { $user } from '@/shared/model/auth'
|
import { $user } from '@/shared/model/auth'
|
||||||
import { ChatMessageBubble } from '@/shared/ui/ChatMessageBubble'
|
import { ChatMessageBubble } from '@/shared/ui/ChatMessageBubble'
|
||||||
import { OrderMessageBody } from '@/shared/ui/OrderMessageBody'
|
import { OrderMessageBody } from '@/shared/ui/OrderMessageBody'
|
||||||
@@ -24,6 +25,7 @@ import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
|||||||
import { UserAvatar } from '@/shared/ui/UserAvatar'
|
import { UserAvatar } from '@/shared/ui/UserAvatar'
|
||||||
|
|
||||||
export function MessagesPage() {
|
export function MessagesPage() {
|
||||||
|
usePageTitle('Сообщения')
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
const [text, setText] = useState('')
|
const [text, setText] = useState('')
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from '@/entities/notification/api/notifications-api'
|
} from '@/entities/notification/api/notifications-api'
|
||||||
import type { UserNotificationSettings } from '@/entities/notification/api/notifications-api'
|
import type { UserNotificationSettings } from '@/entities/notification/api/notifications-api'
|
||||||
import { isSyntheticEmail } from '@/shared/lib/is-synthetic-email'
|
import { isSyntheticEmail } from '@/shared/lib/is-synthetic-email'
|
||||||
|
import { usePageTitle } from '@/shared/lib/use-page-title'
|
||||||
import { $user } from '@/shared/model/auth'
|
import { $user } from '@/shared/model/auth'
|
||||||
|
|
||||||
function isOrderStatusChangesOn(s: UserNotificationSettings): boolean {
|
function isOrderStatusChangesOn(s: UserNotificationSettings): boolean {
|
||||||
@@ -27,6 +28,7 @@ const orderStatusChangesPayload = (on: boolean) => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export function NotificationsPage() {
|
export function NotificationsPage() {
|
||||||
|
usePageTitle('Уведомления')
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const user = useUnit($user)
|
const user = useUnit($user)
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import { ORDER_STATUSES } from '@/shared/constants/order'
|
|||||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||||
import { groupOrdersByStatus } from '@/shared/lib/group-orders-by-status'
|
import { groupOrdersByStatus } from '@/shared/lib/group-orders-by-status'
|
||||||
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
||||||
|
import { usePageTitle } from '@/shared/lib/use-page-title'
|
||||||
|
|
||||||
export function OrdersPage() {
|
export function OrdersPage() {
|
||||||
|
usePageTitle('Заказы')
|
||||||
const ordersQuery = useQuery({
|
const ordersQuery = useQuery({
|
||||||
queryKey: ['me', 'orders'],
|
queryKey: ['me', 'orders'],
|
||||||
queryFn: fetchMyOrders,
|
queryFn: fetchMyOrders,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Divider from '@mui/material/Divider'
|
|||||||
import Stack from '@mui/material/Stack'
|
import Stack from '@mui/material/Stack'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useUnit } from 'effector-react'
|
import { useUnit } from 'effector-react'
|
||||||
|
import { usePageTitle } from '@/shared/lib/use-page-title'
|
||||||
import { $user } from '@/shared/model/auth'
|
import { $user } from '@/shared/model/auth'
|
||||||
import { AuthMethodsSection } from './AuthMethodsSection'
|
import { AuthMethodsSection } from './AuthMethodsSection'
|
||||||
import { AvatarSection } from './AvatarSection'
|
import { AvatarSection } from './AvatarSection'
|
||||||
@@ -11,6 +12,7 @@ import { DeleteAccountSection } from './DeleteAccountSection'
|
|||||||
import { ProfileSection } from './ProfileSection'
|
import { ProfileSection } from './ProfileSection'
|
||||||
|
|
||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
|
usePageTitle('Настройки')
|
||||||
const user = useUnit($user)
|
const user = useUnit($user)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
STORE_OP_ADDR,
|
STORE_OP_ADDR,
|
||||||
STORE_PUBLIC_SITE_URL,
|
STORE_PUBLIC_SITE_URL,
|
||||||
} from '@/shared/config'
|
} from '@/shared/config'
|
||||||
|
import { usePageTitle } from '@/shared/lib/use-page-title'
|
||||||
|
|
||||||
const SITE_URL = STORE_PUBLIC_SITE_URL || (typeof window !== 'undefined' ? window.location.origin : '')
|
const SITE_URL = STORE_PUBLIC_SITE_URL || (typeof window !== 'undefined' ? window.location.origin : '')
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ const sections = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export function PrivacyPolicyPage() {
|
export function PrivacyPolicyPage() {
|
||||||
|
usePageTitle('Политика конфиденциальности')
|
||||||
return (
|
return (
|
||||||
<Box sx={{ maxWidth: 800, mx: 'auto', py: { xs: 3, md: 5 }, px: { xs: 2, md: 0 } }}>
|
<Box sx={{ maxWidth: 800, mx: 'auto', py: { xs: 3, md: 5 }, px: { xs: 2, md: 0 } }}>
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { ProductReviewsList } from '@/features/product-review'
|
|||||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||||
import { getOriginalWebpUrl } from '@/shared/lib/get-original-webp-url'
|
import { getOriginalWebpUrl } from '@/shared/lib/get-original-webp-url'
|
||||||
import { reviewsCountRu } from '@/shared/lib/reviews-count-ru'
|
import { reviewsCountRu } from '@/shared/lib/reviews-count-ru'
|
||||||
|
import { usePageTitle } from '@/shared/lib/use-page-title'
|
||||||
import { $user } from '@/shared/model/auth'
|
import { $user } from '@/shared/model/auth'
|
||||||
import { OptimizedImage } from '@/shared/ui/OptimizedImage'
|
import { OptimizedImage } from '@/shared/ui/OptimizedImage'
|
||||||
|
|
||||||
@@ -39,6 +40,8 @@ export function ProductPage() {
|
|||||||
enabled: Boolean(id),
|
enabled: Boolean(id),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
usePageTitle(productQuery.data?.title ?? null)
|
||||||
|
|
||||||
const imageUrls = useMemo(() => {
|
const imageUrls = useMemo(() => {
|
||||||
const p = productQuery.data
|
const p = productQuery.data
|
||||||
if (!p) return []
|
if (!p) return []
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
STORE_OP_INN,
|
STORE_OP_INN,
|
||||||
STORE_OP_ADDR,
|
STORE_OP_ADDR,
|
||||||
} from '@/shared/config'
|
} from '@/shared/config'
|
||||||
|
import { usePageTitle } from '@/shared/lib/use-page-title'
|
||||||
|
|
||||||
const SITE_URL = STORE_PUBLIC_SITE_URL || (typeof window !== 'undefined' ? window.location.origin : '')
|
const SITE_URL = STORE_PUBLIC_SITE_URL || (typeof window !== 'undefined' ? window.location.origin : '')
|
||||||
|
|
||||||
@@ -147,6 +148,7 @@ const sections = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export function TermsPage() {
|
export function TermsPage() {
|
||||||
|
usePageTitle('Пользовательское соглашение')
|
||||||
return (
|
return (
|
||||||
<Box sx={{ maxWidth: 800, mx: 'auto', py: { xs: 3, md: 5 }, px: { xs: 2, md: 0 } }}>
|
<Box sx={{ maxWidth: 800, mx: 'auto', py: { xs: 3, md: 5 }, px: { xs: 2, md: 0 } }}>
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -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])
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user