Files
iiEasy/strapiService.ts
2026-02-04 20:49:18 +05:00

442 lines
17 KiB
TypeScript
Executable File

import { Post, NewsArticle, ResearchPaper, BusinessStory, ServiceItemData, ClientLogo, BusinessCourse, StudentProgram, AcceleratorProject, InvestmentProject, Vacancy, GalleryItem } from './types';
const STRAPI_URL = 'https://n8n.iieasy.ru';
const API_TOKEN = '91344ae88ae3e496f72d6ae9c157a3e929c3078b6269b57d3751d81026880cca6e7dcec074bffd77c8fe853dce9308f8d7dd494cf7b8c804a2875f0f4c24c0419cee196adf247d01cd5ddd5893d325bff34ad236febe0b508114ec906b12423e658d7210be94f2fcc37184334e11065538531c6d935de5becf81f2a48fdd6318';
interface StrapiResponse<T> {
data: StrapiDataItem<T>[];
meta: {
pagination: {
page: number;
pageSize: number;
pageCount: number;
total: number;
};
};
}
interface StrapiDataItem<T_Attributes> {
id: number;
attributes: T_Attributes;
}
interface StrapiImageAttributes {
id?: number;
url: string;
name?: string;
alternativeText?: string | null;
caption?: string | null;
width?: number;
height?: number;
formats?: {
thumbnail?: StrapiImageFormat;
small?: StrapiImageFormat;
medium?: StrapiImageFormat;
large?: StrapiImageFormat;
};
hash?: string;
ext?: string;
mime?: string;
size?: number;
provider?: string;
provider_metadata?: any;
createdAt?: string;
updatedAt?: string;
publishedAt?: string;
}
interface StrapiImageFormat {
name: string;
hash: string;
ext: string;
mime: string;
path: string | null;
width: number;
height: number;
size: number;
sizeInBytes?: number;
url: string;
}
interface StrapiMediaPopulated {
data: {
id: number;
attributes: StrapiImageAttributes;
} | null;
}
interface StrapiMultipleMediaPopulated {
data: {
id: number;
attributes: StrapiImageAttributes;
}[] | null;
}
const getFullImageUrl = (mediaFieldData?: StrapiImageAttributes | StrapiMediaPopulated): string => {
const placeholderBase = 'https://picsum.photos/seed/';
const placeholderFallback = `${placeholderBase}placeholder-fallback/600/400`;
if (!mediaFieldData) {
console.warn('getFullImageUrl called with undefined mediaFieldData');
return placeholderFallback;
}
// Handle case where mediaFieldData is already attributes
if ('url' in mediaFieldData && typeof mediaFieldData.url === 'string' && !('data' in mediaFieldData)) {
if (mediaFieldData.url.startsWith('http://') || mediaFieldData.url.startsWith('https://')) {
return mediaFieldData.url;
}
return `${STRAPI_URL}${mediaFieldData.url}`;
}
// Handle case where mediaFieldData has a 'data' property (populated media field)
if ('data' in mediaFieldData && mediaFieldData.data?.attributes?.url) {
const url = mediaFieldData.data.attributes.url;
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
return `${STRAPI_URL}${url}`;
}
console.warn('getFullImageUrl could not extract URL from mediaFieldData:', mediaFieldData);
return `${placeholderBase}no-url-extracted/600/400`;
};
// Updated rich text renderer to allow raw HTML
const transformRichTextToString = (nodes: any[] | undefined): string => {
if (!nodes || !Array.isArray(nodes)) {
return '';
}
// Helper to escape attributes to prevent some forms of XSS.
const escapeAttribute = (text: string) =>
(text || '').replace(/"/g, '&quot;');
// Helper to escape HTML for code blocks intended for display.
const escapeHtmlForCodeDisplay = (text: string) =>
(text || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
const serializeNodeToHtml = (node: any): string => {
// For text nodes, we now trust the content and do not escape it.
// This allows users to write raw HTML directly in the editor.
if (node.type === 'text') {
let text = node.text || ''; // Raw text, not escaped.
if (node.bold) text = `<strong>${text}</strong>`;
if (node.italic) text = `<em>${text}</em>`;
if (node.underline) text = `<u>${text}</u>`;
if (node.strikethrough) text = `<s>${text}</s>`;
if (node.code) text = `<code>${text}</code>`;
return text;
}
const childrenHtml = node.children?.map(serializeNodeToHtml).join('') || '';
switch (node.type) {
case 'heading':
return `<h${node.level || 1}>${childrenHtml}</h${node.level || 1}>`;
case 'paragraph':
return `<p>${childrenHtml || ''}</p>`;
case 'quote':
return `<blockquote>${childrenHtml}</blockquote>`;
case 'list':
const listTag = node.format === 'ordered' ? 'ol' : 'ul';
return `<${listTag}>${childrenHtml}</${listTag}>`;
case 'list-item':
return `<li>${childrenHtml}</li>`;
case 'link':
return `<a href="${escapeAttribute(node.url)}" target="_blank" rel="noopener noreferrer">${childrenHtml}</a>`;
case 'image':
if (!node.image?.url) return '';
const imageUrl = getFullImageUrl(node.image);
const altText = node.image.alternativeText || '';
const caption = node.image.caption ? `<figcaption>${node.image.caption}</figcaption>` : ''; // Caption is not escaped
return `<figure><img src="${imageUrl}" alt="${escapeAttribute(altText)}" loading="lazy"/>${caption}</figure>`;
case 'code':
// For code blocks, we explicitly escape the content for display.
const codeContent = node.children?.[0]?.text || '';
return `<pre><code class="language-${node.language || ''}">${escapeHtmlForCodeDisplay(codeContent)}</code></pre>`;
default:
// For any other block types that just wrap children
return childrenHtml;
}
};
return nodes.map(serializeNodeToHtml).join('');
};
const transformRichTextToListArray = (richTextField: any[] | undefined): string[] => {
if (!richTextField || !Array.isArray(richTextField)) {
return [];
}
const result: string[] = [];
richTextField.forEach(block => {
if (block.type === 'paragraph' && Array.isArray(block.children)) {
const paragraphText = block.children
.filter((child: any) => child.type === 'text' && typeof child.text === 'string')
.map((child: any) => child.text)
.join('');
if (paragraphText) {
paragraphText.split('\n').forEach(line => {
const trimmedLine = line.trim();
if (trimmedLine) result.push(trimmedLine);
});
}
} else if (block.type === 'list' && Array.isArray(block.children)) {
block.children.forEach((listItem: any) => {
if (listItem.type === 'list-item' && Array.isArray(listItem.children)) {
const listItemText = listItem.children
.filter((child: any) => child.type === 'text' && typeof child.text === 'string')
.map((child: any) => child.text)
.join('')
.trim();
if (listItemText) result.push(listItemText);
}
});
}
});
return result;
};
const transformGalleryItems = (galleryData: StrapiMultipleMediaPopulated | undefined): GalleryItem[] => {
if (!galleryData || !Array.isArray(galleryData.data)) {
return [];
}
return galleryData.data.map((mediaItem: StrapiDataItem<StrapiImageAttributes>) => {
const attributes = mediaItem.attributes;
const type = attributes.mime && attributes.mime.startsWith('video') ? 'video' : 'image';
return {
id: mediaItem.id.toString(),
type: type,
url: getFullImageUrl(attributes),
altText: attributes.alternativeText || undefined,
caption: attributes.caption || undefined,
};
});
};
const transformPost = (item: any): Post => {
return {
id: item.id.toString(),
title: item.title || 'Untitled Post',
category: item.category || 'Uncategorized',
date: item.date || (item.publishedAt ? new Date(item.publishedAt).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }) : undefined),
publishedAt: item.publishedAt,
readTime: item.readTime,
imageUrl: getFullImageUrl(item.image),
videoUrl: item.videoUrl,
isFeatured: item.isFeatured || false,
description: item.description,
fullContent: Array.isArray(item.fullContent) ? transformRichTextToString(item.fullContent) : (item.fullContent || ''),
gallery: transformGalleryItems(item.gallery as StrapiMultipleMediaPopulated | undefined),
href: item.slug || `post-${item.id}`,
};
};
const transformNewsArticle = (item: any): NewsArticle => {
return {
id: item.id.toString(),
title: item.title || 'Untitled News Article',
category: item.category || 'General News',
date: item.date || (item.publishedAt ? new Date(item.publishedAt).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }) : undefined),
publishedAt: item.publishedAt,
imageUrl: getFullImageUrl(item.image),
href: item.slug || `news-${item.id}`,
description: item.description,
fullContent: Array.isArray(item.fullContent) ? transformRichTextToString(item.fullContent) : (item.fullContent || ''),
gallery: transformGalleryItems(item.gallery as StrapiMultipleMediaPopulated | undefined),
};
};
const transformResearchPaper = (item: any): ResearchPaper => {
let authorsArray: string[] | undefined = undefined;
if (Array.isArray(item.authors)) {
authorsArray = item.authors.map((author: any) => {
if (typeof author === 'string') return author;
if (typeof author === 'object' && author !== null && typeof author.name === 'string') return author.name;
return 'Unknown Author';
});
} else if (typeof item.authors === 'string') {
authorsArray = [item.authors];
}
return {
id: item.id.toString(),
title: item.title || 'Untitled Research Paper',
category: item.category || 'Research',
date: item.date || (item.publishedAt ? new Date(item.publishedAt).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }) : undefined),
publishedAt: item.publishedAt,
imageUrl: getFullImageUrl(item.image),
href: item.slug || `research-${item.id}`,
authors: authorsArray,
abstract: item.abstract,
fullContent: Array.isArray(item.fullContent) ? transformRichTextToString(item.fullContent) : (item.fullContent || ''),
gallery: transformGalleryItems(item.gallery as StrapiMultipleMediaPopulated | undefined),
};
};
const transformBusinessStory = (item: any): BusinessStory => {
return {
id: item.id.toString(),
title: item.title || 'Untitled Business Story',
category: item.category || 'Case Study',
imageUrl: getFullImageUrl(item.image),
href: item.slug || `biz-${item.id}`,
description: item.description,
fullContent: Array.isArray(item.fullContent) ? transformRichTextToString(item.fullContent) : (item.fullContent || ''),
gallery: transformGalleryItems(item.gallery as StrapiMultipleMediaPopulated | undefined),
};
};
const transformServiceItem = (item: any): ServiceItemData => {
return {
icon: item.icon || 'CpuChipIcon',
title: item.title || 'Untitled Service',
description: item.description || '',
};
};
const transformClientLogo = (item: any): ClientLogo => {
return {
id: item.id.toString(),
name: item.name || 'Unnamed Client',
imageUrl: getFullImageUrl(item.image),
};
};
const transformBusinessCourse = (item: any): BusinessCourse => {
return {
icon: item.icon || 'AcademicCapIcon',
title: item.title || 'Untitled Business Course',
description: item.description || '',
};
};
const transformStudentProgram = (item: any): StudentProgram => {
return {
icon: item.icon || 'AcademicCapIcon',
title: item.title || 'Untitled Student Program',
targetAudience: item.targetAudience || '',
description: item.description || '',
};
};
const transformAcceleratorProject = (item: any): AcceleratorProject => {
return {
id: item.id.toString(),
name: item.name || 'Untitled Accelerator Project',
tagline: item.tagline || '',
description: Array.isArray(item.description_rt) ? transformRichTextToString(item.description_rt) : (typeof item.description === 'string' ? item.description : ''),
imageUrl: getFullImageUrl(item.image),
category: item.category || 'General AI',
websiteUrl: item.websiteUrl,
statuspro: item.statuspro || 'Active',
};
};
const transformInvestmentProject = (item: any): InvestmentProject => {
return {
id: item.id.toString(),
name: item.name || 'Untitled Investment Project',
industry: item.industry || 'AI Sector',
problem: Array.isArray(item.problem_rt) ? transformRichTextToString(item.problem_rt) : (typeof item.problem === 'string' ? item.problem : ''),
solution: Array.isArray(item.solution_rt) ? transformRichTextToString(item.solution_rt) : (typeof item.solution === 'string' ? item.solution : ''),
teamSize: item.teamSize || 1,
fundingSought: item.fundingSought || 'N/A',
imageUrl: getFullImageUrl(item.image),
contactEmail: item.contactEmail,
};
};
const transformVacancy = (item: any): Vacancy => {
return {
id: item.id.toString(),
title: item.title || 'Untitled Vacancy',
department: item.department || 'N/A',
location: item.location || 'N/A',
type: item.jobType || 'Full-time',
description: item.shortDescription || '',
fullDescription: transformRichTextToString(item.fullDescription_rt),
responsibilities: transformRichTextToListArray(item.responsibilities_rt),
qualifications: transformRichTextToListArray(item.qualifications_rt),
offer: transformRichTextToListArray(item.offer_rt),
href: item.slug || item.id.toString(),
};
};
async function fetchData<T_Transformed>(
endpoint: string,
transformFn: (item: any) => T_Transformed
): Promise<T_Transformed[]> {
try {
const response = await fetch(`${STRAPI_URL}/api/${endpoint}?populate=*`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
},
});
if (!response.ok) {
let errorBody = "Could not read error body.";
try { errorBody = await response.text(); } catch (e) { /* ignore */ }
console.error(`Strapi fetch error for ${endpoint}: ${response.status} ${response.statusText}. Body: ${errorBody}`);
throw new Error(`Failed to fetch ${endpoint}: ${response.status}`);
}
let jsonResponse;
try {
jsonResponse = await response.json();
} catch (e: any) {
console.error(`Error parsing JSON for ${endpoint}:`, e.message);
try {
const textResponse = await response.text();
console.error(`Raw response text for ${endpoint}: ${textResponse.substring(0, 500)}...`);
} catch { /* ignore if reading text also fails */ }
throw new Error(`Failed to parse JSON for ${endpoint}`);
}
if (!jsonResponse.data) {
console.warn(`No 'data' field in Strapi response for ${endpoint}:`, jsonResponse);
return [];
}
if (!Array.isArray(jsonResponse.data)) {
console.warn(`Data for ${endpoint} is not an array:`, jsonResponse.data);
return [];
}
const transformedData: T_Transformed[] = [];
for (const item of jsonResponse.data) {
try {
transformedData.push(transformFn(item.attributes ? { id: item.id, ...item.attributes } : item));
} catch (transformError: any) {
console.error(`Error transforming item with id ${item?.id || 'unknown'} from ${endpoint}: ${transformError.message}`, item, transformError.stack);
}
}
return transformedData;
} catch (error: any) {
console.error(`Critical error fetching or transforming data for ${endpoint}: ${error.message}`, error.stack);
return [];
}
}
export const fetchPosts = (): Promise<Post[]> => fetchData('posts', transformPost);
export const fetchNewsArticles = (): Promise<NewsArticle[]> => fetchData('news-articles', transformNewsArticle);
export const fetchResearchPapers = (): Promise<ResearchPaper[]> => fetchData('research-papers', transformResearchPaper);
export const fetchBusinessStories = (): Promise<BusinessStory[]> => fetchData('business-stories', transformBusinessStory);
export const fetchServiceItems = (): Promise<ServiceItemData[]> => fetchData('services', transformServiceItem);
export const fetchClientLogos = (): Promise<ClientLogo[]> => fetchData('client-logos', transformClientLogo);
export const fetchBusinessCourses = (): Promise<BusinessCourse[]> => fetchData('business-courses', transformBusinessCourse);
export const fetchStudentPrograms = (): Promise<StudentProgram[]> => fetchData('student-programs', transformStudentProgram);
export const fetchAcceleratorProjects = (): Promise<AcceleratorProject[]> => fetchData('accelerator-projects', transformAcceleratorProject);
export const fetchInvestmentProjects = (): Promise<InvestmentProject[]> => fetchData('investment-opportunities', transformInvestmentProject);
export const fetchVacancies = (): Promise<Vacancy[]> => fetchData('vacancies', transformVacancy);