Files
shop-server/client/src/widgets/reviews-block/ui/ReviewsBlock.tsx
T
@kirill.komarov 6885e39017 base commit
2026-05-03 20:30:21 +05:00

137 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/product/api/reviews-api'
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 && (
<Typography color="text.secondary">Пока нет опубликованных отзывов о товарах.</Typography>
)}
{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}>
<Stack direction="row" spacing={1.5} sx={{ minWidth: { sm: 220 }, 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} />
</Box>
</Stack>
{r.imageUrl && (
<Box
component="img"
src={r.imageUrl}
alt="Фото к отзыву"
sx={{
mt: 1.5,
width: 120,
height: 120,
objectFit: 'cover',
borderRadius: 1.5,
border: 1,
borderColor: 'divider',
}}
/>
)}
</Paper>
)
})}
</Stack>
)}
</Paper>
)
}