base commit
This commit is contained in:
@@ -1,29 +1,23 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import Button from '@mui/material/Button'
|
||||
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 Box from '@mui/material/Box'
|
||||
import Link from '@mui/material/Link'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
import type { Swiper as SwiperType } from 'swiper/types'
|
||||
import 'swiper/css'
|
||||
import { Link as RouterLink } from 'react-router-dom'
|
||||
import { useUnit } from 'effector-react'
|
||||
import { addToCart } from '@/entities/cart/api/cart-api'
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
import 'swiper/css'
|
||||
import type { Product } from '@/entities/product/model/types'
|
||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||
import { $user } from '@/shared/model/auth'
|
||||
import type { Swiper as SwiperType } from 'swiper/types'
|
||||
|
||||
type Props = { product: Product; mediaHeight?: number }
|
||||
type Props = { product: Product; mediaHeight?: number; actions?: ReactNode }
|
||||
|
||||
export function ProductCard({ product, mediaHeight = 200 }: Props) {
|
||||
const qc = useQueryClient()
|
||||
const user = useUnit($user)
|
||||
export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
|
||||
const swiperRef = useRef<SwiperType | null>(null)
|
||||
const imageUrls = useMemo(() => {
|
||||
const fromImages = (product.images ?? [])
|
||||
@@ -46,11 +40,6 @@ export function ProductCard({ product, mediaHeight = 200 }: Props) {
|
||||
swiperRef.current.slideTo(idx, 0)
|
||||
}
|
||||
|
||||
const addMut = useMutation({
|
||||
mutationFn: () => addToCart({ productId: product.id, qty: 1 }),
|
||||
onSuccess: () => void qc.invalidateQueries({ queryKey: ['me', 'cart'] }),
|
||||
})
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant="outlined"
|
||||
@@ -160,18 +149,7 @@ export function ProductCard({ product, mediaHeight = 200 }: Props) {
|
||||
<Typography variant="h6" color="primary">
|
||||
{formatPriceRub(product.priceCents)}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={!user || addMut.isPending}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
addMut.mutate()
|
||||
}}
|
||||
>
|
||||
{user ? 'В корзину' : 'Войдите, чтобы купить'}
|
||||
</Button>
|
||||
{actions}
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
@@ -10,11 +11,8 @@ import Stack from '@mui/material/Stack'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined'
|
||||
import * as maplibregl from 'maplibre-gl'
|
||||
import Map, { Marker } from 'react-map-gl/maplibre'
|
||||
import type { MapMouseEvent } from 'react-map-gl/maplibre'
|
||||
import type { MapRef } from 'react-map-gl/maplibre'
|
||||
import Map, { Marker, type MapMouseEvent, type MapRef } from 'react-map-gl/maplibre'
|
||||
|
||||
type NominatimItem = { display_name: string; lat: string; lon: string }
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { AddToCartButton } from './ui/AddToCartButton'
|
||||
@@ -0,0 +1,37 @@
|
||||
import Button from '@mui/material/Button'
|
||||
import type { ButtonProps } from '@mui/material/Button'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useUnit } from 'effector-react'
|
||||
import { addToCart } from '@/entities/cart/api/cart-api'
|
||||
import { $user } from '@/shared/model/auth'
|
||||
|
||||
type Props = {
|
||||
productId: string
|
||||
qty?: number
|
||||
loggedOutLabel?: string
|
||||
} & Omit<ButtonProps, 'onClick'>
|
||||
|
||||
export function AddToCartButton(props: Props) {
|
||||
const { productId, qty = 1, loggedOutLabel = 'Войдите, чтобы купить', disabled, children, ...rest } = props
|
||||
const qc = useQueryClient()
|
||||
const user = useUnit($user)
|
||||
|
||||
const addMut = useMutation({
|
||||
mutationFn: () => addToCart({ productId, qty }),
|
||||
onSuccess: () => void qc.invalidateQueries({ queryKey: ['me', 'cart'] }),
|
||||
})
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...rest}
|
||||
disabled={Boolean(disabled) || !user || addMut.isPending}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
addMut.mutate()
|
||||
}}
|
||||
>
|
||||
{user ? (children ?? 'В корзину') : loggedOutLabel}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
postAdminOrderMessage,
|
||||
setAdminOrderStatus,
|
||||
} from '@/entities/order/api/admin-order-api'
|
||||
import { ORDER_STATUS_TRANSITIONS, type OrderStatus } from '@/shared/constants/order'
|
||||
import { clearAdminToken, getAdminToken, setAdminToken } from '@/shared/lib/admin-token'
|
||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||
|
||||
@@ -98,14 +99,7 @@ export function AdminOrdersPage() {
|
||||
const nextStatuses = useMemo(() => {
|
||||
const s = detail?.status
|
||||
if (!s) return []
|
||||
const map: Record<string, string[]> = {
|
||||
DRAFT: ['PENDING_PAYMENT', 'CANCELLED'],
|
||||
PENDING_PAYMENT: ['PAID', 'CANCELLED'],
|
||||
PAID: ['IN_PROGRESS', 'CANCELLED'],
|
||||
IN_PROGRESS: ['SHIPPED', 'CANCELLED'],
|
||||
SHIPPED: ['DONE'],
|
||||
}
|
||||
return map[s] ?? []
|
||||
return ORDER_STATUS_TRANSITIONS[s as OrderStatus] ?? []
|
||||
}, [detail?.status])
|
||||
|
||||
return (
|
||||
@@ -168,7 +162,7 @@ export function AdminOrdersPage() {
|
||||
<MenuItem value="">
|
||||
<em>Все</em>
|
||||
</MenuItem>
|
||||
{['DRAFT', 'PENDING_PAYMENT', 'PAID', 'IN_PROGRESS', 'SHIPPED', 'DONE', 'CANCELLED'].map((s) => (
|
||||
{Object.keys(ORDER_STATUS_TRANSITIONS).map((s) => (
|
||||
<MenuItem key={s} value={s}>
|
||||
{s}
|
||||
</MenuItem>
|
||||
|
||||
@@ -7,8 +7,8 @@ import Stack from '@mui/material/Stack'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useUnit } from 'effector-react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
import { $user, tokenSet } from '@/shared/model/auth'
|
||||
|
||||
@@ -12,8 +12,8 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useUnit } from 'effector-react'
|
||||
import { Link as RouterLink } from 'react-router-dom'
|
||||
import { fetchMyCart, removeCartItem, setCartQty } from '@/entities/cart/api/cart-api'
|
||||
import { $user } from '@/shared/model/auth'
|
||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||
import { $user } from '@/shared/model/auth'
|
||||
|
||||
export function CartPage() {
|
||||
const user = useUnit($user)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
@@ -9,9 +10,8 @@ import Stack from '@mui/material/Stack'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { Link as RouterLink, useNavigate } from 'react-router-dom'
|
||||
import { useUnit } from 'effector-react'
|
||||
import { Link as RouterLink, useNavigate } from 'react-router-dom'
|
||||
import { fetchMyCart } from '@/entities/cart/api/cart-api'
|
||||
import { createOrder } from '@/entities/order/api/order-api'
|
||||
import { fetchMyAddresses } from '@/entities/user/api/address-api'
|
||||
|
||||
@@ -21,6 +21,7 @@ import Typography from '@mui/material/Typography'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchCategories, fetchPublicProducts } from '@/entities/product/api/product-api'
|
||||
import { ProductCard } from '@/entities/product/ui/ProductCard'
|
||||
import { AddToCartButton } from '@/features/cart/add-to-cart'
|
||||
|
||||
export function HomePage() {
|
||||
const [categorySlug, setCategorySlug] = useState<string>('')
|
||||
@@ -315,7 +316,11 @@ export function HomePage() {
|
||||
<Grid container spacing={2}>
|
||||
{products.map((p) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={p.id}>
|
||||
<ProductCard product={p} mediaHeight={mediaHeight} />
|
||||
<ProductCard
|
||||
product={p}
|
||||
mediaHeight={mediaHeight}
|
||||
actions={<AddToCartButton productId={p.id} variant="contained" size="small" />}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
@@ -6,7 +7,6 @@ import Stack from '@mui/material/Stack'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Link as RouterLink, useParams } from 'react-router-dom'
|
||||
import { fetchMyOrder, payOrderStub, postOrderMessage } from '@/entities/order/api/order-api'
|
||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||
|
||||
@@ -2,28 +2,23 @@ import { useMemo, useState } from 'react'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Dialog from '@mui/material/Dialog'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Skeleton from '@mui/material/Skeleton'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
import { Navigation } from 'swiper/modules'
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
import 'swiper/css'
|
||||
import 'swiper/css/navigation'
|
||||
import { useUnit } from 'effector-react'
|
||||
import { addToCart } from '@/entities/cart/api/cart-api'
|
||||
import { fetchPublicProduct } from '@/entities/product/api/product-api'
|
||||
import { AddToCartButton } from '@/features/cart/add-to-cart'
|
||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||
import { $user } from '@/shared/model/auth'
|
||||
|
||||
export function ProductPage() {
|
||||
const { id } = useParams()
|
||||
const qc = useQueryClient()
|
||||
const user = useUnit($user)
|
||||
const [viewerOpen, setViewerOpen] = useState(false)
|
||||
const [viewerIndex, setViewerIndex] = useState(0)
|
||||
|
||||
@@ -33,15 +28,6 @@ export function ProductPage() {
|
||||
enabled: Boolean(id),
|
||||
})
|
||||
|
||||
const addMut = useMutation({
|
||||
mutationFn: async () => {
|
||||
const pid = productQuery.data?.id
|
||||
if (!pid) throw new Error('Товар ещё не загружен')
|
||||
await addToCart({ productId: pid, qty: 1 })
|
||||
},
|
||||
onSuccess: () => void qc.invalidateQueries({ queryKey: ['me', 'cart'] }),
|
||||
})
|
||||
|
||||
const imageUrls = useMemo(() => {
|
||||
const p = productQuery.data
|
||||
if (!p) return []
|
||||
@@ -151,14 +137,7 @@ export function ProductPage() {
|
||||
{formatPriceRub(p.priceCents)}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!user || addMut.isPending || !p}
|
||||
onClick={() => addMut.mutate()}
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
{user ? 'В корзину' : 'Войдите, чтобы купить'}
|
||||
</Button>
|
||||
<AddToCartButton productId={p.id} variant="contained" sx={{ alignSelf: 'flex-start' }} />
|
||||
|
||||
{p.description ? (
|
||||
<Typography sx={{ whiteSpace: 'pre-wrap' }}>{p.description}</Typography>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export const ORDER_STATUSES = [
|
||||
'DRAFT',
|
||||
'PENDING_PAYMENT',
|
||||
'PAID',
|
||||
'IN_PROGRESS',
|
||||
'SHIPPED',
|
||||
'DONE',
|
||||
'CANCELLED',
|
||||
] as const
|
||||
|
||||
export type OrderStatus = (typeof ORDER_STATUSES)[number]
|
||||
|
||||
export const ORDER_STATUS_TRANSITIONS: Record<OrderStatus, OrderStatus[]> = {
|
||||
DRAFT: ['PENDING_PAYMENT', 'CANCELLED'],
|
||||
PENDING_PAYMENT: ['PAID', 'CANCELLED'],
|
||||
PAID: ['IN_PROGRESS', 'CANCELLED'],
|
||||
IN_PROGRESS: ['SHIPPED', 'CANCELLED'],
|
||||
SHIPPED: ['DONE'],
|
||||
DONE: [],
|
||||
CANCELLED: [],
|
||||
}
|
||||
|
||||
export function canTransitionOrderStatus(from: string, to: string): boolean {
|
||||
if (from === to) return true
|
||||
const f = from as OrderStatus
|
||||
const list = ORDER_STATUS_TRANSITIONS[f]
|
||||
return Array.isArray(list) ? list.includes(to as OrderStatus) : false
|
||||
}
|
||||
Reference in New Issue
Block a user