astro_avtourist/frontend/src/layouts/ArticleLayout.astro

346 lines
No EOL
14 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import "@styles/global.css";
import { SITE_TITLE_SUFFIX } from "@constants";
import Header from "@components/layout/header/Header.astro";
import Footer from "@components/layout/footer/Footer.astro";
import Breadcrumbs from "@components/base/Breadcrumbs.astro";
import ConsultationModal from "@components/base/ConsultationModal.astro";
import Toast from "@components/base/Toast.astro";
import PostSocialShare from "@components/blog/PostSocialShare.astro";
import PostReactionButtons from "@components/blog/PostReactionButtons.astro";
import PostViewCounter from "@components/blog/PostViewCounter.astro";
export interface Props {
title: string;
description: string;
canonicalLink?: string;
breadcrumbs?: Array<{ label: string; href?: string }>;
heroImage: string;
heroAlt: string;
category: string;
postTitle: string;
date: string;
author: string;
readTime: string;
readmeTime: string;
postId: string;
postUrl: string;
initialLikes?: number;
initialDislikes?: number;
initialViews?: number;
}
const {
title,
description,
canonicalLink,
breadcrumbs,
heroImage,
heroAlt,
category,
postTitle,
date,
author,
readTime,
readmeTime,
postId,
postUrl,
initialLikes = 0,
initialDislikes = 0,
initialViews = 0
} = Astro.props;
---
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicons/favicon.svg" />
<link rel="icon" href="/favicons/favicon.ico" />
<title>{title} {SITE_TITLE_SUFFIX}</title>
<meta name="description" content={description} />
{canonicalLink && <link rel="canonical" href={canonicalLink} />}
<link rel="sitemap" href="/sitemap-index.xml" />
</head>
<body>
<Toast />
<Header />
<main class="main-content">
{breadcrumbs && breadcrumbs.length > 0 && (
<div class="breadcrumbs-wrapper">
<div class="site-container">
<Breadcrumbs items={breadcrumbs} />
</div>
</div>
)}
<!-- HERO SECTION С ЭФФЕКТОМ ПРОЗРЕНИЯ -->
<section class="article-hero">
<div class="article-hero-image">
<img src={heroImage} alt={heroAlt} loading="eager" width="800" height="400" />
<div class="article-hero-overlay"></div>
</div>
<div class="article-hero-content">
<div class="site-container h-full">
<div class="article-hero-inner">
<div class="article-hero-top">
<div class="category-strip">
<span class="category-text">{category}</span>
</div>
<h1 class="article-title">{postTitle}</h1>
</div>
<div class="article-hero-bottom">
<div class="article-meta">
<span class="meta-item">
<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="meta-icon"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"></rect><line x1="16" x2="16" y1="2" y2="6"></line><line x1="8" x2="8" y1="2" y2="6"></line><line x1="3" x2="21" y1="10" y2="10"></line></svg>
{date}
</span>
<span class="meta-item">
<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="meta-icon"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
<span class="meta-author">{author}</span>
</span>
<span class="meta-item">
<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="meta-icon"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
{readTime} мин.
</span>
<span class="meta-item">
<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="meta-icon"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg>
<span class="meta-views" data-post-id={postId}>{initialViews}</span>
</span>
</div>
<div class="article-actions">
<PostReactionButtons postId={postId} initialLikes={initialLikes} initialDislikes={initialDislikes} />
<PostSocialShare title={postTitle} url={postUrl} />
</div>
</div>
</div>
</div>
</div>
</section>
<!-- BODY SECTION: СЕТКА С ЛИПКИМ САЙДБАРОМ -->
<div class="article-body">
<div class="site-container">
<div class="article-layout-grid">
<div class="article-main">
<slot />
</div>
<aside class="article-sidebar" id="sticky-sidebar">
<slot name="sidebar" />
</aside>
</div>
</div>
</div>
</main>
<Footer />
<style>
/* ЧЕРНАЯ НУМЕРАЦИЯ */
.article-content-wrapper { counter-reset: post-h2; }
.article-content-wrapper .post-content h2 { counter-reset: post-h3; counter-increment: post-h2; display: flex; align-items: baseline; gap: 0.6rem; color: #1e3050; font-weight: 800; }
.article-content-wrapper .post-content h2::before { content: counter(post-h2) "."; color: #1e3050 !important; flex-shrink: 0; }
.article-content-wrapper .post-content h3 { counter-increment: post-h3; display: flex; align-items: baseline; gap: 0.5rem; color: #1e3050; font-weight: 700; }
.article-content-wrapper .post-content h3::before { content: counter(post-h2) "." counter(post-h3) "."; color: #1e3050 !important; font-size: 0.9em; flex-shrink: 0; }
/* Исключаем заголовок "Читайте также" из нумерации */
.article-content-wrapper .related-posts .section-title {
counter-increment: none !important;
display: block !important;
}
.article-content-wrapper .related-posts .section-title::before {
content: none !important;
}
/* Исключаем секцию комментариев из нумерации */
.article-content-wrapper .comments-wrapper h2,
.article-content-wrapper .comments-wrapper h2::before {
counter-increment: none !important;
content: none !important;
}
.article-content-wrapper .comments-wrapper h3,
.article-content-wrapper .comments-wrapper h3::before {
counter-increment: none !important;
content: none !important;
}
.article-content-wrapper .comments-wrapper .section-title,
.article-content-wrapper .comments-wrapper .section-title::before {
counter-increment: none !important;
content: none !important;
}
/* СКРОЛЛ-ФИКС */
.article-content-wrapper h2, .article-content-wrapper h3 { scroll-margin-top: 125px; }
/* СТИЛЬ АКТИВНОГО ПУНКТА В TOC */
.toc-item.active .toc-link {
color: #eac26e !important;
font-weight: 700;
background: rgba(234, 194, 110, 0.1);
}
.toc-item.active .toc-link::before { color: #eac26e !important; }
html, body { overflow-x: clip !important; }
</style>
<style>
.h-full { height: 100%; }
.main-content { padding-top: 0; overflow: visible; }
.breadcrumbs-wrapper { padding-top: 5.5rem; background: #f8fafc; }
/* HERO SECTION */
.article-hero { position: relative; width: 100%; height: 550px; background: #0a1a2e; overflow: hidden; }
.article-hero-image { position: absolute; inset: 0; z-index: 1; }
.article-hero-image img { width: 100%; height: 100%; object-fit: cover; filter: brightness(0.35) grayscale(0.2); transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1); transform: scale(1.04); }
.article-hero-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, transparent 0%, rgba(10, 26, 46, 0.9) 100%); z-index: 2; transition: opacity 0.7s ease; }
.article-hero:hover .article-hero-image img { filter: brightness(1) grayscale(0); transform: scale(1); }
.article-hero:hover .article-hero-overlay { opacity: 0.4; }
.article-hero-content { position: relative; z-index: 10; height: 100%; }
.article-hero-inner { display: flex; flex-direction: column; height: 100%; padding: 3rem 0; }
.article-hero-top { margin-bottom: 2rem; }
.category-strip { background: #eac26e; padding: 0.5rem 1.5rem; display: inline-block; border-radius: 4px; margin-bottom: 2rem; }
.category-text { color: #0a1a2e; font-weight: 800; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; font-family: 'Merriweather', Georgia, serif; }
.article-title { color: #fff; font-size: clamp(2rem, 5vw, 3.5rem); font-weight: 900; line-height: 1.15; max-width: 950px; text-shadow: 0 4px 15px rgba(0,0,0,0.4); font-family: 'Merriweather', Georgia, serif; }
.article-hero-bottom { margin-top: auto; display: flex; justify-content: space-between; align-items: center; padding-top: 2rem; border-top: 1px solid rgba(255, 255, 255, 0.15); }
.article-actions { display: flex; align-items: center; gap: 2rem; }
.article-meta { display: flex; gap: 2rem; justify-content: flex-start; width: auto; }
.meta-item { display: flex; align-items: center; gap: 0.6rem; color: #fff; font-size: 0.95rem; white-space: nowrap; }
.meta-icon { width: 1.1rem; height: 1.1rem; color: #eac26e; }
.meta-author { color: #eac26e; font-weight: 700; }
/* GRID & STICKY SIDEBAR */
.article-body { padding: 0 0 4rem; background: #f8fafc; overflow: visible; }
.article-layout-grid { display: flex; gap: 3.5rem; align-items: flex-start; }
.article-main { flex: 1; min-width: 0; }
.article-content-wrapper { background: #fff; padding: 1.5rem 2.5rem 2.5rem; border-radius: 2rem; box-shadow: 0 4px 30px rgba(0,0,0,0.02); }
.article-sidebar { width: 320px; flex-shrink: 0; position: sticky; top: 110px; z-index: 50; }
@media (max-width: 1200px) {
.article-sidebar { display: none; }
.article-layout-grid { flex-direction: column; }
}
@media (max-width: 1024px) {
.article-hero-bottom { flex-direction: column; align-items: flex-start; gap: 1.5rem; }
.article-actions { width: 100%; justify-content: flex-start; }
.article-meta { gap: 1.5rem; }
}
@media (max-width: 768px) {
.article-hero { height: auto; min-height: 385px; }
.article-title { font-size: 1.5rem; text-align: center; }
.article-hero-top { text-align: center; display: flex; flex-direction: column; align-items: center; }
.category-strip { margin-bottom: 1.5rem; padding: 0.4rem 1rem; }
.category-text { font-size: 0.7rem; }
.article-hero-bottom { flex-direction: column; align-items: center; gap: 2rem; text-align: center; }
.article-actions { width: 100%; justify-content: center; align-items: center; gap: 1.5rem; }
.share-wrapper { display: flex; align-items: flex-end; }
.article-content-wrapper { padding: 2.5rem 1.5rem; padding-top: 0; }
.article-hero-inner { padding: 2rem 0; }
.meta-item { font-size: 0.85rem; }
.article-meta { gap: 0.75rem; flex-wrap: wrap; justify-content: center; }
.article-content-wrapper .post-content h2 { font-size: 1.25rem; text-align: center; justify-content: center; }
.article-content-wrapper .post-content h3 { font-size: 1.1rem; text-align: center; justify-content: center; }
}
</style>
<script>
function setupArticleLogic() {
const sidebar = document.getElementById('sticky-sidebar');
const content = document.getElementById('post-content');
if (!sidebar || !content) return;
const tocLinks = sidebar.querySelectorAll<HTMLAnchorElement>('.toc-link');
const tocItems = sidebar.querySelectorAll('.toc-item');
const articleHeadings = content.querySelectorAll('h2, h3');
const HEADER_OFFSET = 120;
// 1. АВТОПРИВЯЗКА ID К ЗАГОЛОВКАМ (Решает проблему heading-0 не найден)
tocLinks.forEach((link, index) => {
const id = link.getAttribute('href')?.slice(1);
const heading = articleHeadings[index];
if (id && heading) {
heading.id = id;
}
});
// 2. ПЛАВНЫЙ СКРОЛЛ
tocLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const id = link.getAttribute('href')?.slice(1);
const target = id ? document.getElementById(id) : null;
if (target) {
const targetPos = target.getBoundingClientRect().top + window.pageYOffset - HEADER_OFFSET;
window.scrollTo({ top: targetPos, behavior: 'smooth' });
history.pushState(null, '', `#${id}`);
}
});
});
// 3. SCROLL SPY (ПОДСВЕТКА)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const activeId = entry.target.id;
tocItems.forEach(item => {
const match = item.getAttribute('data-toc-target') === activeId;
item.classList.toggle('active', match);
});
}
});
}, {
root: null,
rootMargin: `-${HEADER_OFFSET}px 0px -70% 0px`,
threshold: 0
});
articleHeadings.forEach(h => observer.observe(h));
// 4. КНОПКА КОНСУЛЬТАЦИИ
const consultationBtn = document.getElementById("consultation-btn");
consultationBtn?.addEventListener("click", () => {
window.dispatchEvent(new CustomEvent("open-modal", { detail: "consultation-modal" }));
});
// 5. СЧЁТЧИК ПРОСМОТРОВ
const viewsEl = document.querySelector('.meta-views') as HTMLElement & { dataset: { postId: string } };
console.log('[Views] Element found:', viewsEl, 'postId:', viewsEl?.dataset?.postId);
if (viewsEl?.dataset?.postId) {
const postId = viewsEl.dataset.postId;
console.log('[Views] Fetching for post:', postId);
fetch(`/api/increment-views?postId=${postId}`)
.then(res => res.json())
.then(data => {
console.log('[Views] Response:', data);
if (data.views !== undefined) {
viewsEl.textContent = formatViews(data.views);
}
})
.catch(err => {
console.error('[Views] Error:', err);
});
}
function formatViews(n: number): string {
if (n >= 1000) {
return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
}
return n.toString();
}
}
document.addEventListener("DOMContentLoaded", setupArticleLogic);
document.addEventListener("astro:page-load", setupArticleLogic);
</script>