add new routing

This commit is contained in:
Andrey Kuvshinov
2025-12-17 23:05:49 +03:00
parent a72d47f6d9
commit 033158e384
9 changed files with 572 additions and 73 deletions

View File

@@ -0,0 +1,216 @@
---
interface Props {
baseUrl: string;
page: number;
hasNextPage: boolean;
window?: number; // сколько страниц вокруг текущей
}
const {
baseUrl,
page,
hasNextPage,
window = 2,
} = Astro.props;
const pageUrl = (p: number) =>
p === 1 ? baseUrl : `${baseUrl}/page/${p}`;
// рассчитываем диапазон
const start = Math.max(1, page - window);
const end = page + window;
---
<nav class="pagination" aria-label="Pagination">
<ul>
<!-- prev -->
{page > 1 && (
<li>
<a href={pageUrl(page - 1)} rel="prev">←</a>
</li>
)}
<!-- первая -->
{start > 1 && (
<>
<li><a href={pageUrl(1)}>1</a></li>
{start > 2 && <li class="dots">…</li>}
</>
)}
<!-- центральные -->
{Array.from({ length: end - start + 1 }, (_, i) => start + i).map(p => (
<li class={p === page ? 'active' : ''}>
<a
href={pageUrl(p)}
aria-current={p === page ? 'page' : undefined}
>
{p}
</a>
</li>
))}
<!-- следующая -->
{hasNextPage && (
<>
<li class="dots">…</li>
<li>
<a href={pageUrl(page + window + 1)}>
{page + window + 1}
</a>
</li>
</>
)}
<!-- next -->
{hasNextPage && (
<li>
<a href={pageUrl(page + 1)} rel="next">→</a>
</li>
)}
</ul>
</nav>
<style>
/* Базовые стили пагинатора */
.pagination {
margin: 2rem 0;
padding: 1rem 0;
border-top: 1px solid #eaeaea;
border-bottom: 1px solid #eaeaea;
}
.pagination ul {
display: flex;
justify-content: center;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
gap: 0.5rem;
}
/* Общие стили для элементов пагинации */
.pagination li {
margin: 0;
}
.pagination a {
display: flex;
align-items: center;
justify-content: center;
min-width: 42px;
height: 42px;
padding: 0 0.5rem;
text-decoration: none;
color: #2c3e50;
font-size: 1.05rem;
font-weight: 500;
border: 2px solid transparent;
border-radius: 4px;
transition: all 0.25s ease;
background-color: #f8f9fa;
}
.pagination a:hover {
background-color: #e9ecef;
border-color: #d0d7de;
color: #1a365d;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* Активная страница */
.pagination li.active a {
background-color: #1a365d;
color: white;
border-color: #1a365d;
font-weight: 600;
cursor: default;
}
.pagination li.active a:hover {
transform: none;
box-shadow: none;
background-color: #1a365d;
}
/* Многоточие */
.pagination li.dots {
display: flex;
align-items: center;
justify-content: center;
min-width: 42px;
height: 42px;
color: #6c757d;
font-size: 1.2rem;
letter-spacing: 1px;
user-select: none;
}
/* Стрелки (предыдущая/следующая) */
.pagination a[rel="prev"],
.pagination a[rel="next"] {
font-size: 1.3rem;
color: #495057;
background-color: transparent;
border: none;
min-width: 46px;
}
.pagination a[rel="prev"]:hover,
.pagination a[rel="next"]:hover {
background-color: #f1f3f5;
color: #1a365d;
}
/* Для первой и последней страницы (если есть отдельные классы) */
.pagination li:first-child a,
.pagination li:last-child a {
font-weight: 600;
}
/* Адаптивность */
@media (max-width: 768px) {
.pagination ul {
gap: 0.25rem;
flex-wrap: wrap;
}
.pagination a {
min-width: 38px;
height: 38px;
font-size: 0.95rem;
}
.pagination li.dots {
min-width: 38px;
height: 38px;
}
}
/* Фокус для доступности */
.pagination a:focus {
outline: 2px solid #1a365d;
outline-offset: 2px;
background-color: #e9ecef;
}
/* Отключенные состояния (если понадобятся в будущем) */
.pagination li.disabled a {
color: #adb5bd;
cursor: not-allowed;
background-color: #f8f9fa;
}
.pagination li.disabled a:hover {
transform: none;
box-shadow: none;
background-color: #f8f9fa;
border-color: transparent;
}
</style>

View File

@@ -1,4 +1,8 @@
---
import FooterMenu from '@components/FooterMenu.astro';
interface Props {
publicationName: string;
organization: string;
@@ -90,6 +94,8 @@ const footerId = `footer-profile`;
<li><a href="">Правила применения рекомендательных технологий</a></li>
</ul>
<FooterMenu />
</div>

View File

@@ -1,6 +1,7 @@
---
import Stores from './LazyStores.astro';
import MainMenu from '@components/MainMenu.astro';
const MENU_ID = 103245;
let menuItems = [];
@@ -14,6 +15,8 @@
<Stores />
</div>
<MainMenu menuId={MENU_ID} />
</header>

View File

@@ -1,71 +1,308 @@
// lib/api/menu-api.ts
import { fetchGraphQL } from './graphql-client.js';
interface MenuItemNode {
uri: string;
url: string;
order: number;
label: string;
export interface MenuItem {
id: string;
databaseId: number;
uri: string;
url: string;
order: number;
label: string;
parentId: string | null;
target: string;
cssClasses: string[];
description: string;
childItems?: {
nodes: MenuItem[];
};
}
interface MenuNode {
name: string;
menuItems: {
nodes: MenuItemNode[];
};
export interface Menu {
id: string;
databaseId: number;
name: string;
slug: string;
locations: string[];
menuItems: {
nodes: MenuItem[];
};
}
interface MenusResponse {
menus: {
nodes: MenuNode[];
};
export type MenuIdentifier =
| { id: number } // По ID меню
| { location: string } // По локации
| { slug: string } // По слагу
| { name: string }; // По имени
/**
* Получить меню по идентификатору
*/
export async function fetchMenu(identifier: MenuIdentifier): Promise<Menu | null> {
try {
// Определяем тип запроса на основе переданного идентификатора
if ('id' in identifier) {
return await fetchMenuById(identifier.id);
}
if ('location' in identifier) {
return await fetchMenuByLocation(identifier.location);
}
if ('slug' in identifier) {
return await fetchMenuBySlug(identifier.slug);
}
if ('name' in identifier) {
return await fetchMenuByName(identifier.name);
}
return null;
} catch (error) {
console.error('Error fetching menu:', error);
return null;
}
}
/**
* Get navigation menu from WordPress
* Получить меню по ID (самый надежный способ)
*/
export async function navQuery(): Promise<MenusResponse> {
try {
const query = `{
menus(where: {location: PRIMARY}) {
nodes {
name
menuItems {
nodes {
uri
url
order
label
async function fetchMenuById(id: number): Promise<Menu | null> {
const query = `
query GetMenuById($id: ID!) {
menu(id: $id, idType: DATABASE_ID) {
id
databaseId
name
slug
locations
menuItems(first: 100) {
nodes {
id
databaseId
uri
url
order
label
parentId
target
cssClasses
description
childItems(first: 50) {
nodes {
id
databaseId
label
uri
url
order
}
}
}
}
}
}
}
}
}`;
`;
return await executeQuery<MenusResponse>(query, {}, "navigation");
} catch (error) {
log.error("Error fetching nav: " + error);
// Return fallback data for development
return {
menus: {
nodes: [
{
name: "Primary",
menuItems: {
nodes: [
{ uri: "/", url: "/", order: 1, label: "Home" },
{ uri: "/about/", url: "/about/", order: 2, label: "About" },
{
uri: "/contact/",
url: "/contact/",
order: 3,
label: "Contact",
},
],
},
},
],
},
};
}
const variables = { id };
const data = await fetchGraphQL(query, variables);
if (data?.menu) {
return {
...data.menu,
menuItems: data.menu.menuItems || { nodes: [] }
};
}
return null;
}
/**
* Получить меню по локации
*/
async function fetchMenuByLocation(location: string): Promise<Menu | null> {
const query = `
query GetMenuByLocation($location: MenuLocationEnum!) {
menus(where: { location: $location }, first: 1) {
nodes {
id
databaseId
name
slug
locations
menuItems(first: 100) {
nodes {
id
databaseId
uri
url
order
label
parentId
target
cssClasses
description
childItems(first: 50) {
nodes {
id
databaseId
label
uri
url
order
}
}
}
}
}
}
}
`;
const variables = { location };
const data = await fetchGraphQL(query, variables);
return data?.menus?.nodes?.[0] || null;
}
/**
* Получить меню по слагу
*/
async function fetchMenuBySlug(slug: string): Promise<Menu | null> {
const query = `
query GetMenuBySlug($slug: String!) {
menus(where: { slug: $slug }, first: 1) {
nodes {
id
databaseId
name
slug
locations
menuItems(first: 100) {
nodes {
id
databaseId
uri
url
order
label
parentId
target
cssClasses
description
}
}
}
}
}
`;
const variables = { slug };
const data = await fetchGraphQL(query, variables);
return data?.menus?.nodes?.[0] || null;
}
/**
* Получить меню по имени
*/
async function fetchMenuByName(name: string): Promise<Menu | null> {
const query = `
query GetMenuByName($name: String!) {
menus(where: { name: $name }, first: 1) {
nodes {
id
databaseId
name
slug
locations
menuItems(first: 100) {
nodes {
id
databaseId
uri
url
order
label
parentId
target
cssClasses
description
}
}
}
}
}
`;
const variables = { name };
const data = await fetchGraphQL(query, variables);
return data?.menus?.nodes?.[0] || null;
}
/**
* Преобразовать плоский список элементов меню в иерархическую структуру
*/
export function buildMenuHierarchy(menuItems: MenuItem[]): MenuItem[] {
const itemsMap = new Map<string, MenuItem>();
const rootItems: MenuItem[] = [];
// Создаем map всех элементов
menuItems.forEach(item => {
itemsMap.set(item.id, {
...item,
childItems: { nodes: [] }
});
});
// Строим иерархию
menuItems.forEach(item => {
const menuItem = itemsMap.get(item.id)!;
if (item.parentId && itemsMap.has(item.parentId)) {
const parent = itemsMap.get(item.parentId)!;
if (!parent.childItems) {
parent.childItems = { nodes: [] };
}
parent.childItems.nodes.push(menuItem);
} else {
rootItems.push(menuItem);
}
});
// Сортируем элементы по order
const sortByOrder = (items: MenuItem[]) =>
items.sort((a, b) => a.order - b.order);
// Рекурсивно сортируем все уровни
function sortRecursive(items: MenuItem[]) {
sortByOrder(items);
items.forEach(item => {
if (item.childItems?.nodes.length) {
sortRecursive(item.childItems.nodes);
}
});
}
sortRecursive(rootItems);
return rootItems;
}
/**
* Получить меню в виде иерархической структуры
*/
export async function getHierarchicalMenu(identifier: MenuIdentifier): Promise<MenuItem[]> {
const menu = await fetchMenu(identifier);
if (!menu || !menu.menuItems?.nodes?.length) {
return [];
}
return buildMenuHierarchy(menu.menuItems.nodes);
}
/**
* Получить меню в виде плоского списка
*/
export async function getFlatMenu(identifier: MenuIdentifier): Promise<MenuItem[]> {
const menu = await fetchMenu(identifier);
if (!menu || !menu.menuItems?.nodes?.length) {
return [];
}
return menu.menuItems.nodes.sort((a, b) => a.order - b.order);
}

View File

@@ -13,6 +13,9 @@ export async function fetchGraphQL(query, variables = {}) {
const json = await res.json();
console.log("Query:\n", query);
console.log("Variables:\n", JSON.stringify(variables, null, 2));
if (json.errors) {
// Выводим полный запрос и переменные для IDE

View File

@@ -1,26 +1,29 @@
---
import { fetchCategory } from '@api/categories';
import MainLayout from '@layouts/MainLayout.astro';
import CategoryArchive from '@templates/CategoryArchive.astro';
import { getCategory } from '@api/categories';
import { getArchivePostsById } from '@api/archiveById';
import { getNodeByURI, getCategoryPosts } from '@lib/api/all';
export const prerender = false; // ISR
//export const revalidate = 60;
export const prerender = false;
const { slug } = Astro.params;
const pathArray = Array.isArray(slug) ? slug : [slug];
const uri = slug ? `/${slug}` : '/';
let response;
let node = null;
// Получаем категорию по цепочке slug
const category = await getCategory(pathArray);
if (!category) return Astro.redirect('/404');
const perPage = 20;
try {
response = await getNodeByURI(uri);
node = response?.nodeByUri;
} catch (error) {
console.error('Error fetching node:', error);
}
// ISR кэширование
//Astro.response.headers.set(
// 'Cache-Control',
// 'public, s-maxage=3600, stale-while-revalidate=86400'
//);
---
<MainLayout title={category.name}>
<h1>{category.name}</h1>
</MainLayout>
Article
<p>{uri}</p>

View File

@@ -0,0 +1,30 @@
---
import MainLayout from '@layouts/MainLayout.astro';
import { getNodeByURI, getCategoryPosts } from '@lib/api/all';
export const prerender = false;
const { slug } = Astro.params;
const uri = slug ? `/${slug}` : '/';
let response;
let node = null;
try {
response = await getNodeByURI(uri);
node = response?.nodeByUri;
} catch (error) {
console.error('Error fetching node:', error);
}
// ISR кэширование
//Astro.response.headers.set(
// 'Cache-Control',
// 'public, s-maxage=3600, stale-while-revalidate=86400'
//);
---
News
<p>{uri}</p>

View File

@@ -14,6 +14,7 @@ body {
.container{
margin: 0 auto;
width: 1200px;
padding-bottom: 60px; /* Подберите высоту под ваш свернутый футер */
}
.header-info{