Новые изменения в проекте

This commit is contained in:
Web-serfer 2026-04-20 20:27:04 +05:00
parent ecb720f751
commit ddc0a26635
13 changed files with 950 additions and 48 deletions

View file

@ -0,0 +1,252 @@
import type { APIRoute } from 'astro';
const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
const POCKETBASE_ID_REGEX = /^[a-z0-9]{15}$/;
export const POST: APIRoute = async ({ request, cookies }) => {
try {
const token = cookies.get('pb_auth')?.value;
if (!token) {
return new Response(
JSON.stringify({ error: 'Требуется авторизация' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
const authResponse = await fetch(
`${POCKETBASE_URL}/api/collections/users/auth-refresh`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);
if (!authResponse.ok) {
return new Response(
JSON.stringify({ error: 'Недействительная сессия' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
const authData = await authResponse.json();
const userId = authData.record?.id;
if (!userId || !POCKETBASE_ID_REGEX.test(userId)) {
return new Response(
JSON.stringify({ error: 'Ошибка идентификации пользователя' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
const body = await request.json();
const { review_id, vote_type } = body;
if (!review_id || !POCKETBASE_ID_REGEX.test(review_id)) {
return new Response(
JSON.stringify({ error: 'Некорректный review_id' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
if (!vote_type || !['like', 'dislike'].includes(vote_type)) {
return new Response(
JSON.stringify({ error: 'Некорректный vote_type' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const existingVoteRes = await fetch(
`${POCKETBASE_URL}/api/collections/review_votes/records?` +
new URLSearchParams({
filter: `review="${review_id}" && user="${userId}"`,
}),
{
headers: { 'Authorization': `Bearer ${token}` },
}
);
let method = 'POST';
let url = `${POCKETBASE_URL}/api/collections/review_votes/records`;
let voteId = null;
let userVote: 'like' | 'dislike' | null = null;
if (existingVoteRes.ok) {
const existingData = await existingVoteRes.json();
if (existingData.items?.length > 0) {
const existing = existingData.items[0];
voteId = existing.id;
if (existing.vote_type === vote_type) {
method = 'DELETE';
url = `${POCKETBASE_URL}/api/collections/review_votes/records/${voteId}`;
} else {
method = 'PATCH';
url = `${POCKETBASE_URL}/api/collections/review_votes/records/${voteId}`;
userVote = vote_type;
}
}
}
const voteBody = method === 'POST'
? JSON.stringify({ review: review_id, user: userId, vote_type })
: method === 'PATCH'
? JSON.stringify({ vote_type })
: null;
const voteRes = await fetch(url, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: voteBody,
});
if (!voteRes.ok && method !== 'DELETE') {
const errorText = await voteRes.text();
console.error('[ReviewVote API] Failed to save vote:', errorText);
throw new Error('Failed to save vote');
}
if (method === 'POST') userVote = vote_type;
if (method === 'DELETE') userVote = null;
const likesRes = await fetch(
`${POCKETBASE_URL}/api/collections/review_votes/records?` +
new URLSearchParams({
filter: `review="${review_id}"`,
fields: 'vote_type',
})
);
let likes = 0;
let dislikes = 0;
if (likesRes.ok) {
const votesData = await likesRes.json();
likes = votesData.items.filter((v: any) => v.vote_type === 'like').length;
dislikes = votesData.items.filter((v: any) => v.vote_type === 'dislike').length;
}
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(
JSON.stringify({ likes, dislikes, userVote }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('[ReviewVote API] Error:', error);
return new Response(
JSON.stringify({ error: 'Внутренняя ошибка сервера' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};
export const GET: APIRoute = async ({ url, cookies }) => {
try {
const reviewId = url.searchParams.get('review_id');
const token = cookies.get('pb_auth')?.value;
if (!reviewId || !POCKETBASE_ID_REGEX.test(reviewId)) {
return new Response(
JSON.stringify({ error: 'Некорректный review_id' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const reviewRes = await fetch(
`${POCKETBASE_URL}/api/collections/reviews/records/${reviewId}`
);
if (!reviewRes.ok) {
return new Response(
JSON.stringify({ error: 'Отзыв не найден' }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
);
}
const votesRes = await fetch(
`${POCKETBASE_URL}/api/collections/review_votes/records?` +
new URLSearchParams({
filter: `review="${reviewId}"`,
fields: 'vote_type',
})
);
let likes = 0;
let dislikes = 0;
if (votesRes.ok) {
const votesData = await votesRes.json();
likes = votesData.items.filter((v: any) => v.vote_type === 'like').length;
dislikes = votesData.items.filter((v: any) => v.vote_type === 'dislike').length;
}
let userVote: 'like' | 'dislike' | null = null;
if (token) {
try {
const authRes = await fetch(
`${POCKETBASE_URL}/api/collections/users/auth-refresh`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
}
);
if (authRes.ok) {
const authData = await authRes.json();
const userId = authData.record?.id;
if (userId && POCKETBASE_ID_REGEX.test(userId)) {
const userVoteRes = await fetch(
`${POCKETBASE_URL}/api/collections/review_votes/records?` +
new URLSearchParams({
filter: `review="${reviewId}" && user="${userId}"`,
}),
{
headers: { 'Authorization': `Bearer ${token}` },
}
);
if (userVoteRes.ok) {
const userVoteData = await userVoteRes.json();
if (userVoteData.items?.length > 0) {
userVote = userVoteData.items[0].vote_type;
}
}
}
}
} catch (e) {
console.error('[ReviewVote API] Error fetching user vote:', e);
}
}
return new Response(
JSON.stringify({ likes, dislikes, userVote }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('[ReviewVote API GET] Error:', error);
return new Response(
JSON.stringify({ error: 'Внутренняя ошибка сервера' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};

View file

@ -2,21 +2,8 @@
import Layout from '@layouts/Layout.astro';
import { SITE_URL } from '@constants';
import PageHero from '@components/base/PageHero.astro';
import Pagination from '@components/base/Pagination.astro';
import ReviewCard from '@components/reviews/ReviewCard.astro';
import ReviewFormContainer from '@components/reviews/ReviewFormContainer.tsx';
import ReviewsList from '@components/reviews/ReviewsList.tsx';
import VotingSummary from '@components/reviews/VotingSummary.astro';
import AuthLockBlock from '@components/base/AuthLockBlock.astro';
import { reviewsData, votingSummary } from '@data/reviewsData';
const REVIEWS_PER_PAGE = 6;
const currentPage = 1;
const totalPages = Math.ceil(reviewsData.length / REVIEWS_PER_PAGE);
const startIndex = 0;
const endIndex = REVIEWS_PER_PAGE;
const paginatedReviews = reviewsData.slice(startIndex, endIndex);
import ReviewsList from '@components/reviews/ReviewsList.astro';
---
<Layout
@ -48,7 +35,7 @@ const paginatedReviews = reviewsData.slice(startIndex, endIndex);
<div class="site-container">
<!-- Блок статистики голосования -->
<ReviewsList client:load />
<ReviewsList />
<!-- Форма для отзыва -->
<section class="review-form-section">
@ -208,11 +195,9 @@ const paginatedReviews = reviewsData.slice(startIndex, endIndex);
// Запуск
setupAnimations();
setupReviewForm();
// Для поддержки View Transitions в Astro
document.addEventListener('astro:after-swap', () => {
setupAnimations();
setupReviewForm();
});
</script>