Files
profile-front/src/lib/api/posts.ts
Profile Profile 684a7bffbf correct tags
2026-03-13 20:22:35 +03:00

804 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { fetchGraphQL } from './graphql-client.js';
import type { ProfileArticle } from '../types/graphql.js';
//кэширование
import { cache } from '@lib/cache/manager.js';
import { CACHE_TTL } from '@lib/cache/cache-ttl';
export interface AnewsPost {
title: string;
uri: string;
date: string;
}
export async function getLatestPosts(first = 14, after = null) {
// Создаем уникальный ключ для кэша
const cacheKey = `latest-posts:${first}:${after || 'first-page'}`;
return await cache.wrap(
cacheKey,
async () => {
const query = `
query GetLatestProfileArticles($first: Int!, $after: String) {
profileArticles(
first: $first
after: $after
where: { orderby: { field: DATE, order: DESC } }
) {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
id
databaseId
title
uri
date
featuredImage {
node {
sourceUrl(size: LARGE)
altText
}
}
author {
node {
id
name
firstName
lastName
avatar {
url
}
uri
}
}
# Соавторы как массив
coauthors {
id
name
firstName
lastName
url
description
}
categories {
nodes {
id
name
color
slug
uri
databaseId
}
}
tags {
nodes {
id
name
slug
uri
}
}
}
}
}
}
`;
const data = await fetchGraphQL(query, { first, after });
// Преобразуем edges в nodes
const posts = data.profileArticles?.edges?.map(edge => edge.node) || [];
return {
posts,
pageInfo: data.profileArticles?.pageInfo || { hasNextPage: false, endCursor: null }
};
},
{ ttl: CACHE_TTL.POSTS } // из конфигурации
);
}
export async function getPostsByCategory(slug, first = 14, after = null) {
// Создаем уникальный ключ для кэша
const cacheKey = `category-posts:${slug}:${first}:${after || 'first-page'}`;
return await cache.wrap(
cacheKey,
async () => {
const query = `
query GetPostsByCategory($first: Int!, $after: String, $slug: ID!) {
category(id: $slug, idType: SLUG) {
name
contentNodes(
first: $first
after: $after
where: {
contentTypes: [PROFILE_ARTICLE, ANEW]
orderby: { field: DATE, order: DESC }
}
) {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
__typename
id
databaseId
uri
date
... on NodeWithTitle {
title
}
... on NodeWithFeaturedImage {
featuredImage {
node {
sourceUrl(size: LARGE)
altText
}
}
}
# Для ваших кастомных типов
... on ProfileArticle {
categories {
nodes {
name
slug
color
}
}
}
... on ANew {
categories {
nodes {
name
slug
color
}
}
}
... on NodeWithAuthor {
author {
node {
name
avatar {
url
}
}
}
}
# Coauthors для ProfileArticle (без description)
... on ProfileArticle {
coauthors {
id
name
firstName
lastName
url
}
}
# Coauthors для ANew (без description)
... on ANew {
coauthors {
id
name
firstName
lastName
url
}
}
}
}
}
}
}
`;
const data = await fetchGraphQL(query, { first, after, slug });
// Обрабатываем посты
const posts = data.category?.contentNodes?.edges?.map(edge => {
const node = edge.node;
// Приводим coauthors к единому формату (если нужно)
if (node.coauthors) {
node.coauthors = node.coauthors.map(author => ({
id: author.id,
name: author.name || '',
firstName: author.firstName || '',
lastName: author.lastName || '',
url: author.url || ''
}));
}
return node;
}) || [];
return {
posts,
pageInfo: data.category?.contentNodes?.pageInfo || {
hasNextPage: false,
endCursor: null
},
categoryName: data.category?.name || ''
};
},
{ ttl: CACHE_TTL.POSTS }
);
}
export async function getPostsByTag(slug, first = 14, after = null) {
// Создаем уникальный ключ для кэша
const cacheKey = `tag-posts:${slug}:${first}:${after || 'first-page'}`;
return await cache.wrap(
cacheKey,
async () => {
const query = `
query GetPostsByTag($first: Int!, $after: String, $slug: ID!) {
tag(id: $slug, idType: SLUG) {
id
databaseId
name
slug
count
description
contentNodes(
first: $first
after: $after
where: {
contentTypes: [PROFILE_ARTICLE, ANEW]
orderby: { field: DATE, order: DESC }
}
) {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
__typename
id
databaseId
uri
date
... on NodeWithTitle {
title
}
... on NodeWithFeaturedImage {
featuredImage {
node {
sourceUrl(size: LARGE)
altText
}
}
}
... on NodeWithAuthor {
author {
node {
name
avatar {
url
}
}
}
}
... on ProfileArticle {
categories {
nodes {
name
slug
}
}
}
... on ANew {
categories {
nodes {
name
slug
}
}
}
... on ProfileArticle {
coauthors {
id
name
}
}
... on ANew {
coauthors {
id
name
}
}
}
}
}
}
}
`;
console.log('Fetching with:', { first, after, slug }); // Добавим лог параметров
const data = await fetchGraphQL(query, { first, after, slug });
// Проверяем структуру data
if (!data?.tag) {
console.log('Tag not found');
return {
posts: [],
pageInfo: { hasNextPage: false, endCursor: null },
tagName: slug,
tagSlug: slug,
tagCount: 0,
tagDescription: ''
};
}
// Обрабатываем посты
const posts = data.tag?.contentNodes?.edges?.map(edge => {
const node = edge.node;
// Приводим данные к единому формату
return {
...node,
// Убеждаемся, что coauthors всегда массив
coauthors: node.coauthors || [],
// Убеждаемся, что categories всегда есть
categories: node.categories || { nodes: [] }
};
}) || [];
return {
posts,
pageInfo: data.tag?.contentNodes?.pageInfo || {
hasNextPage: false,
endCursor: null
},
tagName: data.tag?.name || slug,
tagSlug: data.tag?.slug || slug,
tagCount: data.tag?.count || 0,
tagDescription: data.tag?.description || ''
};
},
{ ttl: CACHE_TTL.POSTS }
);
}
//последние новости (кэшированная версия)
export async function getLatestAnews(count = 12): Promise<AnewsPost[]> {
const cacheKey = `latest-anews:${count}`;
return await cache.wrap(
cacheKey,
async () => {
const query = `
query GetAnews($count: Int!) {
aNews(first: $count, where: {orderby: {field: DATE, order: DESC}}) {
nodes {
title
uri
date
}
}
}
`;
const data = await fetchGraphQL(query, { count });
return data.aNews?.nodes || [];
},
{ ttl: CACHE_TTL.POSTS }
);
}
// Получить ProfileArticle по databaseId
export async function getProfileArticleById(databaseId) {
const query = `
query GetProfileArticleById($id: ID!) {
profileArticle(id: $id, idType: DATABASE_ID) {
id
databaseId
title
secondaryTitle
content
excerpt
uri
slug
date
modified
status
featuredImage {
node {
id
sourceUrl
altText
caption
description
mediaDetails {
width
height
}
}
}
author {
node {
id
name
firstName
lastName
avatar {
url
}
description
uri
}
}
# Соавторы как массив
coauthors {
id
name
firstName
lastName
url
description
}
categories {
nodes {
id
name
slug
uri
description
}
}
tags {
nodes {
id
name
slug
uri
}
}
}
}
`;
const data = await fetchGraphQL(query, { id: databaseId });
return data?.profileArticle || null;
}
/**
* Получить Anews пост по databaseId
*/
export async function getAnewById(databaseId: number): Promise<SingleAnewsPost | null> {
const cacheKey = `anews:${databaseId}`;
return await cache.wrap(
cacheKey,
async () => {
const query = `
query GetAnewById($id: ID!) {
aNew(id: $id, idType: DATABASE_ID) {
id
databaseId
title
content
excerpt
uri
slug
date
modified
status
featuredImage {
node {
id
sourceUrl
altText
caption
mediaDetails {
width
height
}
}
}
author {
node {
id
name
firstName
lastName
avatar {
url
}
description
uri
}
}
categories {
nodes {
id
name
slug
uri
description
}
}
tags {
nodes {
id
name
slug
uri
}
}
}
}
`;
try {
const data = await fetchGraphQL(query, { id: databaseId });
return data?.aNew || null;
} catch (error) {
console.error(`Error fetching Anews post with ID ${databaseId}:`, error);
return null;
}
},
{ ttl: CACHE_TTL.POST_DETAIL } // Можно использовать отдельный TTL для деталей постов
);
}
/**
* Получить тег по slug
*/
export async function getTagBySlug(slug) {
const query = `
query GetTagBySlug($slug: ID!) {
tag(id: $slug, idType: SLUG) {
id
databaseId
name
slug
description
count
}
}
`;
try {
const data = await fetchGraphQL(query, { slug });
return data?.tag || null;
} catch (error) {
console.error('Error fetching tag:', error);
return null;
}
}
/**
* Получить тег с постами по slug с пагинацией
*/
export async function getTagWithPostsPaginated(
tagSlug: string,
perPage: number = 12,
page: number = 1
) {
const offset = (page - 1) * perPage;
const query = `
query GetTagWithPostsPaginated($slug: ID!, $first: Int!, $offset: Int!) {
tag(id: $slug, idType: SLUG) {
id
databaseId
name
slug
description
count
seo {
title
metaDesc
canonical
opengraphTitle
opengraphDescription
opengraphImage {
sourceUrl
}
}
posts(
first: $first
offset: $offset
where: {
orderby: {
field: DATE,
order: DESC
}
}
) {
nodes {
id
databaseId
title
excerpt
uri
slug
date
modified
featuredImage {
node {
sourceUrl(size: MEDIUM_LARGE)
altText
caption
mediaDetails {
width
height
}
}
}
author {
node {
id
name
firstName
lastName
avatar {
url
}
}
}
categories {
nodes {
id
name
slug
uri
}
}
tags {
nodes {
id
name
slug
}
}
seo {
title
metaDesc
}
}
pageInfo {
offsetPagination {
total
}
}
}
}
}
`;
try {
const data = await fetchGraphQL(query, {
slug: tagSlug,
first: perPage,
offset
});
const tag = data?.tag;
if (!tag) {
return {
tag: null,
posts: [],
total: 0,
totalPages: 0,
currentPage: page,
hasNext: false,
hasPrevious: false
};
}
const posts = tag.posts?.nodes || [];
const total = tag.posts?.pageInfo?.offsetPagination?.total || 0;
const totalPages = Math.ceil(total / perPage);
return {
tag,
posts,
total,
totalPages,
currentPage: page,
hasNext: page < totalPages,
hasPrevious: page > 1,
perPage
};
} catch (error) {
console.error('Error fetching tag with posts:', error);
return {
tag: null,
posts: [],
total: 0,
totalPages: 0,
currentPage: page,
hasNext: false,
hasPrevious: false
};
}
}
/**
* Получить общее количество постов для всех тегов
*/
export async function getAllTagsWithCount(first = 100) {
const query = `
query GetAllTagsWithCount($first: Int!) {
tags(first: $first, where: { hideEmpty: true }) {
nodes {
id
databaseId
name
slug
count
posts(first: 1) {
pageInfo {
offsetPagination {
total
}
}
}
}
}
}
`;
const data = await fetchGraphQL(query, { first });
return data?.tags?.nodes || [];
}