feat(sidebar): узкое меню, субменю историй с hover, эффект text-fade
- Сужено боковое меню (56px), убрана иконка Home - Субменю историй при наведении: полная высота, на всю ширину, z-9999 - Класс text-fade для плавного обрезания длинного текста - Убраны скругления в субменю - Chatwoot, изменения в posts-mcs и прочие обновления Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,14 +2,34 @@ import { searchSearxng, type SearxngSearchResult } from '@/lib/searxng';
|
||||
import configManager from '@/lib/config';
|
||||
import { getSearxngURL } from '@/lib/config/serverRegistry';
|
||||
|
||||
const GHOST_URL = process.env.GHOST_URL?.trim() ?? '';
|
||||
const GHOST_CONTENT_API_KEY = process.env.GHOST_CONTENT_API_KEY?.trim() ?? '';
|
||||
const PLACEHOLDER_IMAGE = 'https://placehold.co/400x225/e5e7eb/6b7280?text=Post';
|
||||
|
||||
type Region = 'america' | 'eu' | 'russia' | 'china';
|
||||
type Topic = 'tech' | 'finance' | 'art' | 'sports' | 'entertainment';
|
||||
type Topic = 'tech' | 'finance' | 'art' | 'sports' | 'entertainment' | 'gooseek';
|
||||
|
||||
interface GhostPost {
|
||||
title: string;
|
||||
excerpt?: string | null;
|
||||
custom_excerpt?: string | null;
|
||||
meta_description?: string | null;
|
||||
plaintext?: string | null;
|
||||
html?: string | null;
|
||||
feature_image?: string | null;
|
||||
url: string;
|
||||
}
|
||||
|
||||
function stripHtml(html: string): string {
|
||||
return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
const SOURCES_BY_REGION: Record<
|
||||
Region,
|
||||
Record<Topic, { query: string[]; links: string[] }>
|
||||
> = {
|
||||
america: {
|
||||
gooseek: { query: [], links: [] },
|
||||
tech: {
|
||||
query: ['technology news', 'latest tech', 'AI', 'science and innovation'],
|
||||
links: ['techcrunch.com', 'wired.com', 'theverge.com', 'arstechnica.com'],
|
||||
@@ -32,6 +52,7 @@ const SOURCES_BY_REGION: Record<
|
||||
},
|
||||
},
|
||||
eu: {
|
||||
gooseek: { query: [], links: [] },
|
||||
tech: {
|
||||
query: ['technology news', 'tech', 'AI', 'innovation'],
|
||||
links: ['techcrunch.com', 'bbc.com', 'theguardian.com', 'reuters.com', 'euronews.com'],
|
||||
@@ -54,6 +75,7 @@ const SOURCES_BY_REGION: Record<
|
||||
},
|
||||
},
|
||||
russia: {
|
||||
gooseek: { query: [], links: [] },
|
||||
tech: {
|
||||
query: ['technology news', 'tech', 'IT', 'innovation'],
|
||||
links: ['tass.com', 'ria.ru', 'interfax.com', 'kommersant.ru', 'vedomosti.ru'],
|
||||
@@ -76,6 +98,7 @@ const SOURCES_BY_REGION: Record<
|
||||
},
|
||||
},
|
||||
china: {
|
||||
gooseek: { query: [], links: [] },
|
||||
tech: {
|
||||
query: ['technology news', 'tech', 'AI', 'innovation'],
|
||||
links: ['scmp.com', 'xinhuanet.com', 'chinadaily.com.cn', 'reuters.com'],
|
||||
@@ -137,8 +160,91 @@ const COUNTRY_TO_REGION: Record<string, Region> = {
|
||||
DK: 'eu',
|
||||
};
|
||||
|
||||
async function fetchDooseekPosts(): Promise<
|
||||
{ title: string; content: string; url: string; thumbnail: string }[]
|
||||
> {
|
||||
if (!GHOST_URL || !GHOST_CONTENT_API_KEY) {
|
||||
throw new Error(
|
||||
'Ghost не настроен. Укажите GHOST_URL и GHOST_CONTENT_API_KEY в .env',
|
||||
);
|
||||
}
|
||||
const base = GHOST_URL.replace(/\/$/, '');
|
||||
const apiUrl = `${base}/ghost/api/content/posts/?key=${GHOST_CONTENT_API_KEY}&limit=50&fields=title,excerpt,custom_excerpt,meta_description,html,feature_image,url&formats=html`;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 15_000);
|
||||
try {
|
||||
const res = await fetch(apiUrl, {
|
||||
signal: controller.signal,
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
throw new Error(
|
||||
'Неверный Ghost Content API Key. Получите ключ в Ghost Admin → Settings → Integrations → Add custom integration.',
|
||||
);
|
||||
}
|
||||
throw new Error(`Ghost API: HTTP ${res.status}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
const posts: GhostPost[] = data.posts ?? [];
|
||||
return posts.map((p) => {
|
||||
const excerpt =
|
||||
p.custom_excerpt?.trim() ||
|
||||
p.meta_description?.trim() ||
|
||||
p.excerpt?.trim() ||
|
||||
p.plaintext?.trim() ||
|
||||
(p.html ? stripHtml(p.html) : '');
|
||||
const content = excerpt
|
||||
? excerpt.slice(0, 300) + (excerpt.length > 300 ? '…' : '')
|
||||
: (p.title ? `${p.title}. Читать далее →` : 'Читать далее →');
|
||||
return {
|
||||
title: p.title || 'Без названия',
|
||||
content,
|
||||
url: p.url,
|
||||
thumbnail: p.feature_image?.trim() || PLACEHOLDER_IMAGE,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
clearTimeout(timeoutId);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = async (req: Request) => {
|
||||
try {
|
||||
const params = new URL(req.url).searchParams;
|
||||
|
||||
const mode: 'normal' | 'preview' =
|
||||
(params.get('mode') as 'normal' | 'preview') || 'normal';
|
||||
const topic: Topic = (params.get('topic') as Topic) || 'tech';
|
||||
|
||||
if (topic === 'gooseek') {
|
||||
try {
|
||||
const blogs = await fetchDooseekPosts();
|
||||
return Response.json({ blogs }, { status: 200 });
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e instanceof Error ? e.message : String(e);
|
||||
const isConnect =
|
||||
msg.includes('ECONNREFUSED') ||
|
||||
msg.includes('fetch failed') ||
|
||||
msg.includes('Failed to fetch') ||
|
||||
msg.includes('AbortError');
|
||||
return Response.json(
|
||||
{
|
||||
message:
|
||||
!GHOST_URL || !GHOST_CONTENT_API_KEY
|
||||
? 'Ghost не настроен. Укажите GHOST_URL и GHOST_CONTENT_API_KEY в .env'
|
||||
: isConnect
|
||||
? 'Не удалось подключиться к Ghost. Проверьте GHOST_URL и доступность сайта.'
|
||||
: `Ошибка загрузки постов: ${msg}`,
|
||||
},
|
||||
{ status: isConnect ? 503 : 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const searxngURL = getSearxngURL();
|
||||
if (!searxngURL?.trim()) {
|
||||
return Response.json(
|
||||
@@ -149,12 +255,6 @@ export const GET = async (req: Request) => {
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
const params = new URL(req.url).searchParams;
|
||||
|
||||
const mode: 'normal' | 'preview' =
|
||||
(params.get('mode') as 'normal' | 'preview') || 'normal';
|
||||
const topic: Topic = (params.get('topic') as Topic) || 'tech';
|
||||
const regionParam = params.get('region') as Region | null;
|
||||
|
||||
let region: Region = 'america';
|
||||
|
||||
Reference in New Issue
Block a user