Новые изменения в компоненты
This commit is contained in:
parent
faf02848ed
commit
735289481c
7 changed files with 151 additions and 3 deletions
|
|
@ -10,7 +10,7 @@ import solidJs from '@astrojs/solid-js';
|
|||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://avtourist-surgut.ru',
|
||||
integrations: [mdx(), icon(), sitemap({
|
||||
integrations: [mdx(), icon({ iconDir: 'src/icons' }), sitemap({
|
||||
filter: (page) => {
|
||||
const blockedPaths = ['/auth/', '/blog/search', '/404'];
|
||||
return !blockedPaths.some(path => page.includes(path));
|
||||
|
|
|
|||
42
frontend/src/components/blog/PostViewCounter.astro
Normal file
42
frontend/src/components/blog/PostViewCounter.astro
Normal 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>
|
||||
1
frontend/src/icons/eye.svg
Normal file
1
frontend/src/icons/eye.svg
Normal 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 |
|
|
@ -9,6 +9,7 @@ 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;
|
||||
|
|
@ -27,6 +28,7 @@ export interface Props {
|
|||
postUrl: string;
|
||||
initialLikes?: number;
|
||||
initialDislikes?: number;
|
||||
initialViews?: number;
|
||||
}
|
||||
|
||||
const {
|
||||
|
|
@ -45,7 +47,8 @@ const {
|
|||
postId,
|
||||
postUrl,
|
||||
initialLikes = 0,
|
||||
initialDislikes = 0
|
||||
initialDislikes = 0,
|
||||
initialViews = 0
|
||||
} = 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>
|
||||
{readmeTime} мин
|
||||
</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">
|
||||
|
|
@ -302,6 +309,33 @@ const {
|
|||
consultationBtn?.addEventListener("click", () => {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -170,3 +170,19 @@ export function getPostImageUrl(post: { image?: string }): string {
|
|||
}
|
||||
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;
|
||||
}
|
||||
53
frontend/src/pages/api/increment-views.ts
Normal file
53
frontend/src/pages/api/increment-views.ts
Normal 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' } }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -4,7 +4,7 @@ import { SITE_URL } from '@constants';
|
|||
import Comments from '@components/blog/comments/Comments.tsx';
|
||||
import RelatedPosts from '@components/blog/RelatedPosts.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';
|
||||
|
||||
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 views = await getPostViews(post.id).catch(() => 0);
|
||||
|
||||
// Конвертируем markdown в HTML
|
||||
const contentHtml = marked(post.content || '');
|
||||
|
|
@ -74,6 +75,7 @@ const heroImage = getPostImageUrl(post);
|
|||
postUrl={currentUrl}
|
||||
initialLikes={likes}
|
||||
initialDislikes={dislikes}
|
||||
initialViews={views}
|
||||
>
|
||||
<!-- Содержимое статьи в article -->
|
||||
<article class="article-content-wrapper" id="post-content">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue