Новые изменения в компоненты

This commit is contained in:
Web-serfer 2026-04-26 23:47:23 +05:00
parent faf02848ed
commit 735289481c
7 changed files with 151 additions and 3 deletions

View file

@ -10,7 +10,7 @@ import solidJs from '@astrojs/solid-js';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: 'https://avtourist-surgut.ru', site: 'https://avtourist-surgut.ru',
integrations: [mdx(), icon(), sitemap({ integrations: [mdx(), icon({ iconDir: 'src/icons' }), sitemap({
filter: (page) => { filter: (page) => {
const blockedPaths = ['/auth/', '/blog/search', '/404']; const blockedPaths = ['/auth/', '/blog/search', '/404'];
return !blockedPaths.some(path => page.includes(path)); return !blockedPaths.some(path => page.includes(path));

View file

@ -0,0 +1,42 @@
---
import { Icon } from 'astro-icon/components';
interface Props {
views: number;
className?: string;
}
const { views, className = '' } = Astro.props;
const formatViews = (n: number) => {
if (n >= 1000) {
return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
}
return n.toString();
};
---
<div class={`post-view-counter ${className}`}>
<Icon name="eye" class="view-icon" />
<span class="view-count">{formatViews(views)}</span>
</div>
<style>
.post-view-counter {
display: flex;
align-items: center;
gap: 0.5rem;
}
.view-icon {
width: 1.1rem;
height: 1.1rem;
color: #eac26e;
}
.view-count {
color: #ffffff;
font-size: 0.95rem;
font-weight: 600;
}
</style>

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5M12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5s5 2.24 5 5s-2.24 5-5 5m0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3s3-1.34 3-3s-1.34-3-3-3"/></svg>

After

Width:  |  Height:  |  Size: 467 B

View file

@ -9,6 +9,7 @@ import ConsultationModal from "@components/base/ConsultationModal.astro";
import Toast from "@components/base/Toast.astro"; import Toast from "@components/base/Toast.astro";
import PostSocialShare from "@components/blog/PostSocialShare.astro"; import PostSocialShare from "@components/blog/PostSocialShare.astro";
import PostReactionButtons from "@components/blog/PostReactionButtons.astro"; import PostReactionButtons from "@components/blog/PostReactionButtons.astro";
import PostViewCounter from "@components/blog/PostViewCounter.astro";
export interface Props { export interface Props {
title: string; title: string;
@ -27,6 +28,7 @@ export interface Props {
postUrl: string; postUrl: string;
initialLikes?: number; initialLikes?: number;
initialDislikes?: number; initialDislikes?: number;
initialViews?: number;
} }
const { const {
@ -45,7 +47,8 @@ const {
postId, postId,
postUrl, postUrl,
initialLikes = 0, initialLikes = 0,
initialDislikes = 0 initialDislikes = 0,
initialViews = 0
} = Astro.props; } = Astro.props;
--- ---
@ -104,6 +107,10 @@ const {
<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> <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>
{readmeTime} мин {readmeTime} мин
</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"><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>
<div class="article-actions"> <div class="article-actions">
@ -302,6 +309,33 @@ const {
consultationBtn?.addEventListener("click", () => { consultationBtn?.addEventListener("click", () => {
window.dispatchEvent(new CustomEvent("open-modal", { detail: "consultation-modal" })); window.dispatchEvent(new CustomEvent("open-modal", { detail: "consultation-modal" }));
}); });
// 5. СЧЁТЧИК ПРОСМОТРОВ
const viewsEl = document.querySelector('.meta-views') as HTMLElement & { dataset: { postId: string } };
if (viewsEl?.dataset?.postId) {
const postId = viewsEl.dataset.postId;
const hasViewed = sessionStorage.getItem(`viewed_${postId}`);
if (!hasViewed) {
fetch(`/api/increment-views?postId=${postId}`, { method: 'POST' })
.then(res => res.json())
.then(data => {
if (data.views !== undefined) {
viewsEl.textContent = formatViews(data.views);
}
})
.catch(() => {});
sessionStorage.setItem(`viewed_${postId}`, 'true');
}
}
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("DOMContentLoaded", setupArticleLogic);

View file

@ -169,4 +169,20 @@ export function getPostImageUrl(post: { image?: string }): string {
return fileUrl; return fileUrl;
} }
return '/images/blog/default.avif'; return '/images/blog/default.avif';
}
export async function getPostViews(postId: string): Promise<number> {
try {
const post = await pb.collection('posts').getOne(postId);
return post.views || 0;
} catch {
return 0;
}
}
export async function incrementPostViews(postId: string): Promise<number> {
const post = await pb.collection('posts').getOne(postId);
const newViews = (post.views || 0) + 1;
await pb.collection('posts').update(postId, { views: newViews });
return newViews;
} }

View file

@ -0,0 +1,53 @@
import type { APIRoute } from 'astro';
const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
const POCKETBASE_ID_REGEX = /^[a-z0-9]{15}$/;
export const POST: APIRoute = async ({ url }) => {
try {
const postId = url.searchParams.get('postId');
if (!postId || !POCKETBASE_ID_REGEX.test(postId)) {
return new Response(
JSON.stringify({ error: 'Некорректный postId' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const postRes = await fetch(
`${POCKETBASE_URL}/api/collections/posts/records/${postId}`,
);
if (!postRes.ok) {
return new Response(
JSON.stringify({ error: 'Пост не найден' }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
);
}
const post = await postRes.json();
const newViews = (post.views || 0) + 1;
await fetch(
`${POCKETBASE_URL}/api/collections/posts/records/${postId}`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ views: newViews }),
}
);
return new Response(
JSON.stringify({ views: newViews }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('[Increment Views API] Error:', error);
return new Response(
JSON.stringify({ error: 'Внутренняя ошибка сервера' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};

View file

@ -4,7 +4,7 @@ import { SITE_URL } from '@constants';
import Comments from '@components/blog/comments/Comments.tsx'; import Comments from '@components/blog/comments/Comments.tsx';
import RelatedPosts from '@components/blog/RelatedPosts.astro'; import RelatedPosts from '@components/blog/RelatedPosts.astro';
import ArticleTableOfContents from '@components/blog/ArticleTableOfContents.astro'; import ArticleTableOfContents from '@components/blog/ArticleTableOfContents.astro';
import { getPostBySlug, getPosts, getPostImageUrl, getPostVotesStats } from '@lib/pb'; import { getPostBySlug, getPosts, getPostImageUrl, getPostVotesStats, getPostViews } from '@lib/pb';
import { marked } from 'marked'; import { marked } from 'marked';
export const prerender = false; export const prerender = false;
@ -22,6 +22,7 @@ if (!post) {
} }
const { likes = 0, dislikes = 0 } = await getPostVotesStats(post.id).catch(() => ({ likes: 0, dislikes: 0 })); const { likes = 0, dislikes = 0 } = await getPostVotesStats(post.id).catch(() => ({ likes: 0, dislikes: 0 }));
const views = await getPostViews(post.id).catch(() => 0);
// Конвертируем markdown в HTML // Конвертируем markdown в HTML
const contentHtml = marked(post.content || ''); const contentHtml = marked(post.content || '');
@ -74,6 +75,7 @@ const heroImage = getPostImageUrl(post);
postUrl={currentUrl} postUrl={currentUrl}
initialLikes={likes} initialLikes={likes}
initialDislikes={dislikes} initialDislikes={dislikes}
initialViews={views}
> >
<!-- Содержимое статьи в article --> <!-- Содержимое статьи в article -->
<article class="article-content-wrapper" id="post-content"> <article class="article-content-wrapper" id="post-content">