Насторил работу блога через Backend
This commit is contained in:
parent
014439d565
commit
edd730b438
33 changed files with 1019 additions and 200 deletions
|
|
@ -1,26 +1,5 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
|
||||
// Сортируем и форматируем для поиска
|
||||
const searchData = posts
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
||||
.map(post => ({
|
||||
title: post.data.title,
|
||||
description: post.data.description,
|
||||
slug: post.id,
|
||||
category: post.data.category,
|
||||
categoryColor: post.data.categoryColor,
|
||||
date: post.data.date.toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
}));
|
||||
|
||||
const title = 'Поиск по статьям';
|
||||
const postsJson = JSON.stringify(searchData);
|
||||
---
|
||||
|
||||
<div id="search-modal" class="modal-overlay" aria-hidden="true">
|
||||
|
|
@ -61,16 +40,33 @@ const postsJson = JSON.stringify(searchData);
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script define:vars={{ postsJson }}>
|
||||
<script>
|
||||
(function() {
|
||||
const modal = document.getElementById('search-modal');
|
||||
const closeBtn = document.getElementById('search-modal-close-btn');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const resultsContainer = document.getElementById('search-results');
|
||||
const posts = JSON.parse(postsJson);
|
||||
let postsCache = null;
|
||||
|
||||
if (!modal || !searchInput || !resultsContainer) return;
|
||||
|
||||
async function fetchPosts() {
|
||||
if (postsCache) return postsCache;
|
||||
try {
|
||||
const res = await fetch('/api/posts?per_page=100');
|
||||
const data = await res.json();
|
||||
postsCache = data.posts || [];
|
||||
return postsCache;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
modal.classList.add('active');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
|
|
@ -86,7 +82,7 @@ const postsJson = JSON.stringify(searchData);
|
|||
resultsContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
function handleSearch(query) {
|
||||
async function handleSearch(query) {
|
||||
const trimmed = query.trim().toLowerCase();
|
||||
|
||||
if (trimmed.length < 2) {
|
||||
|
|
@ -94,6 +90,8 @@ const postsJson = JSON.stringify(searchData);
|
|||
return;
|
||||
}
|
||||
|
||||
const posts = await fetchPosts();
|
||||
|
||||
const filtered = posts.filter(post =>
|
||||
post.title.toLowerCase().includes(trimmed) ||
|
||||
post.description.toLowerCase().includes(trimmed) ||
|
||||
|
|
@ -112,10 +110,9 @@ const postsJson = JSON.stringify(searchData);
|
|||
return;
|
||||
}
|
||||
|
||||
// вывод динамического списка статей
|
||||
resultsContainer.innerHTML = filtered.map(post => `
|
||||
<a href="/blog/${post.id}" class="search-result-item">
|
||||
<span class="result-date">${post.date}</span>
|
||||
<a href="/blog/${post.slug}" class="search-result-item">
|
||||
<span class="result-date">${formatDate(post.date)}</span>
|
||||
<h4 class="result-title">${post.title}</h4>
|
||||
<p class="result-description">${post.description.substring(0, 120)}...</p>
|
||||
<span class="result-arrow">
|
||||
|
|
|
|||
|
|
@ -1,29 +1,28 @@
|
|||
---
|
||||
import BlogCard from '@components/blog/BlogCard.astro';
|
||||
|
||||
interface CollectionEntry {
|
||||
interface Post {
|
||||
id: string;
|
||||
data: {
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
categoryColor: string;
|
||||
date: Date;
|
||||
readTime: string;
|
||||
imageUrl: string;
|
||||
};
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
categoryColor: string;
|
||||
date: string;
|
||||
readTime: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
posts: CollectionEntry[];
|
||||
posts: Post[];
|
||||
currentSlug?: string;
|
||||
}
|
||||
|
||||
const { posts, currentSlug } = Astro.props;
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
|
|
@ -32,7 +31,7 @@ const formatDate = (date: Date) => {
|
|||
|
||||
// Фильтруем текущую статью
|
||||
const filteredPosts = currentSlug
|
||||
? posts.filter(post => post.id !== currentSlug).slice(0, 3)
|
||||
? posts.filter(post => post.slug !== currentSlug).slice(0, 3)
|
||||
: posts.slice(0, 3);
|
||||
---
|
||||
|
||||
|
|
@ -42,14 +41,14 @@ const filteredPosts = currentSlug
|
|||
<div class="related-grid">
|
||||
{filteredPosts.map((post) => (
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
category={post.data.category}
|
||||
categoryColor={post.data.categoryColor}
|
||||
date={formatDate(post.data.date)}
|
||||
readTime={post.data.readTime}
|
||||
imageUrl={post.data.imageUrl}
|
||||
slug={`/blog/${post.id}`}
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
category={post.category}
|
||||
categoryColor={post.categoryColor}
|
||||
date={formatDate(post.date)}
|
||||
readTime={post.readTime}
|
||||
imageUrl={post.imageUrl}
|
||||
slug={`/blog/${post.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
74
frontend/src/lib/pb.ts
Normal file
74
frontend/src/lib/pb.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const PB_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||
|
||||
export const pb = new PocketBase(PB_URL);
|
||||
|
||||
export interface Post {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
author: string;
|
||||
category: string;
|
||||
categoryColor: string;
|
||||
date: string;
|
||||
readTime: string;
|
||||
imageUrl: string;
|
||||
content?: string;
|
||||
draft: boolean;
|
||||
}
|
||||
|
||||
export async function getPosts(options?: {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
category?: string;
|
||||
search?: string;
|
||||
}): Promise<{ posts: Post[]; total: number; page: number; totalPages: number }> {
|
||||
const page = options?.page || 1;
|
||||
const perPage = options?.perPage || 10;
|
||||
|
||||
const filter: string[] = ['draft = false'];
|
||||
|
||||
if (options?.category && options.category !== 'Все') {
|
||||
filter.push(`category = "${options.category}"`);
|
||||
}
|
||||
|
||||
if (options?.search) {
|
||||
filter.push(`(title ~ "${options.search}" || description ~ "${options.search}")`);
|
||||
}
|
||||
|
||||
const result = await pb.collection('posts').getList(page, perPage, {
|
||||
filter: filter.join(' && '),
|
||||
sort: '-date',
|
||||
});
|
||||
|
||||
return {
|
||||
posts: (result.items || []) as unknown as Post[],
|
||||
total: result.totalItems || 0,
|
||||
page: (result.pageInfo?.page || 1),
|
||||
totalPages: result.totalPages || 1,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPostBySlug(slug: string): Promise<Post | null> {
|
||||
const result = await pb.collection('posts').getList(1, 1, {
|
||||
filter: `slug="${slug}" && draft = false`,
|
||||
});
|
||||
|
||||
if (!result.items || result.totalItems === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.items[0] as unknown as Post;
|
||||
}
|
||||
|
||||
export async function getAllCategories(): Promise<string[]> {
|
||||
const result = await pb.collection('posts').getFullList({
|
||||
filter: 'draft = false',
|
||||
fields: 'category',
|
||||
});
|
||||
|
||||
const categories = (result || []).map((post: any) => post.category).filter(Boolean);
|
||||
return ['Все', ...new Set(categories)];
|
||||
}
|
||||
45
frontend/src/pages/api/auth/sign-in.ts
Normal file
45
frontend/src/pages/api/auth/sign-in.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const POST: APIRoute = async ({ request, cookies }) => {
|
||||
try {
|
||||
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
|
||||
const data = await request.json();
|
||||
|
||||
const { email, password } = data;
|
||||
|
||||
if (!email || !password) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Email и пароль обязательны'
|
||||
}), { status: 400 });
|
||||
}
|
||||
|
||||
const authRecord = await pb.collection('users').authWithPassword(email, password);
|
||||
|
||||
cookies.set('pb_auth', JSON.stringify(pb.authStore.exportToCookie()), {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
user: {
|
||||
id: authRecord.id,
|
||||
name: authRecord.name,
|
||||
email: authRecord.email,
|
||||
}
|
||||
}), { status: 200 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Sign in error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || 'Неверный email или пароль'
|
||||
}), { status: 401 });
|
||||
}
|
||||
};
|
||||
10
frontend/src/pages/api/auth/sign-out.ts
Normal file
10
frontend/src/pages/api/auth/sign-out.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
|
||||
export const POST: APIRoute = async ({ cookies }) => {
|
||||
cookies.delete('pb_auth', { path: '/' });
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Вы успешно вышли из аккаунта'
|
||||
}), { status: 200 });
|
||||
};
|
||||
45
frontend/src/pages/api/auth/sign-up.ts
Normal file
45
frontend/src/pages/api/auth/sign-up.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const POST: APIRoute = async ({ request, redirect }) => {
|
||||
try {
|
||||
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
|
||||
const data = await request.json();
|
||||
|
||||
const { name, email, phone, password } = data;
|
||||
|
||||
if (!email || !password) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Email и пароль обязательны'
|
||||
}), { status: 400 });
|
||||
}
|
||||
|
||||
const record = await pb.collection('users').create({
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
});
|
||||
|
||||
await pb.collection('users').authWithPassword(email, password);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
record: {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
email: record.email,
|
||||
}
|
||||
}), { status: 201 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Sign up error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || 'Ошибка при регистрации'
|
||||
}), { status: 400 });
|
||||
}
|
||||
};
|
||||
40
frontend/src/pages/api/consultation.ts
Normal file
40
frontend/src/pages/api/consultation.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
|
||||
const data = await request.json();
|
||||
|
||||
const { name, phone, service } = data;
|
||||
|
||||
if (!name || !phone) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Имя и телефон обязательны'
|
||||
}), { status: 400 });
|
||||
}
|
||||
|
||||
const record = await pb.collection('consultations').create({
|
||||
name,
|
||||
phone,
|
||||
service: service || '',
|
||||
status: 'new',
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Заявка отправлена! Мы свяжемся с вами в течение 15 минут.',
|
||||
id: record.id
|
||||
}), { status: 201 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Consultation error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || 'Ошибка при отправке заявки'
|
||||
}), { status: 400 });
|
||||
}
|
||||
};
|
||||
48
frontend/src/pages/api/posts/[slug].ts
Normal file
48
frontend/src/pages/api/posts/[slug].ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const GET: APIRoute = async ({ params }) => {
|
||||
try {
|
||||
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
|
||||
const { slug } = params;
|
||||
|
||||
if (!slug) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'slug обязателен'
|
||||
}), { status: 400 });
|
||||
}
|
||||
|
||||
const result = await pb.collection('posts').getList(1, 1, {
|
||||
filter: `slug="${slug}" && draft = false`,
|
||||
});
|
||||
|
||||
if (result.totalItems === 0) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Пост не найден'
|
||||
}), { status: 404 });
|
||||
}
|
||||
|
||||
const post = result.items[0];
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
id: post.id,
|
||||
slug: post.slug,
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
author: post.author,
|
||||
category: post.category,
|
||||
categoryColor: post.categoryColor,
|
||||
date: post.date,
|
||||
readTime: post.readTime,
|
||||
imageUrl: post.imageUrl,
|
||||
content: post.content,
|
||||
}), { status: 200 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('API post error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
error: error.message || 'Ошибка при получении поста'
|
||||
}), { status: 500 });
|
||||
}
|
||||
};
|
||||
54
frontend/src/pages/api/posts/index.ts
Normal file
54
frontend/src/pages/api/posts/index.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const GET: APIRoute = async ({ url }) => {
|
||||
try {
|
||||
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
|
||||
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const perPage = parseInt(url.searchParams.get('per_page') || '10');
|
||||
const category = url.searchParams.get('category');
|
||||
const search = url.searchParams.get('search');
|
||||
|
||||
const filter: string[] = ['draft = false'];
|
||||
|
||||
if (category) {
|
||||
filter.push(`category = "${category}"`);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
filter.push(`(title ~ "${search}" || description ~ "${search}")`);
|
||||
}
|
||||
|
||||
const result = await pb.collection('posts').getList(page, perPage, {
|
||||
filter: filter.join(' && '),
|
||||
sort: '-date',
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
posts: result.items.map(post => ({
|
||||
id: post.id,
|
||||
slug: post.slug,
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
author: post.author,
|
||||
category: post.category,
|
||||
categoryColor: post.categoryColor,
|
||||
date: post.date,
|
||||
readTime: post.readTime,
|
||||
imageUrl: post.imageUrl,
|
||||
})),
|
||||
total: result.totalItems,
|
||||
page: result.pageInfo.page,
|
||||
perPage: result.pageInfo.perPage,
|
||||
totalPages: result.totalPages,
|
||||
}), { status: 200 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('API posts error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
error: error.message || 'Ошибка при получении постов'
|
||||
}), { status: 500 });
|
||||
}
|
||||
};
|
||||
|
|
@ -4,16 +4,10 @@ import { SITE_URL } from '@constants';
|
|||
import PostCommentForm from '@components/blog/PostCommentForm.astro';
|
||||
import RelatedPosts from '@components/blog/RelatedPosts.astro';
|
||||
import ArticleTableOfContents from '@components/blog/ArticleTableOfContents.astro';
|
||||
import { getCollection, getEntry, render } from 'astro:content';
|
||||
import { getPostBySlug, getPosts } from '@lib/pb';
|
||||
import { marked } from 'marked';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog') as { id: string; data: Record<string, any> }[];
|
||||
return posts.map((post: { id: string }) => ({
|
||||
params: { slug: post.id },
|
||||
}));
|
||||
}
|
||||
export const prerender = false;
|
||||
|
||||
const slug = Astro.params.slug;
|
||||
|
||||
|
|
@ -21,87 +15,77 @@ if (!slug) {
|
|||
return Astro.redirect('/blog');
|
||||
}
|
||||
|
||||
const post = await getEntry('blog', slug);
|
||||
const post = await getPostBySlug(slug);
|
||||
|
||||
if (!post) {
|
||||
return Astro.redirect('/blog');
|
||||
}
|
||||
|
||||
const { Content } = await render(post);
|
||||
// Конвертируем markdown в HTML
|
||||
const contentHtml = marked(post.content || '');
|
||||
|
||||
// Извлекаем заголовки из MDX тела для оглавления
|
||||
const body = post.body || '';
|
||||
// Извлекаем заголовки для оглавления
|
||||
const headingRegex = /^(#{2,3})\s+(.+)$/gm;
|
||||
const tocItems: { id: string; text: string; level: number }[] = [];
|
||||
const body = post.content || '';
|
||||
let match;
|
||||
let headingIndex = 0;
|
||||
while ((match = headingRegex.exec(body)) !== null) {
|
||||
const level = match[1].length;
|
||||
const text = match[2].trim();
|
||||
// Генерируем ID из текста заголовка
|
||||
const id = text.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-');
|
||||
tocItems.push({ level, id: `heading-${headingIndex++}`, text });
|
||||
}
|
||||
|
||||
console.log('=== TOC ITEMS ===', tocItems);
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Логика авторизации (пока статичная переменная)
|
||||
const isAuthorized = false;
|
||||
// Для related posts берем те же категории
|
||||
const { posts: relatedPosts } = await getPosts({ perPage: 4, category: post.category });
|
||||
const filteredRelated = relatedPosts.filter(p => p.slug !== slug).slice(0, 3);
|
||||
|
||||
// URL текущей страницы
|
||||
const currentUrl = `${SITE_URL}/blog/${post.id}`;
|
||||
|
||||
// Получаем все посты для блока "Читайте также"
|
||||
const allPosts = await getCollection('blog');
|
||||
const currentUrl = `${SITE_URL}/blog/${slug}`;
|
||||
---
|
||||
|
||||
<ArticleLayout
|
||||
title={`${post.data.title} — автоюрист в Сургуте`}
|
||||
description={post.data.description}
|
||||
canonicalLink={`${SITE_URL}/blog/${post.id}`}
|
||||
title={`${post.title} — автоюрист в Сургуте`}
|
||||
description={post.description}
|
||||
canonicalLink={`${SITE_URL}/blog/${slug}`}
|
||||
breadcrumbs={[
|
||||
{ label: 'Главная', href: '/' },
|
||||
{ label: 'Блог', href: '/blog' },
|
||||
{ label: post.data.title }
|
||||
{ label: post.title }
|
||||
]}
|
||||
heroImage={post.data.imageUrl}
|
||||
heroAlt={post.data.title}
|
||||
category={post.data.category}
|
||||
postTitle={post.data.title}
|
||||
date={formatDate(post.data.date)}
|
||||
author={post.data.author}
|
||||
readTime={post.data.readTime}
|
||||
heroImage={post.imageUrl}
|
||||
heroAlt={post.title}
|
||||
category={post.category}
|
||||
postTitle={post.title}
|
||||
date={formatDate(post.date)}
|
||||
author={post.author}
|
||||
readTime={post.readTime}
|
||||
postId={post.id}
|
||||
postUrl={currentUrl}
|
||||
initialLikes={12}
|
||||
initialDislikes={2}
|
||||
>
|
||||
<!-- Содержимое статьи -->
|
||||
<div class="post-content">
|
||||
<Content />
|
||||
</div>
|
||||
<div class="post-content" set:html={contentHtml} />
|
||||
|
||||
<!-- Форма комментариев -->
|
||||
<PostCommentForm
|
||||
postId={post.id}
|
||||
isAuthorized={isAuthorized}
|
||||
isAuthorized={false}
|
||||
/>
|
||||
|
||||
<!-- Похожие статьи -->
|
||||
<RelatedPosts
|
||||
posts={allPosts}
|
||||
currentSlug={post.id}
|
||||
posts={filteredRelated}
|
||||
currentSlug={slug}
|
||||
/>
|
||||
|
||||
<!-- Оглавление в сайдбаре -->
|
||||
|
|
@ -168,4 +152,4 @@ const allPosts = await getCollection('blog');
|
|||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -7,32 +7,21 @@ import BlogCard from '@components/blog/BlogCard.astro';
|
|||
import Pagination from '@components/base/Pagination.astro';
|
||||
import CTA from '@components/base/CTA.astro';
|
||||
import SearchModal from '@components/base/SearchModal.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
|
||||
// Сортируем посты по дате (новые сверху)
|
||||
const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
import { getPosts, getAllCategories } from '@lib/pb';
|
||||
|
||||
const POSTS_PER_PAGE = 6;
|
||||
const currentPage = 1;
|
||||
const totalPages = Math.ceil(sortedPosts.length / POSTS_PER_PAGE);
|
||||
|
||||
const startIndex = 0;
|
||||
const endIndex = POSTS_PER_PAGE;
|
||||
const paginatedPosts = sortedPosts.slice(startIndex, endIndex);
|
||||
const { posts, total, totalPages } = await getPosts({ page: currentPage, perPage: POSTS_PER_PAGE });
|
||||
const categories = await getAllCategories();
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Категории
|
||||
const categories = ['Все', ...new Set(posts.map((post: any) => post.data.category))];
|
||||
---
|
||||
|
||||
<Layout
|
||||
|
|
@ -66,17 +55,17 @@ const categories = ['Все', ...new Set(posts.map((post: any) => post.data.cate
|
|||
<section class="blog-grid-section">
|
||||
<div class="site-container">
|
||||
<div class="blog-grid" id="blog-grid">
|
||||
{paginatedPosts.map((post: any) => (
|
||||
<article class="blog-card-wrapper" data-category={post.data.category}>
|
||||
{posts.map((post: any) => (
|
||||
<article class="blog-card-wrapper" data-category={post.category}>
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
category={post.data.category}
|
||||
categoryColor={post.data.categoryColor}
|
||||
date={formatDate(post.data.date)}
|
||||
readTime={post.data.readTime}
|
||||
imageUrl={post.data.imageUrl}
|
||||
slug={`/blog/${post.id}`}
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
category={post.category}
|
||||
categoryColor={post.categoryColor}
|
||||
date={formatDate(post.date)}
|
||||
readTime={post.readTime}
|
||||
imageUrl={post.imageUrl}
|
||||
slug={`/blog/${post.slug}`}
|
||||
/>
|
||||
</article>
|
||||
))}
|
||||
|
|
@ -206,4 +195,4 @@ const categories = ['Все', ...new Set(posts.map((post: any) => post.data.cate
|
|||
setupAnimations();
|
||||
setupFilter();
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -7,49 +7,28 @@ import BlogCard from '@components/blog/BlogCard.astro';
|
|||
import Pagination from '@components/base/Pagination.astro';
|
||||
import CTA from '@components/base/CTA.astro';
|
||||
import SearchModal from '@components/base/SearchModal.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getPosts, getAllCategories } from '@lib/pb';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
const POSTS_PER_PAGE = 6;
|
||||
const totalPages = Math.ceil(posts.length / POSTS_PER_PAGE);
|
||||
|
||||
return Array.from({ length: totalPages }, (_, i) => ({
|
||||
params: { page: String(i + 2) }, // Начинаем со 2-й страницы (1-я это /blog/)
|
||||
}));
|
||||
}
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
|
||||
// Сортируем посты по дате (новые сверху)
|
||||
const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
export const prerender = false;
|
||||
|
||||
const POSTS_PER_PAGE = 6;
|
||||
const currentPage = Number(Astro.params.page) || 1;
|
||||
const totalPages = Math.ceil(sortedPosts.length / POSTS_PER_PAGE);
|
||||
|
||||
const startIndex = (currentPage - 1) * POSTS_PER_PAGE;
|
||||
const endIndex = startIndex + POSTS_PER_PAGE;
|
||||
const paginatedPosts = sortedPosts.slice(startIndex, endIndex);
|
||||
const { posts, total, totalPages } = await getPosts({ page: currentPage, perPage: POSTS_PER_PAGE });
|
||||
const categories = await getAllCategories();
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Категории
|
||||
const categories = ['Все', ...new Set(posts.map(post => post.data.category))];
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Блог — страница ${currentPage} — автоюрист в Сургуте"
|
||||
description="Полезные статьи и советы по автоспорам, ДТП, ОСАГО, лишении прав и защите прав водителей. Страница ${currentPage}."
|
||||
title={`Блог — страница ${currentPage} — автоюрист в Сургуте`}
|
||||
description={`Полезные статьи и советы по автоспорам, ДТП, ОСАГО, лишении прав и защите прав водителей. Страница ${currentPage}.`}
|
||||
canonicalLink={`${SITE_URL}/blog/page/${currentPage}`}
|
||||
breadcrumbs={[
|
||||
{ label: 'Главная', href: '/' },
|
||||
|
|
@ -78,17 +57,17 @@ const categories = ['Все', ...new Set(posts.map(post => post.data.category))]
|
|||
<section class="blog-grid-section">
|
||||
<div class="site-container">
|
||||
<div class="blog-grid" id="blog-grid">
|
||||
{paginatedPosts.map((post: any) => (
|
||||
<article class="blog-card-wrapper" data-category={post.data.category}>
|
||||
{posts.map((post: any) => (
|
||||
<article class="blog-card-wrapper" data-category={post.category}>
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
category={post.data.category}
|
||||
categoryColor={post.data.categoryColor}
|
||||
date={formatDate(post.data.date)}
|
||||
readTime={post.data.readTime}
|
||||
imageUrl={post.data.imageUrl}
|
||||
slug={`/blog/${post.id}`}
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
category={post.category}
|
||||
categoryColor={post.categoryColor}
|
||||
date={formatDate(post.date)}
|
||||
readTime={post.readTime}
|
||||
imageUrl={post.imageUrl}
|
||||
slug={`/blog/${post.slug}`}
|
||||
/>
|
||||
</article>
|
||||
))}
|
||||
|
|
@ -179,4 +158,4 @@ const categories = ['Все', ...new Set(posts.map(post => post.data.category))]
|
|||
|
||||
setupAnimations();
|
||||
document.addEventListener('astro:after-swap', setupAnimations);
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -3,40 +3,23 @@ import Layout from '@layouts/Layout.astro';
|
|||
import { SITE_URL } from '@constants';
|
||||
import BlogCard from '@components/blog/BlogCard.astro';
|
||||
import SearchModal from '@components/base/SearchModal.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getPosts } from '@lib/pb';
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
|
||||
// Получаем параметр поиска из URL
|
||||
const url = new URL(Astro.request.url);
|
||||
const searchQuery = url.searchParams.get('q') || '';
|
||||
|
||||
const { posts: searchResults } = searchQuery
|
||||
? await getPosts({ perPage: 20, search: searchQuery })
|
||||
: { posts: [], total: 0, page: 1, totalPages: 1 };
|
||||
|
||||
const breadcrumbsItems = [
|
||||
{ label: 'Главная', href: '/' },
|
||||
{ label: 'Блог', href: '/blog' },
|
||||
{ label: searchQuery ? `Поиск: "${searchQuery}"` : 'Поиск' }
|
||||
];
|
||||
|
||||
// Функция поиска по статьям
|
||||
function searchArticles(query: string, allPosts: typeof posts) {
|
||||
if (!query.trim()) return [];
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
return allPosts.filter(post => {
|
||||
const titleMatch = post.data.title.toLowerCase().includes(lowerQuery);
|
||||
const descriptionMatch = post.data.description.toLowerCase().includes(lowerQuery);
|
||||
const categoryMatch = post.data.category.toLowerCase().includes(lowerQuery);
|
||||
|
||||
return titleMatch || descriptionMatch || categoryMatch;
|
||||
});
|
||||
}
|
||||
|
||||
const searchResults = searchArticles(searchQuery, posts);
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
|
|
@ -89,14 +72,14 @@ const formatDate = (date: Date) => {
|
|||
<div class="results-grid">
|
||||
{searchResults.map((post: any) => (
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
category={post.data.category}
|
||||
categoryColor={post.data.categoryColor}
|
||||
date={formatDate(post.data.date)}
|
||||
readTime={post.data.readTime}
|
||||
imageUrl={post.data.imageUrl}
|
||||
slug={`/blog/${post.id}`}
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
category={post.category}
|
||||
categoryColor={post.categoryColor}
|
||||
date={formatDate(post.date)}
|
||||
readTime={post.readTime}
|
||||
imageUrl={post.imageUrl}
|
||||
slug={`/blog/${post.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -149,7 +132,6 @@ const formatDate = (date: Date) => {
|
|||
}));
|
||||
});
|
||||
|
||||
// Автоматически открываем поиск при загрузке страницы без параметров
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const query = urlParams.get('q');
|
||||
|
|
@ -338,4 +320,4 @@ const formatDate = (date: Date) => {
|
|||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue