Создан компонент поста
This commit is contained in:
parent
674ef7fe04
commit
d0f41672d1
32 changed files with 2082 additions and 289 deletions
73
frontend/src/components/base/AuthLockBlock.astro
Normal file
73
frontend/src/components/base/AuthLockBlock.astro
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
buttonText: string;
|
||||
buttonHref: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'Авторизуйтесь, чтобы продолжить',
|
||||
description = 'Для продолжения, пожалуйста, войдите в личный кабинет.',
|
||||
buttonText = 'Войти в кабинет',
|
||||
buttonHref = '/auth/sign-in',
|
||||
className = ''
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<div class={`auth-lock-card ${className}`}>
|
||||
<div class="lock-icon-container">
|
||||
<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="lock-icon">
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="lock-title">{title}</h3>
|
||||
<p class="lock-text" style:display={description ? 'block' : 'none'}>{description}</p>
|
||||
<a href={buttonHref} class="auth-button">{buttonText}</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.auth-lock-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background: #f8fafc;
|
||||
border: 2px dashed #e2e8f0;
|
||||
border-radius: 1.5rem;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lock-icon-container {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
background: #ffffff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.lock-icon { width: 2rem; height: 2rem; }
|
||||
.lock-title { color: #1e293b; font-size: 1.5rem; font-weight: 700; margin: 0 0 1rem; }
|
||||
.lock-text { color: #64748b; line-height: 1.6; margin: 0 0 2rem; max-width: 400px; }
|
||||
|
||||
.auth-button {
|
||||
background: #1e293b;
|
||||
color: #ffffff;
|
||||
padding: 0.875rem 2rem;
|
||||
border-radius: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.auth-button:hover { background: #0f172a; transform: translateY(-2px); }
|
||||
</style>
|
||||
|
|
@ -1,17 +1,25 @@
|
|||
---
|
||||
import { blogPosts } from '@data/blogData';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
|
||||
// Сортируем и форматируем для поиска
|
||||
const searchData = posts
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
||||
.map(post => ({
|
||||
title: post.data.title,
|
||||
description: post.data.description,
|
||||
slug: post.id,
|
||||
category: post.data.category,
|
||||
categoryColor: post.data.categoryColor,
|
||||
date: post.data.date.toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
}));
|
||||
|
||||
const title = 'Поиск по статьям';
|
||||
|
||||
const searchData = blogPosts.map(post => ({
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
slug: post.slug,
|
||||
category: post.category,
|
||||
categoryColor: post.categoryColor,
|
||||
date: post.date
|
||||
}));
|
||||
|
||||
const postsJson = JSON.stringify(searchData);
|
||||
---
|
||||
|
||||
|
|
@ -106,7 +114,7 @@ const postsJson = JSON.stringify(searchData);
|
|||
|
||||
// вывод динамического списка статей
|
||||
resultsContainer.innerHTML = filtered.map(post => `
|
||||
<a href="/blog/${post.slug}" class="search-result-item">
|
||||
<a href="/blog/${post.id}" class="search-result-item">
|
||||
<span class="result-date">${post.date}</span>
|
||||
<h4 class="result-title">${post.title}</h4>
|
||||
<p class="result-description">${post.description.substring(0, 120)}...</p>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const formatDate = (dateStr: string) => {
|
|||
};
|
||||
---
|
||||
|
||||
<article class="blog-card animate-on-scroll" data-animation="fade-up">
|
||||
<article class="blog-card" data-animation="fade-up">
|
||||
<a href={slug} class="card-link">
|
||||
<!-- Изображение -->
|
||||
<div class="card-image">
|
||||
|
|
|
|||
346
frontend/src/components/blog/PostCommentForm.astro
Normal file
346
frontend/src/components/blog/PostCommentForm.astro
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
---
|
||||
import AuthLockBlock from '@components/base/AuthLockBlock.astro';
|
||||
|
||||
interface Props {
|
||||
postId: string;
|
||||
isAuthorized?: boolean;
|
||||
}
|
||||
|
||||
const { postId, isAuthorized = false } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="comment-section" data-post-id={postId}>
|
||||
<h3 class="comment-title">Комментарии</h3>
|
||||
|
||||
{isAuthorized ? (
|
||||
<form class="comment-form" id="comment-form" data-post-id={postId}>
|
||||
<div class="form-group">
|
||||
<label for="comment-text" class="form-label">Ваш комментарий *</label>
|
||||
<textarea
|
||||
id="comment-text"
|
||||
name="comment"
|
||||
class="form-textarea"
|
||||
placeholder="Напишите ваш комментарий..."
|
||||
rows="4"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="submit-btn">
|
||||
<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="btn-icon">
|
||||
<path d="m22 2-7 20-4-9-9-4Z"></path>
|
||||
<path d="M22 2 11 13"></path>
|
||||
</svg>
|
||||
Отправить комментарий
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<AuthLockBlock
|
||||
title="Авторизуйтесь, чтобы оставить комментарий"
|
||||
description=""
|
||||
buttonText="Войти в кабинет"
|
||||
buttonHref="/auth/sign-in"
|
||||
/>
|
||||
)}
|
||||
|
||||
<!-- Список комментариев -->
|
||||
<div class="comments-list" id="comments-list">
|
||||
<!-- Пример комментарителя (заглушка для демонстрации) -->
|
||||
<div class="comment-item">
|
||||
<div class="comment-header">
|
||||
<div class="comment-author">
|
||||
<div class="author-avatar">А</div>
|
||||
<div class="author-info">
|
||||
<span class="author-name">Алексей Петров</span>
|
||||
<span class="comment-date">2 апреля 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-content">
|
||||
<p>Спасибо за полезную статью! Столкнулся с похожей ситуацией, теперь знаю куда обращаться за помощью.</p>
|
||||
</div>
|
||||
<button class="comment-reply-btn">Ответить</button>
|
||||
|
||||
<!-- Ответ на комментарий -->
|
||||
<div class="comment-reply">
|
||||
<div class="comment-header">
|
||||
<div class="comment-author">
|
||||
<div class="author-avatar reply-avatar">Ю</div>
|
||||
<div class="author-info">
|
||||
<span class="author-name">Юрист АВ</span>
|
||||
<span class="comment-date">3 апреля 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-content">
|
||||
<p>Алексей, спасибо за ваш комментарий! Обращайтесь — мы всегда готовы помочь в решении автоспоров.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ещё один комментарий -->
|
||||
<div class="comment-item">
|
||||
<div class="comment-header">
|
||||
<div class="comment-author">
|
||||
<div class="author-avatar" style="background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);">М</div>
|
||||
<div class="author-info">
|
||||
<span class="author-name">Мария Иванова</span>
|
||||
<span class="comment-date">5 апреля 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-content">
|
||||
<p>Очень подробная инструкция! Подскажите, а если ДТП произошло без пострадавших, можно ли обойтись без вызова ГИБДД?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.comment-section {
|
||||
margin-top: 3rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 2px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.comment-title {
|
||||
color: #1e293b;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
padding: 1rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
outline: none;
|
||||
background: #f8fafc;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-textarea:focus {
|
||||
border-color: #d4af37;
|
||||
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.1);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: linear-gradient(135deg, #d4af37 0%, #eac26e 100%);
|
||||
color: #1e293b;
|
||||
border: none;
|
||||
padding: 0.875rem 1.75rem;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 15px rgba(212, 175, 55, 0.3);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(212, 175, 55, 0.4);
|
||||
}
|
||||
|
||||
/* Auth Lock Card - на всю ширину */
|
||||
.comment-section :global(.auth-lock-card) {
|
||||
max-width: 100% !important;
|
||||
padding: 2rem 1.5rem !important;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.comment-section :global(.lock-icon-container) {
|
||||
width: 3rem !important;
|
||||
height: 3rem !important;
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.comment-section :global(.lock-icon) {
|
||||
width: 1.25rem !important;
|
||||
height: 1.25rem !important;
|
||||
}
|
||||
|
||||
.comment-section :global(.lock-title) {
|
||||
font-size: 1.1rem !important;
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
.comment-section :global(.lock-text) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.comment-section :global(.auth-button) {
|
||||
padding: 0.6rem 1.5rem !important;
|
||||
font-size: 0.9rem !important;
|
||||
}
|
||||
|
||||
/* Comments List */
|
||||
.comments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #d4af37 0%, #eac26e 100%);
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.reply-avatar {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #4ade80 100%) !important;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
color: #475569;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.comment-content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.comment-reply-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #d4af37;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 0;
|
||||
margin-top: 0.5rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.comment-reply-btn:hover {
|
||||
color: #b8942e;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Ответ на комментарий */
|
||||
.comment-reply {
|
||||
margin-top: 1rem;
|
||||
margin-left: 2rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-left: 3px solid #d4af37;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comment-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.comment-reply {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const setupCommentForm = () => {
|
||||
const form = document.getElementById('comment-form') as HTMLFormElement;
|
||||
if (form) {
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
const comment = formData.get('comment');
|
||||
|
||||
if (comment) {
|
||||
console.log('Новый комментарий:', comment);
|
||||
alert('Спасибо! Ваш комментарий добавлен.');
|
||||
form.reset();
|
||||
|
||||
// Здесь можно добавить логику для добавления комментарителя в список
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setupCommentForm();
|
||||
document.addEventListener('astro:page-load', setupCommentForm);
|
||||
</script>
|
||||
139
frontend/src/components/blog/PostReactionButtons.astro
Normal file
139
frontend/src/components/blog/PostReactionButtons.astro
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
interface Props {
|
||||
initialLikes?: number;
|
||||
initialDislikes?: number;
|
||||
postId: string;
|
||||
}
|
||||
|
||||
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="Нравится">
|
||||
<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>
|
||||
</svg>
|
||||
<span class="reaction-count" data-count="likes">{initialLikes}</span>
|
||||
</button>
|
||||
|
||||
<button class="reaction-btn dislike-btn" data-action="dislike" aria-label="Не нравится">
|
||||
<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>
|
||||
</svg>
|
||||
<span class="reaction-count" data-count="dislikes">{initialDislikes}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.post-reactions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.reaction-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.reaction-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.reaction-count {
|
||||
min-width: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.like-btn:hover,
|
||||
.like-btn.active {
|
||||
background: #22c55e;
|
||||
border-color: #22c55e;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.dislike-btn:hover,
|
||||
.dislike-btn.active {
|
||||
background: #ef4444;
|
||||
border-color: #ef4444;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.reaction-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.reaction-btn:hover .reaction-icon {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.reaction-btn {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.post-reactions').forEach((container) => {
|
||||
const likeBtn = container.querySelector('.like-btn');
|
||||
const dislikeBtn = container.querySelector('.dislike-btn');
|
||||
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;
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
120
frontend/src/components/blog/PostSocialShare.astro
Normal file
120
frontend/src/components/blog/PostSocialShare.astro
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
url: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const { title, url, className = '' } = Astro.props;
|
||||
|
||||
const encodedTitle = encodeURIComponent(title);
|
||||
const encodedUrl = encodeURIComponent(url);
|
||||
---
|
||||
|
||||
<div class={`post-social-share ${className}`}>
|
||||
<span class="share-label">Поделиться:</span>
|
||||
<div class="social-icons">
|
||||
<!-- Telegram -->
|
||||
<a
|
||||
href={`https://t.me/share/url?url=${encodedUrl}&text=${encodedTitle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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>
|
||||
</a>
|
||||
|
||||
<!-- VK -->
|
||||
<a
|
||||
href={`https://vk.com/share.php?url=${encodedUrl}&title=${encodedTitle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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>
|
||||
</a>
|
||||
|
||||
<!-- WhatsApp -->
|
||||
<a
|
||||
href={`https://api.whatsapp.com/send?text=${encodedTitle}%20${encodedUrl}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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>
|
||||
</a>
|
||||
|
||||
<!-- Одноклассники -->
|
||||
<a
|
||||
href={`https://connect.ok.ru/offer?url=${encodedUrl}&title=${encodedTitle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.post-social-share {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.share-label {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.social-icons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.social-icon svg {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
|
||||
.social-icon.telegram { background: #0088cc; }
|
||||
.social-icon.vk { background: #0077FF; }
|
||||
.social-icon.whatsapp { background: #25D366; }
|
||||
.social-icon.odnoklassniki { background: #EE8208; }
|
||||
|
||||
.social-icon:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.post-social-share {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
91
frontend/src/components/blog/RelatedPosts.astro
Normal file
91
frontend/src/components/blog/RelatedPosts.astro
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
import BlogCard from '@components/blog/BlogCard.astro';
|
||||
|
||||
interface CollectionEntry {
|
||||
id: string;
|
||||
data: {
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
categoryColor: string;
|
||||
date: Date;
|
||||
readTime: string;
|
||||
imageUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
posts: CollectionEntry[];
|
||||
currentSlug?: string;
|
||||
}
|
||||
|
||||
const { posts, currentSlug } = Astro.props;
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Фильтруем текущую статью
|
||||
const filteredPosts = currentSlug
|
||||
? posts.filter(post => post.id !== currentSlug).slice(0, 3)
|
||||
: posts.slice(0, 3);
|
||||
---
|
||||
|
||||
<section class="related-posts">
|
||||
<h3 class="section-title">Читайте также</h3>
|
||||
|
||||
<div class="related-grid">
|
||||
{filteredPosts.map((post) => (
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
category={post.data.category}
|
||||
categoryColor={post.data.categoryColor}
|
||||
date={formatDate(post.data.date)}
|
||||
readTime={post.data.readTime}
|
||||
imageUrl={post.data.imageUrl}
|
||||
slug={`/blog/${post.id}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style is:global>
|
||||
.related-posts {
|
||||
margin-top: 3rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 2px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #1e293b;
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
font-weight: 800;
|
||||
margin: 0 0 2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.related-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.related-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.related-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue