main
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Failing after 2m44s

This commit is contained in:
Harun CAN
2026-02-10 12:28:10 +03:00
parent c5804e3b53
commit 05435cbaf8
35 changed files with 2635 additions and 261 deletions

View File

@@ -0,0 +1,371 @@
"use client";
import { Box, Heading, VStack, Text, Card, HStack, Badge, Button, Input, SimpleGrid, Spinner, Icon } from "@chakra-ui/react";
import { LuTrendingUp, LuSearch, LuSparkles, LuRefreshCw, LuArrowRight, LuGlobe, LuHash, LuZoomIn } from "react-icons/lu";
import { toaster } from "@/components/ui/feedback/toaster";
import { useState } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "@/i18n/navigation";
interface Trend {
id: string;
title: string;
description?: string;
score: number;
volume?: number;
source: string;
keywords: string[];
relatedTopics: string[];
url?: string;
}
export default function TrendsPage() {
const { data: session } = useSession();
const router = useRouter();
const [niche, setNiche] = useState("");
const [trends, setTrends] = useState<Trend[]>([]);
const [isScanning, setIsScanning] = useState(false);
const [isDeepScanning, setIsDeepScanning] = useState(false);
const [selectedTrend, setSelectedTrend] = useState<Trend | null>(null);
const [scanCount, setScanCount] = useState(0);
const [translatingId, setTranslatingId] = useState<string | null>(null);
const [translatedContent, setTranslatedContent] = useState<Record<string, { title: string; description?: string }>>({});
const handleScanTrends = async () => {
if (!niche.trim()) {
toaster.create({ title: "Please enter a niche or topic", type: "warning" });
return;
}
setIsScanning(true);
try {
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (session?.accessToken) {
headers['Authorization'] = `Bearer ${session.accessToken}`;
}
const res = await fetch('/api/backend/trends/scan', {
method: 'POST',
headers,
body: JSON.stringify({
keywords: [niche],
country: 'TR',
}),
});
if (res.ok) {
const response = await res.json();
// Backend wraps response in { success, status, message, data }
const trendsData = response.data || response.trends || response;
const trendsArray = Array.isArray(trendsData) ? trendsData : [];
setTrends(trendsArray);
setScanCount(1);
toaster.create({
title: `${trendsArray.length} trend bulundu`,
type: trendsArray.length > 0 ? "success" : "info"
});
} else {
throw new Error('Failed to scan trends');
}
} catch (error) {
console.error('Scan error:', error);
toaster.create({ title: "Failed to scan trends", type: "error" });
} finally {
setIsScanning(false);
}
};
// DEEPER RESEARCH - Prepends new results and uses all languages
const handleDeepResearch = async () => {
if (!niche.trim()) {
toaster.create({ title: "Please enter a niche or topic first", type: "warning" });
return;
}
setIsDeepScanning(true);
try {
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (session?.accessToken) {
headers['Authorization'] = `Bearer ${session.accessToken}`;
}
// Use different keywords for deeper research
const deepKeywords = [
niche,
`${niche} trends`,
`${niche} news`,
`latest ${niche}`,
];
const res = await fetch('/api/backend/trends/scan', {
method: 'POST',
headers,
body: JSON.stringify({
keywords: deepKeywords,
country: 'TR',
allLanguages: true, // Fetch global trends
}),
});
if (res.ok) {
const response = await res.json();
const trendsData = response.data || response.trends || response;
const newTrends = Array.isArray(trendsData) ? trendsData : [];
// Prepend new trends, filter duplicates by title
const existingTitles = new Set(trends.map(t => t.title.toLowerCase()));
const uniqueNewTrends = newTrends.filter(
(t: Trend) => !existingTitles.has(t.title.toLowerCase())
);
setTrends(prev => [...uniqueNewTrends, ...prev]);
setScanCount(prev => prev + 1);
toaster.create({
title: `${uniqueNewTrends.length} yeni trend eklendi (Üste eklendi)`,
description: `Toplam: ${trends.length + uniqueNewTrends.length} trend`,
type: uniqueNewTrends.length > 0 ? "success" : "info"
});
} else {
throw new Error('Failed to deep scan');
}
} catch (error) {
console.error('Deep scan error:', error);
toaster.create({ title: "Derin araştırma başarısız", type: "error" });
} finally {
setIsDeepScanning(false);
}
};
const handleTranslate = async (trend: Trend) => {
if (translatedContent[trend.id]) return;
setTranslatingId(trend.id);
try {
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (session?.accessToken) {
headers['Authorization'] = `Bearer ${session.accessToken}`;
}
// Translate title and description together for efficiency
const textToTranslate = `${trend.title}\n---\n${trend.description || ""}`;
const res = await fetch('/api/backend/trends/translate', {
method: 'POST',
headers,
body: JSON.stringify({
text: textToTranslate,
targetLanguage: 'Turkish',
}),
});
if (res.ok) {
const responseData = await res.json();
// Handle potential 'data' wrapping from NestJS interceptors
const data = responseData.data || responseData;
const translatedText = data.translatedText;
if (translatedText && typeof translatedText === 'string') {
const [newTitle, ...descParts] = translatedText.split('\n---\n');
setTranslatedContent(prev => ({
...prev,
[trend.id]: {
title: newTitle.trim(),
description: descParts.join('\n---\n').trim(),
}
}));
} else {
throw new Error('Invalid translation response');
}
}
} catch (error) {
console.error('Translation error:', error);
toaster.create({ title: "Çeviri başarısız", type: "error" });
} finally {
setTranslatingId(null);
}
};
const handleCreateContent = (trend: Trend) => {
// Navigate to generate page with pre-filled trend data
const params = new URLSearchParams();
params.set('topic', trend.title);
if (trend.description) {
params.set('description', trend.description);
}
if (trend.keywords && trend.keywords.length > 0) {
params.set('keywords', JSON.stringify(trend.keywords));
}
if (trend.source) {
params.set('source', trend.source);
}
router.push(`/generate?${params.toString()}`);
};
return (
<VStack align="stretch" gap={6} p={6}>
{/* Header */}
<HStack justify="space-between">
<Box>
<Heading size="xl">Trend Araştırması</Heading>
<Text color="fg.muted">Güncel trendleri keşfet ve içerik oluştur</Text>
</Box>
</HStack>
{/* Search Box */}
<Card.Root>
<Card.Body>
<VStack gap={4}>
<HStack w="100%" gap={4}>
<Input
placeholder="Niş veya konu girin (örn: yapay zeka, kripto, fitness)"
value={niche}
onChange={(e) => setNiche(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleScanTrends()}
flex={1}
/>
<Button
colorPalette="blue"
onClick={handleScanTrends}
disabled={isScanning}
>
{isScanning ? <Spinner size="sm" /> : <LuSearch />}
{isScanning ? "Taranıyor..." : "Trend Tara"}
</Button>
</HStack>
<Text fontSize="sm" color="fg.muted">
Google Trends, Twitter, Reddit ve haber kaynaklarından güncel trendleri tarar
</Text>
</VStack>
</Card.Body>
</Card.Root>
{/* Deeper Research Button - Shows after initial scan */}
{trends.length > 0 && (
<HStack justify="space-between" align="center">
<Text color="fg.muted">
{trends.length} trend bulundu (Tarama #{scanCount})
</Text>
<Button
colorPalette="purple"
variant="outline"
onClick={handleDeepResearch}
disabled={isDeepScanning || isScanning}
>
{isDeepScanning ? <Spinner size="sm" /> : <LuZoomIn />}
{isDeepScanning ? "Derin Araştırma Yapılıyor..." : "Daha Derin Araştır"}
</Button>
</HStack>
)}
{/* Results */}
{trends.length > 0 && (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} gap={4}>
{trends.map((trend) => (
<Card.Root
key={trend.id}
cursor="pointer"
borderWidth={selectedTrend?.id === trend.id ? "2px" : "1px"}
borderColor={selectedTrend?.id === trend.id ? "blue.500" : "border.subtle"}
onClick={() => setSelectedTrend(trend)}
_hover={{ shadow: "md" }}
>
<Card.Header pb={2}>
<HStack justify="space-between">
<Badge colorPalette="blue" variant="subtle">
<LuTrendingUp /> {Math.round(trend.score)}
</Badge>
<Badge variant="outline">{trend.source}</Badge>
</HStack>
</Card.Header>
<Card.Body pt={0}>
<VStack align="start" gap={2}>
<Heading size="md">
{translatedContent[trend.id]?.title || trend.title}
</Heading>
{(translatedContent[trend.id]?.description || trend.description) && (
<Text fontSize="sm" color="fg.muted" lineClamp={2}>
{translatedContent[trend.id]?.description || trend.description}
</Text>
)}
{translatedContent[trend.id] && (
<Badge colorPalette="orange" size="xs" variant="outline">
AI Çeviri
</Badge>
)}
{trend.keywords.length > 0 && (
<HStack flexWrap="wrap" gap={1}>
{trend.keywords.slice(0, 3).map((kw, i) => (
<Badge key={i} size="sm" variant="subtle">
<LuHash /> {kw}
</Badge>
))}
</HStack>
)}
<HStack w="100%" justify="space-between" pt={2} flexWrap="wrap">
<HStack gap={2}>
{trend.url && (
<Button
size="sm"
variant="ghost"
paddingInline={2}
onClick={(e) => {
e.stopPropagation();
window.open(trend.url, '_blank');
}}
>
<LuGlobe /> Kaynak
</Button>
)}
{!translatedContent[trend.id] && trend.relatedTopics?.some(t => ['EN', 'DE'].includes(t)) && (
<Button
size="sm"
variant="ghost"
colorPalette="blue"
loading={translatingId === trend.id}
onClick={(e) => {
e.stopPropagation();
handleTranslate(trend);
}}
>
<LuRefreshCw /> Çevir
</Button>
)}
</HStack>
<Button
size="sm"
colorPalette="green"
onClick={(e) => {
e.stopPropagation();
handleCreateContent(trend);
}}
>
<LuSparkles /> İçerik Oluştur
</Button>
</HStack>
</VStack>
</Card.Body>
</Card.Root>
))}
</SimpleGrid>
)}
{/* Empty State */}
{!isScanning && trends.length === 0 && (
<Card.Root>
<Card.Body>
<VStack py={10} gap={4}>
<Icon as={LuTrendingUp} boxSize={12} color="fg.muted" />
<Text color="fg.muted" textAlign="center">
Henüz trend taraması yapılmadı.<br />
Bir niş veya konu girerek başlayın.
</Text>
</VStack>
</Card.Body>
</Card.Root>
)}
</VStack>
);
}