Новы глобальные изменения компонентоы

This commit is contained in:
Web-serfer 2026-04-17 17:35:17 +05:00
parent 5d7bb04bf1
commit a269d3459e
43 changed files with 1667 additions and 517 deletions

View file

@ -9,7 +9,12 @@ const { initialLikes = 0, initialDislikes = 0, postId } = Astro.props;
---
<div class="post-reactions" data-post-id={postId}>
<button class="reaction-btn like-btn" data-action="like" aria-label="Нравится">
<button
class="reaction-btn like-btn"
data-action="like"
aria-label="Нравится"
type="button"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="reaction-icon">
<path d="M7 10v12"></path>
<path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"></path>
@ -17,7 +22,12 @@ const { initialLikes = 0, initialDislikes = 0, postId } = Astro.props;
<span class="reaction-count" data-count="likes">{initialLikes}</span>
</button>
<button class="reaction-btn dislike-btn" data-action="dislike" aria-label="Не нравится">
<button
class="reaction-btn dislike-btn"
data-action="dislike"
aria-label="Не нравится"
type="button"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="reaction-icon">
<path d="M17 14V2"></path>
<path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"></path>
@ -87,53 +97,160 @@ const { initialLikes = 0, initialDislikes = 0, postId } = Astro.props;
font-size: 0.8rem;
}
}
:global(.toast-link) {
color: #60a5fa;
text-decoration: underline;
font-size: 0.875rem;
margin-left: 0.5rem;
flex-shrink: 0;
}
:global(.toast-link:hover) {
color: #93c5fd;
}
</style>
<script>
document.querySelectorAll('.post-reactions').forEach((container) => {
const likeBtn = container.querySelector('.like-btn');
const dislikeBtn = container.querySelector('.dislike-btn');
import { pb } from '../../lib/pb';
console.log('PostReactionButtons loaded');
function showToast(message: string, type: string = 'info', duration: number = 3000, link?: { text: string; href: string }) {
console.log('showToast called:', message, type);
const container = document.querySelector('.toast-container');
console.log('toast-container found:', container);
if (!container) {
console.error('Toast container not found!');
return;
}
const toast = document.createElement('div');
toast.className = `toast-item toast-${type}`;
toast.innerHTML = `
<div class="toast-icon">
${type === 'success' ? '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>' : ''}
${type === 'error' ? '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>' : ''}
${type === 'warning' ? '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512"><path fill="currentColor" d="M240 176h32v176h-32zm0 208h32v32h-32z"/><path fill="currentColor" d="M274.014 16h-36.028L16 445.174V496h480v-50.826ZM464 464H48v-11.041L256 50.826l208 402.133Z"/></svg>' : ''}
${type === 'info' ? '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>' : ''}
</div>
<span class="toast-message">${message}</span>
${link ? `<a href="${link.href}" class="toast-link">${link.text}</a>` : ''}
<button class="toast-close" aria-label="Закрыть">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
`;
const closeBtn = toast.querySelector('.toast-close');
closeBtn?.addEventListener('click', () => {
toast.classList.remove('show');
toast.classList.add('hide');
setTimeout(() => toast.remove(), 300);
});
container.appendChild(toast);
requestAnimationFrame(() => toast.classList.add('show'));
if (duration > 0) {
setTimeout(() => {
toast.classList.remove('show');
toast.classList.add('hide');
setTimeout(() => toast.remove(), 300);
}, duration);
}
}
async function handleVote(postId: string, voteType: 'like' | 'dislike') {
const container = document.querySelector(`[data-post-id="${postId}"]`);
if (!container) return;
const likeBtn = container.querySelector('.like-btn') as HTMLButtonElement;
const dislikeBtn = container.querySelector('.dislike-btn') as HTMLButtonElement;
const likesCount = container.querySelector('[data-count="likes"]');
const dislikesCount = container.querySelector('[data-count="dislikes"]');
let likes = parseInt(likesCount?.textContent || '0');
let dislikes = parseInt(dislikesCount?.textContent || '0');
let userAction: 'like' | 'dislike' | null = null;
if (!pb.authStore.isValid) {
console.log('pb.authStore.isValid:', pb.authStore.isValid);
console.log('pb.authStore.model:', pb.authStore.model);
console.log('pb.authStore.token:', pb.authStore.token ? 'exists' : 'none');
showToast('Войдите, чтобы голосовать', 'warning', 5000, { text: 'Войти', href: '/auth/sign-in' });
return;
}
try {
const formData = new FormData();
formData.append('post_id', postId);
formData.append('vote_type', voteType);
const response = await fetch('/api/votes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ post_id: postId, vote_type: voteType }),
credentials: 'include',
});
if (!response.ok) {
const error = await response.json();
console.error('Vote error response:', error);
throw new Error(error.message || error.details || 'Ошибка голосования');
}
const result = await response.json();
if (likesCount) likesCount.textContent = result.likes.toString();
if (dislikesCount) dislikesCount.textContent = result.dislikes.toString();
if (result.userVote === 'like') {
likeBtn?.classList.add('active');
dislikeBtn?.classList.remove('active');
} else if (result.userVote === 'dislike') {
dislikeBtn?.classList.add('active');
likeBtn?.classList.remove('active');
} else {
likeBtn?.classList.remove('active');
dislikeBtn?.classList.remove('active');
}
showToast('Ваш голос учтён', 'success', 2000);
} catch (error) {
console.error('Ошибка голосования:', error);
showToast('Не удалось отправить голос. Попробуйте позже.', 'error', 3000);
}
}
document.querySelectorAll('.post-reactions').forEach((container) => {
const postId = (container as HTMLElement).dataset.postId;
const likeBtn = container.querySelector('.like-btn');
const dislikeBtn = container.querySelector('.dislike-btn');
async function loadUserVote() {
if (!pb.authStore.isValid) return;
try {
const response = await fetch(`/api/votes?post_id=${postId}`, {
credentials: 'include',
});
if (response.ok) {
const result = await response.json();
if (result.userVote === 'like') {
likeBtn?.classList.add('active');
} else if (result.userVote === 'dislike') {
dislikeBtn?.classList.add('active');
}
}
} catch (e) {
console.error('Ошибка загрузки голоса:', e);
}
}
loadUserVote();
likeBtn?.addEventListener('click', () => {
if (userAction === 'like') {
likes--;
userAction = null;
likeBtn.classList.remove('active');
} else {
if (userAction === 'dislike') {
dislikes--;
dislikeBtn?.classList.remove('active');
}
likes++;
userAction = 'like';
likeBtn.classList.add('active');
}
if (likesCount) likesCount.textContent = likes.toString();
if (dislikesCount) dislikesCount.textContent = dislikes.toString();
if (postId) handleVote(postId, 'like');
});
dislikeBtn?.addEventListener('click', () => {
if (userAction === 'dislike') {
dislikes--;
userAction = null;
dislikeBtn.classList.remove('active');
} else {
if (userAction === 'like') {
likes--;
likeBtn?.classList.remove('active');
}
dislikes++;
userAction = 'dislike';
dislikeBtn.classList.add('active');
}
if (likesCount) likesCount.textContent = likes.toString();
if (dislikesCount) dislikesCount.textContent = dislikes.toString();
if (postId) handleVote(postId, 'dislike');
});
});
</script>

View file

@ -1,4 +1,9 @@
---
import telegramIcon from '../../icons/telegram.svg?raw';
import vkIcon from '../../icons/vk.svg?raw';
import whatsappIcon from '../../icons/whatsapp.svg?raw';
import okIcon from '../../icons/ok.svg?raw';
interface Props {
title: string;
url: string;
@ -22,9 +27,7 @@ const encodedUrl = encodeURIComponent(url);
class="social-icon telegram"
aria-label="Поделиться в Telegram"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
<span class="icon-wrapper" set:html={telegramIcon} />
</a>
<!-- VK -->
@ -35,9 +38,7 @@ const encodedUrl = encodeURIComponent(url);
class="social-icon vk"
aria-label="Поделиться в VK"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M15.684 0H8.316C1.592 0 0 1.592 0 8.316v7.368C0 22.408 1.592 24 8.316 24h7.368C22.408 24 24 22.408 24 15.684V8.316C24 1.592 22.391 0 15.684 0zm3.692 17.123h-1.744c-.66 0-.864-.525-2.05-1.727-1.033-1-1.49-1.135-1.744-1.135-.356 0-.458.102-.458.593v1.575c0 .424-.135.678-1.253.678-1.846 0-3.896-1.118-5.335-3.202C4.624 10.857 4.03 8.57 4.03 8.096c0-.254.102-.491.593-.491h1.744c.44 0 .61.203.78.677.847 2.49 2.27 4.675 2.862 4.675.22 0 .322-.102.322-.66V9.721c-.068-1.186-.695-1.287-.695-1.71 0-.204.17-.407.44-.407h2.744c.373 0 .508.203.508.643v3.473c0 .372.17.508.271.508.22 0 .407-.136.813-.542 1.27-1.422 2.18-3.61 2.18-3.61.119-.254.322-.491.763-.491h1.744c.525 0 .644.27.525.643-.22 1.017-2.354 4.031-2.354 4.031-.186.305-.254.44 0 .78.186.254.796.779 1.203 1.253.745.847 1.32 1.558 1.473 2.05.17.49-.085.744-.576.744z"/>
</svg>
<span class="icon-wrapper" set:html={vkIcon} />
</a>
<!-- WhatsApp -->
@ -48,9 +49,7 @@ const encodedUrl = encodeURIComponent(url);
class="social-icon whatsapp"
aria-label="Поделиться в WhatsApp"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413z"/>
</svg>
<span class="icon-wrapper" set:html={whatsappIcon} />
</a>
<!-- Одноклассники -->
@ -61,9 +60,7 @@ const encodedUrl = encodeURIComponent(url);
class="social-icon odnoklassniki"
aria-label="Поделиться в Одноклассниках"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M14.505 17.043c.098.07.222.102.373.102.15 0 .274-.032.372-.102a.597.597 0 0 0 .214-.348.89.89 0 0 0 .064-.408 1.09 1.09 0 0 0-.136-.443c-.09-.153-.212-.277-.365-.373l-.01-.006c-.154-.097-.275-.222-.363-.373a.997.997 0 0 1-.136-.438.86.86 0 0 1 .063-.397.577.577 0 0 1 .204-.338.636.636 0 0 1 .372-.102c.15 0 .274.034.372.102a.577.577 0 0 1 .204.338.86.86 0 0 1 .063.397.997.997 0 0 1-.136.438c-.088.151-.209.276-.363.373l-.01.006c-.153.096-.275.22-.365.373a1.09 1.09 0 0 0-.136.443.89.89 0 0 0 .064.408.597.597 0 0 0 .214.348zM12 13.5c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5 1.5.672 1.5 1.5-.672 1.5-1.5 1.5zm6.793-9.216A11.952 11.952 0 0 0 12 .5C5.373.5.008 5.865.008 12.5c0 2.768.929 5.372 2.534 7.458.065.085.136.165.208.244.016.018.034.034.05.051l.01.011c.135.147.277.288.425.421.043.039.087.077.131.114.129.111.263.216.4.315.076.056.154.109.233.161.116.077.235.149.357.217.095.054.191.105.29.151.115.054.234.102.355.145.097.034.195.065.295.091.12.032.241.058.365.079.097.016.195.03.293.039.131.012.264.017.398.017.135 0 .268-.005.4-.017.098-.009.196-.023.293-.039.124-.021.245-.047.365-.079.1-.026.198-.057.295-.091.121-.043.24-.091.355-.145.099-.046.195-.097.29-.151.122-.068.241-.14.357-.217.079-.052.157-.105.233-.161.137-.099.271-.204.4-.315.044-.037.088-.075.131-.114.148-.133.29-.274.425-.421l.01-.011c.016-.017.034-.033.05-.051.072-.079.143-.159.208-.244A11.953 11.953 0 0 0 23.992 12.5C23.992 5.865 18.627.5 12 .5z"/>
</svg>
<span class="icon-wrapper" set:html={okIcon} />
</a>
</div>
</div>
@ -97,7 +94,7 @@ const encodedUrl = encodeURIComponent(url);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.social-icon svg {
.social-icon :global(svg) {
width: 1.125rem;
height: 1.125rem;
}