Files
Content-Hunter_FE/src/app/[locale]/(dashboard)/trends/page.tsx
Harun CAN 05435cbaf8
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Failing after 2m44s
main
2026-02-10 12:28:10 +03:00

372 lines
16 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 { 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>
);
}