Merge branch 'refactor'
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export { ReviewSection } from './ui/ReviewSection'
|
||||
export { ReviewDialog } from './ui/ReviewDialog'
|
||||
@@ -0,0 +1,150 @@
|
||||
import { useState } from 'react'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import Dialog from '@mui/material/Dialog'
|
||||
import DialogActions from '@mui/material/DialogActions'
|
||||
import DialogContent from '@mui/material/DialogContent'
|
||||
import DialogTitle from '@mui/material/DialogTitle'
|
||||
import Rating from '@mui/material/Rating'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import axios from 'axios'
|
||||
import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
||||
|
||||
type Props = {
|
||||
productTitle: string | null
|
||||
open: boolean
|
||||
isPending: boolean
|
||||
isUploadingImage: boolean
|
||||
error: unknown
|
||||
uploadError: unknown
|
||||
onClose: () => void
|
||||
onSubmit: (params: { rating: number; text: string; imageUrl: string | null }) => void
|
||||
onUploadImage: (file: File) => void
|
||||
}
|
||||
|
||||
function reviewSubmitErrorMessage(err: unknown): string {
|
||||
if (axios.isAxiosError(err)) {
|
||||
const status = err.response?.status
|
||||
const raw = err.response?.data
|
||||
const apiMsg =
|
||||
raw && typeof raw === 'object' && 'error' in raw && typeof (raw as { error: unknown }).error === 'string'
|
||||
? (raw as { error: string }).error
|
||||
: null
|
||||
if (status === 409 || apiMsg?.toLowerCase().includes('уже')) {
|
||||
return 'Вы уже оставляли отзыв на этот товар.'
|
||||
}
|
||||
return apiMsg || err.message || 'Не удалось отправить отзыв'
|
||||
}
|
||||
if (err instanceof Error) return err.message
|
||||
return 'Не удалось отправить отзыв'
|
||||
}
|
||||
|
||||
export function ReviewDialog({
|
||||
productTitle,
|
||||
open,
|
||||
isPending,
|
||||
isUploadingImage,
|
||||
error,
|
||||
uploadError,
|
||||
onClose,
|
||||
onSubmit,
|
||||
onUploadImage,
|
||||
}: Props) {
|
||||
const [rating, setRating] = useState<number>(5)
|
||||
const [text, setText] = useState('')
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null)
|
||||
|
||||
const reset = () => {
|
||||
setRating(5)
|
||||
setText('')
|
||||
setImageUrl(null)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
if (isPending) return
|
||||
reset()
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (isPending) return
|
||||
onSubmit({ rating, text: text.trim(), imageUrl })
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} fullWidth maxWidth="sm">
|
||||
<DialogTitle>Отзыв: {productTitle}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
Оценка
|
||||
</Typography>
|
||||
<Rating
|
||||
value={rating}
|
||||
onChange={(_, v) => {
|
||||
if (v !== null) setRating(v)
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<RichTextMessageEditor value={text} onChange={setText} placeholder="Комментарий (необязательно)" />
|
||||
</Box>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 2, alignItems: { sm: 'center' } }}>
|
||||
<Button component="label" variant="outlined" disabled={isUploadingImage}>
|
||||
{imageUrl ? 'Заменить фото' : 'Прикрепить фото'}
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
onUploadImage(file)
|
||||
e.currentTarget.value = ''
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
{imageUrl && (
|
||||
<Button color="error" variant="text" onClick={() => setImageUrl(null)} disabled={isPending}>
|
||||
Удалить фото
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
{imageUrl && (
|
||||
<Box
|
||||
component="img"
|
||||
src={imageUrl}
|
||||
alt="Фото к отзыву"
|
||||
sx={{
|
||||
mt: 1,
|
||||
width: 120,
|
||||
height: 120,
|
||||
objectFit: 'cover',
|
||||
borderRadius: 1.5,
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{uploadError && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
Не удалось загрузить фото. Разрешены png, jpg, jpeg, webp.
|
||||
</Alert>
|
||||
)}
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{reviewSubmitErrorMessage(error)}
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} disabled={isPending}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button variant="contained" disabled={isPending} onClick={handleSubmit}>
|
||||
Отправить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useState } from 'react'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { ReviewDialog } from './ReviewDialog'
|
||||
|
||||
type EligibileItem = {
|
||||
productId: string
|
||||
title: string
|
||||
hasReview: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
items: EligibileItem[]
|
||||
isSubmitPending: boolean
|
||||
isUploadPending: boolean
|
||||
submitError: unknown
|
||||
uploadError: unknown
|
||||
onSubmitReview: (params: { productId: string; rating: number; text: string; imageUrl: string | null }) => void
|
||||
onUploadImage: (file: File) => Promise<{ url: string }>
|
||||
}
|
||||
|
||||
export function ReviewSection({
|
||||
items,
|
||||
isSubmitPending,
|
||||
isUploadPending,
|
||||
submitError,
|
||||
uploadError,
|
||||
onSubmitReview,
|
||||
onUploadImage,
|
||||
}: Props) {
|
||||
const [target, setTarget] = useState<{ productId: string; title: string } | null>(null)
|
||||
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null)
|
||||
|
||||
if (items.length === 0) return null
|
||||
|
||||
return (
|
||||
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 2, p: 2, bgcolor: 'background.paper' }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Отзывы
|
||||
</Typography>
|
||||
<Typography color="text.secondary" variant="body2" sx={{ mb: 2 }}>
|
||||
Поделитесь впечатлением о товарах. Отзывы появляются после модерации.
|
||||
</Typography>
|
||||
<Stack spacing={1}>
|
||||
{items.map((row) => (
|
||||
<Stack
|
||||
key={row.productId}
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={1}
|
||||
sx={{ alignItems: { sm: 'center' }, justifyContent: 'space-between' }}
|
||||
>
|
||||
<Typography sx={{ flexGrow: 1 }}>{row.title}</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
disabled={row.hasReview}
|
||||
onClick={() => setTarget({ productId: row.productId, title: row.title })}
|
||||
>
|
||||
{row.hasReview ? 'Отзыв отправлен' : 'Оставить отзыв'}
|
||||
</Button>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<ReviewDialog
|
||||
productTitle={target?.title ?? null}
|
||||
open={Boolean(target)}
|
||||
isPending={isSubmitPending}
|
||||
isUploadingImage={isUploadPending}
|
||||
error={submitError}
|
||||
uploadError={uploadError}
|
||||
onClose={() => {
|
||||
setTarget(null)
|
||||
setUploadedImageUrl(null)
|
||||
}}
|
||||
onSubmit={(params) => {
|
||||
if (!target) return
|
||||
onSubmitReview({
|
||||
productId: target.productId,
|
||||
...params,
|
||||
imageUrl: uploadedImageUrl,
|
||||
})
|
||||
}}
|
||||
onUploadImage={async (file) => {
|
||||
const result = await onUploadImage(file)
|
||||
setUploadedImageUrl(result.url)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user