156 lines
6.2 KiB
TypeScript
156 lines
6.2 KiB
TypeScript
import StarRoundedIcon from '@mui/icons-material/StarRounded'
|
||
import Alert from '@mui/material/Alert'
|
||
import Avatar from '@mui/material/Avatar'
|
||
import Box from '@mui/material/Box'
|
||
import Paper from '@mui/material/Paper'
|
||
import Rating from '@mui/material/Rating'
|
||
import Skeleton from '@mui/material/Skeleton'
|
||
import Stack from '@mui/material/Stack'
|
||
import Typography from '@mui/material/Typography'
|
||
import { useQuery } from '@tanstack/react-query'
|
||
import { Link as RouterLink } from 'react-router-dom'
|
||
import { fetchLatestApprovedReviews } from '@/entities/review/api/reviews-api'
|
||
import { OptimizedImage } from '@/shared/ui/OptimizedImage'
|
||
import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent'
|
||
|
||
function initials(display: string) {
|
||
const s = display.trim()
|
||
if (!s) return '?'
|
||
return s.slice(0, 1).toUpperCase()
|
||
}
|
||
|
||
function formatReviewDate(iso: string): string {
|
||
try {
|
||
return new Date(iso).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
export function ReviewsBlock() {
|
||
const q = useQuery({
|
||
queryKey: ['reviews', 'latest', 5],
|
||
queryFn: () => fetchLatestApprovedReviews(5),
|
||
})
|
||
|
||
const items = !q.isLoading && !q.isError && q.data ? q.data.items : []
|
||
|
||
return (
|
||
<Paper variant="outlined" sx={{ p: { xs: 2, sm: 3 }, borderRadius: 2, bgcolor: 'background.paper' }}>
|
||
<Stack spacing={0.75} sx={{ mb: 2 }}>
|
||
<Typography variant="h5">Отзывы</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
Последние отзывы о товарах
|
||
</Typography>
|
||
</Stack>
|
||
|
||
{q.isLoading && (
|
||
<Stack spacing={2}>
|
||
{[1, 2, 3].map((i) => (
|
||
<Skeleton key={i} variant="rounded" height={92} sx={{ borderRadius: 2 }} />
|
||
))}
|
||
</Stack>
|
||
)}
|
||
{q.isError && <Alert severity="error">Не удалось загрузить отзывы.</Alert>}
|
||
{!q.isLoading && !q.isError && q.data && items.length === 0 && (
|
||
<Box sx={{ py: 4 }}>
|
||
<Typography variant="h6" color="text.secondary" sx={{ mb: 2 }}>
|
||
Отзывов пока нет
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 400 }}>
|
||
Будьте первым, кто оставит отзыв о наших товарах. Ваше мнение поможет другим покупателям сделать правильный
|
||
выбор.
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
{items.length > 0 && (
|
||
<Stack spacing={2}>
|
||
{items.map((r, i) => {
|
||
const zebra = i % 2 === 0
|
||
const text = typeof r.text === 'string' && r.text.trim() ? r.text.trim() : 'Без комментария'
|
||
return (
|
||
<Paper
|
||
key={r.id}
|
||
variant={zebra ? undefined : 'outlined'}
|
||
elevation={zebra ? 0 : undefined}
|
||
sx={{
|
||
p: { xs: 1.75, sm: 2 },
|
||
borderRadius: 2,
|
||
...(zebra ? { bgcolor: 'action.hover' } : {}),
|
||
}}
|
||
>
|
||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ alignItems: { sm: 'flex-start' } }}>
|
||
{r.imageUrl && (
|
||
<Box
|
||
sx={{
|
||
width: { xs: 80, sm: 80 },
|
||
height: { xs: 80, sm: 80 },
|
||
borderRadius: 1.5,
|
||
border: 1,
|
||
borderColor: 'divider',
|
||
overflow: 'hidden',
|
||
flexShrink: 0,
|
||
alignSelf: { xs: 'flex-start', sm: 'center' },
|
||
}}
|
||
>
|
||
<OptimizedImage
|
||
src={r.imageUrl}
|
||
alt="Фото к отзыву"
|
||
widths={[320, 640]}
|
||
sizes="80px"
|
||
sx={{
|
||
width: '100%',
|
||
height: '100%',
|
||
objectFit: 'cover',
|
||
}}
|
||
/>
|
||
</Box>
|
||
)}
|
||
<Stack direction="row" spacing={1.5} sx={{ minWidth: { sm: 200 }, alignItems: 'center' }}>
|
||
<Avatar sx={{ bgcolor: 'primary.main', color: 'primary.contrastText', fontWeight: 800 }}>
|
||
{initials(r.authorDisplay)}
|
||
</Avatar>
|
||
<Box>
|
||
<Typography sx={{ fontWeight: 800, lineHeight: 1.15 }}>{r.authorDisplay}</Typography>
|
||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
||
<Rating
|
||
value={r.rating}
|
||
readOnly
|
||
size="small"
|
||
icon={<StarRoundedIcon fontSize="inherit" />}
|
||
emptyIcon={<StarRoundedIcon fontSize="inherit" />}
|
||
/>
|
||
<Typography variant="caption" color="text.secondary">
|
||
{formatReviewDate(r.createdAt)}
|
||
</Typography>
|
||
</Stack>
|
||
<Typography
|
||
variant="caption"
|
||
component={RouterLink}
|
||
to={`/products/${r.productId}`}
|
||
sx={{
|
||
display: 'block',
|
||
mt: 0.25,
|
||
color: 'primary.main',
|
||
textDecoration: 'none',
|
||
'&:hover': { textDecoration: 'underline' },
|
||
}}
|
||
>
|
||
{r.productTitle}
|
||
</Typography>
|
||
</Box>
|
||
</Stack>
|
||
|
||
<Box sx={{ color: 'text.secondary', flex: 1 }}>
|
||
<RichTextMessageContent value={text} tone="review" />
|
||
</Box>
|
||
</Stack>
|
||
</Paper>
|
||
)
|
||
})}
|
||
</Stack>
|
||
)}
|
||
</Paper>
|
||
)
|
||
}
|