From eba6ea9b8de79a3156b2bfb0c0c1443b25150b15 Mon Sep 17 00:00:00 2001 From: Harun CAN Date: Sat, 14 Mar 2026 14:01:11 +0300 Subject: [PATCH] main --- src/app/[locale]/(dashboard)/trends/page.tsx | 51 +- src/app/[locale]/layout.tsx | 7 +- src/app/api/auth/[...nextauth]/route.ts | 95 +-- src/app/api/backend/[...path]/route.ts | 23 +- .../content/ContentPreviewDialog.tsx | 156 +++- src/components/content/ContentTable.tsx | 97 ++- src/components/generate/GenerateWizard.tsx | 203 ++++- .../generate/GeneratedContentResult.tsx | 793 ++++++++++++++++-- src/components/ui/provider.tsx | 5 +- src/lib/auth-options.ts | 98 +++ tsconfig.tsbuildinfo | 2 +- 11 files changed, 1243 insertions(+), 287 deletions(-) create mode 100644 src/lib/auth-options.ts diff --git a/src/app/[locale]/(dashboard)/trends/page.tsx b/src/app/[locale]/(dashboard)/trends/page.tsx index 95533ab..b753e07 100644 --- a/src/app/[locale]/(dashboard)/trends/page.tsx +++ b/src/app/[locale]/(dashboard)/trends/page.tsx @@ -3,7 +3,7 @@ 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 { useState, useEffect, useCallback } from "react"; import { useSession } from "next-auth/react"; import { useRouter } from "@/i18n/navigation"; @@ -19,6 +19,16 @@ interface Trend { url?: string; } +const TRENDS_CACHE_KEY = 'ch_trendAnalysisCache'; + +interface TrendsCache { + niche: string; + trends: Trend[]; + scanCount: number; + translatedContent: Record; + timestamp: number; +} + export default function TrendsPage() { const { data: session } = useSession(); const router = useRouter(); @@ -31,6 +41,45 @@ export default function TrendsPage() { const [translatingId, setTranslatingId] = useState(null); const [translatedContent, setTranslatedContent] = useState>({}); + // Load cached trend data from sessionStorage on mount + useEffect(() => { + try { + const cached = sessionStorage.getItem(TRENDS_CACHE_KEY); + if (cached) { + const data: TrendsCache = JSON.parse(cached); + if (Date.now() - data.timestamp < 30 * 60 * 1000) { + setNiche(data.niche || ""); + setTrends(data.trends || []); + setScanCount(data.scanCount || 0); + setTranslatedContent(data.translatedContent || {}); + } + } + } catch (e) { + console.warn('Failed to load cached trends:', e); + } + }, []); + + // Persist trend data to sessionStorage whenever it changes + const persistToSession = useCallback(() => { + if (trends.length === 0 && !niche) return; + try { + const cache: TrendsCache = { + niche, + trends, + scanCount, + translatedContent, + timestamp: Date.now(), + }; + sessionStorage.setItem(TRENDS_CACHE_KEY, JSON.stringify(cache)); + } catch (e) { + console.warn('Failed to cache trends:', e); + } + }, [niche, trends, scanCount, translatedContent]); + + useEffect(() => { + persistToSession(); + }, [persistToSession]); + const handleScanTrends = async () => { if (!niche.trim()) { toaster.create({ title: "Please enter a niche or topic", type: "warning" }); diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 85f0838..5938c77 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -11,6 +11,9 @@ const bricolage = Bricolage_Grotesque({ subsets: ['latin'], }); +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth-options"; + export default async function RootLayout({ children, params, @@ -23,6 +26,8 @@ export default async function RootLayout({ notFound(); } + const session = await getServerSession(authOptions); + return ( @@ -33,7 +38,7 @@ export default async function RootLayout({ - {children} + {children} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 55dfbc1..62a9268 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,97 +1,6 @@ -import baseUrl from "@/config/base-url"; -import { authService } from "@/lib/api/example/auth/service"; import NextAuth from "next-auth"; -import Credentials from "next-auth/providers/credentials"; +import { authOptions } from "@/lib/auth-options"; - - - -function randomToken() { - return Math.random().toString(36).substring(2) + Date.now().toString(36); -} - -const isMockMode = process.env.NEXT_PUBLIC_ENABLE_MOCK_MODE === "true"; - -const handler = NextAuth({ - providers: [ - Credentials({ - name: "Credentials", - credentials: { - email: { label: "Email", type: "text" }, - password: { label: "Password", type: "password" }, - }, - async authorize(credentials) { - console.log("credentials", credentials); - if (!credentials?.email || !credentials?.password) { - throw new Error("Email ve şifre gereklidir."); - } - - // Eğer mock mod aktifse backend'e gitme - if (isMockMode) { - return { - id: credentials.email, - name: credentials.email.split("@")[0], - email: credentials.email, - accessToken: randomToken(), - refreshToken: randomToken(), - }; - } - - // Normal mod: backend'e istek at - const res = await authService.login({ - email: credentials.email, - password: credentials.password, - }); - - console.log("res", res); - - const response = res; - - // Backend returns ApiResponse - // Structure: { data: { accessToken, refreshToken, expiresIn, user }, message, statusCode } - if (!res.success || !response?.data?.accessToken) { - throw new Error(response?.message || "Giriş başarısız"); - } - - const { accessToken, refreshToken, user } = response.data; - - return { - id: user.id, - name: user.firstName - ? `${user.firstName} ${user.lastName || ""}`.trim() - : user.email.split("@")[0], - email: user.email, - accessToken, - refreshToken, - roles: user.roles || [], - }; - }, - }), - ], - callbacks: { - async jwt({ token, user }: any) { - if (user) { - token.accessToken = user.accessToken; - token.refreshToken = user.refreshToken; - token.id = user.id; - token.roles = user.roles; - } - return token; - }, - async session({ session, token }: any) { - session.user.id = token.id; - session.user.roles = token.roles; - session.accessToken = token.accessToken; - session.refreshToken = token.refreshToken; - return session; - }, - }, - pages: { - signIn: "/signin", - error: "/signin", - }, - session: { strategy: "jwt" }, - secret: process.env.NEXTAUTH_SECRET, -}); +const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; diff --git a/src/app/api/backend/[...path]/route.ts b/src/app/api/backend/[...path]/route.ts index 084a004..1f0c546 100644 --- a/src/app/api/backend/[...path]/route.ts +++ b/src/app/api/backend/[...path]/route.ts @@ -3,14 +3,12 @@ import { NextRequest, NextResponse } from 'next/server'; -const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3001'; +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000'; async function proxyRequest(request: NextRequest, pathSegments: string[]) { const targetPath = pathSegments.join('/'); const targetUrl = `${BACKEND_URL}/api/${targetPath}`; - console.log(`[Proxy] Forwarding ${request.method} to ${targetUrl}`); - // Get request body for non-GET requests let body: string | undefined; if (request.method !== 'GET' && request.method !== 'HEAD') { @@ -73,25 +71,30 @@ async function proxyRequest(request: NextRequest, pathSegments: string[]) { } } -// Next.js 16 uses context.params directly (not a Promise) -type RouteContext = { params: { path: string[] } }; +// Next.js 15+ uses context.params as a Promise +type RouteContext = { params: Promise<{ path: string[] }> }; export async function GET(request: NextRequest, context: RouteContext) { - return proxyRequest(request, context.params.path); + const { path } = await context.params; + return proxyRequest(request, path); } export async function POST(request: NextRequest, context: RouteContext) { - return proxyRequest(request, context.params.path); + const { path } = await context.params; + return proxyRequest(request, path); } export async function PUT(request: NextRequest, context: RouteContext) { - return proxyRequest(request, context.params.path); + const { path } = await context.params; + return proxyRequest(request, path); } export async function PATCH(request: NextRequest, context: RouteContext) { - return proxyRequest(request, context.params.path); + const { path } = await context.params; + return proxyRequest(request, path); } export async function DELETE(request: NextRequest, context: RouteContext) { - return proxyRequest(request, context.params.path); + const { path } = await context.params; + return proxyRequest(request, path); } diff --git a/src/components/content/ContentPreviewDialog.tsx b/src/components/content/ContentPreviewDialog.tsx index 53a53b0..fb57f8b 100644 --- a/src/components/content/ContentPreviewDialog.tsx +++ b/src/components/content/ContentPreviewDialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { Box, Text, VStack, HStack, Badge, Separator } from "@chakra-ui/react"; +import { Box, Text, VStack, HStack, Badge, Separator, Button, Textarea } from "@chakra-ui/react"; import { DialogBody, DialogCloseTrigger, @@ -9,24 +9,32 @@ import { DialogRoot, DialogTitle, } from "@/components/ui/overlays/dialog"; -import { LuCalendar, LuGlobe, LuFileText } from "react-icons/lu"; +import { LuCalendar, LuGlobe, LuFileText, LuPencil, LuSave, LuHash, LuCopy, LuCheck } from "react-icons/lu"; +import { useState } from "react"; +import { useSession } from "next-auth/react"; +import { toaster } from "@/components/ui/feedback/toaster"; interface ContentItem { - id: number; + id: string; title: string; - platform: string; + platform?: string; + type?: string; status: string; - date: string; + date?: string; + createdAt?: string; + body?: string; + hashtags?: string[]; } interface ContentPreviewDialogProps { item: ContentItem | null; open: boolean; onOpenChange: (details: { open: boolean }) => void; + onContentUpdated?: () => void; } const getStatusColor = (status: string) => { - switch (status) { + switch (status?.toLowerCase()) { case "published": return "green"; case "draft": return "gray"; case "scheduled": return "blue"; @@ -35,45 +43,143 @@ const getStatusColor = (status: string) => { } }; -export function ContentPreviewDialog({ item, open, onOpenChange }: ContentPreviewDialogProps) { +export function ContentPreviewDialog({ item, open, onOpenChange, onContentUpdated }: ContentPreviewDialogProps) { + const { data: session } = useSession(); + const [isEditing, setIsEditing] = useState(false); + const [editedBody, setEditedBody] = useState(""); + const [isSaving, setIsSaving] = useState(false); + const [copied, setCopied] = useState(false); + if (!item) return null; + const displayBody = item.body || "No content body available."; + const displayPlatform = item.type || item.platform || "Unknown"; + const displayDate = item.createdAt || item.date || ""; + + const handleStartEdit = () => { + setEditedBody(displayBody); + setIsEditing(true); + }; + + const handleSave = async () => { + if (!item.id || !session?.accessToken) return; + setIsSaving(true); + try { + const res = await fetch(`/api/backend/content/${item.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${session.accessToken}`, + }, + body: JSON.stringify({ body: editedBody }), + }); + if (!res.ok) throw new Error('Failed to update'); + toaster.create({ title: "Saved!", description: "Content updated.", type: "success" }); + setIsEditing(false); + onContentUpdated?.(); + } catch (err) { + toaster.create({ title: "Save failed", type: "error" }); + } finally { + setIsSaving(false); + } + }; + + const handleCopy = () => { + const text = isEditing ? editedBody : displayBody; + const hashtags = item.hashtags?.join(" ") || ""; + navigator.clipboard.writeText(hashtags ? `${text}\n\n${hashtags}` : text); + setCopied(true); + toaster.create({ title: "Copied!", type: "success", duration: 2000 }); + setTimeout(() => setCopied(false), 2000); + }; + return ( - {item.title} + {item.title || "Content Preview"} - + - {item.status.toUpperCase()} + {item.status?.toUpperCase()} - {item.platform} - - - - {item.date} + {displayPlatform} + {displayDate && ( + + + {new Date(displayDate).toLocaleDateString()} + + )} - - - - Content Preview + + + + + Content + + + + {isEditing ? ( + + ) : ( + + )} + - - This is a preview of the content for "{item.title}". In a real scenario, this would show the full post text, images, or video script generated by the system. - - - Generated content will appear here... - + + {isEditing ? ( +