145 lines
3.8 KiB
Text
145 lines
3.8 KiB
Text
|
|
---
|
|||
|
|
import ReviewCard from './ReviewCard.astro';
|
|||
|
|
import VotingSummary from './VotingSummary.astro';
|
|||
|
|
|
|||
|
|
interface ReviewRecord {
|
|||
|
|
id: string;
|
|||
|
|
name: string;
|
|||
|
|
surname?: string;
|
|||
|
|
profession?: string;
|
|||
|
|
text: string;
|
|||
|
|
rating: number;
|
|||
|
|
status: string;
|
|||
|
|
votesCount: number;
|
|||
|
|
created: string;
|
|||
|
|
expand?: {
|
|||
|
|
user?: {
|
|||
|
|
id: string;
|
|||
|
|
name: string;
|
|||
|
|
firstName?: string;
|
|||
|
|
email: string;
|
|||
|
|
avatar?: string;
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
status?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { status = 'published' } = Astro.props;
|
|||
|
|
|
|||
|
|
// Загрузка данных с сервера
|
|||
|
|
let reviews: ReviewRecord[] = [];
|
|||
|
|
let error: string | null = null;
|
|||
|
|
let averageRating = 0;
|
|||
|
|
let totalVotes = 0;
|
|||
|
|
let totalReviews = 0;
|
|||
|
|
let ratingDistribution: Record<number, number> = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const filter = `status = "${status}"`;
|
|||
|
|
const expand = 'user';
|
|||
|
|
const sort = '-created';
|
|||
|
|
|
|||
|
|
const response = await fetch(
|
|||
|
|
`${POCKETBASE_URL}/api/collections/reviews/records?expand=${expand}&filter=${encodeURIComponent(filter)}&sort=${sort}`
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (response.ok) {
|
|||
|
|
const data = await response.json();
|
|||
|
|
reviews = data.items || [];
|
|||
|
|
|
|||
|
|
// Расчёт статистики
|
|||
|
|
totalReviews = reviews.length;
|
|||
|
|
totalVotes = reviews.reduce((sum, r) => sum + (r.votesCount || 0), 0);
|
|||
|
|
const totalRating = reviews.reduce((sum, r) => sum + r.rating, 0);
|
|||
|
|
averageRating = totalReviews > 0 ? parseFloat((totalRating / totalReviews).toFixed(1)) : 0;
|
|||
|
|
|
|||
|
|
ratingDistribution = reviews.reduce((acc, r) => {
|
|||
|
|
acc[r.rating] = (acc[r.rating] || 0) + 1;
|
|||
|
|
return acc;
|
|||
|
|
}, {} as Record<number, number>);
|
|||
|
|
} else {
|
|||
|
|
error = 'Не удалось загрузить отзывы';
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('[ReviewsList] Error:', e);
|
|||
|
|
error = 'Ошибка загрузки отзывов';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Цвета для аватарок
|
|||
|
|
const colors = [
|
|||
|
|
'bg-blue-100 text-blue-600', 'bg-teal-100 text-teal-600', 'bg-orange-100 text-orange-600',
|
|||
|
|
'bg-pink-100 text-pink-600', 'bg-purple-100 text-purple-600', 'bg-indigo-100 text-indigo-600',
|
|||
|
|
'bg-green-100 text-green-600', 'bg-yellow-100 text-yellow-600', 'bg-red-100 text-red-600'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const getAvatarInfo = (name: string) => {
|
|||
|
|
const initial = name.charAt(0).toUpperCase();
|
|||
|
|
const color = colors[initial.charCodeAt(0) % colors.length];
|
|||
|
|
return { initial, color };
|
|||
|
|
};
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
{error && (
|
|||
|
|
<div class="text-center py-12 text-red-500">
|
|||
|
|
<p>{error}</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{!error && (
|
|||
|
|
<>
|
|||
|
|
{reviews.length > 0 && (
|
|||
|
|
<VotingSummary
|
|||
|
|
averageRating={averageRating}
|
|||
|
|
totalVotes={totalVotes}
|
|||
|
|
totalReviews={totalReviews}
|
|||
|
|
ratingDistribution={ratingDistribution}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div class="reviews-grid">
|
|||
|
|
{reviews.length > 0 ? (
|
|||
|
|
reviews.map((review) => {
|
|||
|
|
const avatarInfo = getAvatarInfo(review.name);
|
|||
|
|
const fullName = `${review.name} ${review.surname || ''}`.trim();
|
|||
|
|
return (
|
|||
|
|
<ReviewCard
|
|||
|
|
name={fullName}
|
|||
|
|
profession={review.profession || 'Клиент'}
|
|||
|
|
text={review.text}
|
|||
|
|
rating={review.rating}
|
|||
|
|
initial={avatarInfo.initial}
|
|||
|
|
color={avatarInfo.color}
|
|||
|
|
date={review.created}
|
|||
|
|
reviewId={review.id}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
})
|
|||
|
|
) : (
|
|||
|
|
<div class="text-center py-12 text-gray-500" style="grid-column: 1 / -1;">
|
|||
|
|
<p>Пока нет отзывов. Будьте первым!</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
.reviews-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|||
|
|
gap: 2rem;
|
|||
|
|
margin: 3rem 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 640px) {
|
|||
|
|
.reviews-grid {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
gap: 1.5rem;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|