diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx b/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx index 96ea435..89446b4 100644 --- a/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx @@ -17,6 +17,13 @@ import { MoreVertical, X, Languages, + Search, + Tag, + Copy, + Check, + TrendingUp, + Zap, + Hash, } from 'lucide-react'; import Link from 'next/link'; import { useState, useEffect } from 'react'; @@ -29,7 +36,9 @@ import { useGenerateSceneImage, useUpscaleSceneImage, useRegenerateScene, - useCancelRender + useCancelRender, + useGenerateSeoTitles, + useSelectSeoTitle } from '@/hooks/use-api'; import { useRenderProgress } from '@/hooks/use-render-progress'; import { SceneCard } from '@/components/project/scene-card'; @@ -47,6 +56,18 @@ const XIcon = ({ size = 16 }: { size?: number }) => ( ); +const YouTubeIcon = ({ size = 16 }: { size?: number }) => ( + + + +); + +const InstagramIcon = ({ size = 16 }: { size?: number }) => ( + + + +); + const STATUS_MAP: Record = { DRAFT: { label: 'Taslak', color: 'text-neutral-400', icon: FileText, bgClass: 'bg-neutral-500/10 border-neutral-500/20' }, GENERATING_SCRIPT: { label: 'Senaryo Üretiliyor', color: 'text-neutral-300', icon: Sparkles, bgClass: 'bg-neutral-500/10 border-neutral-500/20' }, @@ -139,7 +160,7 @@ export default function ProjectDetailPage() { try { setIsTranslating(true); const res = await apiClient.post(`/projects/${id}/translate`, { targetLanguage }); - toast.success("Proje başarıyla çevrildi!"); + toast.success({ title: "Proje başarıyla çevrildi!" }); setShowTranslateModal(false); setTargetLanguage(""); // refetch() to maybe update some states, or router.push to the new project @@ -172,9 +193,13 @@ export default function ProjectDetailPage() { const generateImageMutation = useGenerateSceneImage(); const upscaleImageMutation = useUpscaleSceneImage(); const regenerateSceneMutation = useRegenerateScene(); + const seoTitlesMutation = useGenerateSeoTitles(); + const selectTitleMutation = useSelectSeoTitle(); const [generatingImageId, setGeneratingImageId] = useState(null); const [upscalingImageId, setUpscalingImageId] = useState(null); + const [copiedField, setCopiedField] = useState(null); + const [activeCaptionTab, setActiveCaptionTab] = useState<'youtube' | 'tiktok' | 'instagram' | 'twitter'>('youtube'); // WebSocket progress const renderState = useRenderProgress( @@ -234,6 +259,40 @@ export default function ProjectDetailPage() { }); }; + // Panoya kopyala + const copyToClipboard = (text: string, field: string) => { + navigator.clipboard.writeText(text); + setCopiedField(field); + toast.success({ title: 'Panoya kopyalandı!' }); + setTimeout(() => setCopiedField(null), 2000); + }; + + // SEO başlıkları üret + const handleGenerateSeoTitles = () => { + seoTitlesMutation.mutate(id, { + onSuccess: () => { + refetch(); + toast.success({ title: '5 yeni SEO başlığı üretildi!' }); + }, + onError: () => { + toast.error({ title: 'SEO başlık üretimi başarısız.' }); + }, + }); + }; + + // SEO başlık seç + const handleSelectTitle = (title: string) => { + selectTitleMutation.mutate({ projectId: id, title }, { + onSuccess: () => { + refetch(); + toast.success({ title: 'Başlık güncellendi!' }); + }, + onError: () => { + toast.error({ title: 'Başlık güncellenemedi.' }); + }, + }); + }; + // Onayla ve gönder const handleApprove = () => { approveMutation.mutate(id, { @@ -612,6 +671,283 @@ export default function ProjectDetailPage() { )} + {/* ── SEO & Sosyal Medya Power Engine ── */} + {hasScript && ( + +
+ {/* Header */} +
+

+ + SEO & Sosyal Medya +

+ {project.seoScore != null && ( +
+
+ + + = 80 ? '#34d399' : project.seoScore >= 50 ? '#fbbf24' : '#f87171'} + strokeWidth="3" + strokeDasharray={`${project.seoScore}, 100`} + strokeLinecap="round" + /> + + + {project.seoScore} + +
+ SEO
Skoru
+
+ )} +
+ + {/* ─── Başlık Yönetimi ─── */} +
+
+

+ 📌 SEO Başlıkları +

+ +
+ + {/* Mevcut başlık */} +
+
+ + Aktif Başlık +
+

{project.seoTitle || project.title}

+
+ + {/* Alternatif başlıklar */} + {project.seoTitleAlts && project.seoTitleAlts.length > 0 && ( +
+ {project.seoTitleAlts.map((alt, i) => ( + + ))} +
+ )} +
+ + {/* ─── SEO Keywords ─── */} + {project.seoKeywords && project.seoKeywords.length > 0 && ( +
+

+ + Anahtar Kelimeler ({project.seoKeywords.length}) +

+
+ {project.seoKeywords.map((kw, i) => { + const colors = [ + 'bg-blue-500/15 text-blue-300 border-blue-500/20', + 'bg-emerald-500/15 text-emerald-300 border-emerald-500/20', + 'bg-amber-500/15 text-amber-300 border-amber-500/20', + 'bg-violet-500/15 text-violet-300 border-violet-500/20', + 'bg-rose-500/15 text-rose-300 border-rose-500/20', + 'bg-cyan-500/15 text-cyan-300 border-cyan-500/20', + ]; + return ( + + + {kw} + + ); + })} +
+
+ )} + + {/* ─── Hashtag'ler ─── */} + {project.scriptJson?.seo?.hashtags && project.scriptJson.seo.hashtags.length > 0 && ( +
+
+

+ + Hashtag'ler +

+ +
+
+ {project.scriptJson.seo.hashtags.map((tag: string, i: number) => ( + + #{tag} + + ))} + {/* Trending hashtag'ler */} + {project.scriptJson.seo.trendingHashtags?.map((tag: string, i: number) => ( + + + #{tag} + + ))} +
+
+ )} + + {/* ─── Sosyal Medya Caption'ları ─── */} + {project.socialContent && ( +
+

+ + Sosyal Medya İçerikleri +

+ + {/* Tab navigasyonu */} +
+ {[ + { key: 'youtube' as const, label: 'YouTube', icon: YouTubeIcon, color: 'text-red-400' }, + { key: 'tiktok' as const, label: 'TikTok', icon: Film, color: 'text-cyan-400' }, + { key: 'instagram' as const, label: 'Instagram', icon: InstagramIcon, color: 'text-pink-400' }, + { key: 'twitter' as const, label: 'X', icon: XIcon, color: 'text-white' }, + ].map((tab) => ( + + ))} +
+ + {/* Caption içeriği */} +
+ {activeCaptionTab === 'youtube' && ( +
+ {project.socialContent.youtubeTitle && ( +
+
+ Başlık + +
+

{project.socialContent.youtubeTitle}

+
+ )} + {project.socialContent.youtubeDescription && ( +
+
+ Açıklama + +
+

{project.socialContent.youtubeDescription}

+
+ )} +
+ )} + + {activeCaptionTab === 'tiktok' && project.socialContent.tiktokCaption && ( +
+
+ TikTok Caption + +
+

{project.socialContent.tiktokCaption}

+
+ )} + + {activeCaptionTab === 'instagram' && project.socialContent.instagramCaption && ( +
+
+ Instagram Caption + +
+

{project.socialContent.instagramCaption}

+
+ )} + + {activeCaptionTab === 'twitter' && project.socialContent.twitterText && ( +
+
+ X (Twitter) Paylaşımı + +
+

{project.socialContent.twitterText}

+
+ )} +
+
+ )} + + {/* ─── SEO Açıklama ─── */} + {project.seoDescription && ( +
+
+ Meta Description + +
+

{project.seoDescription}

+
+ )} +
+
+ )} + {/* ── Boş durum (senaryo yok) ── */} {!hasScript && isEditable && ( diff --git a/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx b/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx index 419eff9..1bc32f9 100644 --- a/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx @@ -107,7 +107,7 @@ export default function ProjectsPage() { try { setIsTranslating(true); const res = await apiClient.post(`/projects/${translateTarget.id}/translate`, { targetLanguage }); - toast.success("Proje başarıyla çevrildi!"); + toast.success({ title: "Proje başarıyla çevrildi!" }); setTranslateTarget(null); setTargetLanguage(""); refetch(); diff --git a/src/hooks/use-api.ts b/src/hooks/use-api.ts index 7f1d9b3..27b5e5f 100644 --- a/src/hooks/use-api.ts +++ b/src/hooks/use-api.ts @@ -182,6 +182,31 @@ export function useCancelRender() { }); } +/** AI ile 5 yeni SEO başlığı üret */ +export function useGenerateSeoTitles() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (projectId: string) => + projectsApi.generateSeoTitles(projectId), + onSuccess: (_data, projectId) => { + qc.invalidateQueries({ queryKey: queryKeys.projects.detail(projectId) }); + }, + }); +} + +/** Alternatif SEO başlıklarından birini seç ve proje başlığını güncelle */ +export function useSelectSeoTitle() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ projectId, title }: { projectId: string; title: string }) => + projectsApi.selectSeoTitle(projectId, title), + onSuccess: (_data, variables) => { + qc.invalidateQueries({ queryKey: queryKeys.projects.detail(variables.projectId) }); + qc.invalidateQueries({ queryKey: queryKeys.projects.all }); + }, + }); +} + /** Sahne güncelleme (narrasyon, prompt) */ export function useUpdateScene() { const qc = useQueryClient(); diff --git a/src/lib/api/api-service.ts b/src/lib/api/api-service.ts index 72ce672..930eefa 100644 --- a/src/lib/api/api-service.ts +++ b/src/lib/api/api-service.ts @@ -30,6 +30,19 @@ export interface Project { renderJobs?: RenderJob[]; sourceType?: 'MANUAL' | 'X_TWEET' | 'YOUTUBE'; sourceTweetData?: Record; + // SEO Power Engine + seoTitle?: string; + seoDescription?: string; + seoKeywords?: string[]; + seoTitleAlts?: string[]; + seoScore?: number; + socialContent?: { + youtubeTitle?: string; + youtubeDescription?: string; + tiktokCaption?: string; + instagramCaption?: string; + twitterText?: string; + }; createdAt: string; updatedAt: string; completedAt?: string; @@ -99,13 +112,17 @@ export interface ScriptJson { language: string; hashtags: string[]; }; - seo?: { + seo: { title: string; description: string; keywords: string[]; hashtags: string[]; + trendingHashtags?: string[]; + estimatedSearchVolume?: string; schemaMarkup: Record; }; + seoTitleAlternatives?: string[]; + seoScore?: number; scenes: Array<{ order: number; title?: string; @@ -363,6 +380,17 @@ export const projectsApi = { `/projects/${id}/cancel-render`, ).then((r) => r.data), + generateSeoTitles: (id: string) => + apiClient.post<{ titles: string[]; seoScore: number; currentTitle: string }>( + `/projects/${id}/generate-seo-titles`, + ).then((r) => r.data), + + selectSeoTitle: (id: string, title: string) => + apiClient.patch( + `/projects/${id}/select-title`, + { title }, + ).then((r) => r.data), + getRenderQueue: () => apiClient.get('/projects/render-queue').then((r) => r.data),