base commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { AboutPage } from './ui/AboutPage'
|
||||
@@ -0,0 +1,87 @@
|
||||
import Box from '@mui/material/Box'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import * as maplibregl from 'maplibre-gl'
|
||||
import Map, { Marker } from 'react-map-gl/maplibre'
|
||||
import { PICKUP_ADDRESS_FULL, PICKUP_COORDINATES } from '@/shared/constants/pickup-point'
|
||||
|
||||
const rasterStyle = {
|
||||
version: 8 as const,
|
||||
sources: {
|
||||
osm: {
|
||||
type: 'raster' as const,
|
||||
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||
tileSize: 256,
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
},
|
||||
},
|
||||
layers: [{ id: 'osm', type: 'raster' as const, source: 'osm' }],
|
||||
}
|
||||
|
||||
export function AboutPage() {
|
||||
const { lat, lng } = PICKUP_COORDINATES
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
О нас
|
||||
</Typography>
|
||||
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
||||
Магазин изделий ручной работы. Мы отвечаем за качество и сроки изготовления всего, что вы видите в каталоге.
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={3}>
|
||||
<Paper variant="outlined" sx={{ p: 2, borderRadius: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Контакты и самовывоз
|
||||
</Typography>
|
||||
<Typography sx={{ mb: 1 }}>
|
||||
Забрать заказ можно по адресу самовывоза (координаты указаны на карте ниже):
|
||||
</Typography>
|
||||
<Typography sx={{ whiteSpace: 'pre-wrap', fontWeight: 600 }}>{PICKUP_ADDRESS_FULL}</Typography>
|
||||
<Typography color="text.secondary" variant="body2" sx={{ mt: 1 }}>
|
||||
Перед визитом мы свяжемся с вами и согласуем время — чтобы заказ точно был готов к выдаче.
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
height: 380,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Map
|
||||
mapLib={maplibregl}
|
||||
initialViewState={{ latitude: lat, longitude: lng, zoom: 16 }}
|
||||
style={{ width: '100%', height: 380 }}
|
||||
mapStyle={rasterStyle}
|
||||
scrollZoom={false}
|
||||
dragRotate={false}
|
||||
dragPan={false}
|
||||
doubleClickZoom={false}
|
||||
keyboard={false}
|
||||
touchZoomRotate={false}
|
||||
>
|
||||
<Marker longitude={lng} latitude={lat} anchor="bottom">
|
||||
<Box
|
||||
sx={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
bgcolor: 'primary.main',
|
||||
borderRadius: '50%',
|
||||
border: 2,
|
||||
borderColor: 'background.paper',
|
||||
boxShadow: 3,
|
||||
}}
|
||||
/>
|
||||
</Marker>
|
||||
</Map>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { formatPriceRub } from '@/shared/lib/format-price'
|
||||
import { groupOrdersByStatus } from '@/shared/lib/group-orders-by-status'
|
||||
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
|
||||
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
||||
import { ChatMessageBubble } from '@/shared/ui/ChatMessageBubble'
|
||||
import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent'
|
||||
import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
||||
|
||||
@@ -207,6 +208,10 @@ export function AdminOrdersPage() {
|
||||
#{detail.id.slice(-8)} · {detail.user.email} · {orderStatusLabelRu(detail.status)} ·{' '}
|
||||
{formatPriceRub(detail.totalCents)}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Получение: {detail.deliveryType === 'pickup' ? 'самовывоз' : 'доставка'}
|
||||
{(detail.paymentMethod ?? 'online') === 'on_pickup' ? ' · оплата при получении' : ' · онлайн-оплата'}
|
||||
</Typography>
|
||||
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ alignItems: { sm: 'center' } }}>
|
||||
<FormControl size="small" sx={{ minWidth: 240 }}>
|
||||
@@ -240,25 +245,13 @@ export function AdminOrdersPage() {
|
||||
</Typography>
|
||||
<Stack spacing={1} sx={{ mb: 1 }}>
|
||||
{detail.messages.map((m) => (
|
||||
<Box
|
||||
key={m.id}
|
||||
sx={{
|
||||
p: 1.25,
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 2,
|
||||
bgcolor: m.authorType === 'admin' ? 'primary.50' : 'grey.100',
|
||||
alignSelf: m.authorType === 'admin' ? 'flex-end' : 'flex-start',
|
||||
width: 'fit-content',
|
||||
maxWidth: '85%',
|
||||
}}
|
||||
>
|
||||
<ChatMessageBubble key={m.id} authorType={m.authorType === 'admin' ? 'user' : 'admin'}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{m.authorType === 'admin' ? 'Админ (вы)' : 'Пользователь'} ·{' '}
|
||||
{new Date(m.createdAt).toLocaleString()}
|
||||
</Typography>
|
||||
<RichTextMessageContent value={m.text} tone="chat" />
|
||||
</Box>
|
||||
</ChatMessageBubble>
|
||||
))}
|
||||
{detail.messages.length === 0 && <Typography color="text.secondary">Нет сообщений.</Typography>}
|
||||
</Stack>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useEffect, 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 Stack from '@mui/material/Stack'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Typography from '@mui/material/Typography'
|
||||
@@ -11,7 +10,6 @@ import { useUnit } from 'effector-react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
import { oauthAuthorizeUrl } from '@/shared/lib/oauth-authorize-url'
|
||||
import { $user, tokenSet } from '@/shared/model/auth'
|
||||
|
||||
type AuthResponse = { token: string; user: { id: string; email: string; name?: string | null; phone?: string | null } }
|
||||
@@ -93,21 +91,9 @@ export function AuthPage() {
|
||||
)}
|
||||
|
||||
<Stack spacing={2} sx={{ maxWidth: 520 }}>
|
||||
<Typography variant="subtitle1">Быстрый вход</Typography>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}>
|
||||
<Button component="a" href={oauthAuthorizeUrl('vk')} variant="outlined" fullWidth>
|
||||
Войти через VK
|
||||
</Button>
|
||||
<Button component="a" href={oauthAuthorizeUrl('yandex')} variant="outlined" fullWidth>
|
||||
Войти через Яндекс
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Divider>или по email</Divider>
|
||||
|
||||
<TextField label="Email" {...register('email')} fullWidth />
|
||||
|
||||
<Typography variant="h6">Вариант 1: Email + код</Typography>
|
||||
<Typography variant="h6">Email + код</Typography>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
||||
<Button variant="outlined" onClick={() => requestCode.mutate()} disabled={!email || requestCode.isPending}>
|
||||
Отправить код
|
||||
|
||||
@@ -3,8 +3,12 @@ import Alert from '@mui/material/Alert'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import FormControl from '@mui/material/FormControl'
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
import InputLabel from '@mui/material/InputLabel'
|
||||
import Link from '@mui/material/Link'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Radio from '@mui/material/Radio'
|
||||
import RadioGroup from '@mui/material/RadioGroup'
|
||||
import Select from '@mui/material/Select'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import TextField from '@mui/material/TextField'
|
||||
@@ -23,6 +27,7 @@ export function CheckoutPage() {
|
||||
const qc = useQueryClient()
|
||||
const navigate = useNavigate()
|
||||
const [deliveryType, setDeliveryType] = useState<'delivery' | 'pickup'>('delivery')
|
||||
const [pickupPayment, setPickupPayment] = useState<'online' | 'on_pickup'>('online')
|
||||
const [addressId, setAddressId] = useState('')
|
||||
const [comment, setComment] = useState('')
|
||||
|
||||
@@ -45,6 +50,7 @@ export function CheckoutPage() {
|
||||
mutationFn: () =>
|
||||
createOrder({
|
||||
deliveryType,
|
||||
paymentMethod: deliveryType === 'delivery' ? 'online' : pickupPayment,
|
||||
addressId: deliveryType === 'delivery' ? selectedAddressId : null,
|
||||
comment: comment.trim() || null,
|
||||
}),
|
||||
@@ -134,7 +140,10 @@ export function CheckoutPage() {
|
||||
value={deliveryType}
|
||||
onChange={(e) => {
|
||||
const v = String(e.target.value)
|
||||
if (v === 'delivery' || v === 'pickup') setDeliveryType(v)
|
||||
if (v === 'delivery' || v === 'pickup') {
|
||||
setDeliveryType(v)
|
||||
if (v === 'delivery') setPickupPayment('online')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="delivery">Доставка</MenuItem>
|
||||
@@ -183,7 +192,35 @@ export function CheckoutPage() {
|
||||
)}
|
||||
|
||||
{deliveryType === 'pickup' && (
|
||||
<Alert severity="info">Самовывоз: адрес доставки не нужен. Мы свяжемся с вами для согласования.</Alert>
|
||||
<>
|
||||
<Alert severity="info" sx={{ mb: 0 }}>
|
||||
Самовывоз: адрес доставки не нужен. Адрес точки выдачи указан на странице{' '}
|
||||
<Link component={RouterLink} to="/about" underline="hover" sx={{ fontWeight: 700 }}>
|
||||
О нас
|
||||
</Link>
|
||||
.
|
||||
</Alert>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom sx={{ mt: 0.5 }}>
|
||||
Оплата
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
value={pickupPayment}
|
||||
onChange={(e) => {
|
||||
const v = String(e.target.value)
|
||||
if (v === 'online' || v === 'on_pickup') setPickupPayment(v)
|
||||
}}
|
||||
>
|
||||
<FormControlLabel value="online" control={<Radio size="small" />} label="Онлайн-оплата" />
|
||||
<FormControlLabel value="on_pickup" control={<Radio size="small" />} label="Оплатить при получении" />
|
||||
</RadioGroup>
|
||||
{pickupPayment === 'on_pickup' && (
|
||||
<Alert severity="info" sx={{ mt: 1 }}>
|
||||
Оплатите заказ наличными или картой при выдаче на самовывозе.
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
|
||||
@@ -371,12 +371,7 @@ export function HomePage() {
|
||||
product={p}
|
||||
mediaHeight={mediaHeight}
|
||||
actions={
|
||||
!isAdmin ? (
|
||||
<ToggleCartIcon
|
||||
productId={p.id}
|
||||
disabledReason={p.inStock && p.quantity === 0 ? 'Нет в наличии' : null}
|
||||
/>
|
||||
) : undefined
|
||||
!isAdmin && !(p.inStock && p.quantity === 0) ? <ToggleCartIcon productId={p.id} /> : undefined
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Link as RouterLink } from 'react-router-dom'
|
||||
import { fetchMyOrder, postOrderMessage } from '@/entities/order/api/order-api'
|
||||
import { fetchMyConversations, markOrderMessagesRead } from '@/entities/user/api/messages-api'
|
||||
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
||||
import { ChatMessageBubble } from '@/shared/ui/ChatMessageBubble'
|
||||
import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent'
|
||||
import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
||||
|
||||
@@ -114,13 +115,15 @@ export function MessagesPage() {
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
secondaryTypographyProps={{
|
||||
sx: {
|
||||
mt: 0.5,
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
slotProps={{
|
||||
secondary: {
|
||||
sx: {
|
||||
mt: 0.5,
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
},
|
||||
},
|
||||
}}
|
||||
secondary={c.preview}
|
||||
@@ -160,24 +163,12 @@ export function MessagesPage() {
|
||||
</Stack>
|
||||
<Stack spacing={1} sx={{ mb: 2, maxHeight: 360, overflow: 'auto' }}>
|
||||
{order.messages.map((m) => (
|
||||
<Box
|
||||
key={m.id}
|
||||
sx={{
|
||||
p: 1.25,
|
||||
borderRadius: 2,
|
||||
bgcolor: m.authorType === 'admin' ? 'grey.100' : 'primary.50',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
alignSelf: m.authorType === 'admin' ? 'flex-start' : 'flex-end',
|
||||
width: 'fit-content',
|
||||
maxWidth: '85%',
|
||||
}}
|
||||
>
|
||||
<ChatMessageBubble key={m.id} authorType={m.authorType === 'admin' ? 'admin' : 'user'}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{m.authorType === 'admin' ? 'Админ' : 'Вы'} · {new Date(m.createdAt).toLocaleString()}
|
||||
</Typography>
|
||||
<RichTextMessageContent value={m.text} tone="chat" />
|
||||
</Box>
|
||||
</ChatMessageBubble>
|
||||
))}
|
||||
{order.messages.length === 0 && <Typography color="text.secondary">Нет сообщений.</Typography>}
|
||||
</Stack>
|
||||
|
||||
@@ -7,6 +7,7 @@ import DialogActions from '@mui/material/DialogActions'
|
||||
import DialogContent from '@mui/material/DialogContent'
|
||||
import DialogTitle from '@mui/material/DialogTitle'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Link from '@mui/material/Link'
|
||||
import Rating from '@mui/material/Rating'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
@@ -22,8 +23,10 @@ import {
|
||||
} from '@/entities/order/api/order-api'
|
||||
import { postProductReview, uploadReviewImage } from '@/entities/product/api/reviews-api'
|
||||
import { markOrderMessagesRead } from '@/entities/user/api/messages-api'
|
||||
import { PICKUP_ADDRESS_FULL } from '@/shared/constants/pickup-point'
|
||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
||||
import { ChatMessageBubble } from '@/shared/ui/ChatMessageBubble'
|
||||
import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent'
|
||||
import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
||||
|
||||
@@ -96,6 +99,7 @@ export function OrderDetailPage() {
|
||||
})
|
||||
|
||||
const order = orderQuery.data?.item
|
||||
const payOnPickup = (order?.paymentMethod ?? 'online') === 'on_pickup'
|
||||
const canSendMessage = text.replace(/<[^>]*>/g, ' ').trim().length > 0
|
||||
|
||||
const eligibilityQuery = useQuery({
|
||||
@@ -199,6 +203,7 @@ export function OrderDetailPage() {
|
||||
</Typography>
|
||||
<Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}>
|
||||
Способ: {order.deliveryType === 'pickup' ? 'Самовывоз' : 'Доставка'}
|
||||
{order.deliveryType === 'pickup' && <> · оплата: {payOnPickup ? 'при получении' : 'онлайн'}</>}
|
||||
</Typography>
|
||||
{order.deliveryType === 'delivery' && (
|
||||
<>
|
||||
@@ -220,9 +225,21 @@ export function OrderDetailPage() {
|
||||
</>
|
||||
)}
|
||||
{order.deliveryType === 'pickup' && (
|
||||
<Typography color="text.secondary">
|
||||
Адрес доставки не требуется. Мы свяжемся для согласования самовывоза.
|
||||
</Typography>
|
||||
<Stack spacing={0.75}>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Адрес самовывоза и карта — на странице{' '}
|
||||
<Link component={RouterLink} to="/about" underline="hover">
|
||||
О нас
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{PICKUP_ADDRESS_FULL}
|
||||
</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Заберите заказ точно ко времени, которое согласуем по телефону или в чате заказа.
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
{order.comment && (
|
||||
<Typography color="text.secondary" variant="body2" sx={{ mt: 1 }}>
|
||||
@@ -235,25 +252,33 @@ export function OrderDetailPage() {
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Оплата
|
||||
</Typography>
|
||||
{order.status === 'PENDING_PAYMENT' && (
|
||||
<>
|
||||
<Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}>
|
||||
Пока это заглушка. После нажатия заказ перейдёт в статус «Проверка оплаты».
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={() => payMut.mutate()} disabled={payMut.isPending}>
|
||||
Оплатить (заглушка)
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{order.status === 'PAYMENT_VERIFICATION' && (
|
||||
<Typography color="info.main" variant="body2">
|
||||
Оплата отправлена на проверку. Мы проверим поступление и обновим статус.
|
||||
</Typography>
|
||||
)}
|
||||
{!['PENDING_PAYMENT', 'PAYMENT_VERIFICATION'].includes(order.status) && (
|
||||
{payOnPickup ? (
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
На этом этапе действий по оплате в этом блоке не требуется.
|
||||
Оплата при получении на точке самовывоза (наличные или карта — по договорённости).
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
{order.status === 'PENDING_PAYMENT' && (
|
||||
<>
|
||||
<Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}>
|
||||
Пока это заглушка. После нажатия заказ перейдёт в статус «Проверка оплаты».
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={() => payMut.mutate()} disabled={payMut.isPending}>
|
||||
Оплатить (заглушка)
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{order.status === 'PAYMENT_VERIFICATION' && (
|
||||
<Typography color="info.main" variant="body2">
|
||||
Оплата отправлена на проверку. Мы проверим поступление и обновим статус.
|
||||
</Typography>
|
||||
)}
|
||||
{!['PENDING_PAYMENT', 'PAYMENT_VERIFICATION'].includes(order.status) && (
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
На этом этапе действий по оплате в этом блоке не требуется.
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -316,24 +341,12 @@ export function OrderDetailPage() {
|
||||
</Typography>
|
||||
<Stack spacing={1} sx={{ mb: 2 }}>
|
||||
{order.messages.map((m) => (
|
||||
<Box
|
||||
key={m.id}
|
||||
sx={{
|
||||
p: 1.25,
|
||||
borderRadius: 2,
|
||||
bgcolor: m.authorType === 'admin' ? 'grey.100' : 'primary.50',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
alignSelf: m.authorType === 'admin' ? 'flex-start' : 'flex-end',
|
||||
width: 'fit-content',
|
||||
maxWidth: '85%',
|
||||
}}
|
||||
>
|
||||
<ChatMessageBubble key={m.id} authorType={m.authorType === 'admin' ? 'admin' : 'user'}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{m.authorType === 'admin' ? 'Админ' : 'Вы'} · {new Date(m.createdAt).toLocaleString()}
|
||||
</Typography>
|
||||
<RichTextMessageContent value={m.text} tone="chat" />
|
||||
</Box>
|
||||
</ChatMessageBubble>
|
||||
))}
|
||||
{order.messages.length === 0 && <Typography color="text.secondary">Пока сообщений нет.</Typography>}
|
||||
</Stack>
|
||||
|
||||
@@ -155,7 +155,7 @@ export function ProductPage() {
|
||||
{formatPriceRub(p.priceCents)}
|
||||
</Typography>
|
||||
|
||||
{!isAdmin && <ToggleCartIcon productId={p.id} size="medium" />}
|
||||
{!isAdmin && !(p.inStock && p.quantity === 0) ? <ToggleCartIcon productId={p.id} size="medium" /> : null}
|
||||
|
||||
{!p.inStock && (
|
||||
<Alert severity="info">
|
||||
|
||||
Reference in New Issue
Block a user