add new routing
This commit is contained in:
216
src/components/ArchivePagination.astro
Normal file
216
src/components/ArchivePagination.astro
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
30
src/pages/news/[...slug].astro
Normal file
30
src/pages/news/[...slug].astro
Normal 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>
|
||||
|
||||
@@ -14,6 +14,7 @@ body {
|
||||
.container{
|
||||
margin: 0 auto;
|
||||
width: 1200px;
|
||||
padding-bottom: 60px; /* Подберите высоту под ваш свернутый футер */
|
||||
}
|
||||
|
||||
.header-info{
|
||||
|
||||
Reference in New Issue
Block a user