generated from fahricansecer/boilerplate-fe
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Failing after 2m44s
372 lines
16 KiB
TypeScript
372 lines
16 KiB
TypeScript
"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>
|
||
);
|
||
}
|