generated from fahricansecer/boilerplate-fe
main
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Failing after 2m44s
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Failing after 2m44s
This commit is contained in:
371
src/app/[locale]/(dashboard)/trends/page.tsx
Normal file
371
src/app/[locale]/(dashboard)/trends/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user