astro_avtourist/frontend/src/layouts/ArticleLayout.astro
2026-04-14 18:59:12 +05:00

280 lines
No EOL
11 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 PostSocialShare from "@components/blog/PostSocialShare.astro";
import PostReactionButtons from "@components/blog/PostReactionButtons.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;
postId: string;
postUrl: string;
initialLikes?: number;
initialDislikes?: number;
}
const {
title,
description,
canonicalLink,
breadcrumbs,
heroImage,
heroAlt,
category,
postTitle,
date,
author,
readTime,
postId,
postUrl,
initialLikes = 0,
initialDislikes = 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>
<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" />
<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>
</div>
<div class="article-actions">
<PostReactionButtons postId={postId} initialLikes={initialLikes} initialDislikes={initialDislikes} />
<div class="share-wrapper">
<PostSocialShare title={postTitle} url={postUrl} />
</div>
</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">
<article class="article-content-wrapper" id="post-content">
<slot />
</article>
</div>
<aside class="article-sidebar" id="sticky-sidebar">
<slot name="sidebar" />
</aside>
</div>
</div>
</div>
</main>
<Footer />
<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" }));
});
}
document.addEventListener("DOMContentLoaded", setupArticleLogic);
document.addEventListener("astro:page-load", setupArticleLogic);
</script>
<style is:global>
/* ЧЕРНАЯ НУМЕРАЦИЯ */
#post-content { counter-reset: post-h2; }
#post-content h2 { counter-reset: post-h3; counter-increment: post-h2; display: flex; align-items: baseline; gap: 0.6rem; color: #1e3050; font-weight: 800; }
#post-content h2::before { content: counter(post-h2) "."; color: #1e3050 !important; flex-shrink: 0; }
#post-content h3 { counter-increment: post-h3; display: flex; align-items: baseline; gap: 0.5rem; color: #1e3050; font-weight: 700; }
#post-content h3::before { content: counter(post-h2) "." counter(post-h3) "."; color: #1e3050 !important; font-size: 0.9em; flex-shrink: 0; }
/* Исключаем заголовок "Читайте также" из нумерации */
#post-content .related-posts .section-title {
counter-increment: none !important;
display: block !important;
}
#post-content .related-posts .section-title::before {
content: none !important;
}
/* СКРОЛЛ-ФИКС */
#post-content h2, #post-content 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; }
.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); }
.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: 2.5rem; }
.meta-item { display: flex; align-items: center; gap: 0.6rem; color: #fff; font-size: 0.95rem; }
.meta-icon { width: 1.1rem; height: 1.1rem; color: #eac26e; }
.meta-author { color: #eac26e; font-weight: 700; }
/* GRID & STICKY SIDEBAR */
.article-body { padding: 4rem 0; 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: 768px) {
.article-hero { height: auto; min-height: 550px; }
.article-title { font-size: 1.75rem; }
.article-hero-bottom { flex-direction: column; align-items: flex-start; gap: 2rem; }
.article-actions { width: 100%; justify-content: space-between; }
.article-content-wrapper { padding: 2.5rem 1.5rem; }
}
</style>