Files
gooseek/backend/webui/src/app/(main)/discover/page.tsx
home 06fe57c765 feat: Go backend, enhanced search, new widgets, Docker deploy
Major changes:
- Add Go backend (backend/) with microservices architecture
- Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler,
  proxy-manager, media-search, fastClassifier, language detection
- New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard,
  UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions
- Improved discover-svc with discover-db integration
- Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md)
- Library-svc: project_id schema migration
- Remove deprecated finance-svc and travel-svc
- Localization improvements across services

Made-with: Cursor
2026-02-27 04:15:32 +03:00

154 lines
6.3 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.
'use client';
import { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion';
import { RefreshCw, ExternalLink, Loader2, Sparkles, Globe, Cpu, DollarSign, Dumbbell } from 'lucide-react';
import * as Tabs from '@radix-ui/react-tabs';
import { fetchDiscover } from '@/lib/api';
import type { DiscoverItem } from '@/lib/types';
const topics = [
{ id: 'tech', label: 'Технологии', icon: Cpu },
{ id: 'finance', label: 'Финансы', icon: DollarSign },
{ id: 'sports', label: 'Спорт', icon: Dumbbell },
] as const;
export default function DiscoverPage() {
const [items, setItems] = useState<DiscoverItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [topic, setTopic] = useState<string>('tech');
const load = useCallback(async () => {
setIsLoading(true);
try {
const data = await fetchDiscover(topic, 'russia');
setItems(data);
} catch {
setItems([]);
} finally {
setIsLoading(false);
}
}, [topic]);
useEffect(() => {
load();
}, [load]);
return (
<div className="h-full overflow-y-auto">
<div className="max-w-3xl mx-auto px-4 sm:px-6 py-6 sm:py-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6 sm:mb-8">
<div>
<h1 className="text-xl sm:text-2xl font-semibold text-primary mb-1 sm:mb-2">Discover</h1>
<p className="text-sm text-secondary">Актуальные новости и события</p>
</div>
<button
onClick={load}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2.5 text-sm text-secondary hover:text-primary bg-surface/40 border border-border/50 hover:border-accent/30 rounded-xl transition-all w-fit"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
<span className="hidden sm:inline">Обновить</span>
</button>
</div>
{/* Topic Tabs */}
<Tabs.Root value={topic} onValueChange={setTopic}>
<Tabs.List className="flex gap-2 mb-6 sm:mb-8 overflow-x-auto pb-1 -mx-4 px-4 sm:mx-0 sm:px-0">
{topics.map((t) => (
<Tabs.Trigger
key={t.id}
value={t.id}
className="flex items-center gap-2 px-4 py-2.5 text-sm rounded-xl border transition-all whitespace-nowrap data-[state=active]:bg-accent/10 data-[state=active]:border-accent/30 data-[state=active]:text-primary bg-surface/30 border-border/30 text-muted hover:text-secondary hover:border-border"
>
<t.icon className="w-4 h-4" />
{t.label}
</Tabs.Trigger>
))}
</Tabs.List>
</Tabs.Root>
{/* Content */}
{isLoading ? (
<div className="flex flex-col items-center justify-center py-16 sm:py-20">
<div className="relative">
<Loader2 className="w-8 h-8 animate-spin text-accent/60" />
<div className="absolute inset-0 blur-xl bg-accent/20" />
</div>
<p className="text-sm text-muted mt-4">Загрузка новостей...</p>
</div>
) : items.length === 0 ? (
<div className="text-center py-16 sm:py-20">
<Globe className="w-12 h-12 mx-auto mb-4 text-muted" />
<p className="text-secondary">Нет новостей по выбранной теме</p>
</div>
) : (
<div className="space-y-3 sm:space-y-4">
{items.map((item, i) => (
<motion.article
key={i}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.05 }}
className="group relative flex flex-col sm:flex-row gap-3 sm:gap-4 p-3 sm:p-4 bg-elevated/40 border border-border/40 rounded-xl hover:border-accent/25 hover:bg-elevated/60 transition-all duration-200"
>
{/* Thumbnail */}
{item.thumbnail && (
<div className="relative w-full sm:w-28 h-40 sm:h-20 rounded-lg overflow-hidden flex-shrink-0 bg-surface/50">
<img
src={item.thumbnail}
alt=""
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
</div>
)}
{/* Content */}
<div className="flex-1 min-w-0">
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-primary line-clamp-2 group-hover:text-accent transition-colors mb-2"
>
{item.title}
</a>
<p className="text-xs text-muted line-clamp-2 mb-3">
{item.content}
</p>
{/* Actions */}
<div className="flex flex-wrap items-center gap-3 sm:gap-4">
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-xs text-secondary hover:text-primary transition-colors"
>
<ExternalLink className="w-3.5 h-3.5" />
Читать
</a>
<button className="flex items-center gap-1.5 text-xs text-accent/80 hover:text-accent transition-colors">
<Sparkles className="w-3.5 h-3.5" />
AI Саммари
</button>
{item.sourcesCount && item.sourcesCount > 1 && (
<span className="text-xs text-faint">
{item.sourcesCount} источников
</span>
)}
</div>
</div>
</motion.article>
))}
</div>
)}
</div>
</div>
);
}