Новые изменения в компонентах
This commit is contained in:
parent
5bb4525f63
commit
d4394a7597
5 changed files with 145 additions and 34 deletions
|
|
@ -1,21 +1,42 @@
|
||||||
---
|
---
|
||||||
import { reviewsData } from '@data/reviewsData';
|
|
||||||
import ReviewCard from '@components/reviews/ReviewCard.astro';
|
import ReviewCard from '@components/reviews/ReviewCard.astro';
|
||||||
|
|
||||||
// Берём 6 последних отзывов
|
interface ReviewRecord {
|
||||||
const latestReviews = [...reviewsData]
|
id: string;
|
||||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
name: string;
|
||||||
.slice(0, 6);
|
surname?: string;
|
||||||
|
profession?: string;
|
||||||
|
text: string;
|
||||||
|
rating: number;
|
||||||
|
votesCount: number;
|
||||||
|
created: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Для эффекта бесконечности клонируем элементы
|
const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||||
const displayReviews = [...latestReviews, ...latestReviews, ...latestReviews];
|
|
||||||
|
let reviews: ReviewRecord[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filter = 'status = "published"';
|
||||||
|
const response = await fetch(
|
||||||
|
`${POCKETBASE_URL}/api/collections/reviews/records?filter=${encodeURIComponent(filter)}&sort=-created&perPage=10`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
reviews = data.items || [];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[ReviewsSlider] Error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayReviews = reviews.length >= 3
|
||||||
|
? [...reviews, ...reviews, ...reviews]
|
||||||
|
: [...reviews, ...reviews];
|
||||||
|
|
||||||
// Функция для получения инициала из имени
|
|
||||||
const getInitial = (name: string) => name.charAt(0).toUpperCase();
|
const getInitial = (name: string) => name.charAt(0).toUpperCase();
|
||||||
|
|
||||||
// Функция для получения цвета аватара (циклически из набора)
|
const colors = ['bg-gradient-1', 'bg-gradient-2', 'bg-gradient-3', 'bg-gradient-4', 'bg-gradient-5', 'bg-gradient-6'];
|
||||||
const colors = ['#eac26e', '#22c55e', '#3b82f6', '#ef4444', '#8b5cf6', '#f59e0b'];
|
|
||||||
const getColor = (index: number) => colors[index % colors.length];
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- КРИТИЧНО: Скрипт скрывает элементы ДО рендера -->
|
<!-- КРИТИЧНО: Скрипт скрывает элементы ДО рендера -->
|
||||||
|
|
@ -49,25 +70,29 @@ const getColor = (index: number) => colors[index % colors.length];
|
||||||
<!-- Окно просмотра -->
|
<!-- Окно просмотра -->
|
||||||
<div id="sliderContainer" class="overflow-x-hidden scroll-smooth">
|
<div id="sliderContainer" class="overflow-x-hidden scroll-smooth">
|
||||||
<div id="sliderTrack" class="flex gap-6 py-4 items-stretch">
|
<div id="sliderTrack" class="flex gap-6 py-4 items-stretch">
|
||||||
{displayReviews.map((review, index) => (
|
{displayReviews.map((review, index) => {
|
||||||
|
const fullName = `${review.name} ${review.surname || ''}`.trim();
|
||||||
|
const colorIndex = index % 6;
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
class="card-item min-w-full md:min-w-[calc(33.333%-1rem)] select-none animate-on-scroll"
|
class="card-item min-w-full md:min-w-[calc(33.333%-1rem)] select-none animate-on-scroll"
|
||||||
data-animation="scale-up"
|
data-animation="scale-up"
|
||||||
data-delay={index * 100 + 300}
|
data-delay={index * 100 + 300}
|
||||||
>
|
>
|
||||||
<ReviewCard
|
<ReviewCard
|
||||||
name={review.name}
|
name={fullName}
|
||||||
car={review.car}
|
profession={review.profession || 'Клиент'}
|
||||||
text={review.text}
|
text={review.text}
|
||||||
rating={review.rating}
|
rating={review.rating}
|
||||||
date={review.date}
|
initial={getInitial(review.name)}
|
||||||
votesCount={review.votesCount || 0}
|
color={colors[colorIndex]}
|
||||||
isHelpful={review.isHelpful || false}
|
date={review.created}
|
||||||
initial={getInitial(review.name)}
|
reviewId={review.id}
|
||||||
color={getColor(index)}
|
initialLikes={review.votesCount || 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const gradientColors = {
|
||||||
};
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<article class="review-card animate-on-scroll" data-animation="fade-up" data-review-id={reviewId}>
|
<article class="review-card animate-on-scroll" data-animation="fade-up" data-review-id={reviewId} data-initial-likes={initialLikes}>
|
||||||
<!-- Декоративный элемент -->
|
<!-- Декоративный элемент -->
|
||||||
<div class="card-decoration"></div>
|
<div class="card-decoration"></div>
|
||||||
|
|
||||||
|
|
@ -480,10 +480,11 @@ const gradientColors = {
|
||||||
|
|
||||||
document.querySelectorAll('.review-card').forEach((card) => {
|
document.querySelectorAll('.review-card').forEach((card) => {
|
||||||
const reviewId = (card as HTMLElement).dataset.reviewId;
|
const reviewId = (card as HTMLElement).dataset.reviewId;
|
||||||
|
const initialLikes = parseInt((card as HTMLElement).dataset.initialLikes || '0', 10);
|
||||||
const likeBtn = card.querySelector('[data-vote="likes"]') as HTMLButtonElement;
|
const likeBtn = card.querySelector('[data-vote="likes"]') as HTMLButtonElement;
|
||||||
const likesCount = card.querySelector('[data-likes-count]');
|
const likesCount = card.querySelector('[data-likes-count]');
|
||||||
|
|
||||||
console.log('[ReviewCard] reviewId:', reviewId, 'likeBtn:', likeBtn, 'likesCount:', likesCount);
|
console.log('[ReviewCard] reviewId:', reviewId, 'likeBtn:', likeBtn, 'likesCount:', likesCount, 'initialLikes:', initialLikes);
|
||||||
|
|
||||||
if (!reviewId) {
|
if (!reviewId) {
|
||||||
console.error('[ReviewCard] No reviewId found!');
|
console.error('[ReviewCard] No reviewId found!');
|
||||||
|
|
@ -492,12 +493,6 @@ const gradientColors = {
|
||||||
|
|
||||||
async function loadVotes() {
|
async function loadVotes() {
|
||||||
console.log('[ReviewCard] Loading votes for:', reviewId);
|
console.log('[ReviewCard] Loading votes for:', reviewId);
|
||||||
if (!pb.authStore.isValid) {
|
|
||||||
console.log('[ReviewCard] Not logged in, keeping initialLikes:', initialLikes);
|
|
||||||
if (likesCount) likesCount.textContent = initialLikes.toString();
|
|
||||||
likeBtn?.classList.remove('active');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/reviews/vote?review_id=${reviewId}`, {
|
const response = await fetch(`/api/reviews/vote?review_id=${reviewId}`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|
@ -507,12 +502,17 @@ const gradientColors = {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('[ReviewCard] Votes data:', data);
|
console.log('[ReviewCard] Votes data:', data);
|
||||||
if (likesCount) likesCount.textContent = data.likes.toString();
|
if (likesCount) likesCount.textContent = data.likes.toString();
|
||||||
if (data.userVote === 'likes') {
|
if (data.userVote === 'likes' && pb.authStore.isValid) {
|
||||||
likeBtn?.classList.add('active');
|
likeBtn?.classList.add('active');
|
||||||
|
} else {
|
||||||
|
likeBtn?.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (likesCount) likesCount.textContent = initialLikes.toString();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[ReviewCard] Error loading votes:', e);
|
console.error('[ReviewCard] Error loading votes:', e);
|
||||||
|
if (likesCount) likesCount.textContent = initialLikes.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ const getAvatarInfo = (name: string) => {
|
||||||
color={avatarInfo.color}
|
color={avatarInfo.color}
|
||||||
date={review.created}
|
date={review.created}
|
||||||
reviewId={review.id}
|
reviewId={review.id}
|
||||||
|
initialLikes={review.votesCount || 0}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,15 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
||||||
console.error('[ReviewVote API] Votes error:', errorText);
|
console.error('[ReviewVote API] Votes error:', errorText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await fetch(`${POCKETBASE_URL}/api/collections/reviews/records/${review_id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ votesCount: likes }),
|
||||||
|
});
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ likes, dislikes, userVote }),
|
JSON.stringify({ likes, dislikes, userVote }),
|
||||||
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
|
|
||||||
76
scripts/create-review.ts
Normal file
76
scripts/create-review.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
|
const PB_URL = process.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||||
|
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
|
||||||
|
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD;
|
||||||
|
|
||||||
|
async function createReview() {
|
||||||
|
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
||||||
|
console.error('❌ Укажите PB_ADMIN_EMAIL и PB_ADMIN_PASSWORD в .env');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pb = new PocketBase(PB_URL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
|
||||||
|
console.log('✅ Подключено к PocketBase');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ Ошибка авторизации admin');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testEmail = 'vasiliy.sinicin@yandex.ru';
|
||||||
|
const testPassword = 'TestPass123!';
|
||||||
|
|
||||||
|
let userId: string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingUser = await pb.collection('users').getList(1, 1, {
|
||||||
|
filter: `email = "${testEmail}"`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser.items.length > 0) {
|
||||||
|
userId = existingUser.items[0].id;
|
||||||
|
console.log('ℹ️ Использован существующий пользователь:', userId);
|
||||||
|
} else {
|
||||||
|
const user = await pb.collection('users').create({
|
||||||
|
email: testEmail,
|
||||||
|
password: testPassword,
|
||||||
|
passwordConfirm: testPassword,
|
||||||
|
name: 'Тестовый Пользователь',
|
||||||
|
});
|
||||||
|
userId = user.id;
|
||||||
|
console.log('✅ Создан пользователь:', userId);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('❌ Ошибка создания пользователя:', e.data || e.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const review = await pb.collection('reviews').create({
|
||||||
|
user: userId,
|
||||||
|
name: 'Василий',
|
||||||
|
surname: 'Синицин',
|
||||||
|
profession: 'Предприниматель',
|
||||||
|
text: 'Помогли вернуть права после лишения. Всё сделали быстро и профессионально. Через 2 месяца права уже были у меня. Рекомендую!',
|
||||||
|
rating: 5,
|
||||||
|
votesCount: 0,
|
||||||
|
status: 'published',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Отзыв создан');
|
||||||
|
console.log(' ID:', review.id);
|
||||||
|
console.log(' rating:', review.rating);
|
||||||
|
console.log(' status:', review.status);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.data?.message?.includes('already exists')) {
|
||||||
|
console.log('ℹ️ Отзыв уже существует');
|
||||||
|
} else {
|
||||||
|
console.error('❌ Ошибка:', e.data || e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createReview();
|
||||||
Loading…
Add table
Add a link
Reference in a new issue