ДобавлениеÐ Яндекс верификации
This commit is contained in:
parent
aa40fac43b
commit
2e892cccfb
17 changed files with 190 additions and 205 deletions
|
|
@ -3,7 +3,8 @@ import { MONTHS } from "@lib/constants";
|
||||||
import NumberedContent from "./NumberedContent.astro";
|
import NumberedContent from "./NumberedContent.astro";
|
||||||
import AuthorConsultation from "./AuthorConsultation.astro";
|
import AuthorConsultation from "./AuthorConsultation.astro";
|
||||||
|
|
||||||
export interface Props {
|
// Определяем интерфейс пропсов
|
||||||
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
excerpt: string;
|
excerpt: string;
|
||||||
author: {
|
author: {
|
||||||
|
|
@ -30,6 +31,7 @@ function formatDate(dateInput: string | Date): string {
|
||||||
return `${date.getDate()} ${MONTHS[date.getMonth()]} ${date.getFullYear()} года`;
|
return `${date.getDate()} ${MONTHS[date.getMonth()]} ${date.getFullYear()} года`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Деструктуризация пропсов через Astro.props с явной типизацией
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
excerpt,
|
excerpt,
|
||||||
|
|
@ -43,101 +45,101 @@ const {
|
||||||
postId,
|
postId,
|
||||||
likes = 0,
|
likes = 0,
|
||||||
dislikes = 0,
|
dislikes = 0,
|
||||||
} = Astro.props;
|
} = Astro.props as Props;
|
||||||
|
|
||||||
// дата поста
|
// Дата поста
|
||||||
const formattedPublishDate = typeof publishDate === "string"
|
const formattedPublishDate = typeof publishDate === "string"
|
||||||
? publishDate
|
? publishDate
|
||||||
: formatDate(publishDate);
|
: formatDate(publishDate);
|
||||||
---
|
---
|
||||||
|
|
||||||
<article class="min-h-screen bg-gray-50">
|
<article class="min-h-screen bg-gray-50">
|
||||||
{
|
{
|
||||||
featuredImage && (
|
featuredImage && (
|
||||||
<div class="relative w-full h-64 md:h-96 lg:h-125 overflow-hidden">
|
<div class="relative w-full h-64 md:h-96 lg:h-125 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src={featuredImage}
|
src={featuredImage}
|
||||||
alt={title}
|
alt={title}
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
{/* Затемнение для контраста текста */}
|
{/* Затемнение для контраста текста */}
|
||||||
<div class="absolute inset-0 bg-linear-to-t from-black/75 via-black/70 to-black/40" />
|
<div class="absolute inset-0 bg-linear-to-t from-black/75 via-black/70 to-black/40" />
|
||||||
|
|
||||||
{/* Декоративные элементы */}
|
{/* Декоративные элементы */}
|
||||||
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
|
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
|
||||||
{/* Золотая линия сверху */}
|
{/* Золотая линия сверху */}
|
||||||
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-24 h-1 bg-linear-to-r from-transparent via-[var(--color-gold)] to-transparent opacity-80"></div>
|
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-24 h-1 bg-linear-to-r from-transparent via-[var(--color-gold)] to-transparent opacity-80"></div>
|
||||||
<!-- Уголки -->
|
<!-- Уголки -->
|
||||||
<div class="absolute top-8 left-8 w-16 h-16 border-t-2 border-l-2 border-gold/40 rounded-tl-lg"></div>
|
<div class="absolute top-8 left-8 w-16 h-16 border-t-2 border-l-2 border-gold/40 rounded-tl-lg"></div>
|
||||||
<div class="absolute top-8 right-8 w-16 h-16 border-t-2 border-r-2 border-gold/40 rounded-tr-lg"></div>
|
<div class="absolute top-8 right-8 w-16 h-16 border-t-2 border-r-2 border-gold/40 rounded-tr-lg"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-12">
|
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-12">
|
||||||
<div class="max-w-4xl mx-auto relative z-10">
|
<div class="max-w-4xl mx-auto relative z-10">
|
||||||
<!-- Категория/тег -->
|
<!-- Категория/тег -->
|
||||||
<div class="flex items-center gap-2 mb-4">
|
<div class="flex items-center gap-2 mb-4">
|
||||||
<span class="px-4 py-1.5 bg-gold/90 backdrop-blur-sm text-white text-xs font-bold uppercase tracking-wider rounded-full shadow-lg">
|
<span class="px-4 py-1.5 bg-gold/90 backdrop-blur-sm text-white text-xs font-bold uppercase tracking-wider rounded-full shadow-lg">
|
||||||
Статья
|
Статья
|
||||||
</span>
|
</span>
|
||||||
<div class="h-px w-12 bg-linear-to-r from-[var(--color-gold)] to-transparent opacity-60"></div>
|
<div class="h-px w-12 bg-linear-to-r from-[var(--color-gold)] to-transparent opacity-60"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Заголовок -->
|
<!-- Заголовок -->
|
||||||
<h1 class="text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-extrabold text-white mb-6 leading-tight drop-shadow-2xl">
|
<h1 class="text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-extrabold text-white mb-6 leading-tight drop-shadow-2xl">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Декоративная линия под заголовком -->
|
<!-- Декоративная линия под заголовком -->
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="h-1 w-20 bg-linear-to-r from-[var(--color-gold)] to-[var(--color-gold)]/60 rounded-full"></div>
|
<div class="h-1 w-20 bg-linear-to-r from-[var(--color-gold)] to-[var(--color-gold)]/60 rounded-full"></div>
|
||||||
<div class="h-1 w-1 bg-[var(--color-gold)] rounded-full"></div>
|
<div class="h-1 w-1 bg-[var(--color-gold)] rounded-full"></div>
|
||||||
<div class="h-1 w-1 bg-[var(--color-gold)] rounded-full"></div>
|
<div class="h-1 w-1 bg-[var(--color-gold)] rounded-full"></div>
|
||||||
<div class="h-1 w-1 bg-[var(--color-gold)] rounded-full"></div>
|
<div class="h-1 w-1 bg-[var(--color-gold)] rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||||
<main class="lg:col-span-8">
|
<main class="lg:col-span-8">
|
||||||
{
|
{
|
||||||
!featuredImage && (
|
!featuredImage && (
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 mb-4 leading-tight">
|
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 mb-4 leading-tight">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl text-gray-600 leading-relaxed">{excerpt}</p>
|
<p class="text-xl text-gray-600 leading-relaxed">{excerpt}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 mb-8 pb-8 border-b border-gray-200">
|
<div class="flex flex-col gap-4 mb-8 pb-8 border-b border-gray-200">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<time class="flex items-center gap-1 text-sm text-gray-500">
|
<time class="flex items-center gap-1 text-sm text-gray-500">
|
||||||
<svg
|
<svg
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
><path
|
><path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
></path></svg
|
></path></svg
|
||||||
>
|
>
|
||||||
{formattedPublishDate}
|
{formattedPublishDate}
|
||||||
</time>
|
</time>
|
||||||
<span class="flex items-center gap-1 text-sm text-gray-500">
|
<span class="flex items-center gap-1 text-sm text-gray-500">
|
||||||
<svg
|
<svg
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
><path
|
><path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
|
|
@ -149,78 +151,78 @@ const formattedPublishDate = typeof publishDate === "string"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
tags.length > 0 && (
|
tags.length > 0 && (
|
||||||
<div class="flex flex-wrap gap-2 mb-8">
|
<div class="flex flex-wrap gap-2 mb-8">
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<span class="px-3 py-1 bg-blue-50 text-blue-600 text-sm font-medium rounded-full hover:bg-blue-100 transition-colors cursor-pointer">
|
<span class="px-3 py-1 bg-blue-50 text-blue-600 text-sm font-medium rounded-full hover:bg-blue-100 transition-colors cursor-pointer">
|
||||||
#{tag}
|
#{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
tableOfContents.length > 0 && (
|
tableOfContents.length > 0 && (
|
||||||
<details
|
<details
|
||||||
open
|
open
|
||||||
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-8 group"
|
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-8 group"
|
||||||
>
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer list-none font-semibold text-gray-900 text-lg select-none">
|
<summary class="flex items-center justify-between cursor-pointer list-none font-semibold text-gray-900 text-lg select-none">
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<svg
|
<svg
|
||||||
class="w-5 h-5 text-blue-500"
|
class="w-5 h-5 text-blue-500"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M4 6h16M4 12h16M4 18h7"
|
d="M4 6h16M4 12h16M4 18h7"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Содержание
|
Содержание
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
class="w-5 h-5 text-gray-400 transition-transform group-open:rotate-180"
|
class="w-5 h-5 text-gray-400 transition-transform group-open:rotate-180"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M19 9l-7 7-7-7"
|
d="M19 9l-7 7-7-7"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 pt-4 border-t border-gray-100">
|
<div class="mt-4 pt-4 border-t border-gray-100">
|
||||||
<ol class="space-y-2">
|
<ol class="space-y-2">
|
||||||
{tableOfContents.map((item, index) => (
|
{tableOfContents.map((item, index) => (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={`#${item.id}`}
|
href={`#${item.id}`}
|
||||||
class="flex items-start gap-2 text-gray-700 hover:text-blue-600 hover:bg-blue-50 p-2 rounded-lg transition-colors group/item"
|
class="flex items-start gap-2 text-gray-700 hover:text-blue-600 hover:bg-blue-50 p-2 rounded-lg transition-colors group/item"
|
||||||
>
|
>
|
||||||
<span class="shrink-0 w-6 h-6 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-sm font-medium group-hover/item:bg-blue-200">
|
<span class="shrink-0 w-6 h-6 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-sm font-medium group-hover/item:bg-blue-200">
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</span>
|
</span>
|
||||||
<span class="leading-tight">{item.title}</span>
|
<span class="leading-tight">{item.title}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- ТЕЛО СТАТЬИ -->
|
<!-- ТЕЛО СТАТЬИ -->
|
||||||
<div
|
<div
|
||||||
class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8 md:p-10"
|
class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8 md:p-10"
|
||||||
>
|
>
|
||||||
<NumberedContent tableOfContents={tableOfContents}>
|
<NumberedContent tableOfContents={tableOfContents}>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
@ -228,18 +230,18 @@ const formattedPublishDate = typeof publishDate === "string"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<blockquote
|
<blockquote
|
||||||
class="relative p-8 my-8 bg-linear-to-r from-blue-50 to-indigo-50 rounded-2xl border-l-4 border-blue-500"
|
class="relative p-8 my-8 bg-linear-to-r from-blue-50 to-indigo-50 rounded-2xl border-l-4 border-blue-500"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="absolute top-4 left-4 w-8 h-8 text-blue-200"
|
class="absolute top-4 left-4 w-8 h-8 text-blue-200"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
><path
|
><path
|
||||||
d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"
|
d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"
|
||||||
></path></svg
|
></path></svg
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
class="relative text-lg md:text-xl text-gray-800 font-medium italic pl-8"
|
class="relative text-lg md:text-xl text-gray-800 font-medium italic pl-8"
|
||||||
>
|
>
|
||||||
"Правовая грамотность — лучшая защита ваших интересов в любой
|
"Правовая грамотность — лучшая защита ваших интересов в любой
|
||||||
жизненной ситуации."
|
жизненной ситуации."
|
||||||
|
|
@ -249,28 +251,28 @@ const formattedPublishDate = typeof publishDate === "string"
|
||||||
<!-- Блок голосования -->
|
<!-- Блок голосования -->
|
||||||
<div class="mt-6 mb-8 flex justify-center">
|
<div class="mt-6 mb-8 flex justify-center">
|
||||||
<div
|
<div
|
||||||
class="post-likes-widget"
|
class="post-likes-widget"
|
||||||
data-post-id={postId}
|
data-post-id={postId}
|
||||||
data-initial-likes={likes}
|
data-initial-likes={likes}
|
||||||
data-initial-dislikes={dislikes}
|
data-initial-dislikes={dislikes}
|
||||||
>
|
>
|
||||||
<div class="vote-group">
|
<div class="vote-group">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="vote-btn like-btn"
|
class="vote-btn like-btn"
|
||||||
aria-label="Нравится"
|
aria-label="Нравится"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="thumb-icon"
|
class="thumb-icon"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -281,21 +283,21 @@ const formattedPublishDate = typeof publishDate === "string"
|
||||||
|
|
||||||
<div class="vote-group">
|
<div class="vote-group">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="vote-btn dislike-btn"
|
class="vote-btn dislike-btn"
|
||||||
aria-label="Не нравится"
|
aria-label="Не нравится"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="thumb-icon"
|
class="thumb-icon"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -304,49 +306,45 @@ const formattedPublishDate = typeof publishDate === "string"
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Инициализация виджета голосования
|
// Инициализация виджета голосования
|
||||||
(function() {
|
(function() {
|
||||||
const widgets = document.querySelectorAll('.post-likes-widget');
|
const widgets = document.querySelectorAll('.post-likes-widget');
|
||||||
|
|
||||||
widgets.forEach(widget => {
|
widgets.forEach(widget => {
|
||||||
const postId = widget.getAttribute('data-post-id');
|
const postId = widget.getAttribute('data-post-id');
|
||||||
const likeBtn = widget.querySelector('.like-btn');
|
const likeBtn = widget.querySelector('.like-btn');
|
||||||
const dislikeBtn = widget.querySelector('.dislike-btn');
|
const dislikeBtn = widget.querySelector('.dislike-btn');
|
||||||
const likeCount = widget.querySelector('.like-count');
|
const likeCount = widget.querySelector('.like-count');
|
||||||
const dislikeCount = widget.querySelector('.dislike-count');
|
const dislikeCount = widget.querySelector('.dislike-count');
|
||||||
|
|
||||||
let likes = parseInt(widget.getAttribute('data-initial-likes') || '0');
|
let likes = parseInt(widget.getAttribute('data-initial-likes') || '0');
|
||||||
let dislikes = parseInt(widget.getAttribute('data-initial-dislikes') || '0');
|
let dislikes = parseInt(widget.getAttribute('data-initial-dislikes') || '0');
|
||||||
let userVote = null;
|
let userVote: string | null = null;
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
let isDisabled = false; // Блокировка кнопок
|
let isDisabled = false;
|
||||||
|
|
||||||
console.log('[PostLikes] Инициализация:', { postId, likes, dislikes });
|
console.log('[PostLikes] Инициализация:', { postId, likes, dislikes });
|
||||||
|
|
||||||
// Блокировка/разблокировка кнопок
|
function setButtonsDisabled(disabled: boolean) {
|
||||||
function setButtonsDisabled(disabled) {
|
|
||||||
isDisabled = disabled;
|
isDisabled = disabled;
|
||||||
if (likeBtn) likeBtn.disabled = disabled;
|
if (likeBtn) (likeBtn as HTMLButtonElement).disabled = disabled;
|
||||||
if (dislikeBtn) dislikeBtn.disabled = disabled;
|
if (dislikeBtn) (dislikeBtn as HTMLButtonElement).disabled = disabled;
|
||||||
widget.style.opacity = disabled ? '0.6' : '1';
|
(widget as HTMLElement).style.opacity = disabled ? '0.6' : '1';
|
||||||
widget.style.pointerEvents = disabled ? 'none' : 'auto';
|
(widget as HTMLElement).style.pointerEvents = disabled ? 'none' : 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка реальных данных
|
|
||||||
async function loadVotes() {
|
async function loadVotes() {
|
||||||
if (!postId) return;
|
if (!postId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/likes?post_id=${encodeURIComponent(postId)}`);
|
const response = await fetch(`/api/likes?post_id=${encodeURIComponent(postId)}`);
|
||||||
console.log('[PostLikes] Ответ API:', response.status);
|
console.log('[PostLikes] Ответ API:', response.status);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json() as { likes: number; dislikes: number; userVote: string | null };
|
||||||
console.log('[PostLikes] Данные:', data);
|
console.log('[PostLikes] Данные:', data);
|
||||||
|
|
||||||
likes = data.likes || 0;
|
likes = data.likes || 0;
|
||||||
dislikes = data.dislikes || 0;
|
dislikes = data.dislikes || 0;
|
||||||
userVote = data.userVote;
|
userVote = data.userVote;
|
||||||
|
|
@ -356,24 +354,20 @@ const formattedPublishDate = typeof publishDate === "string"
|
||||||
console.error('[PostLikes] Ошибка загрузки:', error);
|
console.error('[PostLikes] Ошибка загрузки:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Голосование
|
async function handleVote(action: 'like' | 'dislike') {
|
||||||
async function handleVote(action) {
|
|
||||||
// Двойная проверка блокировки
|
|
||||||
if (isLoading || isDisabled) {
|
if (isLoading || isDisabled) {
|
||||||
console.log('[PostLikes] Заблокировано, пропускаем');
|
console.log('[PostLikes] Заблокировано, пропускаем');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[PostLikes] Голосование:', action);
|
console.log('[PostLikes] Голосование:', action);
|
||||||
|
|
||||||
if (!postId) return;
|
if (!postId) return;
|
||||||
|
|
||||||
// Блокируем кнопки СРАЗУ
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
setButtonsDisabled(true);
|
setButtonsDisabled(true);
|
||||||
|
|
||||||
// Оптимистичное обновление
|
|
||||||
if (action === 'like') {
|
if (action === 'like') {
|
||||||
if (userVote === 'like') {
|
if (userVote === 'like') {
|
||||||
likes = Math.max(0, likes - 1);
|
likes = Math.max(0, likes - 1);
|
||||||
|
|
@ -400,7 +394,7 @@ const formattedPublishDate = typeof publishDate === "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/likes', {
|
const response = await fetch('/api/likes', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -408,43 +402,39 @@ const formattedPublishDate = typeof publishDate === "string"
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify({ post_id: postId, vote_type: action }),
|
body: JSON.stringify({ post_id: postId, vote_type: action }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json() as { error?: string; likes?: number; dislikes?: number; userVote?: string | null };
|
||||||
console.log('[PostLikes] Ответ сервера:', response.status, data);
|
console.log('[PostLikes] Ответ сервера:', response.status, data);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
alert('Для голосования необходимо войти в систему');
|
alert('Для голосования необходимо войти в систему');
|
||||||
loadVotes(); // Откат
|
loadVotes();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error(data.error || 'Ошибка');
|
throw new Error(data.error || 'Ошибка');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем реальными данными с сервера
|
|
||||||
likes = data.likes || 0;
|
likes = data.likes || 0;
|
||||||
dislikes = data.dislikes || 0;
|
dislikes = data.dislikes || 0;
|
||||||
userVote = data.userVote;
|
userVote = data.userVote || null;
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|
||||||
console.log('[PostLikes] Успешно:', { likes, dislikes, userVote });
|
console.log('[PostLikes] Успешно:', { likes, dislikes, userVote });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[PostLikes] Ошибка:', error);
|
console.error('[PostLikes] Ошибка:', error);
|
||||||
alert('Не удалось сохранить голос');
|
alert('Не удалось сохранить голос');
|
||||||
loadVotes(); // Откат
|
loadVotes();
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
// НЕ разблокируем кнопки - голосование окончено
|
|
||||||
// Кнопки остаются заблокированными для повторного голосования
|
|
||||||
console.log('[PostLikes] Голосование завершено');
|
console.log('[PostLikes] Голосование завершено');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление UI
|
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
if (likeCount) likeCount.textContent = likes.toString();
|
if (likeCount) likeCount.textContent = likes.toString();
|
||||||
if (dislikeCount) dislikeCount.textContent = dislikes.toString();
|
if (dislikeCount) dislikeCount.textContent = dislikes.toString();
|
||||||
|
|
||||||
if (likeBtn && dislikeBtn) {
|
if (likeBtn && dislikeBtn) {
|
||||||
if (userVote === 'like') {
|
if (userVote === 'like') {
|
||||||
likeBtn.classList.add('active');
|
likeBtn.classList.add('active');
|
||||||
|
|
@ -458,21 +448,16 @@ const formattedPublishDate = typeof publishDate === "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработчики
|
|
||||||
likeBtn?.addEventListener('click', () => handleVote('like'));
|
likeBtn?.addEventListener('click', () => handleVote('like'));
|
||||||
dislikeBtn?.addEventListener('click', () => handleVote('dislike'));
|
dislikeBtn?.addEventListener('click', () => handleVote('dislike'));
|
||||||
|
|
||||||
// Загружаем реальные данные
|
|
||||||
loadVotes();
|
loadVotes();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Боковой блок -->
|
|
||||||
<aside class="lg:col-span-4">
|
<aside class="lg:col-span-4">
|
||||||
<AuthorConsultation author={author} />
|
<AuthorConsultation author={author} />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
@ -517,7 +502,6 @@ const formattedPublishDate = typeof publishDate === "string"
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Лайк */
|
|
||||||
.vote-btn.like-btn:hover {
|
.vote-btn.like-btn:hover {
|
||||||
border-color: #86efac;
|
border-color: #86efac;
|
||||||
background: #f0fdf4;
|
background: #f0fdf4;
|
||||||
|
|
@ -530,7 +514,6 @@ const formattedPublishDate = typeof publishDate === "string"
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Дизлайк */
|
|
||||||
.vote-btn.dislike-btn:hover {
|
.vote-btn.dislike-btn:hover {
|
||||||
border-color: #fca5a5;
|
border-color: #fca5a5;
|
||||||
background: #fef2f2;
|
background: #fef2f2;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ interface Props {
|
||||||
pagePath?: string;
|
pagePath?: string;
|
||||||
blogPostTitle?: string;
|
blogPostTitle?: string;
|
||||||
bodyClass?: string;
|
bodyClass?: string;
|
||||||
noContainer?: boolean; // Новый проп для отключения контейнера
|
noContainer?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = Astro.props || {};
|
const props = Astro.props || {};
|
||||||
|
|
@ -46,6 +46,8 @@ if (Astro.request) {
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
{description ? <meta name="description" content={description} /> : null}
|
{description ? <meta name="description" content={description} /> : null}
|
||||||
{canonicalLink ? <link rel="canonical" href={canonicalLink} /> : null}
|
{canonicalLink ? <link rel="canonical" href={canonicalLink} /> : null}
|
||||||
|
<!-- yandex verification -->
|
||||||
|
<meta name="yandex-verification" content="b10d32bf1e46a882" />
|
||||||
</head>
|
</head>
|
||||||
<body class={`${props.bodyClass || "bg-gray-50"}`}>
|
<body class={`${props.bodyClass || "bg-gray-50"}`}>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue