Compare commits

...

11 Commits

Author SHA1 Message Date
Harun CAN ecf0612205 main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-05-11 08:00:13 +02:00
Harun CAN fc5ceeebb6 main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-05-11 07:32:47 +02:00
Harun CAN 4b1abf1996 main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-05-09 05:58:09 +02:00
Harun CAN 1f8f24fcf5 main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-05-06 10:47:57 +02:00
Harun CAN d3a83bf901 main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-05-01 01:12:21 +02:00
Harun CAN ff76ead6d4 main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-05-01 00:45:20 +02:00
Harun CAN 565a7ba3b9 main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-04-30 17:12:27 +02:00
Harun CAN bc9a1587a8 fix: toast api call syntax
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-04-30 16:48:44 +02:00
Harun CAN 7d161fdb3d main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-04-30 16:08:37 +02:00
Harun CAN 51ec6bd0fd main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-04-30 13:46:56 +02:00
Harun CAN 1b980f637b main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-04-30 13:46:23 +02:00
45 changed files with 10668 additions and 1487 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "next/core-web-vitals",
"rules": {
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }]
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
# pnpm kurulumu (workspace kuralı gereği) # pnpm kurulumu (workspace kuralı gereği)
RUN corepack enable && corepack prepare pnpm@latest --activate RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
COPY package.json pnpm-lock.yaml ./ COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile RUN pnpm install --frozen-lockfile
@@ -13,7 +13,7 @@ RUN pnpm install --frozen-lockfile
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
RUN corepack enable && corepack prepare pnpm@latest --activate RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
+2 -22
View File
@@ -1,25 +1,5 @@
import { dirname } from 'path'; export default [
import { fileURLToPath } from 'url';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends('next/core-web-vitals', 'next/typescript', 'prettier'),
{ {
ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'], ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
}, }
{
rules: {
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
},
},
]; ];
export default eslintConfig;
+2804
View File
File diff suppressed because it is too large Load Diff
View File
+3 -1
View File
@@ -6,7 +6,7 @@
"dev": "next dev --webpack -p 3001", "dev": "next dev --webpack -p 3001",
"build": "next build --webpack", "build": "next build --webpack",
"start": "next start", "start": "next start",
"lint": "eslint" "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@chakra-ui/react": "^3.28.0", "@chakra-ui/react": "^3.28.0",
@@ -15,10 +15,12 @@
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@tailwindcss/postcss": "^4.2.2", "@tailwindcss/postcss": "^4.2.2",
"@tanstack/react-query": "^5.90.16", "@tanstack/react-query": "^5.90.16",
"@tanstack/react-virtual": "^3.13.24",
"autoprefixer": "^10.4.27", "autoprefixer": "^10.4.27",
"axios": "^1.13.1", "axios": "^1.13.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.20",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"i18next": "^25.6.0", "i18next": "^25.6.0",
"lucide-react": "^1.7.0", "lucide-react": "^1.7.0",
+28
View File
@@ -26,6 +26,9 @@ importers:
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^5.90.16 specifier: ^5.90.16
version: 5.95.2(react@19.2.0) version: 5.95.2(react@19.2.0)
'@tanstack/react-virtual':
specifier: ^3.13.24
version: 3.13.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
autoprefixer: autoprefixer:
specifier: ^10.4.27 specifier: ^10.4.27
version: 10.4.27(postcss@8.5.8) version: 10.4.27(postcss@8.5.8)
@@ -38,6 +41,9 @@ importers:
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
dayjs:
specifier: ^1.11.20
version: 1.11.20
framer-motion: framer-motion:
specifier: ^12.38.0 specifier: ^12.38.0
version: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -1185,6 +1191,15 @@ packages:
peerDependencies: peerDependencies:
react: ^18 || ^19 react: ^18 || ^19
'@tanstack/react-virtual@3.13.24':
resolution: {integrity: sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/virtual-core@3.14.0':
resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==}
'@tybys/wasm-util@0.10.1': '@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -1948,6 +1963,9 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dayjs@1.11.20:
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
debug@3.2.7: debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies: peerDependencies:
@@ -4539,6 +4557,14 @@ snapshots:
'@tanstack/query-core': 5.95.2 '@tanstack/query-core': 5.95.2
react: 19.2.0 react: 19.2.0
'@tanstack/react-virtual@3.13.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@tanstack/virtual-core': 3.14.0
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
'@tanstack/virtual-core@3.14.0': {}
'@tybys/wasm-util@0.10.1': '@tybys/wasm-util@0.10.1':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -5637,6 +5663,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
is-data-view: 1.0.2 is-data-view: 1.0.2
dayjs@1.11.20: {}
debug@3.2.7: debug@3.2.7:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
+3
View File
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Sparkles, Mail, Lock, Loader2, ArrowRight } from "lucide-react"; import { Sparkles, Mail, Lock, Loader2, ArrowRight } from "lucide-react";
import { useQueryClient } from "@tanstack/react-query";
export default function SignInPage() { export default function SignInPage() {
const router = useRouter(); const router = useRouter();
@@ -12,6 +13,7 @@ export default function SignInPage() {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const queryClient = useQueryClient();
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -35,6 +37,7 @@ export default function SignInPage() {
return; return;
} }
queryClient.clear();
router.replace("/dashboard"); router.replace("/dashboard");
} catch { } catch {
setError("Bağlantı hatası. Lütfen tekrar deneyin."); setError("Bağlantı hatası. Lütfen tekrar deneyin.");
@@ -0,0 +1,540 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";
import { useToast } from "@/components/ui/toast";
import {
Type,
AtSign,
PlaySquare,
FileText,
Link2,
Upload,
File,
X,
Loader2,
ArrowRight,
Wand2,
Eye,
MessageSquare,
Heart,
Repeat2,
Sparkles,
Settings2,
ChevronDown
} from "lucide-react";
import {
useCreateFromText,
useCreateFromTweet,
useCreateFromYoutube,
useCreateFromDocument,
useTweetPreview,
} from "@/hooks/use-api";
import {
LanguageSelector,
StyleSelector,
DurationSelector,
AspectRatioSelector,
} from "@/components/projects/ProjectConfiguration";
type TabType = "text" | "x" | "youtube" | "document";
export default function CreateProjectPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { toast } = useToast();
// Tab State
const [activeTab, setActiveTab] = useState<TabType>("text");
// Read ?tab= from URL on mount
useEffect(() => {
const tabParam = searchParams.get("tab") as TabType;
if (tabParam && ["text", "x", "youtube", "document"].includes(tabParam)) {
setActiveTab(tabParam);
}
}, [searchParams]);
// Shared Configurations State
const [style, setStyle] = useState("CINEMATIC");
const [cinematicReference, setCinematicReference] = useState("");
const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
const [language, setLanguage] = useState("tr");
const [showAdvanced, setShowAdvanced] = useState(true);
// API Hooks
const createFromText = useCreateFromText();
const createFromTweet = useCreateFromTweet();
const createFromYoutube = useCreateFromYoutube();
const createFromDocument = useCreateFromDocument();
const tweetPreview = useTweetPreview();
// ----- Inputs State -----
// TEXT
const [textInput, setTextInput] = useState("");
// X / TWEET
const [tweetUrl, setTweetUrl] = useState("");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [previewData, setPreviewData] = useState<any>(null);
const isValidTweetUrl = /https?:\/\/(x\.com|twitter\.com)\/\w+\/status\/\d+/.test(tweetUrl);
// YOUTUBE
const [youtubeUrl, setYoutubeUrl] = useState("");
const isValidYoutubeUrl = /https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/.+/.test(youtubeUrl);
// DOCUMENT
const fileInputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null);
// Is Any Mutation Pending?
const isPending =
createFromText.isPending ||
createFromTweet.isPending ||
createFromYoutube.isPending ||
createFromDocument.isPending;
// Handlers
const handlePreviewTweet = async () => {
if (!isValidTweetUrl) {
toast("error", "Geçerli bir X/Twitter URL'si girin.");
return;
}
try {
const result = await tweetPreview.mutateAsync(tweetUrl);
const preview = result && typeof result === 'object' && 'data' in result ? (result as any).data : result;
setPreviewData(preview);
toast("success", "Tweet başarıyla yüklendi!");
} catch {
toast("error", "Tweet yüklenemedi. URL'yi kontrol edin.");
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const selectedFile = e.target.files[0];
if (selectedFile.size > 10 * 1024 * 1024) {
toast("error", "Dosya boyutu 10MB'dan küçük olmalıdır.");
return;
}
setFile(selectedFile);
}
};
const clearFile = () => {
setFile(null);
if (fileInputRef.current) fileInputRef.current.value = "";
};
const handleGenerate = async () => {
try {
let result: any;
if (activeTab === "text") {
if (!textInput.trim()) {
toast("error", "Lütfen bir konu, hikaye veya metin girin.");
return;
}
result = await createFromText.mutateAsync({
text: textInput,
language,
aspectRatio,
videoStyle: style,
cinematicReference: cinematicReference || undefined,
targetDuration: duration,
});
} else if (activeTab === "x") {
if (!isValidTweetUrl) {
toast("error", "Lütfen geçerli bir Tweet URL'si girin.");
return;
}
result = await createFromTweet.mutateAsync({
tweetUrl,
language,
aspectRatio,
videoStyle: style,
cinematicReference: cinematicReference || undefined,
targetDuration: duration,
});
} else if (activeTab === "youtube") {
if (!isValidYoutubeUrl) {
toast("error", "Lütfen geçerli bir YouTube URL'si girin.");
return;
}
result = await createFromYoutube.mutateAsync({
youtubeUrl,
language,
aspectRatio,
videoStyle: style,
cinematicReference: cinematicReference || undefined,
targetDuration: duration,
});
} else if (activeTab === "document") {
if (!file) {
toast("error", "Lütfen bir belge yükleyin.");
return;
}
result = await createFromDocument.mutateAsync({
file,
language,
aspectRatio,
videoStyle: style,
cinematicReference: cinematicReference || undefined,
targetDuration: duration,
});
}
toast("success", "Proje başarıyla oluşturuldu!");
if (result?.id) {
router.push(`/dashboard/projects/${result.id}`);
} else {
router.push("/dashboard/projects");
}
} catch (error: any) {
if (error?.response?.status === 500) {
toast("error", "Yapay zeka yoğunluktan ötürü sahne üretimini tamamlayamadı veya yanıt yapısı geçersiz. Lütfen 'Video Üret' butonuna tekrar tıklayın.");
} else {
toast("error", error?.response?.data?.message || "Proje oluşturulurken bir hata oluştu.");
}
}
};
const tabs = [
{ id: "text", label: "Metin", icon: Type },
{ id: "x", label: "X/Twitter", icon: AtSign },
{ id: "youtube", label: "YouTube", icon: PlaySquare },
{ id: "document", label: "Belge", icon: FileText },
];
return (
<div className="max-w-4xl mx-auto space-y-8 pb-24">
{/* Header */}
<div className="text-center space-y-3 pb-2 pt-4">
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
Yeni Proje Üret
</h1>
<p className="text-[var(--color-text-muted)] text-sm md:text-base max-w-xl mx-auto">
İstediğiniz kaynağı seçin ve yapay zeka sizin için dakikalar içinde profesyonel bir video oluştursun.
</p>
</div>
{/* Tabs */}
<div className="flex p-1 space-x-1 bg-[var(--color-bg-elevated)] rounded-2xl w-full max-w-2xl mx-auto border border-[var(--color-border-faint)] overflow-x-auto no-scrollbar shadow-inner">
{tabs.map((tab) => {
const isActive = activeTab === tab.id;
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as TabType)}
className={cn(
"relative flex-1 flex items-center justify-center gap-2 py-3 px-4 text-sm font-medium rounded-xl transition-all whitespace-nowrap",
isActive
? "text-[var(--color-bg-base)]"
: "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-surface)]"
)}
>
{isActive && (
<motion.div
layoutId="active-tab"
className="absolute inset-0 bg-[var(--color-text-primary)] rounded-xl shadow-md"
initial={false}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
/>
)}
<span className="relative z-10 flex items-center gap-2">
<Icon size={16} />
<span>{tab.label}</span>
</span>
</button>
);
})}
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
{/* Left Column: Source Input */}
<div className="lg:col-span-7 space-y-6">
<div className="card p-6 md:p-8 space-y-6 min-h-[320px] flex flex-col relative overflow-hidden">
{/* Background Icon Watermark */}
<div className="absolute -bottom-6 -right-6 opacity-[0.02] pointer-events-none">
{activeTab === "text" && <Type size={180} />}
{activeTab === "x" && <AtSign size={180} />}
{activeTab === "youtube" && <PlaySquare size={180} />}
{activeTab === "document" && <FileText size={180} />}
</div>
{/* Content Based on Tab */}
<AnimatePresence mode="wait">
<motion.div
key={activeTab}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.2 }}
className="flex-1 flex flex-col"
>
{/* TEXT INPUT */}
{activeTab === "text" && (
<div className="space-y-4 flex-1 flex flex-col">
<label className="text-sm font-medium text-[var(--color-text-secondary)] block">
Fikriniz veya Metniniz
</label>
<textarea
value={textInput}
onChange={(e) => setTextInput(e.target.value)}
placeholder="Örn: Evrenin başlangıcı ve kara deliklerin sırları hakkında detaylı ve sürükleyici bir anlatım..."
className="w-full flex-1 min-h-[200px] bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl px-4 py-4 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-bg-inverted)]/50 resize-none transition-all placeholder:text-[var(--color-text-ghost)] shadow-inner"
/>
</div>
)}
{/* X / TWITTER INPUT */}
{activeTab === "x" && (
<div className="space-y-4">
<label className="text-sm font-medium text-[var(--color-text-secondary)] block flex items-center gap-1.5">
<Link2 size={14} className="text-cyan-400" />
Tweet URL
</label>
<div className="flex flex-col sm:flex-row gap-3">
<input
type="url"
value={tweetUrl}
onChange={(e) => {
setTweetUrl(e.target.value);
setPreviewData(null);
}}
placeholder="https://x.com/username/status/123456..."
className="flex-1 bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl px-4 py-3.5 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-bg-inverted)]/50 transition-all placeholder:text-[var(--color-text-ghost)]"
/>
<button
onClick={handlePreviewTweet}
disabled={!isValidTweetUrl || tweetPreview.isPending}
className={cn(
"px-6 py-3.5 rounded-xl text-sm font-semibold flex items-center justify-center gap-2 transition-all shrink-0",
isValidTweetUrl
? "btn-primary"
: "bg-[var(--color-bg-elevated)] text-[var(--color-text-ghost)] cursor-not-allowed"
)}
>
{tweetPreview.isPending ? (
<Loader2 size={16} className="animate-spin" />
) : (
<>
<Eye size={16} /> Önizle
</>
)}
</button>
</div>
{/* Preview Area */}
<AnimatePresence>
{previewData && previewData.tweet && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="p-5 rounded-xl border border-[var(--color-border-faint)] bg-[var(--color-bg-deep)] shadow-inner mt-4"
>
<div className="flex items-center gap-3 mb-3">
{previewData.tweet.author?.avatarUrl ? (
<img src={previewData.tweet.author.avatarUrl} alt="" className="w-10 h-10 rounded-full" />
) : (
<div className="w-10 h-10 rounded-full bg-[var(--color-bg-surface)] flex items-center justify-center border border-[var(--color-border-faint)]">
<Link2 size={16} className="text-neutral-400" />
</div>
)}
<div>
<p className="text-sm font-bold text-[var(--color-text-primary)]">{previewData.tweet.author?.name || "X Kullanıcısı"}</p>
<p className="text-xs text-[var(--color-text-muted)]">@{previewData.tweet.author?.username || "username"}</p>
</div>
</div>
<p className="text-[13px] text-[var(--color-text-secondary)] leading-relaxed mb-4 line-clamp-4">
{previewData.tweet.text}
</p>
{previewData.tweet.media && previewData.tweet.media.length > 0 && (
<div className="flex gap-2 overflow-x-auto pb-4">
{previewData.tweet.media.slice(0, 4).map((m: any, i: number) => (
<div
key={i}
className="w-20 h-20 rounded-lg bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] overflow-hidden flex-shrink-0"
>
{m.type === "photo" ? (
<img
src={m.url}
alt={`Media ${i + 1}`}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-[var(--color-text-ghost)]">
<Link2 size={20} />
</div>
)}
</div>
))}
</div>
)}
<div className="flex items-center gap-6 text-[var(--color-text-muted)] text-xs font-medium">
<span className="flex items-center gap-1.5"><MessageSquare size={14} /> {previewData.tweet.metrics?.replies || 0}</span>
<span className="flex items-center gap-1.5"><Repeat2 size={14} /> {previewData.tweet.metrics?.retweets || 0}</span>
<span className="flex items-center gap-1.5"><Heart size={14} /> {previewData.tweet.metrics?.likes || 0}</span>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)}
{/* YOUTUBE INPUT */}
{activeTab === "youtube" && (
<div className="space-y-4">
<label className="text-sm font-medium text-[var(--color-text-secondary)] block flex items-center gap-1.5">
<Link2 size={14} className="text-red-500" />
YouTube Video URL
</label>
<input
type="url"
value={youtubeUrl}
onChange={(e) => setYoutubeUrl(e.target.value)}
placeholder="https://youtube.com/watch?v=... veya https://youtu.be/..."
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl px-4 py-4 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-bg-inverted)]/50 transition-all placeholder:text-[var(--color-text-ghost)]"
/>
<div className="mt-4 p-4 rounded-xl bg-red-500/5 border border-red-500/10 text-sm text-[var(--color-text-muted)] flex items-start gap-3">
<Sparkles size={16} className="text-red-400 shrink-0 mt-0.5" />
<p>YouTube veya Shorts bağlantınızı yapıştırın, sistemimiz orijinal videonun transkriptini çıkararak yeni bir senaryoya dönüştürecektir.</p>
</div>
</div>
)}
{/* DOCUMENT INPUT */}
{activeTab === "document" && (
<div className="space-y-4">
<label className="text-sm font-medium text-[var(--color-text-secondary)] block">
Belge Yükle (.pdf, .docx, .txt vb.)
</label>
{!file ? (
<div
onClick={() => fileInputRef.current?.click()}
className="w-full h-48 border-2 border-dashed border-[var(--color-border-default)] rounded-xl flex flex-col items-center justify-center gap-4 bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-elevated)] transition-colors cursor-pointer"
>
<div className="w-12 h-12 rounded-full bg-[var(--color-bg-elevated)] flex items-center justify-center text-[var(--color-text-muted)] shadow-sm">
<Upload size={24} />
</div>
<div className="text-center">
<p className="text-sm font-medium text-[var(--color-text-primary)]">Tıklayın veya sürükleyin</p>
<p className="text-xs text-[var(--color-text-ghost)] mt-1.5">Maksimum 10MB boyutunda dökümanlar</p>
</div>
</div>
) : (
<div className="w-full p-5 border border-[var(--color-border-default)] rounded-xl flex items-center justify-between bg-[var(--color-bg-elevated)]">
<div className="flex items-center gap-4 overflow-hidden">
<div className="w-12 h-12 rounded-xl bg-[var(--color-bg-surface)] flex items-center justify-center text-[var(--color-text-secondary)] shrink-0 border border-[var(--color-border-faint)]">
<File size={24} />
</div>
<div className="truncate">
<p className="text-sm font-bold text-[var(--color-text-primary)] truncate mb-1">{file.name}</p>
<p className="text-xs text-[var(--color-text-ghost)] font-medium">{(file.size / 1024 / 1024).toFixed(2)} MB</p>
</div>
</div>
<button
onClick={clearFile}
className="w-10 h-10 flex items-center justify-center rounded-full hover:bg-[var(--color-bg-surface)] text-[var(--color-text-muted)] hover:text-red-500 transition-colors shrink-0"
>
<X size={18} />
</button>
</div>
)}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept=".pdf,.doc,.docx,.txt,.csv"
className="hidden"
/>
</div>
)}
</motion.div>
</AnimatePresence>
</div>
</div>
{/* Right Column: Settings & Submit */}
<div className="lg:col-span-5 space-y-6">
<div className="card p-5 md:p-6 space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-base font-bold text-[var(--color-text-primary)] flex items-center gap-2">
<Settings2 size={18} className="text-[var(--color-text-muted)]" />
Proje Ayarları
</h2>
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className="text-xs font-medium text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] flex items-center gap-1 transition-colors"
>
{showAdvanced ? "Gizle" : "Göster"}
<ChevronDown size={14} className={cn("transition-transform", showAdvanced && "rotate-180")} />
</button>
</div>
<AnimatePresence initial={false}>
{showAdvanced && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="space-y-6 overflow-hidden"
>
<LanguageSelector value={language} onChange={setLanguage} />
<StyleSelector
value={style}
onChange={setStyle}
cinematicReference={cinematicReference}
onCinematicReferenceChange={setCinematicReference}
/>
<DurationSelector value={duration} onChange={setDuration} />
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
</motion.div>
)}
</AnimatePresence>
</div>
{/* Submit Button */}
<button
onClick={handleGenerate}
disabled={
isPending ||
(activeTab === "text" && !textInput.trim()) ||
(activeTab === "x" && !isValidTweetUrl) ||
(activeTab === "youtube" && !isValidYoutubeUrl) ||
(activeTab === "document" && !file)
}
className={cn(
"w-full group relative overflow-hidden flex items-center justify-center gap-3 px-8 py-4 rounded-2xl font-bold text-white shadow-none transition-all",
"bg-gradient-to-r from-violet-600 to-cyan-500 hover:shadow-[0_0_20px_rgba(34,211,238,0.4)] hover:scale-[1.02] border border-white/10",
"disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed disabled:hover:shadow-none"
)}
>
{isPending ? (
<>
<Loader2 size={20} className="animate-spin" />
<span>Proje Hazırlanıyor...</span>
</>
) : (
<>
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
<span>Video Projesi Üret</span>
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</>
)}
</button>
</div>
</div>
</div>
);
}
@@ -1,210 +0,0 @@
"use client";
import { useState, useRef } from "react";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { useCreateFromDocument } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
import {
FileText,
Upload,
Loader2,
ArrowRight,
Sparkles,
Wand2,
X,
File,
} from "lucide-react";
import {
LanguageSelector,
StyleSelector,
DurationSelector,
AspectRatioSelector,
} from "@/components/projects/ProjectConfiguration";
export default function DocumentToVideoPage() {
const router = useRouter();
const { toast } = useToast();
const createFromDoc = useCreateFromDocument();
const fileInputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null);
const [style, setStyle] = useState("CINEMATIC");
const [cinematicReference, setCinematicReference] = useState("");
const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
const [language, setLanguage] = useState("tr");
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const selectedFile = e.target.files[0];
// Boyut kontrolü (örn: 10MB)
if (selectedFile.size > 10 * 1024 * 1024) {
toast("error", "Dosya boyutu 10MB'dan küçük olmalıdır.");
return;
}
setFile(selectedFile);
}
};
const clearFile = () => {
setFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleGenerate = async () => {
if (!file) {
toast("error", "Lütfen bir belge yükleyin.");
return;
}
try {
const result: any = await createFromDoc.mutateAsync({
file,
language,
aspectRatio,
videoStyle: style,
cinematicReference: cinematicReference ? cinematicReference : undefined,
targetDuration: duration,
});
toast("success", "Belge → Video projesi oluşturuldu!");
router.push(`/dashboard/projects/${result.id}`);
} catch {
toast("error", "Proje oluşturulurken bir hata oluştu.");
}
};
return (
<div className="max-w-3xl mx-auto space-y-8 pb-24">
{/* Header */}
<div className="text-center space-y-3 pb-4">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
<FileText size={32} />
</div>
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
Belgeden Video Üret
</h1>
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
PDF, Word veya Text dosyalarınızı yükleyin, yapay zeka sizin için profesyonel bir videoya dönüştürsün
</p>
</div>
{/* Main Form */}
<div className="card p-6 md:p-8 space-y-6">
{/* File Upload */}
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
Belge Yükle (.pdf, .docx, .txt, vb.)
</label>
{!file ? (
<div
onClick={() => fileInputRef.current?.click()}
className="w-full h-32 border-2 border-dashed border-[var(--color-border-default)] rounded-xl flex flex-col items-center justify-center gap-3 bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-elevated)] transition-colors cursor-pointer"
>
<div className="w-10 h-10 rounded-full bg-[var(--color-bg-elevated)] flex items-center justify-center text-[var(--color-text-muted)]">
<Upload size={20} />
</div>
<div className="text-center">
<p className="text-sm font-medium text-[var(--color-text-primary)]">Tıklayın veya sürükleyin</p>
<p className="text-xs text-[var(--color-text-ghost)] mt-1">Maksimum 10MB</p>
</div>
</div>
) : (
<div className="w-full p-4 border border-[var(--color-border-default)] rounded-xl flex items-center justify-between bg-[var(--color-bg-elevated)]">
<div className="flex items-center gap-3 overflow-hidden">
<div className="w-10 h-10 rounded-lg bg-[var(--color-bg-surface)] flex items-center justify-center text-[var(--color-text-secondary)] shrink-0">
<File size={20} />
</div>
<div className="truncate">
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate">{file.name}</p>
<p className="text-xs text-[var(--color-text-ghost)]">{(file.size / 1024 / 1024).toFixed(2)} MB</p>
</div>
</div>
<button
onClick={clearFile}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-[var(--color-bg-surface)] text-[var(--color-text-muted)] transition-colors shrink-0"
>
<X size={16} />
</button>
</div>
)}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept=".pdf,.doc,.docx,.txt,.csv"
className="hidden"
/>
</div>
{/* Configurations */}
<div className="space-y-8 pt-6 border-t border-[var(--color-border-faint)]">
<LanguageSelector value={language} onChange={setLanguage} />
<StyleSelector
value={style}
onChange={setStyle}
cinematicReference={cinematicReference}
onCinematicReferenceChange={setCinematicReference}
/>
<DurationSelector value={duration} onChange={setDuration} />
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
</div>
</div>
{/* Action Button */}
<div className="flex justify-end">
<button
onClick={handleGenerate}
disabled={createFromDoc.isPending || !file}
className={cn(
"group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
"bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
"disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
)}
>
{createFromDoc.isPending ? (
<>
<Loader2 size={20} className="animate-spin" />
<span>Yapay Zeka Belgeyi Okuyor...</span>
</>
) : (
<>
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
<span>Belgeden Video Üret</span>
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</>
)}
</button>
</div>
{/* Info Box */}
<div className="card p-5 bg-[var(--color-bg-surface)]">
<div className="flex items-start gap-3">
<Sparkles size={20} className="text-[var(--color-text-secondary)] shrink-0 mt-0.5" />
<div className="space-y-2">
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
Nasıl Çalışır?
</h3>
<ol className="text-xs text-[var(--color-text-muted)] space-y-1.5 list-decimal list-inside">
<li>PDF, Word veya TXT formatında bir metin dosyası yükleyin</li>
<li>MarkItDown AI teknolojisiyle belgeniz analiz edilip özetlenir</li>
<li>Belgenin içeriğine en uygun görseller ve anlatım senaryosu çıkarılır</li>
<li>Sizin seçtiğiniz dil, stil ve süreye göre yepyeni bir video oluşturulur</li>
</ol>
</div>
</div>
</div>
</div>
);
}
@@ -25,6 +25,7 @@ import Link from "next/link";
import { DashboardCharts } from "@/components/dashboard/dashboard-charts"; import { DashboardCharts } from "@/components/dashboard/dashboard-charts";
import { RecentProjects } from "@/components/dashboard/recent-projects"; import { RecentProjects } from "@/components/dashboard/recent-projects";
import { TweetImportCard } from "@/components/dashboard/tweet-import-card"; import { TweetImportCard } from "@/components/dashboard/tweet-import-card";
import { YoutubeImportCard } from "@/components/dashboard/youtube-import-card";
import { useDashboardStats, useCreditBalance } from "@/hooks/use-api"; import { useDashboardStats, useCreditBalance } from "@/hooks/use-api";
const stagger = { const stagger = {
@@ -139,7 +140,7 @@ export default function DashboardPage() {
</p> </p>
</div> </div>
<Link <Link
href="/dashboard/projects/new" href="/dashboard/create-project"
className="btn-primary flex items-center gap-2 text-sm" className="btn-primary flex items-center gap-2 text-sm"
> >
<Plus size={16} /> <Plus size={16} />
@@ -183,7 +184,7 @@ export default function DashboardPage() {
{/* ── Hızlı Eylemler ── */} {/* ── Hızlı Eylemler ── */}
<motion.div variants={fadeUp} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3"> <motion.div variants={fadeUp} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<Link <Link
href="/dashboard/projects/new" href="/dashboard/create-project"
className="group card-surface p-4 flex items-center gap-4 hover:border-neutral-500/30" className="group card-surface p-4 flex items-center gap-4 hover:border-neutral-500/30"
> >
<div className="w-11 h-11 rounded-xl bg-[var(--color-bg-inverted)] flex items-center justify-center shrink-0 shadow-lg group-hover:shadow-neutral-500/20 transition-shadow"> <div className="w-11 h-11 rounded-xl bg-[var(--color-bg-inverted)] flex items-center justify-center shrink-0 shadow-lg group-hover:shadow-neutral-500/20 transition-shadow">
@@ -241,12 +242,17 @@ export default function DashboardPage() {
{/* ── Tweet Import + Grafikler ── */} {/* ── Tweet Import + Grafikler ── */}
<motion.div variants={fadeUp} className="grid grid-cols-1 lg:grid-cols-5 gap-4"> <motion.div variants={fadeUp} className="grid grid-cols-1 lg:grid-cols-5 gap-4">
<div id="tweet-import" className="lg:col-span-2"> <div id="tweet-import" className="lg:col-span-2 flex flex-col gap-4">
<TweetImportCard <TweetImportCard
onProjectCreated={(id) => { onProjectCreated={(id) => {
window.location.href = `/dashboard/projects/${id}`; window.location.href = `/dashboard/projects/${id}`;
}} }}
/> />
<YoutubeImportCard
onProjectCreated={(id) => {
window.location.href = `/dashboard/projects/${id}`;
}}
/>
</div> </div>
<div className="lg:col-span-3"> <div className="lg:col-span-3">
<DashboardCharts /> <DashboardCharts />
@@ -1,7 +1,8 @@
'use client'; 'use client';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { motion } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { import {
ArrowLeft, ArrowLeft,
Play, Play,
@@ -16,9 +17,18 @@ import {
Trash2, Trash2,
MoreVertical, MoreVertical,
X, X,
Languages,
Search,
Tag,
Copy,
Check,
TrendingUp,
Zap,
Hash,
} from 'lucide-react'; } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { import {
useProject, useProject,
useGenerateScript, useGenerateScript,
@@ -28,14 +38,19 @@ import {
useGenerateSceneImage, useGenerateSceneImage,
useUpscaleSceneImage, useUpscaleSceneImage,
useRegenerateScene, useRegenerateScene,
useCancelRender useCancelRender,
useGenerateSeoTitles,
useSelectSeoTitle,
useGenerateSocialContent
} from '@/hooks/use-api'; } from '@/hooks/use-api';
import { useRenderProgress } from '@/hooks/use-render-progress'; import { useRenderProgress } from '@/hooks/use-render-progress';
import { SceneCard } from '@/components/project/scene-card'; import { SceneCard } from '@/components/project/scene-card';
import { RenderProgress } from '@/components/project/render-progress'; import { RenderProgress } from '@/components/project/render-progress';
import { VideoPlayer } from '@/components/project/video-player'; import { VideoPlayer } from '@/components/project/video-player';
import { projectsApi } from '@/lib/api/api-service'; import { projectsApi, apiClient } from '@/lib/api/api-service';
import { CINEMATIC_REFERENCES } from '@/constants/cinematic-references'; import { CINEMATIC_REFERENCES } from '@/constants/cinematic-references';
import { languages } from '@/components/projects/ProjectConfiguration';
import { toaster as toast } from '@/components/ui/feedback/toaster';
// X (Twitter) ikonunu burada da tanımlıyoruz // X (Twitter) ikonunu burada da tanımlıyoruz
const XIcon = ({ size = 16 }: { size?: number }) => ( const XIcon = ({ size = 16 }: { size?: number }) => (
@@ -44,6 +59,18 @@ const XIcon = ({ size = 16 }: { size?: number }) => (
</svg> </svg>
); );
const YouTubeIcon = ({ size = 16 }: { size?: number }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M23.5 6.19a3.02 3.02 0 0 0-2.12-2.14C19.53 3.5 12 3.5 12 3.5s-7.53 0-9.38.55A3.02 3.02 0 0 0 .5 6.19 31.7 31.7 0 0 0 0 12a31.7 31.7 0 0 0 .5 5.81 3.02 3.02 0 0 0 2.12 2.14c1.85.55 9.38.55 9.38.55s7.53 0 9.38-.55a3.02 3.02 0 0 0 2.12-2.14A31.7 31.7 0 0 0 24 12a31.7 31.7 0 0 0-.5-5.81zM9.55 15.57V8.43L15.82 12l-6.27 3.57z" />
</svg>
);
const InstagramIcon = ({ size = 16 }: { size?: number }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 1 0 0 12.324 6.162 6.162 0 0 0 0-12.324zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6.406-11.845a1.44 1.44 0 1 0 0 2.881 1.44 1.44 0 0 0 0-2.881z" />
</svg>
);
const STATUS_MAP: Record<string, { label: string; color: string; icon: React.ElementType; bgClass: string }> = { const STATUS_MAP: Record<string, { label: string; color: string; icon: React.ElementType; bgClass: string }> = {
DRAFT: { label: 'Taslak', color: 'text-neutral-400', icon: FileText, bgClass: 'bg-neutral-500/10 border-neutral-500/20' }, 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' }, GENERATING_SCRIPT: { label: 'Senaryo Üretiliyor', color: 'text-neutral-300', icon: Sparkles, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
@@ -127,6 +154,27 @@ export default function ProjectDetailPage() {
const [regeneratingSceneId, setRegeneratingSceneId] = useState<string | null>(null); const [regeneratingSceneId, setRegeneratingSceneId] = useState<string | null>(null);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [showTranslateModal, setShowTranslateModal] = useState(false);
const [targetLanguage, setTargetLanguage] = useState<string>("");
const [isTranslating, setIsTranslating] = useState(false);
const confirmTranslate = async () => {
if (!id || !targetLanguage) return;
try {
setIsTranslating(true);
const res = await apiClient.post(`/projects/${id}/translate`, { targetLanguage });
toast.success({ title: "Proje başarıyla çevrildi!" });
setShowTranslateModal(false);
setTargetLanguage("");
// refetch() to maybe update some states, or router.push to the new project
router.push(`/dashboard/projects/${res.data.id}`);
} catch (err: any) {
toast.error(err?.response?.data?.message || "Çeviri sırasında bir hata oluştu.");
} finally {
setIsTranslating(false);
}
};
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
}, []); }, []);
@@ -145,12 +193,25 @@ export default function ProjectDetailPage() {
const deleteMutation = useDeleteProject(); const deleteMutation = useDeleteProject();
const cancelRenderMutation = useCancelRender(); const cancelRenderMutation = useCancelRender();
// Virtualization for long-form video scenes
const parentRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count: project?.scenes?.length || 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 300, // Estimated height of a SceneCard
overscan: 5, // Render 5 items outside of the visible area
});
const generateImageMutation = useGenerateSceneImage(); const generateImageMutation = useGenerateSceneImage();
const upscaleImageMutation = useUpscaleSceneImage(); const upscaleImageMutation = useUpscaleSceneImage();
const regenerateSceneMutation = useRegenerateScene(); const regenerateSceneMutation = useRegenerateScene();
const seoTitlesMutation = useGenerateSeoTitles();
const selectTitleMutation = useSelectSeoTitle();
const generateSocialMutation = useGenerateSocialContent();
const [generatingImageId, setGeneratingImageId] = useState<string | null>(null); const [generatingImageId, setGeneratingImageId] = useState<string | null>(null);
const [upscalingImageId, setUpscalingImageId] = useState<string | null>(null); const [upscalingImageId, setUpscalingImageId] = useState<string | null>(null);
const [copiedField, setCopiedField] = useState<string | null>(null);
const [activeCaptionTab, setActiveCaptionTab] = useState<'youtube' | 'tiktok' | 'instagram' | 'twitter'>('youtube');
// WebSocket progress // WebSocket progress
const renderState = useRenderProgress( const renderState = useRenderProgress(
@@ -210,9 +271,43 @@ 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 // Onayla ve gönder
const handleApprove = () => { const handleApprove = () => {
approveMutation.mutate(id, { approveMutation.mutate({ projectId: id }, {
onSuccess: () => refetch(), onSuccess: () => refetch(),
}); });
}; };
@@ -328,6 +423,12 @@ export default function ProjectDetailPage() {
> >
<RefreshCw size={14} /> Yenile <RefreshCw size={14} /> Yenile
</button> </button>
<button
onClick={() => { setShowMenu(false); setShowTranslateModal(true); }}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-elevated)] rounded-lg transition-colors"
>
<Languages size={14} /> Çevir
</button>
<button <button
onClick={() => { handleDelete(); setShowMenu(false); }} onClick={() => { handleDelete(); setShowMenu(false); }}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-400 hover:bg-red-500/10 rounded-lg transition-colors" className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
@@ -562,10 +663,34 @@ export default function ProjectDetailPage() {
</span> </span>
</div> </div>
<div className="space-y-3"> <div
{project.scenes!.map((scene) => ( ref={parentRef}
className="w-full h-[800px] overflow-auto pr-2 rounded-xl"
>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const scene = project.scenes![virtualRow.index];
return (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
paddingBottom: '12px',
}}
>
<SceneCard <SceneCard
key={scene.id}
scene={scene} scene={scene}
isEditable={isEditable} isEditable={isEditable}
isRendering={isRendering} isRendering={isRendering}
@@ -577,8 +702,303 @@ export default function ProjectDetailPage() {
isGeneratingImage={generatingImageId === scene.id} isGeneratingImage={generatingImageId === scene.id}
isUpscalingImage={upscalingImageId === scene.id} isUpscalingImage={upscalingImageId === scene.id}
/> />
</div>
);
})}
</div>
</div>
</motion.div>
)}
{/* ── SEO & Sosyal Medya Power Engine ── */}
{hasScript && (
<motion.div variants={fadeUp}>
<div className="card-surface p-5 rounded-2xl border border-[var(--color-border-subtle)]">
{/* Header */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-4">
<h2 className="text-sm font-semibold text-[var(--color-text-primary)] flex items-center gap-2">
<TrendingUp size={15} className="text-emerald-400" />
SEO & Sosyal Medya
</h2>
<button
onClick={() => generateSocialMutation.mutate(id as string)}
disabled={generateSocialMutation.isPending}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-medium bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-300 hover:from-emerald-500/30 hover:to-teal-500/30 border border-emerald-500/20 transition-all disabled:opacity-50"
title="Eksik SEO veya Sosyal Medya içeriklerini yapay zeka ile yeniden üret"
>
{generateSocialMutation.isPending ? (
<Loader2 size={12} className="animate-spin" />
) : (
<RefreshCw size={12} />
)}
Tümünü Yeniden Üret
</button>
</div>
{project.seoScore != null && (
<div className="flex items-center gap-2">
<div className="relative w-10 h-10">
<svg className="w-10 h-10 -rotate-90" viewBox="0 0 36 36">
<path
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="rgba(255,255,255,0.1)"
strokeWidth="3"
/>
<path
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke={project.seoScore >= 80 ? '#34d399' : project.seoScore >= 50 ? '#fbbf24' : '#f87171'}
strokeWidth="3"
strokeDasharray={`${project.seoScore}, 100`}
strokeLinecap="round"
/>
</svg>
<span className="absolute inset-0 flex items-center justify-center text-[10px] font-bold text-white">
{project.seoScore}
</span>
</div>
<span className="text-[10px] text-[var(--color-text-muted)]">SEO<br/>Skoru</span>
</div>
)}
</div>
{/* ─── Başlık Yönetimi ─── */}
<div className="mb-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider">
📌 SEO Başlıkları
</h3>
<button
onClick={handleGenerateSeoTitles}
disabled={seoTitlesMutation.isPending}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-medium bg-gradient-to-r from-violet-500/20 to-purple-500/20 text-violet-300 hover:from-violet-500/30 hover:to-purple-500/30 border border-violet-500/20 transition-all disabled:opacity-50"
>
{seoTitlesMutation.isPending ? (
<Loader2 size={12} className="animate-spin" />
) : (
<Zap size={12} />
)}
5 Yeni Başlık Üret
</button>
</div>
{/* Mevcut başlık */}
<div className="bg-[var(--color-bg-elevated)] rounded-xl p-3 mb-2 border border-emerald-500/20">
<div className="flex items-center gap-2 mb-1">
<CheckCircle2 size={12} className="text-emerald-400" />
<span className="text-[10px] text-emerald-400 font-medium uppercase">Aktif Başlık</span>
</div>
<p className="text-sm font-medium text-white">{project.seoTitle || project.title}</p>
</div>
{/* Alternatif başlıklar */}
{project.seoTitleAlts && project.seoTitleAlts.length > 0 && (
<div className="space-y-1.5">
{project.seoTitleAlts.map((alt, i) => (
<button
key={i}
onClick={() => handleSelectTitle(alt)}
disabled={selectTitleMutation.isPending || alt === (project.seoTitle || project.title)}
className={`w-full text-left p-2.5 rounded-lg border text-xs transition-all ${
alt === (project.seoTitle || project.title)
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300'
: 'border-[var(--color-border-subtle)] hover:border-violet-500/30 hover:bg-violet-500/5 text-[var(--color-text-secondary)]'
} disabled:opacity-40`}
>
<span className="text-[10px] text-[var(--color-text-ghost)] mr-2">#{i + 1}</span>
{alt}
</button>
))} ))}
</div> </div>
)}
</div>
{/* ─── SEO Keywords ─── */}
{project.seoKeywords && project.seoKeywords.length > 0 && (
<div className="mb-5">
<h3 className="text-xs font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2 flex items-center gap-1.5">
<Search size={12} />
Anahtar Kelimeler ({project.seoKeywords.length})
</h3>
<div className="flex flex-wrap gap-1.5">
{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 (
<span
key={i}
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-medium border ${colors[i % colors.length]}`}
>
<Tag size={10} />
{kw}
</span>
);
})}
</div>
</div>
)}
{/* ─── Hashtag'ler ─── */}
{project.scriptJson?.seo?.hashtags && project.scriptJson.seo.hashtags.length > 0 && (
<div className="mb-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider flex items-center gap-1.5">
<Hash size={12} />
Hashtag&apos;ler
</h3>
<button
onClick={() => copyToClipboard(
project.scriptJson!.seo.hashtags.map((h: string) => `#${h}`).join(' '),
'hashtags'
)}
className="flex items-center gap-1 text-[10px] text-[var(--color-text-muted)] hover:text-white transition-colors"
>
{copiedField === 'hashtags' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} />}
Tümünü Kopyala
</button>
</div>
<div className="flex flex-wrap gap-1.5">
{project.scriptJson.seo.hashtags.map((tag: string, i: number) => (
<span
key={i}
className="px-2.5 py-1 rounded-full text-[11px] font-medium bg-sky-500/10 text-sky-300 border border-sky-500/20"
>
#{tag}
</span>
))}
{/* Trending hashtag'ler */}
{project.scriptJson.seo.trendingHashtags?.map((tag: string, i: number) => (
<span
key={`trend-${i}`}
className="px-2.5 py-1 rounded-full text-[11px] font-medium bg-orange-500/15 text-orange-300 border border-orange-500/20 flex items-center gap-1"
>
<TrendingUp size={10} />
#{tag}
</span>
))}
</div>
</div>
)}
{/* ─── Sosyal Medya Caption'ları ─── */}
{project.socialContent && (
<div>
<h3 className="text-xs font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider mb-3 flex items-center gap-1.5">
<Sparkles size={12} />
Sosyal Medya İçerikleri
</h3>
{/* Tab navigasyonu */}
<div className="flex gap-1 mb-3 p-1 bg-[var(--color-bg-elevated)] rounded-xl">
{[
{ 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) => (
<button
key={tab.key}
onClick={() => setActiveCaptionTab(tab.key)}
className={`flex-1 flex items-center justify-center gap-1.5 py-2 rounded-lg text-[11px] font-medium transition-all ${
activeCaptionTab === tab.key
? 'bg-[var(--color-bg-surface)] text-white shadow-sm'
: 'text-[var(--color-text-muted)] hover:text-white'
}`}
>
<tab.icon size={12} className={activeCaptionTab === tab.key ? tab.color : ''} />
<span className="hidden sm:inline">{tab.label}</span>
</button>
))}
</div>
{/* Caption içeriği */}
<div className="bg-[var(--color-bg-elevated)] rounded-xl p-4 border border-[var(--color-border-subtle)]">
{activeCaptionTab === 'youtube' && (
<div className="space-y-3">
{project.socialContent.youtubeTitle && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">Başlık</span>
<button onClick={() => copyToClipboard(project.socialContent!.youtubeTitle!, 'yt-title')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'yt-title' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-sm font-medium text-white">{project.socialContent.youtubeTitle}</p>
</div>
)}
{project.socialContent.youtubeDescription && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">Açıklama</span>
<button onClick={() => copyToClipboard(project.socialContent!.youtubeDescription!, 'yt-desc')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'yt-desc' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-xs text-[var(--color-text-secondary)] whitespace-pre-line leading-relaxed">{project.socialContent.youtubeDescription}</p>
</div>
)}
</div>
)}
{activeCaptionTab === 'tiktok' && project.socialContent.tiktokCaption && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">TikTok Caption</span>
<button onClick={() => copyToClipboard(project.socialContent!.tiktokCaption!, 'tiktok')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'tiktok' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-xs text-[var(--color-text-secondary)] whitespace-pre-line leading-relaxed">{project.socialContent.tiktokCaption}</p>
</div>
)}
{activeCaptionTab === 'instagram' && project.socialContent.instagramCaption && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">Instagram Caption</span>
<button onClick={() => copyToClipboard(project.socialContent!.instagramCaption!, 'instagram')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'instagram' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-xs text-[var(--color-text-secondary)] whitespace-pre-line leading-relaxed">{project.socialContent.instagramCaption}</p>
</div>
)}
{activeCaptionTab === 'twitter' && project.socialContent.twitterText && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">X (Twitter) Paylaşımı</span>
<button onClick={() => copyToClipboard(project.socialContent!.twitterText!, 'twitter')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'twitter' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-xs text-[var(--color-text-secondary)] whitespace-pre-line leading-relaxed">{project.socialContent.twitterText}</p>
</div>
)}
</div>
</div>
)}
{/* ─── SEO Açıklama ─── */}
{project.seoDescription && (
<div className="mt-4 pt-4 border-t border-[var(--color-border-subtle)]">
<div className="flex items-center justify-between mb-1.5">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">Meta Description</span>
<button onClick={() => copyToClipboard(project.seoDescription!, 'seo-desc')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'seo-desc' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-xs text-[var(--color-text-secondary)] leading-relaxed">{project.seoDescription}</p>
</div>
)}
</div>
</motion.div> </motion.div>
)} )}
@@ -672,6 +1092,98 @@ export default function ProjectDetailPage() {
</div> </div>
</motion.div> </motion.div>
)} )}
{/* ─── Çeviri Modal ─── */}
{mounted && createPortal(
<div className="portal-container">
<AnimatePresence>
{showTranslateModal && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
onClick={() => !isTranslating && setShowTranslateModal(false)}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
className="relative bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] rounded-2xl p-6 max-w-sm w-full mx-4 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
{/* Kapatma butonu */}
<button
onClick={() => setShowTranslateModal(false)}
disabled={isTranslating}
className="absolute top-3 right-3 p-1 rounded-lg text-[var(--color-text-ghost)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50"
>
<X size={16} />
</button>
{/* Icon */}
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-blue-500/10 mb-4">
<Languages size={22} className="text-blue-400" />
</div>
{/* İçerik */}
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-1.5">
Projeyi Çevir
</h3>
<p className="text-sm text-[var(--color-text-muted)] mb-3">
"{project?.title}" projesini başka bir dile çevirin. (1 kredi)
</p>
<select
value={targetLanguage}
onChange={(e) => setTargetLanguage(e.target.value)}
disabled={isTranslating}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-2.5 text-sm text-[var(--color-text-primary)] mb-5 outline-none focus:border-blue-500"
>
<option value="">Dil Seçin...</option>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.flag} {lang.label}
</option>
))}
</select>
{/* Butonlar */}
<div className="flex items-center gap-3">
<button
onClick={() => setShowTranslateModal(false)}
disabled={isTranslating}
className="flex-1 px-4 py-2.5 rounded-xl border border-[var(--color-border-default)] text-sm font-medium text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50"
>
İptal
</button>
<button
onClick={confirmTranslate}
disabled={isTranslating || !targetLanguage}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isTranslating ? (
<>
<Loader2 size={14} className="animate-spin" />
Çevriliyor...
</>
) : (
<>
<Languages size={14} />
Çevir (1 🪙)
</>
)}
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>,
document.body
)}
</motion.div> </motion.div>
); );
} }
@@ -1,286 +0,0 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import {
ArrowLeft,
ArrowRight,
Sparkles,
Loader2,
Check,
Wand2,
} from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
import { useCreateProject } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
import { projectsApi } from "@/lib/api/api-service";
import {
LanguageSelector,
StyleSelector,
DurationSelector,
AspectRatioSelector,
languages,
videoStyles,
aspectRatios,
} from "@/components/projects/ProjectConfiguration";
const steps = ["Konu & Dil", "Stil & Süre", "AI Senaryo"];
export default function NewProjectPage() {
const router = useRouter();
const createProject = useCreateProject();
const toast = useToast();
const [currentStep, setCurrentStep] = useState(0);
const [topic, setTopic] = useState("");
const [language, setLanguage] = useState("tr");
const [style, setStyle] = useState("CINEMATIC");
const [cinematicReference, setCinematicReference] = useState("");
const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
const canProceed = currentStep === 0 ? topic.trim().length >= 5 : true;
const isGenerating = createProject.isPending;
const handleGenerate = async () => {
try {
// Backend DTO alanları: prompt (zorunlu), videoStyle, title, language, aspectRatio, targetDuration
const result = await createProject.mutateAsync({
title: topic.slice(0, 80),
prompt: topic,
language,
videoStyle: style,
cinematicReference: cinematicReference ? cinematicReference : undefined,
targetDuration: duration,
aspectRatio,
});
toast.success("Proje başarıyla oluşturuldu! AI senaryo üretiliyor...");
const projectId = result?.id;
if (projectId) {
// Proje oluşturulduktan sonra otomatik senaryo üretimini tetikle
projectsApi.generateScript(projectId).catch((err) => {
console.error("Senaryo üretimi başlatılamadı:", err);
});
router.push(`/dashboard/projects/${projectId}`);
} else {
router.push("/dashboard/projects");
}
} catch {
toast.error("Proje oluşturulurken bir hata oluştu. Lütfen tekrar deneyin.");
}
};
return (
<div className="max-w-2xl mx-auto">
{/* Geri + Başlık */}
<div className="flex items-center gap-3 mb-6">
<Link
href="/dashboard/projects"
className="w-9 h-9 rounded-xl flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-elevated)] transition-colors"
>
<ArrowLeft size={18} />
</Link>
<div>
<h1 className="font-[family-name:var(--font-display)] text-xl font-bold">Yeni Proje</h1>
<p className="text-xs text-[var(--color-text-muted)]">AI ile video oluştur</p>
</div>
</div>
{/* Step Indicator */}
<div className="flex items-center gap-2 mb-8">
{steps.map((step, i) => (
<div key={step} className="flex items-center gap-2">
<button
onClick={() => i < currentStep && setCurrentStep(i)}
className={cn(
"flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium transition-all",
i === currentStep
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
: i < currentStep
? "bg-[var(--color-bg-inverted)]/10 text-[var(--color-text-primary)] border border-[var(--color-bg-inverted)]/20 cursor-pointer"
: "text-[var(--color-text-ghost)] border border-[var(--color-border-faint)]"
)}
>
{i < currentStep ? (
<Check size={12} />
) : (
<span className="w-4 h-4 rounded-full border border-current flex items-center justify-center text-[10px]">
{i + 1}
</span>
)}
<span className="hidden sm:inline">{step}</span>
</button>
{i < steps.length - 1 && (
<div className={cn(
"w-6 h-px",
i < currentStep ? "bg-[var(--color-bg-inverted)]/40" : "bg-[var(--color-border-faint)]"
)} />
)}
</div>
))}
</div>
{/* Step Content */}
<AnimatePresence mode="wait">
{currentStep === 0 && (
<motion.div
key="step-0"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
className="space-y-6"
>
{/* Konu */}
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
<Sparkles size={14} className="inline mr-1.5 text-[var(--color-text-primary)]" />
Videonun Konusu
</label>
<textarea
value={topic}
onChange={(e) => setTopic(e.target.value)}
placeholder="Örn: Boötes Boşluğu — evrendeki en büyük boşluk ve gizemi..."
className="w-full h-32 px-4 py-3 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-primary)] text-sm placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-[var(--color-border-default)] transition-all resize-none"
/>
<p className="text-[11px] text-[var(--color-text-ghost)] mt-1.5">
Ne kadar detaylı yazarsan, AI o kadar iyi senaryo üretir ({topic.length} karakter)
</p>
</div>
{/* Dil Seçimi */}
<LanguageSelector value={language} onChange={setLanguage} />
</motion.div>
)}
{currentStep === 1 && (
<motion.div
key="step-1"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
className="space-y-6"
>
{/* Video Stili */}
<StyleSelector
value={style}
onChange={setStyle}
cinematicReference={cinematicReference}
onCinematicReferenceChange={setCinematicReference}
/>
{/* Süre */}
<DurationSelector value={duration} onChange={setDuration} />
{/* En-Boy Oranı */}
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
</motion.div>
)}
{currentStep === 2 && (
<motion.div
key="step-2"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
className="space-y-6"
>
{/* Özet */}
<div className="card-surface p-5 space-y-4">
<h3 className="font-[family-name:var(--font-display)] text-base font-semibold">Proje Özeti</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-[var(--color-text-muted)] text-xs">Konu</span>
<p className="font-medium mt-0.5 line-clamp-2">{topic || "—"}</p>
</div>
<div>
<span className="text-[var(--color-text-muted)] text-xs">Dil</span>
<p className="font-medium mt-0.5">
{languages.find((l) => l.code === language)?.flag}{" "}
{languages.find((l) => l.code === language)?.label}
</p>
</div>
<div>
<span className="text-[var(--color-text-muted)] text-xs">Stil</span>
<p className="font-medium mt-0.5">
{videoStyles.find((s) => s.id === style)?.emoji}{" "}
{videoStyles.find((s) => s.id === style)?.label}
</p>
</div>
<div>
<span className="text-[var(--color-text-muted)] text-xs">Süre / Oran</span>
<p className="font-medium mt-0.5">{duration}s {aspectRatios.find((a) => a.id === aspectRatio)?.label}</p>
</div>
</div>
</div>
{/* Generate butonu */}
<button
onClick={handleGenerate}
disabled={isGenerating}
className={cn(
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
isGenerating
? "bg-[var(--color-bg-inverted)]/20 text-[var(--color-text-primary)] cursor-wait"
: "btn-primary text-lg"
)}
>
{isGenerating ? (
<>
<Loader2 size={20} className="animate-spin" />
<span>AI Senaryo Üretiliyor...</span>
</>
) : (
<>
<Wand2 size={20} />
<span>AI ile Senaryo Üret</span>
</>
)}
</button>
<p className="text-center text-[11px] text-[var(--color-text-ghost)]">
Bu işlem 1 kredi kullanır Tahmini süre: ~15 saniye
</p>
</motion.div>
)}
</AnimatePresence>
{/* Navigation Buttons */}
<div className="flex items-center justify-between mt-8 pt-4 border-t border-[var(--color-border-faint)]">
<button
onClick={() => setCurrentStep((s) => Math.max(0, s - 1))}
disabled={currentStep === 0}
className={cn(
"flex items-center gap-1.5 text-sm font-medium transition-colors",
currentStep === 0
? "text-[var(--color-text-ghost)] cursor-not-allowed"
: "text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)]"
)}
>
<ArrowLeft size={16} />
Geri
</button>
{currentStep < steps.length - 1 && (
<button
onClick={() => setCurrentStep((s) => Math.min(steps.length - 1, s + 1))}
disabled={!canProceed}
className={cn(
"flex items-center gap-1.5 px-5 py-2 rounded-xl text-sm font-semibold transition-all",
canProceed
? "btn-primary"
: "bg-[var(--color-bg-elevated)] text-[var(--color-text-ghost)] cursor-not-allowed"
)}
>
İleri
<ArrowRight size={16} />
</button>
)}
</div>
</div>
);
}
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState, useMemo, useCallback } from "react"; import { useState, useMemo, useCallback, useEffect } from "react";
import { createPortal } from "react-dom";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { import {
Plus, Plus,
@@ -14,10 +15,16 @@ import {
Loader2, Loader2,
Trash2, Trash2,
X, X,
Languages,
ChevronDown,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useProjects, useDeleteProject } from "@/hooks/use-api"; import { useProjects, useDeleteProject } from "@/hooks/use-api";
import { languages } from "@/components/projects/ProjectConfiguration";
import { toaster as toast } from '@/components/ui/feedback/toaster';
import { apiClient } from "@/lib/api/api-service";
const statusFilters = [ const statusFilters = [
{ id: "all", label: "Tümü" }, { id: "all", label: "Tümü" },
@@ -79,16 +86,55 @@ interface ProjectItem {
language?: string; language?: string;
progress?: number; progress?: number;
creditsUsed?: number; creditsUsed?: number;
parentId?: string | null;
} }
export default function ProjectsPage() { export default function ProjectsPage() {
const router = useRouter();
const [activeFilter, setActiveFilter] = useState("all"); const [activeFilter, setActiveFilter] = useState("all");
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null); const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null);
const [expandedProjects, setExpandedProjects] = useState<Record<string, boolean>>({});
const { data, isLoading } = useProjects({ limit: 100 }); const [translateTarget, setTranslateTarget] = useState<ProjectItem | null>(null);
const [targetLanguage, setTargetLanguage] = useState<string>("");
const [isTranslating, setIsTranslating] = useState(false);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const { data, isLoading, refetch } = useProjects({ limit: 100 });
const deleteMutation = useDeleteProject(); const deleteMutation = useDeleteProject();
const confirmTranslate = async () => {
if (!translateTarget || !targetLanguage) return;
try {
setIsTranslating(true);
const res = await apiClient.post(`/projects/${translateTarget.id}/translate`, { targetLanguage });
toast.success({ title: "Proje başarıyla çevrildi!" });
// Çeviri tamamlanınca ana projeyi otomatik olarak genişletiyoruz
setExpandedProjects((prev) => ({ ...prev, [translateTarget.id]: true }));
setTranslateTarget(null);
setTargetLanguage("");
refetch();
// Yönlendirmek istersen: router.push(`/dashboard/projects/${res.data.id}`);
} catch (err: any) {
toast.error(err?.response?.data?.message || "Çeviri sırasında bir hata oluştu.");
} finally {
setIsTranslating(false);
}
};
const toggleExpand = useCallback((e: React.MouseEvent, id: string) => {
e.preventDefault();
e.stopPropagation();
setExpandedProjects((prev) => ({ ...prev, [id]: !prev[id] }));
}, []);
// Silme onay modal'ını aç (native confirm yerine) // Silme onay modal'ını aç (native confirm yerine)
const openDeleteConfirm = useCallback((e: React.MouseEvent, project: ProjectItem) => { const openDeleteConfirm = useCallback((e: React.MouseEvent, project: ProjectItem) => {
e.preventDefault(); e.preventDefault();
@@ -125,6 +171,21 @@ export default function ProjectsPage() {
}); });
}, [projects, activeFilter, searchQuery]); }, [projects, activeFilter, searchQuery]);
const rootProjects = useMemo(() => {
return filtered.filter(p => !p.parentId);
}, [filtered]);
const childrenMap = useMemo(() => {
const map: Record<string, ProjectItem[]> = {};
filtered.forEach(p => {
if (p.parentId) {
if (!map[p.parentId]) map[p.parentId] = [];
map[p.parentId].push(p);
}
});
return map;
}, [filtered]);
return ( return (
<div className="max-w-5xl mx-auto space-y-6"> <div className="max-w-5xl mx-auto space-y-6">
{/* Başlık */} {/* Başlık */}
@@ -228,16 +289,24 @@ export default function ProjectsPage() {
)} )}
</div> </div>
) : ( ) : (
filtered.map((project) => { rootProjects.map((project) => {
const st = statusMap[project.status] ?? statusMap.draft; const children = childrenMap[project.id] || [];
const hasChildren = children.length > 0;
const isExpanded = expandedProjects[project.id];
const renderCard = (p: ProjectItem, isChild = false) => {
const st = statusMap[p.status] ?? statusMap.draft;
const StIcon = st.icon; const StIcon = st.icon;
return ( return (
<div <div
key={project.id} key={p.id}
className="flex items-center rounded-xl card hover:border-neutral-400 dark:hover:border-neutral-600 transition-all group relative" className={cn(
"flex items-center rounded-xl card hover:border-neutral-400 dark:hover:border-neutral-600 transition-all group relative",
isChild && "bg-[var(--color-bg-surface)]"
)}
> >
<Link <Link
href={`/dashboard/projects/${project.id}`} href={`/dashboard/projects/${p.id}`}
className="flex items-center gap-4 p-4 flex-1 min-w-0" className="flex items-center gap-4 p-4 flex-1 min-w-0"
> >
<div <div
@@ -247,11 +316,11 @@ export default function ProjectsPage() {
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-[var(--color-text-secondary)] transition-colors"> <p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-[var(--color-text-secondary)] transition-colors">
{project.title} {p.title}
</p> </p>
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]"> <div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]">
<span> <span>
{new Date(project.createdAt).toLocaleDateString( {new Date(p.createdAt).toLocaleDateString(
"tr-TR", "tr-TR",
{ {
day: "numeric", day: "numeric",
@@ -260,10 +329,10 @@ export default function ProjectsPage() {
}, },
)} )}
</span> </span>
{project.language && <span> {project.language}</span>} {p.language && <span> {p.language}</span>}
{typeof project.creditsUsed === "number" && {typeof p.creditsUsed === "number" &&
project.creditsUsed > 0 && ( p.creditsUsed > 0 && (
<span> {project.creditsUsed} kredi</span> <span> {p.creditsUsed} kredi</span>
)} )}
</div> </div>
</div> </div>
@@ -278,13 +347,53 @@ export default function ProjectsPage() {
/> />
</Link> </Link>
<div className="flex items-center">
<button <button
onClick={(e) => openDeleteConfirm(e, project)} onClick={(e) => {
className="p-2 rounded-lg text-[var(--color-text-ghost)] hover:text-red-400 hover:bg-red-500/10 transition-colors shrink-0 z-10 mr-3" e.preventDefault();
e.stopPropagation();
setTranslateTarget(p);
}}
className="p-2 rounded-lg text-[var(--color-text-ghost)] hover:text-blue-400 hover:bg-blue-500/10 transition-colors shrink-0 z-10"
title="Projeyi Çevir"
>
<Languages size={16} />
</button>
<button
onClick={(e) => openDeleteConfirm(e, p)}
className="p-2 rounded-lg text-[var(--color-text-ghost)] hover:text-red-400 hover:bg-red-500/10 transition-colors shrink-0 z-10 mx-1"
title="Projeyi Sil" title="Projeyi Sil"
> >
<Trash2 size={16} /> <Trash2 size={16} />
</button> </button>
{!isChild && hasChildren && (
<button
onClick={(e) => toggleExpand(e, p.id)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-medium text-[var(--color-text-ghost)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-surface)] transition-colors shrink-0 z-10 mr-1 border border-transparent hover:border-[var(--color-border-faint)]"
title="Çevirileri Göster"
>
<span className="flex items-center gap-1">
<Languages size={12} />
{children.length} Çeviri
</span>
<ChevronDown size={14} className={cn("transition-transform", isExpanded && "rotate-180")} />
</button>
)}
</div>
</div>
);
};
return (
<div key={project.id} className="space-y-2">
{renderCard(project)}
{isExpanded && hasChildren && (
<div className="pl-6 md:pl-10 space-y-2 relative before:absolute before:left-[1.25rem] md:before:left-[2.25rem] before:top-0 before:bottom-0 before:w-px before:bg-neutral-200 dark:before:bg-neutral-800">
{children.map(child => renderCard(child, true))}
</div>
)}
</div> </div>
); );
}) })
@@ -293,6 +402,8 @@ export default function ProjectsPage() {
)} )}
{/* ─── Silme Onay Modal ─── */} {/* ─── Silme Onay Modal ─── */}
{mounted && createPortal(
<div className="portal-container">
<AnimatePresence> <AnimatePresence>
{deleteTarget && ( {deleteTarget && (
<motion.div <motion.div
@@ -367,6 +478,96 @@ export default function ProjectsPage() {
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
{/* ─── Çeviri Modal ─── */}
<AnimatePresence>
{translateTarget && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
onClick={() => !isTranslating && setTranslateTarget(null)}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
className="relative bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] rounded-2xl p-6 max-w-sm w-full mx-4 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
{/* Kapatma butonu */}
<button
onClick={() => setTranslateTarget(null)}
disabled={isTranslating}
className="absolute top-3 right-3 p-1 rounded-lg text-[var(--color-text-ghost)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50"
>
<X size={16} />
</button>
{/* Icon */}
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-blue-500/10 mb-4">
<Languages size={22} className="text-blue-400" />
</div> </div>
);
{/* İçerik */}
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-1.5">
Projeyi Çevir
</h3>
<p className="text-sm text-[var(--color-text-muted)] mb-3">
"{translateTarget.title}" projesini başka bir dile çevirin. (1 kredi)
</p>
<select
value={targetLanguage}
onChange={(e) => setTargetLanguage(e.target.value)}
disabled={isTranslating}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-2.5 text-sm text-[var(--color-text-primary)] mb-5"
>
<option value="">Dil Seçin...</option>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.flag} {lang.label}
</option>
))}
</select>
{/* Butonlar */}
<div className="flex items-center gap-3">
<button
onClick={() => setTranslateTarget(null)}
disabled={isTranslating}
className="flex-1 px-4 py-2.5 rounded-xl border border-[var(--color-border-default)] text-sm font-medium text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50"
>
İptal
</button>
<button
onClick={confirmTranslate}
disabled={isTranslating || !targetLanguage}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isTranslating ? (
<>
<Loader2 size={14} className="animate-spin" />
Çevriliyor...
</>
) : (
<>
<Languages size={14} />
Çevir (1 🪙)
</>
)}
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>,
document.body
)}
</div>
);
} }
@@ -1,22 +0,0 @@
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('TEXT TO VIDEO ERROR:', error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<pre>{error.message}</pre>
<pre>{error.stack}</pre>
</div>
)
}
@@ -1,130 +0,0 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { useCreateFromText } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
import { Loader2, ArrowRight, Wand2, Type } from "lucide-react";
import {
LanguageSelector,
StyleSelector,
DurationSelector,
AspectRatioSelector,
} from "@/components/projects/ProjectConfiguration";
export default function TextToVideoPage() {
const router = useRouter();
const { toast } = useToast();
const createFromText = useCreateFromText();
const [textInput, setTextInput] = useState("");
const [style, setStyle] = useState("CINEMATIC");
const [cinematicReference, setCinematicReference] = useState("");
const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
const [language, setLanguage] = useState("tr");
const handleGenerate = async () => {
if (!textInput.trim()) {
toast("error", "Lütfen bir konu, hikaye veya metin girin.");
return;
}
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = await createFromText.mutateAsync({
text: textInput,
language,
aspectRatio,
videoStyle: style,
cinematicReference: cinematicReference ? cinematicReference : undefined,
targetDuration: duration,
});
toast("success", "Video projesi başarıyla oluşturuldu!");
router.push(`/dashboard/projects/${result.id}`);
} catch (error) {
toast("error", "Proje oluşturulurken bir hata oluştu.");
}
};
return (
<div className="max-w-3xl mx-auto space-y-8 pb-24">
{/* Header */}
<div className="text-center space-y-3 pb-4">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
<Type size={32} />
</div>
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
Metinden Video Üret
</h1>
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
İstediğiniz konuyu, bir hikayeyi veya makaleyi kopyalayıp yapıştırın; yapay zeka sizin için detaylı bir video senaryosu üretsin.
</p>
</div>
{/* Input */}
<div className="card p-6 md:p-8 space-y-6">
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
Fikriniz veya Metniniz
</label>
<textarea
value={textInput}
onChange={(e) => setTextInput(e.target.value)}
placeholder="Örn: Evrenin başlangıcı ve kara deliklerin sırları hakkında detaylı ve sürükleyici bir anlatım..."
rows={6}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-bg-inverted)]/50 resize-none transition-all placeholder:text-[var(--color-text-ghost)]"
/>
</div>
{/* Configurations */}
<div className="space-y-8 pt-6 border-t border-[var(--color-border-faint)]">
<LanguageSelector value={language} onChange={setLanguage} />
<StyleSelector
value={style}
onChange={setStyle}
cinematicReference={cinematicReference}
onCinematicReferenceChange={setCinematicReference}
/>
<DurationSelector value={duration} onChange={setDuration} />
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
</div>
</div>
{/* Action Button */}
<div className="flex justify-end">
<button
onClick={handleGenerate}
disabled={createFromText.isPending || !textInput.trim()}
className={cn(
"group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
"bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
"disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
)}
>
{createFromText.isPending ? (
<>
<Loader2 size={20} className="animate-spin" />
<span>Yapay Zeka Senaryoyu Yazıyor...</span>
</>
) : (
<>
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
<span>Video Projesi Oluştur</span>
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</>
)}
</button>
</div>
</div>
);
}
@@ -0,0 +1,179 @@
"use client";
import { Wrench, Video, ArrowRight, Mic } from "lucide-react";
import Link from "next/link";
import { motion, useMotionTemplate, useMotionValue, useSpring } from "framer-motion";
import { useState, useRef } from "react";
function ToolCard({
href,
title,
description,
icon: Icon,
colorClass,
spotlightColor
}: {
href: string,
title: string,
description: string,
icon: any,
colorClass: string,
spotlightColor: string
}) {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const rotateX = useSpring(0, { stiffness: 300, damping: 20 });
const rotateY = useSpring(0, { stiffness: 300, damping: 20 });
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
mouseX.set(x);
mouseY.set(y);
const xPct = (x / rect.width) - 0.5;
const yPct = (y / rect.height) - 0.5;
// Hover üst kenar (yPct negatif) -> top backward (pozitif rotateX)
rotateX.set(-yPct * 20);
// Hover sağ kenar (xPct pozitif) -> right backward (pozitif rotateY)
rotateY.set(xPct * 20);
};
const handleMouseLeave = () => {
rotateX.set(0);
rotateY.set(0);
};
return (
<Link href={href} className="block h-full" style={{ perspective: 1200 }}>
<motion.div
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{
rotateX,
rotateY,
transformStyle: "preserve-3d",
}}
whileHover={{ scale: 1.02 }}
className="group relative h-full glass p-6 rounded-2xl border border-[var(--color-border-faint)] overflow-hidden"
>
{/* Spotlight & Glowing Border effect */}
<motion.div
className="pointer-events-none absolute -inset-px opacity-0 transition duration-500 group-hover:opacity-100 z-10"
style={{
background: useMotionTemplate`
radial-gradient(
400px circle at ${mouseX}px ${mouseY}px,
${spotlightColor},
transparent 40%
)
`,
}}
/>
{/* Border glow specifically tracking mouse */}
<motion.div
className="pointer-events-none absolute inset-0 rounded-2xl opacity-0 transition duration-500 group-hover:opacity-100"
style={{
boxShadow: useMotionTemplate`
inset 0 0 0 1px rgba(255, 255, 255, 0.1),
0 0 20px 2px ${spotlightColor}
`,
background: useMotionTemplate`
radial-gradient(
200px circle at ${mouseX}px ${mouseY}px,
rgba(255,255,255,0.1),
transparent 40%
)
`,
}}
/>
{/* İçerik, Z-ekseninde hafifçe öne çıkarılır ki 3D efekti belirginleşsin */}
<div style={{ transform: "translateZ(40px)" }} className="flex flex-col h-full pointer-events-none">
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-bl-full -z-10 transition-transform group-hover:scale-110" />
<div className={`w-12 h-12 rounded-xl flex items-center justify-center mb-6 shadow-lg ${colorClass}`}>
<Icon size={24} />
</div>
<h3 className="text-xl font-bold text-white mb-3 font-[family-name:var(--font-display)]">
{title}
</h3>
<p className="text-[var(--color-text-ghost)] text-sm mb-6 leading-relaxed flex-grow">
{description}
</p>
<div className={`flex items-center text-sm font-medium ${colorClass.split(" ")[1]}`}>
<span>Aracı Başlat</span>
<ArrowRight size={16} className="ml-2 group-hover:translate-x-1 transition-transform" />
</div>
</div>
</motion.div>
</Link>
);
}
export default function ToolsPage() {
return (
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-10">
<div className="flex items-center gap-3 text-[var(--color-primary)] mb-3">
<Wrench size={24} className="animate-pulse-subtle" />
<h2 className="text-sm font-semibold tracking-widest uppercase">Gelişmiş Araçlar</h2>
</div>
<h1 className="text-4xl md:text-5xl font-[family-name:var(--font-display)] font-bold text-white mb-4 tracking-tight">
İçerik Strateji Merkezi
</h1>
<p className="text-[var(--color-text-muted)] text-lg max-w-2xl leading-relaxed">
Yapay zeka destekli analiz araçlarıyla içeriklerinizi optimize edin, kitle analizleri yapın ve rakip stratejilerini çözün.
</p>
</div>
{/* Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<ToolCard
href="/dashboard/tools/youtube-analyzer"
title="YouTube Video & Yorum Analizi"
description="Uzun format videoların transkriptlerini ve on binlerce yorumu tek tıkla analiz edin. Duygu durumları, özetler ve yepyeni içerik fikirleri elde edin."
icon={Video}
colorClass="bg-red-500/20 text-red-400"
spotlightColor="rgba(239, 68, 68, 0.15)"
/>
<ToolCard
href="/dashboard/tools/youtube-seo"
title="YouTube SEO Power Engine"
description="Videolarınızı sıralamada zirveye taşıyın. A/B test başlıkları, kanca analizi, long-tail keywordler ve viral kapak görseli promptları elde edin."
icon={Wrench}
colorClass="bg-orange-500/20 text-orange-400"
spotlightColor="rgba(249, 115, 22, 0.15)"
/>
<ToolCard
href="/dashboard/tools/tube-strategist"
title="Tube Strategist"
description="Eksiksiz veri analizi ve viral nöro-pazarlama motoru. İçeriğinizin stratejisini yapay zeka ile inşa edin."
icon={Video}
colorClass="bg-blue-500/20 text-blue-400"
spotlightColor="rgba(59, 130, 246, 0.15)"
/>
<ToolCard
href="/dashboard/tools/voicebox"
title="VoiceBox Studio"
description="Açık kaynak, yerel ve limitsiz yapay zeka ses stüdyosu. Videolarınız için klonlanmış veya varsayılan ultra-gerçekçi sesler (TTS) üretin."
icon={Mic}
colorClass="bg-purple-500/20 text-purple-400"
spotlightColor="rgba(168, 85, 247, 0.15)"
/>
</div>
</div>
);
}
@@ -0,0 +1,818 @@
"use client";
import React, { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import {
ArrowLeft, Zap, Video, Film, Loader2, PlayCircle, Eye, MessageCircle,
Settings, FileText, CheckCircle2, Sparkles, Target, AlignLeft, Users, Clock, User, FilePlus, X
} from 'lucide-react';
import {
getProjectById, ProjectResponse, addVideoToProject, addDocumentToProject,
updateProject, createEpisode, getTopicSuggestions, TopicSuggestion, EpisodeResponse,
generateCommunityIdeas
} from '../services/strategistApi';
class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean, error: Error | null}> {
constructor(props: {children: React.ReactNode}) { super(props); this.state = { hasError: false, error: null }; }
static getDerivedStateFromError(error: Error) { return { hasError: true, error }; }
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error("ErrorBoundary caught an error", error, errorInfo); }
render() {
if (this.state.hasError) {
return (
<div className="p-10 flex flex-col items-center justify-center text-center">
<h2 className="text-xl font-bold text-red-500 mb-4">Sayfa Yüklenirken Hata Oluştu (ErrorBoundary)</h2>
<pre className="text-left bg-gray-900 p-4 rounded text-xs text-red-300 w-full max-w-2xl overflow-auto">{this.state.error?.toString()}</pre>
<button onClick={() => window.location.reload()} className="mt-6 px-4 py-2 bg-red-500 text-white rounded font-bold">Sayfayı Yenile</button>
</div>
);
}
return this.props.children;
}
}
export default function StrategistHubPage() {
const router = useRouter();
const params = useParams();
const projectId = params.projectId as string;
const [project, setProject] = useState<ProjectResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState("");
// Dataset State
const [activeTab, setActiveTab] = useState<'videos' | 'documents'>('videos');
// Add Video State
const [videoUrl, setVideoUrl] = useState("");
const [isAddingVideo, setIsAddingVideo] = useState(false);
const [videoError, setVideoError] = useState("");
// Add Document State
const [docTitle, setDocTitle] = useState("");
const [docContent, setDocContent] = useState("");
const [docType, setDocType] = useState<'transcript' | 'comments'>('transcript');
const [isAddingDoc, setIsAddingDoc] = useState(false);
const [docError, setDocError] = useState("");
// Settings Modal State
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isFormatExpanded, setIsFormatExpanded] = useState(false);
const [settingsForm, setSettingsForm] = useState({
name: "", tone: "", targetDuration: "", speakerName: "", targetAudience: "", formatDescription: ""
});
const [isUpdatingSettings, setIsUpdatingSettings] = useState(false);
// New Episode Modal State
const [isEpisodeModalOpen, setIsEpisodeModalOpen] = useState(false);
const [episodeForm, setEpisodeForm] = useState({
topic: "", format: "", targetAudience: "", duration: ""
});
const [isAiTopic, setIsAiTopic] = useState(false);
const [suggestions, setSuggestions] = useState<TopicSuggestion[]>([]);
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
const [isGeneratingIdeas, setIsGeneratingIdeas] = useState(false);
const [isCreatingEpisode, setIsCreatingEpisode] = useState(false);
const [episodeError, setEpisodeError] = useState("");
const fetchProject = async () => {
try {
setIsLoading(true);
const data = await getProjectById(projectId);
setProject(data);
setSettingsForm({
name: data.name || "",
tone: data.tone || "",
targetDuration: data.targetDuration || "",
speakerName: data.speakerName || "",
targetAudience: data.targetAudience || "",
formatDescription: data.formatDescription || ""
});
// Set initial values for episode modal if not set
setEpisodeForm(prev => ({
...prev,
targetAudience: data.targetAudience || "",
duration: data.targetDuration || "",
format: data.formatDescription ? data.formatDescription.substring(0, 50) + '...' : ""
}));
} catch (err: any) {
setError("Proje yüklenemedi.");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (projectId) {
fetchProject();
}
}, [projectId]);
const handleUpdateSettings = async (e: React.FormEvent) => {
e.preventDefault();
setIsUpdatingSettings(true);
try {
await updateProject(projectId, settingsForm);
await fetchProject();
setIsSettingsOpen(false);
} catch (err) {
console.error("Failed to update settings", err);
} finally {
setIsUpdatingSettings(false);
}
};
const handleAddVideo = async (e: React.FormEvent) => {
e.preventDefault();
if (!videoUrl) return;
setIsAddingVideo(true);
setVideoError("");
try {
await addVideoToProject(projectId, videoUrl);
setVideoUrl("");
await fetchProject();
} catch (err: any) {
setVideoError(err?.response?.data?.message || "Video eklenemedi.");
} finally {
setIsAddingVideo(false);
}
};
const handleAddDocument = async (e: React.FormEvent) => {
e.preventDefault();
if (!docTitle || !docContent) return;
setIsAddingDoc(true);
setDocError("");
try {
await addDocumentToProject(projectId, docTitle, docContent, docType);
setDocTitle("");
setDocContent("");
await fetchProject();
} catch (err: any) {
setDocError(err?.response?.data?.message || "Doküman eklenemedi.");
} finally {
setIsAddingDoc(false);
}
};
const handleGetSuggestions = async () => {
setIsLoadingSuggestions(true);
try {
const res = await getTopicSuggestions(projectId);
setSuggestions(res.suggestions || []);
} catch (err) {
console.error("Failed to get suggestions", err);
} finally {
setIsLoadingSuggestions(false);
}
};
const handleGenerateCommunityIdeas = async () => {
setIsGeneratingIdeas(true);
try {
await generateCommunityIdeas(projectId);
await fetchProject();
} catch (err) {
console.error("Failed to generate community ideas", err);
alert("Topluluk analizi yapılırken bir hata oluştu.");
} finally {
setIsGeneratingIdeas(false);
}
};
const handleCreateEpisode = async (e: React.FormEvent) => {
e.preventDefault();
const finalTopic = isAiTopic ? "AI_AUTO" : episodeForm.topic;
if (!finalTopic && !isAiTopic) {
setEpisodeError("Lütfen bir konu başlığı girin veya yapay zeka önerisi seçin.");
return;
}
setIsCreatingEpisode(true);
setEpisodeError("");
try {
await createEpisode(
projectId,
finalTopic,
episodeForm.format,
episodeForm.targetAudience,
episodeForm.duration
);
setIsEpisodeModalOpen(false);
setEpisodeForm(prev => ({ ...prev, topic: "" }));
setIsAiTopic(false);
setSuggestions([]);
await fetchProject(); // Refresh the list
} catch (err: any) {
setEpisodeError(err?.response?.data?.message || "Bölüm oluşturulamadı.");
} finally {
setIsCreatingEpisode(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<Loader2 className="w-10 h-10 animate-spin text-red-500" />
</div>
);
}
if (!project) {
return (
<div className="max-w-4xl mx-auto py-20 text-center">
<h2 className="text-2xl font-bold mb-4 text-[var(--color-text-primary)]">Proje Bulunamadı</h2>
<button onClick={() => router.push(`/${params.locale}/dashboard/tools/tube-strategist`)} className="text-red-500 font-bold underline">
Geri Dön
</button>
</div>
);
}
const youtubeVideos = project.videos?.filter(v => !v.videoId.startsWith('doc://')) || [];
const manualDocs = project.videos?.filter(v => v.videoId.startsWith('doc://')) || [];
return (
<ErrorBoundary>
<div className="max-w-6xl mx-auto space-y-8 pb-24 font-sans px-4 sm:px-0 pt-8">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<button
onClick={() => router.push(`/${params.locale}/dashboard/tools/tube-strategist`)}
className="w-10 h-10 rounded-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] flex items-center justify-center text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:border-[var(--color-border-default)] transition-colors"
>
<ArrowLeft size={20} />
</button>
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-[var(--color-text-primary)]">{project.name}</h1>
<div className="px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider bg-purple-500/10 text-purple-500 border border-purple-500/20 flex items-center gap-1.5">
<Film size={12} /> Hub Merkezi
</div>
</div>
<p className="text-sm text-[var(--color-text-secondary)] mt-1 font-medium">
Formatınızı yönetin, verisetinizi genişletin ve yeni bölümler tasarlayın.
</p>
</div>
</div>
<button
onClick={() => setIsSettingsOpen(true)}
className="px-4 py-2 bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] hover:bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] font-bold text-sm rounded-xl flex items-center gap-2 transition-all"
>
<Settings size={16} /> Ayarları Düzenle
</button>
</div>
{/* Format Info Card */}
<div className="card p-6 border border-[var(--color-border-faint)] bg-gradient-to-br from-[var(--color-bg-elevated)] to-[var(--color-bg-base)]">
<div className="flex items-start justify-between">
<div className="space-y-4 flex-1">
<h3 className="text-lg font-bold text-[var(--color-text-primary)] flex items-center gap-2">
<Target className="text-blue-500" size={20} /> Format & Konsept
</h3>
<div className="text-sm text-[var(--color-text-secondary)] leading-relaxed whitespace-pre-wrap">
{project.formatDescription ? (
<>
{isFormatExpanded || project.formatDescription.length <= 500
? project.formatDescription
: `${project.formatDescription.slice(0, 500)}...`}
{project.formatDescription.length > 500 && (
<button
onClick={() => setIsFormatExpanded(!isFormatExpanded)}
className="text-blue-500 hover:text-blue-400 font-medium ml-2 underline underline-offset-2"
>
{isFormatExpanded ? 'Daha az göster' : 'Devamını gör'}
</button>
)}
</>
) : (
"Format açıklaması girilmemiş. Ayarlardan ekleyebilirsiniz."
)}
</div>
<div className="flex flex-wrap items-center gap-4 pt-2">
<div className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-secondary)] bg-[var(--color-bg-surface)] px-3 py-1.5 rounded-lg border border-[var(--color-border-default)]">
<Users size={14} className="text-purple-500" /> {project.targetAudience || '-'}
</div>
<div className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-secondary)] bg-[var(--color-bg-surface)] px-3 py-1.5 rounded-lg border border-[var(--color-border-default)]">
<AlignLeft size={14} className="text-green-500" /> {project.tone || '-'}
</div>
<div className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-secondary)] bg-[var(--color-bg-surface)] px-3 py-1.5 rounded-lg border border-[var(--color-border-default)]">
<Clock size={14} className="text-orange-500" /> {project.targetDuration || '-'}
</div>
<div className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-secondary)] bg-[var(--color-bg-surface)] px-3 py-1.5 rounded-lg border border-[var(--color-border-default)]">
<User size={14} className="text-blue-500" /> {project.speakerName || '-'}
</div>
</div>
</div>
</div>
</div>
<div className="grid lg:grid-cols-2 gap-8">
{/* Left Column: DATASET */}
<div className="space-y-6">
<div className="card p-6 border border-[var(--color-border-faint)] bg-[var(--color-bg-elevated)]">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-bold text-[var(--color-text-primary)] flex items-center gap-2">
<Video className="text-red-500" size={20} />
Veriseti ({project.videos?.length || 0})
</h2>
</div>
{/* Tabs */}
<div className="flex items-center gap-2 mb-6 p-1 bg-[var(--color-bg-base)] rounded-xl border border-[var(--color-border-default)]">
<button
onClick={() => setActiveTab('videos')}
className={`flex-1 py-2 text-xs font-bold rounded-lg transition-colors ${activeTab === 'videos' ? 'bg-[var(--color-bg-elevated)] shadow-sm text-[var(--color-text-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}`}
>
YouTube Videoları ({youtubeVideos.length})
</button>
<button
onClick={() => setActiveTab('documents')}
className={`flex-1 py-2 text-xs font-bold rounded-lg transition-colors ${activeTab === 'documents' ? 'bg-[var(--color-bg-elevated)] shadow-sm text-[var(--color-text-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}`}
>
Metin / Doküman ({manualDocs.length})
</button>
</div>
{activeTab === 'videos' && (
<>
<form onSubmit={handleAddVideo} className="flex items-center gap-3 mb-6 bg-[var(--color-bg-base)] p-2 rounded-xl border border-[var(--color-border-default)]">
<input
type="url"
placeholder="https://youtube.com/watch?v=..."
value={videoUrl}
onChange={(e) => setVideoUrl(e.target.value)}
className="flex-1 bg-transparent border-none px-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-0"
required
/>
<button
type="submit"
disabled={isAddingVideo || !videoUrl.trim()}
className="px-4 py-2 rounded-lg bg-[var(--color-bg-elevated)] border border-[var(--color-border-strong)] text-[var(--color-text-primary)] font-bold text-xs flex items-center gap-2 hover:bg-[var(--color-bg-surface)] disabled:opacity-50 transition-colors"
>
{isAddingVideo ? <Loader2 size={14} className="animate-spin" /> : <Video size={14} />}
Ekle
</button>
</form>
{videoError && <p className="text-xs text-red-500 mb-4">{videoError}</p>}
{youtubeVideos.length === 0 ? (
<div className="py-8 flex flex-col items-center justify-center text-center border-2 border-dashed border-[var(--color-border-default)] rounded-2xl bg-[var(--color-bg-base)]">
<PlayCircle className="w-8 h-8 text-[var(--color-text-ghost)] mb-2" />
<p className="text-[var(--color-text-secondary)] text-sm font-medium">Video eklenmedi.</p>
</div>
) : (
<div className="space-y-3 max-h-[400px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-[var(--color-border-strong)] scrollbar-track-transparent">
{youtubeVideos.map((vid: any) => (
<div key={vid.id} className="p-3 rounded-xl bg-[var(--color-bg-base)] border border-[var(--color-border-default)] flex gap-3">
<img src={vid.thumbnail} alt={vid.title} className="w-24 aspect-video object-cover rounded-lg shadow-sm flex-shrink-0" />
<div className="flex-1 min-w-0 flex flex-col justify-center">
<h3 className="text-[13px] font-bold text-[var(--color-text-primary)] line-clamp-2 leading-tight mb-1.5">
{vid.title}
</h3>
<div className="flex items-center gap-3 text-[10px] font-medium text-[var(--color-text-secondary)]">
<span className="flex items-center gap-1"><Eye size={12} className="text-blue-400" /> {Number(vid.viewCount || 0).toLocaleString('tr-TR')}</span>
<span className="flex items-center gap-1"><MessageCircle size={12} className="text-purple-400" /> {Number(vid.totalComments || 0).toLocaleString('tr-TR')}</span>
</div>
</div>
</div>
))}
</div>
)}
</>
)}
{activeTab === 'documents' && (
<>
<form onSubmit={handleAddDocument} className="mb-6 space-y-3 bg-[var(--color-bg-base)] p-4 rounded-xl border border-[var(--color-border-default)]">
<div className="flex gap-3">
<input
type="text"
placeholder="Doküman Başlığı"
value={docTitle}
onChange={(e) => setDocTitle(e.target.value)}
className="flex-1 bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-lg px-3 py-2 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-1 focus:ring-red-500"
required
/>
<select
value={docType}
onChange={(e) => setDocType(e.target.value as any)}
className="bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-lg px-3 py-2 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-1 focus:ring-red-500"
>
<option value="transcript">Transkript</option>
<option value="comments">Yorumlar</option>
</select>
</div>
<textarea
placeholder="Metin içeriğini buraya yapıştırın..."
value={docContent}
onChange={(e) => setDocContent(e.target.value)}
rows={4}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-lg px-3 py-2 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-1 focus:ring-red-500 resize-none"
required
/>
<button
type="submit"
disabled={isAddingDoc || !docTitle.trim() || !docContent.trim()}
className="w-full px-4 py-2.5 rounded-lg bg-[var(--color-bg-elevated)] border border-[var(--color-border-strong)] text-[var(--color-text-primary)] font-bold text-xs flex items-center justify-center gap-2 hover:bg-[var(--color-bg-surface)] disabled:opacity-50 transition-colors"
>
{isAddingDoc ? <Loader2 size={14} className="animate-spin" /> : <FilePlus size={14} />}
Dokümanı Verisetine Ekle
</button>
</form>
{docError && <p className="text-xs text-red-500 mb-4">{docError}</p>}
{manualDocs.length === 0 ? (
<div className="py-8 flex flex-col items-center justify-center text-center border-2 border-dashed border-[var(--color-border-default)] rounded-2xl bg-[var(--color-bg-base)]">
<FileText className="w-8 h-8 text-[var(--color-text-ghost)] mb-2" />
<p className="text-[var(--color-text-secondary)] text-sm font-medium">Manuel doküman eklenmedi.</p>
</div>
) : (
<div className="space-y-3 max-h-[300px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-[var(--color-border-strong)] scrollbar-track-transparent">
{manualDocs.map((vid: any) => (
<div key={vid.id} className="p-3 rounded-xl bg-[var(--color-bg-base)] border border-[var(--color-border-default)] flex gap-3 items-center">
<div className="w-12 h-12 rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-strong)] flex items-center justify-center flex-shrink-0">
<FileText className={vid.totalComments > 0 ? "text-purple-500" : "text-blue-500"} size={20} />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-[13px] font-bold text-[var(--color-text-primary)] line-clamp-1 mb-1">
{vid.title}
</h3>
<div className="text-[10px] font-medium text-[var(--color-text-secondary)]">
{vid.totalComments > 0 ? "Yorum Verisi" : "Transkript Verisi"}
</div>
</div>
</div>
))}
</div>
)}
</>
)}
</div>
</div>
{/* Right Column: EPISODES */}
<div className="space-y-6">
<div className="card p-6 border border-[var(--color-border-faint)] bg-[var(--color-bg-base)] shadow-inner">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-bold text-[var(--color-text-primary)] flex items-center gap-2">
<Film className="text-orange-500" size={20} />
Bölüm Tasarımları ({project.episodes?.length || 0})
</h2>
<button
onClick={() => setIsEpisodeModalOpen(true)}
disabled={project.videos.length === 0}
className="px-4 py-2 bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white font-bold text-xs rounded-lg flex items-center gap-1.5 shadow-md shadow-red-500/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
title={project.videos.length === 0 ? "Önce en az 1 referans video eklemelisiniz." : ""}
>
<Zap size={14} /> Yeni Bölüm
</button>
</div>
{!project.episodes || project.episodes.length === 0 ? (
<div className="py-16 flex flex-col items-center justify-center text-center">
<div className="w-16 h-16 rounded-2xl bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] flex items-center justify-center mb-4 shadow-sm">
<Sparkles className="w-8 h-8 text-orange-500" />
</div>
<h3 className="text-base font-bold text-[var(--color-text-primary)] mb-1">Henüz Bölüm Yok</h3>
<p className="text-[var(--color-text-secondary)] text-sm max-w-[250px] mx-auto">
Verisetinizi ekledikten sonra ilk bölüm tasarımınızı oluşturun.
</p>
</div>
) : (
<div className="space-y-4">
{project.episodes.map((ep: any) => (
<div
key={ep.id}
onClick={() => router.push(`/${params.locale}/dashboard/tools/tube-strategist/${projectId}/episode/${ep.id}`)}
className="group cursor-pointer p-4 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] hover:border-orange-500/50 hover:shadow-lg hover:shadow-orange-500/5 transition-all relative overflow-hidden"
>
<div className="absolute top-0 left-0 w-1 h-full bg-orange-500/20 group-hover:bg-orange-500 transition-colors" />
<div className="flex items-start justify-between mb-3">
<div className="flex-1 pr-4">
<h3 className="text-[15px] font-bold text-[var(--color-text-primary)] line-clamp-1 mb-1">
{ep.topic === 'AI_AUTO' ? '✨ Yapay Zeka Belirliyor...' : ep.topic}
</h3>
<div className="flex items-center gap-3 text-[11px] text-[var(--color-text-secondary)] font-medium">
<span className="flex items-center gap-1"><Users size={12} /> {ep.targetAudience}</span>
<span className="flex items-center gap-1"><Clock size={12} /> {ep.duration}</span>
</div>
</div>
{/* Status Badge */}
<div className={`px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider rounded-md whitespace-nowrap ${
ep.status === 'COMPLETED' ? 'bg-green-500/10 text-green-500 border border-green-500/20' :
ep.status === 'ANALYZING' ? 'bg-blue-500/10 text-blue-500 border border-blue-500/20 animate-pulse' :
'bg-[var(--color-bg-surface)] text-[var(--color-text-secondary)] border border-[var(--color-border-strong)]'
}`}>
{ep.status === 'COMPLETED' ? 'TAMAMLANDI' : ep.status === 'ANALYZING' ? 'ANALİZ EDİLİYOR' : 'BEKLİYOR'}
</div>
</div>
{/* Mini Reports Indicators (Only if completed) */}
{ep.status === 'COMPLETED' && ep.masterAnalysis && (
<div className="flex items-center gap-2 pt-3 mt-3 border-t border-[var(--color-border-faint)]">
<span className={`text-[10px] font-bold px-2 py-0.5 rounded flex items-center gap-1 ${ep.masterAnalysis.seo ? 'bg-blue-500/10 text-blue-500' : 'bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)]'}`}>
{ep.masterAnalysis.seo && <CheckCircle2 size={10} />} SEO
</span>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded flex items-center gap-1 ${ep.masterAnalysis.commercial ? 'bg-purple-500/10 text-purple-500' : 'bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)]'}`}>
{ep.masterAnalysis.commercial && <CheckCircle2 size={10} />} Sponsorluk
</span>
</div>
)}
</div>
))}
</div>
)}
</div>
{/* Community Demand Radar Section */}
<div className="card p-6 border border-purple-500/20 shadow-[0_8px_30px_rgb(168,85,247,0.1)] mt-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-purple-500/10 flex items-center justify-center text-purple-500">
<Sparkles size={20} />
</div>
<div>
<h2 className="text-xl font-bold text-[var(--color-text-primary)]">Gelecek Bölüm Radarı</h2>
<p className="text-sm text-[var(--color-text-secondary)]">İzleyici talepleri ve analizleri</p>
</div>
</div>
<button
onClick={handleGenerateCommunityIdeas}
disabled={isGeneratingIdeas || (youtubeVideos.length === 0 && manualDocs.length === 0)}
className="px-4 py-2 bg-purple-500 hover:bg-purple-600 disabled:opacity-50 text-white font-bold text-sm rounded-xl flex items-center gap-2 transition-colors"
>
{isGeneratingIdeas ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles size={16} />}
{project.communityInsights ? "Yeniden Analiz Et" : "Kitleyi Analiz Et"}
</button>
</div>
{project.communityInsights?.insights && project.communityInsights.insights.length > 0 ? (
<div className="grid grid-cols-1 gap-4">
{project.communityInsights.insights.map((idea: any, idx: number) => (
<div key={idx} className="bg-[var(--color-bg-elevated)] p-4 rounded-xl border border-[var(--color-border-faint)] hover:border-purple-500/30 transition-colors">
<div className="flex items-start justify-between mb-2">
<h3 className="font-bold text-[var(--color-text-primary)] text-sm">{idea.topic}</h3>
<div className="flex flex-col items-end">
<span className="text-[10px] font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-1">Virallik Skoru</span>
<div className="flex items-center gap-2">
<div className="w-16 h-2 bg-[var(--color-bg-default)] rounded-full overflow-hidden">
<div
className="h-full rounded-full bg-purple-500"
style={{ width: `${idea.viralityScore}%` }}
/>
</div>
<span className="text-xs font-bold text-[var(--color-text-primary)]">{idea.viralityScore}</span>
</div>
</div>
</div>
<p className="text-xs text-[var(--color-text-secondary)] mb-3 line-clamp-2">{idea.demandReason}</p>
<div className="bg-[var(--color-bg-default)] p-2 rounded-lg border border-[var(--color-border-faint)]">
<span className="text-[10px] text-purple-500 font-bold block mb-1">Önerilen Başlık</span>
<p className="text-xs text-[var(--color-text-primary)] font-medium">"{idea.proposedTitle}"</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<p className="text-[var(--color-text-secondary)] text-sm">Henüz bir kitle analizi bulunmuyor. Referans ekledikten sonra analizi başlatabilirsiniz.</p>
</div>
)}
</div>
</div>
</div>
</div>
{/* Settings Modal */}
{isSettingsOpen && (
<div className="fixed inset-0 z-50 overflow-y-auto bg-black/60 backdrop-blur-sm">
<div className="flex min-h-full items-center justify-center p-4 py-10">
<div className="bg-[var(--color-bg-elevated)] w-full max-w-2xl rounded-2xl border border-[var(--color-border-faint)] shadow-2xl flex flex-col">
<div className="flex items-center justify-between p-6 border-b border-[var(--color-border-faint)]">
<h2 className="text-xl font-bold text-[var(--color-text-primary)]">Proje Ayarları</h2>
<button onClick={() => setIsSettingsOpen(false)} className="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
<X size={20} />
</button>
</div>
<form onSubmit={handleUpdateSettings} className="p-6 space-y-5">
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">Proje Adı</label>
<input
type="text"
value={settingsForm.name}
onChange={(e) => setSettingsForm({...settingsForm, name: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">Format / Ana Konsept</label>
<textarea
value={settingsForm.formatDescription}
onChange={(e) => setSettingsForm({...settingsForm, formatDescription: e.target.value})}
rows={4}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50 resize-none"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">Hedef Kitle</label>
<input
type="text"
value={settingsForm.targetAudience}
onChange={(e) => setSettingsForm({...settingsForm, targetAudience: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">Hedef Süre</label>
<input
type="text"
value={settingsForm.targetDuration}
onChange={(e) => setSettingsForm({...settingsForm, targetDuration: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">Sunucu / Yüz</label>
<input
type="text"
value={settingsForm.speakerName}
onChange={(e) => setSettingsForm({...settingsForm, speakerName: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">İçerik Tonu</label>
<input
type="text"
value={settingsForm.tone}
onChange={(e) => setSettingsForm({...settingsForm, tone: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
</div>
<div className="pt-4 flex justify-end gap-3">
<button type="button" onClick={() => setIsSettingsOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">İptal</button>
<button type="submit" disabled={isUpdatingSettings} className="px-5 py-2.5 bg-white text-black font-bold text-sm rounded-xl flex items-center gap-2 hover:bg-gray-100 disabled:opacity-50">
{isUpdatingSettings ? <Loader2 size={16} className="animate-spin" /> : "Değişiklikleri Kaydet"}
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* New Episode Modal */}
{isEpisodeModalOpen && (
<div className="fixed inset-0 z-50 overflow-y-auto bg-black/60 backdrop-blur-sm">
<div className="flex min-h-full items-center justify-center p-4 py-10">
<div className="bg-[var(--color-bg-elevated)] w-full max-w-3xl rounded-2xl border border-[var(--color-border-faint)] shadow-2xl flex flex-col">
<div className="flex items-center justify-between p-6 border-b border-[var(--color-border-faint)]">
<div>
<h2 className="text-xl font-bold text-[var(--color-text-primary)] flex items-center gap-2">
<Zap className="text-orange-500" /> Yeni Bölüm Tasarla
</h2>
<p className="text-sm text-[var(--color-text-secondary)] mt-1">
Bu format için yeni bir bölümün Ön-Yapım (Pre-Production) sürecini başlatın.
</p>
</div>
<button onClick={() => setIsEpisodeModalOpen(false)} className="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
<X size={20} />
</button>
</div>
<form onSubmit={handleCreateEpisode} className="p-6 space-y-6">
{/* Topic Section */}
<div className="p-5 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-base)] space-y-4">
<div className="flex items-center justify-between">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5">
<Target size={14} className="text-red-500" /> Konu Başlığı
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={isAiTopic}
onChange={(e) => {
setIsAiTopic(e.target.checked);
if (!e.target.checked) setSuggestions([]);
}}
className="rounded border-[var(--color-border-strong)] text-orange-500 focus:ring-orange-500 bg-[var(--color-bg-surface)]"
/>
<span className="text-xs font-bold text-[var(--color-text-primary)] flex items-center gap-1">
<Sparkles size={12} className="text-orange-500" /> Konuyu Yapay Zeka Belirlesin
</span>
</label>
</div>
{!isAiTopic ? (
<input
type="text"
value={episodeForm.topic}
onChange={(e) => setEpisodeForm({...episodeForm, topic: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-strong)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
placeholder="Örn: Modern İlişkilerde Sınır Çizmek"
/>
) : (
<div className="space-y-4">
{suggestions.length === 0 ? (
<div className="flex flex-col items-center justify-center p-6 border border-dashed border-orange-500/30 rounded-xl bg-orange-500/5">
<p className="text-sm text-[var(--color-text-secondary)] mb-4 text-center max-w-md">
Yapay zeka, verisetinizdeki boşlukları ve izleyici yorumlarındaki talepleri analiz ederek size 5 benzersiz konu önerebilir.
</p>
<button
type="button"
onClick={handleGetSuggestions}
disabled={isLoadingSuggestions}
className="px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white font-bold text-sm rounded-lg flex items-center gap-2 transition-colors disabled:opacity-50"
>
{isLoadingSuggestions ? <Loader2 size={16} className="animate-spin" /> : <Sparkles size={16} />}
5 Konu Önerisi Getir
</button>
</div>
) : (
<div className="grid gap-3">
{suggestions.map((sug, idx) => (
<div
key={idx}
onClick={() => {
setEpisodeForm({...episodeForm, topic: sug.title});
setIsAiTopic(false);
}}
className="p-4 rounded-xl border border-[var(--color-border-strong)] bg-[var(--color-bg-surface)] hover:border-orange-500 cursor-pointer transition-all group"
>
<h4 className="font-bold text-[14px] text-[var(--color-text-primary)] mb-1 group-hover:text-orange-500 transition-colors">{sug.title}</h4>
<p className="text-xs text-[var(--color-text-secondary)] mb-2 line-clamp-2">{sug.description}</p>
<div className="text-[10px] text-blue-400 bg-blue-500/10 px-2 py-1 rounded inline-block">
<span className="font-bold">Neden?</span> {sug.reasoning}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
{/* Other Params */}
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5">
<Users size={14}/> Hedef Kitle
</label>
<input
type="text"
value={episodeForm.targetAudience}
onChange={(e) => setEpisodeForm({...episodeForm, targetAudience: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5">
<Clock size={14}/> Uzunluk
</label>
<input
type="text"
value={episodeForm.duration}
onChange={(e) => setEpisodeForm({...episodeForm, duration: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
</div>
{episodeError && <p className="text-sm text-red-500 font-medium">{episodeError}</p>}
<div className="pt-4 flex justify-end gap-3 border-t border-[var(--color-border-faint)]">
<button type="button" onClick={() => setIsEpisodeModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">İptal</button>
<button
type="submit"
disabled={isCreatingEpisode || (!isAiTopic && !episodeForm.topic.trim())}
className="px-6 py-2.5 bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white font-bold text-sm rounded-xl flex items-center gap-2 shadow-lg shadow-red-500/20 disabled:opacity-50 transition-all"
>
{isCreatingEpisode ? <Loader2 size={16} className="animate-spin" /> : <Zap size={16} />}
Bölüm Taslağını Oluştur
</button>
</div>
</form>
</div>
</div>
</div>
)}
</ErrorBoundary>
);
}
@@ -0,0 +1,19 @@
import React from 'react';
import { UploadCloud } from 'lucide-react';
export const LegacyUploader = () => {
return (
<div className="card p-12 flex flex-col items-center justify-center text-center border border-dashed border-[var(--color-border-default)]">
<div className="w-20 h-20 bg-[var(--color-bg-elevated)] rounded-full flex items-center justify-center mb-6">
<UploadCloud className="w-10 h-10 text-[var(--color-text-ghost)]" />
</div>
<h3 className="text-xl font-bold text-[var(--color-text-primary)] mb-2">Eski TXT Yükleyici</h3>
<p className="text-[var(--color-text-secondary)] text-sm max-w-md mb-8">
(Legacy) TXT dosyalarını manuel olarak yükleyerek analiz yapmak için bu alanı kullanabilirsiniz.
</p>
<button className="px-6 py-3 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] text-white font-bold flex items-center gap-2">
Dosya Seç
</button>
</div>
);
};
@@ -0,0 +1,179 @@
import React, { useEffect, useRef, useState } from 'react';
import { GoogleGenAI, LiveServerMessage, Modality } from "@google/genai";
import { audioContexts, decode, decodeAudioData, float32ToPcm16, encode } from '../services/strategistApi';
import { Mic, MicOff, Volume2, X, Loader2 } from 'lucide-react';
import { motion } from 'framer-motion';
interface LiveBrainstormProps {
context: string;
onClose: () => void;
}
const LiveBrainstorm: React.FC<LiveBrainstormProps> = ({ context, onClose }) => {
const [isActive, setIsActive] = useState(false);
const [status, setStatus] = useState("Bağlanmaya Hazır");
const [isLoading, setIsLoading] = useState(false);
const sessionRef = useRef<Promise<any> | null>(null);
const nextStartTimeRef = useRef<number>(0);
const sourcesRef = useRef<Set<AudioBufferSourceNode>>(new Set());
const startSession = async () => {
setIsLoading(true);
setStatus("Bağlanıyor...");
try {
// TR: Google GenAI istemcisini başlat. Guideline'a göre API key doğrudan process.env'den alınmalı.
// EN: Initialize Google GenAI client. API key must be taken directly from process.env per guidelines.
const ai = new GoogleGenAI({ apiKey: process.env.NEXT_PUBLIC_GEMINI_API_KEY });
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const sessionPromise = ai.live.connect({
model: 'gemini-2.5-flash-native-audio-preview-12-2025',
config: {
responseModalities: [Modality.AUDIO],
systemInstruction: `Sen uzman bir YouTube Stratejistisin. Şu an şu video planı üzerine konuşuyoruz: ${context}.
Kısa, enerjik ve videoyu nasıl daha viral yapabileceğimize odaklanan cevaplar ver.
Stratejik, samimi ve yaratıcı ol.`,
speechConfig: {
voiceConfig: { prebuiltVoiceConfig: { voiceName: 'Kore' } }
}
},
callbacks: {
onopen: () => {
setStatus("Bağlandı! Konuşabilirsiniz.");
setIsActive(true);
setIsLoading(false);
const source = audioContexts.input.createMediaStreamSource(stream);
const processor = audioContexts.input.createScriptProcessor(4096, 1, 1);
processor.onaudioprocess = (e) => {
const inputData = e.inputBuffer.getChannelData(0);
const pcmData = float32ToPcm16(inputData);
const base64 = encode(pcmData);
// TR: Yarış durumunu önlemek için sessionPromise kullan.
// EN: Use sessionPromise to prevent race conditions.
sessionPromise.then(session => {
session.sendRealtimeInput({
media: {
mimeType: 'audio/pcm;rate=16000',
data: base64
}
});
});
};
source.connect(processor);
processor.connect(audioContexts.input.destination);
},
onmessage: async (msg: LiveServerMessage) => {
const base64Audio = msg.serverContent?.modelTurn?.parts?.[0]?.inlineData?.data;
if (base64Audio) {
// TR: Gelen ses verisini decode et ve oynatma kuyruğuna ekle.
// EN: Decode incoming audio data and add to playback queue.
const audioData = await decode(base64Audio);
const audioBuffer = await decodeAudioData(audioData, audioContexts.output);
const source = audioContexts.output.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContexts.output.destination);
const currentTime = audioContexts.output.currentTime;
const startTime = Math.max(currentTime, nextStartTimeRef.current);
source.start(startTime);
nextStartTimeRef.current = startTime + audioBuffer.duration;
sourcesRef.current.add(source);
source.onended = () => sourcesRef.current.delete(source);
}
},
onclose: () => {
setStatus("Bağlantı Kesildi");
setIsActive(false);
setIsLoading(false);
},
onerror: (err) => {
console.error(err);
setStatus("Bir hata oluştu");
setIsActive(false);
setIsLoading(false);
}
}
});
sessionRef.current = sessionPromise;
} catch (e) {
console.error(e);
setStatus("Mikrofon erişimi engellendi");
setIsLoading(false);
}
};
const stopSession = () => {
window.location.reload();
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-[var(--color-bg-base)]/80 backdrop-blur-xl flex items-center justify-center z-[100] p-4"
>
<motion.div
initial={{ scale: 0.95, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.95, opacity: 0, y: 20 }}
className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] rounded-[2rem] p-8 w-full max-w-md shadow-2xl relative overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-red-500 to-orange-500"></div>
{isActive && (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-red-500/10 rounded-full blur-3xl animate-pulse pointer-events-none"></div>
)}
<div className="flex justify-between items-center mb-8 relative z-10">
<h2 className="text-xl font-[family-name:var(--font-display)] font-bold flex items-center gap-3 text-[var(--color-text-primary)]">
<Volume2 className="text-red-500" /> Canlı Beyin Fırtınası
</h2>
<button onClick={onClose} className="text-[var(--color-text-ghost)] hover:text-[var(--color-text-primary)] transition-colors p-2 bg-[var(--color-bg-surface)] rounded-full hover:bg-[var(--color-bg-hover)] border border-[var(--color-border-faint)]">
<X size={18} />
</button>
</div>
<div className="text-center mb-10 relative z-10">
<div className={`text-4xl font-[family-name:var(--font-display)] font-black mb-3 tracking-tighter transition-all duration-700 ${isActive ? 'text-red-500 scale-110 drop-shadow-[0_0_15px_rgba(239,68,68,0.5)]' : 'text-[var(--color-border-hover)]'}`}>
{isActive ? "DİNLİYORUM" : "ÇEVRİMDIŞI"}
</div>
<p className="text-[var(--color-text-secondary)] font-medium text-sm">{status}</p>
</div>
<div className="flex justify-center relative z-10">
{!isActive ? (
<button
onClick={startSession}
disabled={isLoading}
className="bg-gradient-to-b from-red-500 to-red-600 hover:from-red-400 hover:to-red-500 text-white rounded-full p-8 transition-all shadow-xl shadow-red-500/20 hover:scale-105 active:scale-95 disabled:opacity-50 border border-red-400/20"
>
{isLoading ? <Loader2 className="animate-spin" size={36}/> : <Mic size={36} />}
</button>
) : (
<button
onClick={stopSession}
className="bg-[var(--color-bg-surface)] hover:bg-[var(--color-border-hover)] text-[var(--color-text-primary)] rounded-full p-8 transition-all shadow-xl border border-[var(--color-border-default)] hover:scale-105 active:scale-95"
>
<MicOff size={36} />
</button>
)}
</div>
<p className="text-[9px] text-center text-[var(--color-text-ghost)] mt-10 relative z-10 uppercase font-black tracking-widest">
Gemini Live Native Audio 24.0kHz PCM
</p>
</motion.div>
</motion.div>
);
};
export default LiveBrainstorm;
@@ -0,0 +1,885 @@
import React, { useState, useEffect } from 'react';
import { StrategyResult, VideoDuration, TargetAudience, NeuroReport, MarketingInsights, SeoAnalysis } from '../types';
import { AreaChart, Area, Tooltip, ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar } from 'recharts';
import { MessageSquare, Mic, Image as ImageIcon, Search, Download, RefreshCw, Printer, Zap, Briefcase, Mail, Plus, Activity, Heart, Lightbulb, MonitorPlay, ChevronRight, Loader2, Target, Brain, TrendingUp, Users, Award, Sparkles, Layout, Database, Copy, Check, AlertTriangle, FileText, Layers, Video, Camera, Handshake, MessageCircle } from 'lucide-react';
import LiveBrainstorm from './LiveBrainstorm';
import { generateThumbnailImage, generateDeepCommercialAnalysis, generateNeuroReport, generateMarketingReport, generateSeoReport } from '../services/strategistApi';
import { motion, AnimatePresence } from 'framer-motion';
interface StrategyViewProps {
strategy: StrategyResult;
onReset: () => void;
onRegenerate?: () => void;
isRegenerating?: boolean;
transcriptsContent?: any[];
commentsContent?: any[];
currentTone: string;
currentDuration: VideoDuration;
speakerName: string;
targetAudience?: TargetAudience;
projectId: string;
}
const StrategyView: React.FC<StrategyViewProps> = ({ strategy: initialStrategy, onReset, onRegenerate, isRegenerating, currentTone, currentDuration, speakerName, targetAudience, projectId }) => {
// Safe defaults for incomplete masterAnalysis data
const safeInitial: StrategyResult = {
title: initialStrategy?.title || 'Strateji Raporu',
thumbnailConcept: initialStrategy?.thumbnailConcept || '',
generatedThumbnail: initialStrategy?.generatedThumbnail,
hook: initialStrategy?.hook || '',
segments: initialStrategy?.segments || [],
chartData: initialStrategy?.chartData || [],
selectedComments: initialStrategy?.selectedComments || [],
interviewQuestions: initialStrategy?.interviewQuestions || [],
wowFactor: initialStrategy?.wowFactor || '',
psychologicalTheme: initialStrategy?.psychologicalTheme || '',
commercialAnalysis: initialStrategy?.commercialAnalysis || { suitableIndustries: [], brandSafetyScore: 0, integrationIdeas: [], monetizationPotential: 'Low', suggestedBrands: [] },
inspiredByGap: initialStrategy?.inspiredByGap,
provenanceNotes: initialStrategy?.provenanceNotes,
neuroReport: initialStrategy?.neuroReport,
marketingInsights: initialStrategy?.marketingInsights,
seoAnalysis: initialStrategy?.seoAnalysis,
projectDNA: initialStrategy?.projectDNA,
trendAnalysis: initialStrategy?.trendAnalysis || [],
comboShorts: initialStrategy?.comboShorts || [],
crisisManagement: initialStrategy?.crisisManagement,
bRollSuggestions: initialStrategy?.bRollSuggestions || [],
communityHooks: initialStrategy?.communityHooks || [],
sponsorIntegration: initialStrategy?.sponsorIntegration || '',
};
const [strategy, setStrategy] = useState<StrategyResult>(safeInitial);
useEffect(() => {
if (initialStrategy) {
setStrategy({
title: initialStrategy.title || 'Strateji Raporu',
thumbnailConcept: initialStrategy.thumbnailConcept || '',
generatedThumbnail: initialStrategy.generatedThumbnail,
hook: initialStrategy.hook || '',
segments: initialStrategy.segments || [],
chartData: initialStrategy.chartData || [],
selectedComments: initialStrategy.selectedComments || [],
interviewQuestions: initialStrategy.interviewQuestions || [],
wowFactor: initialStrategy.wowFactor || '',
psychologicalTheme: initialStrategy.psychologicalTheme || '',
commercialAnalysis: initialStrategy.commercialAnalysis || { suitableIndustries: [], brandSafetyScore: 0, integrationIdeas: [], monetizationPotential: 'Low', suggestedBrands: [] },
inspiredByGap: initialStrategy.inspiredByGap,
provenanceNotes: initialStrategy.provenanceNotes,
neuroReport: initialStrategy.neuroReport,
marketingInsights: initialStrategy.marketingInsights,
seoAnalysis: initialStrategy.seoAnalysis,
projectDNA: initialStrategy.projectDNA,
trendAnalysis: initialStrategy.trendAnalysis || [],
comboShorts: initialStrategy.comboShorts || [],
crisisManagement: initialStrategy.crisisManagement,
bRollSuggestions: initialStrategy.bRollSuggestions || [],
communityHooks: initialStrategy.communityHooks || [],
sponsorIntegration: initialStrategy.sponsorIntegration || '',
});
}
}, [initialStrategy]);
const [activeTab, setActiveTab] = useState<'strategy' | 'neuro' | 'marketing' | 'seo' | 'commercial'>('strategy');
const [showLive, setShowLive] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [copied, setCopied] = useState(false);
const [isThumbnailExpanded, setIsThumbnailExpanded] = useState(false);
// Modular Analysis Handlers - All update the central 'strategy' object
const handleNeuroAnalysis = async () => {
setIsGenerating(true);
try {
const report = await generateNeuroReport(projectId);
setStrategy(prev => ({ ...prev, neuroReport: report }));
} catch (e) { alert("Nöro-analiz başarısız oldu."); }
setIsGenerating(false);
};
const handleMarketingAnalysis = async () => {
setIsGenerating(true);
try {
const report = await generateMarketingReport(projectId);
setStrategy(prev => ({ ...prev, marketingInsights: report }));
} catch (e) { alert("Marketing analizi başarısız oldu."); }
setIsGenerating(false);
};
const handleSeoAnalysis = async () => {
setIsGenerating(true);
try {
const report = await generateSeoReport(projectId);
if (report) {
setStrategy(prev => ({ ...prev, seoAnalysis: report }));
}
} catch (e) { alert("SEO analizi sırasında hata."); }
setIsGenerating(false);
};
const handleDeepCommercial = async () => {
setIsGenerating(true);
try {
const report = await generateDeepCommercialAnalysis(projectId);
setStrategy(prev => ({
...prev,
commercialAnalysis: { ...prev.commercialAnalysis, deepAnalysis: report }
}));
} catch (e) { alert("Ticari analiz başarısız."); }
setIsGenerating(false);
};
const handleGenThumbnail = async () => {
setIsGenerating(true);
try {
const url = await generateThumbnailImage(projectId, strategy.thumbnailConcept);
if (url) {
setStrategy(prev => ({ ...prev, generatedThumbnail: url }));
}
} catch (e) { alert("Görsel üretilemedi."); }
setIsGenerating(false);
};
// FULL EXPORT: JSON Format (Object dump)
const exportJSON = () => {
const blob = new Blob([JSON.stringify(strategy, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `TubeStrategist_Master_${strategy.title.replace(/\s+/g, '_')}.json`;
a.click();
alert("Master JSON Save (Tüm veriler dahil) indirildi.");
};
// MASTER EXPORT: HTML Format (Visual dump of EVERYTHING)
const exportHTML = () => {
const htmlContent = `
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<title>${strategy.title} - Master Strateji Raporu</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
body { font-family: 'Inter', sans-serif; padding: 40px; color: #1e293b; line-height: 1.6; background: #f1f5f9; }
.master-container { max-width: 1000px; margin: auto; background: white; padding: 60px; border-radius: 30px; box-shadow: 0 20px 50px rgba(0,0,0,0.05); }
h1 { font-size: 48px; font-weight: 900; color: #e11d48; margin-bottom: 10px; line-height: 1.1; }
.hook-box { background: #fff1f2; border-left: 6px solid #e11d48; padding: 25px; border-radius: 0 15px 15px 0; margin: 30px 0; font-style: italic; font-size: 20px; color: #881337; }
.section-title { font-size: 24px; font-weight: 900; color: #0f172a; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-top: 60px; margin-bottom: 30px; display: flex; align-items: center; gap: 10px; text-transform: uppercase; letter-spacing: 1px; }
.thumbnail { width: 100%; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); margin-bottom: 40px; }
.card { background: #f8fafc; border: 1px solid #e2e8f0; padding: 25px; border-radius: 15px; margin-bottom: 20px; }
.badge { background: #e11d48; color: white; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 900; text-transform: uppercase; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.tag { display: inline-block; background: #e2e8f0; color: #475569; padding: 4px 10px; border-radius: 6px; font-size: 11px; font-weight: bold; margin: 2px; }
.question-list { counter-reset: q-counter; list-style: none; padding: 0; }
.question-item { display: flex; gap: 15px; margin-bottom: 15px; background: #fff; padding: 15px; border-radius: 12px; border: 1px solid #f1f5f9; }
.question-item::before { counter-increment: q-counter; content: counter(q-counter); background: #e11d48; color: white; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 900; flex-shrink: 0; }
.neuro-score { background: #0f172a; color: white; padding: 10px 20px; border-radius: 10px; text-align: center; }
.email-draft { background: #0f172a; color: #cbd5e1; padding: 30px; border-radius: 15px; font-family: monospace; font-size: 13px; white-space: pre-wrap; line-height: 1.8; }
footer { text-align: center; margin-top: 60px; font-size: 10px; color: #94a3b8; font-weight: 900; letter-spacing: 2px; text-transform: uppercase; }
</style>
</head>
<body>
<div class="master-container">
<h1>${strategy.title}</h1>
<p style="color: #64748b; font-weight: bold;">TubeStrategist Master Analysis Report</p>
${strategy.generatedThumbnail ? `<img src="${strategy.generatedThumbnail}" class="thumbnail" />` : ''}
<div class="hook-box">"${strategy.hook}"</div>
<div class="grid-2">
<div class="card"><strong>Psikolojik Tema:</strong><br/>${strategy.psychologicalTheme}</div>
<div class="card"><strong>WoW Faktörü:</strong><br/>${strategy.wowFactor}</div>
</div>
<h2 class="section-title">🎬 Senaryo Akışı & Zaman Çizelgesi</h2>
${(strategy.segments || []).map(s => `
<div class="card" style="border-left: 5px solid #e11d48;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span class="badge">${s.duration}</span>
<span style="font-size: 11px; font-weight: 900; color: #e11d48; text-transform: uppercase;">${s.neuroObjective || 'Viral Katılım'}</span>
</div>
<h3 style="margin: 0 0 10px 0; font-size: 18px;">${s.type}</h3>
<p style="font-size: 14px; color: #475569;">${s.description}</p>
<div style="margin-top: 10px;">
${(s.keyPoints || []).map(k => `<span class="tag">${k}</span>`).join('')}
</div>
</div>
`).join('')}
<h2 class="section-title">🎤 Kalbe Dokunan 20 Soru</h2>
<div class="question-list">
${(strategy.interviewQuestions || []).map(q => `<div class="question-item">${q}</div>`).join('')}
</div>
${strategy.neuroReport ? `
<h2 class="section-title">🧠 Nöro-Pazarlama Laboratuvarı</h2>
<div class="grid-2">
<div class="card">
<strong>Göz Odağı (Eye Tracking):</strong><br/>
<p style="font-size: 13px;">${strategy.neuroReport.eyeTrackingFocus}</p>
</div>
<div class="card">
<strong>Renk Psikolojisi:</strong><br/>
<p style="font-size: 13px;">${strategy.neuroReport.colorPsychology}</p>
</div>
</div>
<div class="card">
<strong>Dopamin Tetikleyiciler:</strong><br/>
${strategy.neuroReport.dopamineTriggers.map(t => `<span class="tag" style="background: #fae8ff; color: #a21caf;">${t}</span>`).join('')}
</div>
` : ''}
${strategy.marketingInsights ? `
<h2 class="section-title">📈 Pazarlama & Viral Stratejisi</h2>
<div class="grid-2">
<div class="card">
<strong>Hedef Personalar:</strong><br/>
<ul style="font-size: 13px; padding-left: 20px;">
${strategy.marketingInsights.targetPersonas.map(p => `<li>${p}</li>`).join('')}
</ul>
</div>
<div class="card">
<strong>Viral Yayılma Kancaları:</strong><br/>
${strategy.marketingInsights.socialMediaHooks.map(h => `<p style="font-size: 11px; background: #eff6ff; padding: 10px; border-radius: 8px;"><strong>${h.platform}:</strong> ${h.text}</p>`).join('')}
</div>
</div>
` : ''}
${strategy.seoAnalysis ? `
<h2 class="section-title">🔍 SEO Master Verileri</h2>
<div class="card">
<strong>Optimize Ana Başlık:</strong><br/>
<p style="font-size: 20px; font-weight: 900; color: #e11d48;">${strategy.seoAnalysis.optimizedTitle}</p>
</div>
<div class="card">
<strong>Etiketler (Copy-Paste):</strong><br/>
<p style="font-size: 12px; font-family: monospace; background: #fff; padding: 15px; border-radius: 10px; border: 1px solid #e2e8f0;">${strategy.seoAnalysis.tags.join(', ')}</p>
</div>
<h3>A/B Başlık Testleri (Neuro-Scores)</h3>
${strategy.seoAnalysis.alternativeTitles.map(alt => `
<div class="question-item" style="justify-content: space-between; align-items: center;">
<span><strong>${alt.title}</strong><br/><small style="color: #64748b;">${alt.psychologicalAngle}</small></span>
<div class="neuro-score"><span style="font-size: 20px; font-weight: 900;">${alt.neuroScore}</span><br/><small>SCORE</small></div>
</div>
`).join('')}
` : ''}
${strategy.commercialAnalysis.deepAnalysis ? `
<h2 class="section-title">🤝 Ticari İş Birliği & Sponsorluk</h2>
<div class="card" style="background: #f0fdf4; border-color: #bbf7d0;">
<strong>Tahmini Gelir Projeksiyonu:</strong><br/>
<span style="font-size: 24px; font-weight: 900; color: #166534;">${strategy.commercialAnalysis.deepAnalysis.estimatedRevenue}</span>
</div>
<h3>Sponsorluk Mail Taslağı</h3>
<div class="email-draft">${strategy.commercialAnalysis.deepAnalysis.emailDraft}</div>
` : ''}
<h2 class="section-title">💬 Seçilmiş İzleyici Yorumları</h2>
${(strategy.selectedComments || []).map(c => `
<div class="card">
<p style="font-style: italic;">"${c.text}"</p>
<p style="font-size: 11px; color: #64748b; margin-top: 10px;">Yazar: <strong>${c.username}</strong> | Kaynak Dosya: <strong>${c.sourceFile}</strong></p>
<div style="background: #f1f5f9; padding: 15px; border-radius: 10px; margin-top: 10px; font-size: 13px;">
<strong>Stratejik İçgörü:</strong> ${c.insightValue}
</div>
</div>
`).join('')}
<footer>TUBE STRATEGIST AI MASTER REPORT • v2.5 FINAL • UNIFIED DATA</footer>
</div>
</body>
</html>
`;
const blob = new Blob([htmlContent], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = `TubeStrategist_MASTER_REPORT_${strategy.title.replace(/\s+/g, '_')}.html`; a.click();
alert("Master Görsel Rapor (Eksiksiz) başarıyla oluşturuldu.");
};
const tabs = [
{ id: 'strategy', label: 'STRATEJİ', icon: <Target size={14} /> },
{ id: 'neuro', label: 'NEURO-LAB', icon: <Brain size={14} /> },
{ id: 'marketing', label: 'MARKETING', icon: <TrendingUp size={14} /> },
{ id: 'seo', label: 'SEO-MASTER', icon: <Search size={14} /> },
{ id: 'commercial', label: 'TİCARİ', icon: <Briefcase size={14} /> }
];
return (
<div className="max-w-7xl mx-auto space-y-8 pb-24 font-sans px-4 sm:px-0">
{/* Lightbox for Thumbnail */}
<AnimatePresence>
{isThumbnailExpanded && strategy.generatedThumbnail && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 backdrop-blur-sm p-4 cursor-zoom-out"
onClick={() => setIsThumbnailExpanded(false)}
>
<motion.img
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
src={strategy.generatedThumbnail}
className="max-w-full max-h-[90vh] object-contain rounded-2xl shadow-2xl"
/>
</motion.div>
)}
</AnimatePresence>
{/* Navigation */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col xl:flex-row justify-between items-center bg-[var(--color-bg-elevated)]/80 p-4 rounded-2xl border border-[var(--color-border-default)] gap-4 sticky top-4 z-50 shadow-2xl backdrop-blur-xl"
>
<div className="flex gap-4">
<button onClick={onReset} className="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] text-sm flex items-center gap-2 font-black uppercase tracking-widest transition-colors">
<ChevronRight className="rotate-180" size={14} /> GERİ DÖN
</button>
{onRegenerate && (
<button
onClick={onRegenerate}
disabled={isRegenerating}
className="text-red-500 hover:text-red-400 text-sm flex items-center gap-2 font-black uppercase tracking-widest transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isRegenerating ? <Loader2 size={14} className="animate-spin" /> : <RefreshCw size={14} />} ANALİZİ YENİDEN YAP
</button>
)}
</div>
<div className="flex gap-2 bg-[var(--color-bg-surface)] p-1.5 rounded-xl border border-[var(--color-border-default)] overflow-x-auto max-w-full custom-scrollbar">
{tabs.map((t) => (
<button
key={t.id} onClick={() => setActiveTab(t.id as any)}
className={`flex items-center gap-2 px-5 py-2 rounded-lg text-[11px] font-black transition-all uppercase tracking-[0.1em] whitespace-nowrap
${activeTab === t.id
? 'bg-gradient-to-r from-red-600 to-orange-500 text-white shadow-lg'
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)]'
}`}
>
{t.icon} {t.label}
</button>
))}
</div>
<div className="flex gap-2">
<button onClick={exportJSON} className="px-4 py-2 bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] hover:border-emerald-500/50 hover:bg-emerald-500/10 rounded-xl text-xs font-bold text-[var(--color-text-primary)] flex items-center gap-2 transition-all shadow-sm active:scale-95 group">
<Database size={14} className="text-emerald-500 group-hover:scale-110 transition-transform" /> JSON
</button>
<button onClick={exportHTML} className="px-4 py-2 bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] hover:border-blue-500/50 hover:bg-blue-500/10 rounded-xl text-xs font-bold text-[var(--color-text-primary)] flex items-center gap-2 transition-all shadow-sm active:scale-95 group">
<Printer size={14} className="text-blue-500 group-hover:scale-110 transition-transform" /> HTML
</button>
<button onClick={() => setShowLive(true)} className="px-4 py-2 bg-gradient-to-r from-red-600 to-orange-500 hover:from-red-500 hover:to-orange-400 rounded-xl text-xs font-bold text-white flex items-center gap-2 transition-all shadow-lg shadow-red-500/20 active:scale-95 group">
<Mic size={14} className="group-hover:scale-110 transition-transform" /> CANLI LAB
</button>
</div>
</motion.div>
<div className="animate-fade-in relative z-10">
{/* TAB: STRATEGY */}
{activeTab === 'strategy' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="grid xl:grid-cols-3 gap-6">
<div className="xl:col-span-2 space-y-6">
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] shadow-2xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-64 bg-red-500/10 rounded-full blur-3xl -mr-32 -mt-32 transition-all group-hover:bg-red-500/20"></div>
<div className="flex justify-between items-start mb-6 relative z-10">
<h1 className="text-3xl md:text-5xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] leading-tight pr-12">{strategy.title}</h1>
<div className="absolute top-0 right-0 flex gap-2">
<span className="bg-red-500/10 text-red-500 border border-red-500/20 text-[9px] font-black px-3 py-1.5 rounded-full uppercase tracking-widest backdrop-blur-sm shadow-inner">Verified Strategy</span>
</div>
</div>
<p className="text-[var(--color-text-secondary)] italic border-l-4 border-red-500 pl-5 py-3 text-lg md:text-xl relative z-10 leading-relaxed bg-[var(--color-bg-surface)]/50 rounded-r-2xl backdrop-blur-sm">"{strategy.hook || strategy.projectDNA?.coreMessage}"</p>
<div className="flex flex-wrap gap-4 mt-8 relative z-10">
<div className="px-4 py-2 bg-red-500/5 rounded-xl border border-red-500/20 flex gap-3 items-center backdrop-blur-sm">
<Heart className="text-red-500" size={16}/>
<p className="text-[10px] text-red-400 font-bold uppercase tracking-widest">Atmosfer: {strategy.projectDNA?.tone || currentTone}</p>
</div>
{strategy.inspiredByGap && (
<div className="px-4 py-2 bg-orange-500/5 rounded-xl border border-orange-500/20 flex gap-3 items-center backdrop-blur-sm">
<Sparkles className="text-orange-500" size={16}/>
<p className="text-[10px] text-orange-400 font-bold uppercase tracking-widest">İçgörü: {strategy.inspiredByGap}</p>
</div>
)}
{strategy.projectDNA?.audiencePersona && (
<div className="px-4 py-2 bg-blue-500/5 rounded-xl border border-blue-500/20 flex gap-3 items-center backdrop-blur-sm">
<Users className="text-blue-500" size={16}/>
<p className="text-[10px] text-blue-400 font-bold uppercase tracking-widest">Hedef Kitle: {strategy.projectDNA.audiencePersona}</p>
</div>
)}
</div>
</div>
{strategy.interviewQuestions && strategy.interviewQuestions.length > 0 && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><MessageSquare size={24} className="text-red-500" /> Kalbe Dokunan 20 Soru</h2>
<div className="grid gap-4">
{strategy.interviewQuestions.map((q, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] p-5 rounded-2xl border border-[var(--color-border-faint)] flex gap-4 items-start hover:border-red-500/30 hover:bg-[var(--color-bg-hover)] transition-all group">
<span className="w-8 h-8 rounded-xl bg-red-500/10 text-red-500 flex items-center justify-center text-[11px] font-bold shrink-0 group-hover:scale-110 transition-transform">{i+1}</span>
<p className="text-[var(--color-text-primary)] text-sm leading-relaxed pt-1.5">{q}</p>
</div>
))}
</div>
</div>
)}
{strategy.trendAnalysis && strategy.trendAnalysis.length > 0 && strategy.trendAnalysis[0].sentimentScore !== undefined && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><TrendingUp size={24} className="text-blue-500" /> Proje Trend Analizi</h2>
<div className="h-[300px] w-full mt-4">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={strategy.trendAnalysis} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorSentiment" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0}/>
</linearGradient>
<linearGradient id="colorArousal" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#ef4444" stopOpacity={0}/>
</linearGradient>
</defs>
<Tooltip
contentStyle={{backgroundColor: 'var(--color-bg-elevated)', borderColor: 'var(--color-border-default)', borderRadius: '12px', color: 'var(--color-text-primary)'}}
labelStyle={{color: 'var(--color-text-secondary)', fontWeight: 'bold', marginBottom: '8px'}}
/>
<Area type="monotone" name="Duygu (Pozitiflik)" dataKey="sentimentScore" stroke="#3b82f6" fillOpacity={1} fill="url(#colorSentiment)" />
<Area type="monotone" name="Heyecan (Arousal)" dataKey="arousalScore" stroke="#ef4444" fillOpacity={1} fill="url(#colorArousal)" />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
)}
{strategy.comboShorts && strategy.comboShorts.length > 0 && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><MonitorPlay size={24} className="text-orange-500" /> Master Combo Shorts (Timecode'lu)</h2>
<div className="grid gap-6">
{strategy.comboShorts.map((short, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] p-6 rounded-2xl shadow-sm hover:shadow-md hover:border-orange-500/30 transition-all">
<h3 className="font-bold text-lg text-[var(--color-text-primary)] mb-3">{short.title}</h3>
<p className="text-[var(--color-text-secondary)] text-sm mb-4 leading-relaxed">{short.description}</p>
<div className="flex flex-wrap gap-2">
{(short.timecodes || []).map((tc, j) => (
<span key={j} className="text-[11px] font-mono bg-orange-500/10 text-orange-500 border border-orange-500/20 px-3 py-1.5 rounded-lg font-bold shadow-sm">
⏱ {tc}
</span>
))}
</div>
</div>
))}
</div>
</div>
)}
{/* YENİ EKLENEN VERİ NOKTALARI */}
{strategy.crisisManagement && strategy.crisisManagement.potentialBacklash && (
<div className="bg-red-500/5 border border-red-500/20 p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-red-500 mb-6 flex items-center gap-3">
<AlertTriangle size={24} /> Kriz & Linç Yönetimi
</h2>
<div className="grid gap-4 md:grid-cols-2">
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-red-500/10 shadow-sm">
<h3 className="font-bold text-sm text-[var(--color-text-secondary)] mb-2 uppercase tracking-wider">Potansiyel Tepki</h3>
<p className="text-[var(--color-text-primary)] leading-relaxed">{strategy.crisisManagement.potentialBacklash}</p>
</div>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-green-500/10 shadow-sm">
<h3 className="font-bold text-sm text-[var(--color-text-secondary)] mb-2 uppercase tracking-wider">PR & Savunma Stratejisi</h3>
<p className="text-[var(--color-text-primary)] leading-relaxed">{strategy.crisisManagement.prStrategy}</p>
</div>
</div>
</div>
)}
{strategy.bRollSuggestions && strategy.bRollSuggestions.length > 0 && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-6 flex items-center gap-3">
<Video size={24} className="text-purple-500" /> B-Roll (Ara Görüntü) Önerileri
</h2>
<div className="flex flex-wrap gap-3">
{strategy.bRollSuggestions.map((broll, i) => (
<div key={i} className="px-4 py-3 bg-[var(--color-bg-surface)] border border-purple-500/20 rounded-xl flex items-center gap-3 shadow-sm hover:border-purple-500/50 transition-colors">
<Camera size={16} className="text-purple-400" />
<span className="text-sm text-[var(--color-text-primary)]">{broll}</span>
</div>
))}
</div>
</div>
)}
{strategy.communityHooks && strategy.communityHooks.length > 0 && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-6 flex items-center gap-3">
<MessageCircle size={24} className="text-cyan-500" /> Topluluk Etkileşim Kancaları
</h2>
<p className="text-sm text-[var(--color-text-secondary)] mb-4">İzleyiciyi videonun sonunda yoruma teşvik edecek kışkırtıcı veya düşündürücü sorular:</p>
<div className="grid gap-3">
{strategy.communityHooks.map((hook, i) => (
<div key={i} className="bg-cyan-500/5 p-4 rounded-xl border border-cyan-500/20 flex items-start gap-3">
<span className="w-6 h-6 rounded-lg bg-cyan-500/20 text-cyan-600 flex items-center justify-center text-xs font-bold shrink-0">{i+1}</span>
<p className="text-[var(--color-text-primary)] text-sm leading-relaxed">{hook}</p>
</div>
))}
</div>
</div>
)}
{strategy.sponsorIntegration && (
<div className="bg-yellow-500/5 border border-yellow-500/20 p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-yellow-600 mb-4 flex items-center gap-3">
<Handshake size={24} /> Doğal Sponsor Geçişi (Native Integration)
</h2>
<p className="text-[var(--color-text-primary)] leading-relaxed p-4 bg-[var(--color-bg-surface)] rounded-xl border border-yellow-500/10 shadow-sm">
{strategy.sponsorIntegration}
</p>
</div>
)}
{/* YENİ EKLENEN VERİ NOKTALARI SONU */}
{strategy.segments && strategy.segments.length > 0 && (
<div className="space-y-6 pt-4">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] flex items-center gap-3"><Activity size={24} className="text-red-500" /> Senaryo Akışı (Süreli Segmentler)</h2>
{strategy.segments.map((s, i) => (
<div key={i} className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-lg group hover:border-[var(--color-border-hover)] transition-all relative overflow-hidden">
<div className="absolute left-0 top-0 bottom-0 w-1.5 bg-gradient-to-b from-red-500 to-orange-500"></div>
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4 mb-5">
<div className="flex items-center gap-3">
<span className="bg-red-500/10 text-red-500 border border-red-500/20 font-mono text-[11px] px-3 py-1 rounded-lg font-black">{s.duration}</span>
<h4 className="font-bold text-xl text-[var(--color-text-primary)] tracking-tight">{s.type}</h4>
</div>
<span className="text-[10px] bg-purple-500/10 text-purple-400 px-4 py-1.5 rounded-full border border-purple-500/20 font-bold uppercase tracking-widest">{s.neuroObjective}</span>
</div>
<p className="text-[var(--color-text-secondary)] text-sm mb-6 leading-relaxed">{s.description}</p>
<div className="flex flex-wrap gap-2">
{(s.keyPoints || []).map((kp, ki) => <span key={ki} className="text-[11px] bg-[var(--color-bg-surface)] px-3 py-1.5 rounded-lg border border-[var(--color-border-faint)] text-[var(--color-text-secondary)] font-medium shadow-sm">{kp}</span>)}
</div>
</div>
))}
</div>
)}
{strategy.selectedComments && strategy.selectedComments.length > 0 && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><Users size={24} className="text-blue-500" /> İzleyici Yorumları & Kaynak Analizi</h2>
<div className="grid gap-6">
{(strategy.selectedComments || []).map((c, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] hover:border-blue-500/30 transition-all">
<div className="flex items-center gap-2 mb-4">
<FileText size={14} className="text-[var(--color-text-ghost)]" />
<span className="text-[10px] font-black text-[var(--color-text-ghost)] uppercase tracking-widest">{c.sourceFile}</span>
<span className="text-[10px] font-black text-blue-500 uppercase tracking-widest ml-auto px-2 py-1 bg-blue-500/10 rounded-md">{c.username}</span>
</div>
<p className="text-[var(--color-text-primary)] italic mb-5 text-sm leading-relaxed border-l-2 border-blue-500/30 pl-4">&quot;{c.text}&quot;</p>
<div className="p-4 bg-blue-500/5 rounded-xl border border-blue-500/10">
<span className="text-[10px] font-black text-blue-400 uppercase tracking-widest block mb-2 flex items-center gap-1.5"><Lightbulb size={12}/> Stratejik Değer:</span>
<p className="text-xs text-[var(--color-text-secondary)] leading-relaxed font-medium">{c.insightValue}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
<div className="space-y-6">
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-6 rounded-[2rem] shadow-xl sticky top-28">
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase tracking-widest mb-4 flex items-center gap-2"><ImageIcon size={14}/> Thumbnail Konsepti</h3>
<div className="aspect-video bg-[var(--color-bg-surface)] rounded-2xl flex items-center justify-center border border-[var(--color-border-faint)] overflow-hidden relative group shadow-inner">
{strategy.generatedThumbnail ? (
<img
src={strategy.generatedThumbnail}
className="w-full h-full object-cover animate-fade-in cursor-zoom-in"
onClick={() => setIsThumbnailExpanded(true)}
/>
) : (
<button onClick={handleGenThumbnail} disabled={isGenerating} className="text-xs bg-red-500/10 hover:bg-red-500/20 text-red-500 px-6 py-3 rounded-xl font-bold transition-all border border-red-500/20 uppercase tracking-widest flex items-center gap-2 shadow-sm active:scale-95 disabled:opacity-50">
{isGenerating ? <Loader2 className="animate-spin" size={16}/> : <Zap size={16}/>} ÖNİZLEME ÜRET
</button>
)}
{strategy.generatedThumbnail && (
<button onClick={handleGenThumbnail} className="absolute bottom-3 right-3 p-2.5 bg-black/60 hover:bg-black/80 backdrop-blur-md rounded-xl text-white opacity-0 group-hover:opacity-100 transition-all border border-white/10 active:scale-95 shadow-xl">
<RefreshCw size={14} />
</button>
)}
</div>
{strategy.thumbnailConcept && (
<div className="mt-5 p-4 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-faint)]">
<p className="text-xs text-[var(--color-text-secondary)] font-medium leading-relaxed">{strategy.thumbnailConcept}</p>
</div>
)}
</div>
{/* Data Integrity Widget */}
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-6 rounded-[2rem] shadow-xl flex flex-col gap-4">
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase tracking-widest flex items-center gap-2"><Database size={14}/> Analiz Durumu</h3>
<div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-faint)]">
<span className="text-[10px] text-[var(--color-text-secondary)] font-bold uppercase tracking-wider">Nöro-Lab</span>
{strategy.neuroReport ? <Check size={14} className="text-emerald-500"/> : <span className="w-2 h-2 rounded-full bg-[var(--color-border-hover)]"></span>}
</div>
<div className="flex justify-between items-center p-3 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-faint)]">
<span className="text-[10px] text-[var(--color-text-secondary)] font-bold uppercase tracking-wider">SEO-Master</span>
{strategy.seoAnalysis ? <Check size={14} className="text-emerald-500"/> : <span className="w-2 h-2 rounded-full bg-[var(--color-border-hover)]"></span>}
</div>
<div className="flex justify-between items-center p-3 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-faint)]">
<span className="text-[10px] text-[var(--color-text-secondary)] font-bold uppercase tracking-wider">Marketing-Hub</span>
{strategy.marketingInsights ? <Check size={14} className="text-emerald-500"/> : <span className="w-2 h-2 rounded-full bg-[var(--color-border-hover)]"></span>}
</div>
<div className="flex justify-between items-center p-3 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-faint)]">
<span className="text-[10px] text-[var(--color-text-secondary)] font-bold uppercase tracking-wider">Ticari-Derin</span>
{strategy.commercialAnalysis?.deepAnalysis ? <Check size={14} className="text-emerald-500"/> : <span className="w-2 h-2 rounded-full bg-[var(--color-border-hover)]"></span>}
</div>
</div>
<div className="mt-2 pt-4 border-t border-[var(--color-border-default)]">
<p className="text-[9px] text-center text-[var(--color-text-ghost)] font-black uppercase tracking-widest flex items-center justify-center gap-1.5"><Layers size={10}/> Master Save her şeyi tek dosyada tutar.</p>
</div>
</div>
</div>
</motion.div>
)}
{/* TAB: NEURO LAB */}
{activeTab === 'neuro' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-8">
{!strategy.neuroReport ? (
<div className="h-[400px] flex flex-col items-center justify-center bg-[var(--color-bg-elevated)]/50 border border-[var(--color-border-default)] rounded-[2rem] border-dashed shadow-inner">
<Brain size={64} className="text-[var(--color-text-ghost)] mb-6 animate-pulse" />
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 uppercase tracking-widest">Nöro-Pazarlama Motoru</h2>
<button onClick={handleNeuroAnalysis} disabled={isGenerating} className="bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white px-10 py-4 rounded-2xl font-black flex items-center gap-3 transition-all shadow-xl shadow-purple-500/20 active:scale-95 disabled:opacity-50">
{isGenerating ? <Loader2 className="animate-spin" size={20} /> : <Zap size={20} />} ANALİZİ BAŞLAT
</button>
</div>
) : (
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] shadow-2xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><Brain className="text-purple-500"/> Nöro-Psikolojik Sentez</h2>
<div className="space-y-6">
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] shadow-inner">
<h4 className="text-[11px] font-black text-purple-400 uppercase mb-3 tracking-widest flex items-center gap-2"><Target size={14}/> Göz Odağı</h4>
<p className="text-sm text-[var(--color-text-secondary)] italic leading-relaxed">"{strategy.neuroReport.eyeTrackingFocus}"</p>
</div>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] shadow-inner">
<h4 className="text-[11px] font-black text-purple-400 uppercase mb-4 tracking-widest flex items-center gap-2"><Activity size={14}/> Dopamin Tetikleyiciler</h4>
<div className="flex flex-wrap gap-2">
{(strategy.neuroReport.dopamineTriggers || []).map((t, i) => <span key={i} className="px-4 py-1.5 bg-purple-500/10 text-purple-400 rounded-xl text-[11px] font-bold border border-purple-500/20 shadow-sm">{t}</span>)}
</div>
</div>
</div>
</div>
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] h-[500px] shadow-2xl flex flex-col">
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase tracking-widest mb-6 text-center">Dikkat Süresi Projeksiyonu</h3>
<div className="flex-1 bg-[var(--color-bg-surface)]/50 rounded-2xl border border-[var(--color-border-faint)] p-4 shadow-inner">
<ResponsiveContainer width="100%" height="100%">
<RadarChart data={strategy.neuroReport.attentionSpans || []}>
<PolarGrid stroke="var(--color-border-default)" />
<PolarAngleAxis dataKey="phase" stroke="var(--color-text-secondary)" fontSize={11} tick={{fill: 'var(--color-text-secondary)', fontWeight: 600}} />
<Radar name="Skor" dataKey="score" stroke="#a855f7" strokeWidth={2} fill="#a855f7" fillOpacity={0.4} />
<Tooltip contentStyle={{backgroundColor: 'var(--color-bg-elevated)', borderColor: 'var(--color-border-default)', borderRadius: '12px', color: 'var(--color-text-primary)'}} itemStyle={{color: '#a855f7', fontWeight: 'bold'}} />
</RadarChart>
</ResponsiveContainer>
</div>
</div>
</div>
)}
</motion.div>
)}
{/* TAB: MARKETING */}
{activeTab === 'marketing' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-8">
{!strategy.marketingInsights ? (
<div className="h-[400px] flex flex-col items-center justify-center bg-[var(--color-bg-elevated)]/50 border border-[var(--color-border-default)] rounded-[2rem] border-dashed shadow-inner">
<TrendingUp size={64} className="text-[var(--color-text-ghost)] mb-6" />
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 uppercase tracking-widest">Marketing Hub</h2>
<button onClick={handleMarketingAnalysis} disabled={isGenerating} className="bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 text-white px-10 py-4 rounded-2xl font-black flex items-center gap-3 transition-all shadow-xl shadow-blue-500/20 active:scale-95 disabled:opacity-50">
{isGenerating ? <Loader2 className="animate-spin" size={20} /> : <Zap size={20} />} PAZARLAMA PLANINI ÜRET
</button>
</div>
) : (
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] shadow-2xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><Users className="text-blue-500"/> Hedef Personalar</h2>
<div className="grid gap-4">
{(strategy.marketingInsights.targetPersonas || []).map((p, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] p-5 rounded-2xl border border-[var(--color-border-faint)] flex items-center gap-4 hover:border-blue-500/30 transition-all group">
<div className="w-10 h-10 shrink-0 rounded-xl bg-blue-500/10 flex items-center justify-center text-blue-500 font-bold text-sm shadow-inner group-hover:scale-110 transition-transform">{i+1}</div>
<p className="text-sm text-[var(--color-text-primary)] font-medium leading-relaxed">{p}</p>
</div>
))}
</div>
</div>
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] shadow-2xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><Award className="text-orange-500"/> Viral Yayılma Kancaları</h2>
<div className="space-y-4">
{(strategy.marketingInsights.socialMediaHooks || []).map((h, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] hover:border-orange-500/30 transition-all relative overflow-hidden group">
<div className="absolute left-0 top-0 bottom-0 w-1 bg-orange-500/50 group-hover:bg-orange-500 transition-colors"></div>
<span className="text-[10px] font-black uppercase text-orange-500 block mb-3 tracking-widest">{h.platform} HOOK</span>
<p className="text-sm italic text-[var(--color-text-secondary)] leading-relaxed">"{h.text}"</p>
</div>
))}
</div>
</div>
</div>
)}
</motion.div>
)}
{/* TAB: SEO MASTER */}
{activeTab === 'seo' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-8">
{!strategy.seoAnalysis ? (
<div className="h-[400px] flex flex-col items-center justify-center bg-[var(--color-bg-elevated)]/50 border border-[var(--color-border-default)] rounded-[2rem] border-dashed shadow-inner">
<Search size={64} className="text-[var(--color-text-ghost)] mb-6" />
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 uppercase tracking-widest">SEO Master Veri Tabanı</h2>
<button onClick={handleSeoAnalysis} disabled={isGenerating} className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white px-10 py-4 rounded-2xl font-black flex items-center gap-3 transition-all active:scale-95 shadow-xl shadow-emerald-500/20 disabled:opacity-50">
{isGenerating ? <Loader2 className="animate-spin" size={20} /> : <Zap size={20} />} SEO ANALİZİNİ BAŞLAT
</button>
</div>
) : (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] shadow-2xl">
<div className="grid lg:grid-cols-12 gap-10">
<div className="lg:col-span-5 space-y-8">
<div>
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-4 flex items-center gap-2 tracking-[0.1em]"><Layout size={14}/> Optimize Ana Başlık</h3>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] text-lg md:text-xl font-bold text-[var(--color-text-primary)] shadow-inner leading-relaxed">{strategy.seoAnalysis.optimizedTitle}</div>
</div>
<div>
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-4 flex items-center gap-2 tracking-[0.1em]"><Target size={14}/> YouTube Etiketleri (Kopyalamaya Hazır)</h3>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] relative group shadow-inner">
<button
onClick={() => {
navigator.clipboard.writeText(strategy.seoAnalysis?.tags?.join(', ') || '');
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}}
className="absolute top-4 right-4 p-2.5 bg-[var(--color-bg-elevated)] rounded-xl hover:bg-[var(--color-bg-hover)] text-[var(--color-text-secondary)] border border-[var(--color-border-default)] transition-all active:scale-90 shadow-sm"
title="Kopyala"
>
{copied ? <Check size={16} className="text-emerald-500"/> : <Copy size={16}/>}
</button>
<p className="text-xs text-[var(--color-text-secondary)] leading-relaxed font-mono pr-10 whitespace-pre-wrap">{strategy.seoAnalysis.tags?.join(', ')}</p>
</div>
<p className="text-[9px] text-[var(--color-text-ghost)] mt-3 font-bold uppercase tracking-widest pl-2">Sponsorlu anahtar kelimeler ve rakip açıkları dahil edilmiştir.</p>
</div>
<div className="p-6 bg-red-500/5 border border-red-500/20 rounded-2xl">
<h3 className="text-[11px] font-black text-red-500 uppercase mb-3 flex items-center gap-2"><AlertTriangle size={14}/> Rakip Boşluğu (Gap)</h3>
<p className="text-sm text-[var(--color-text-primary)] leading-relaxed font-medium">{strategy.seoAnalysis.competitorGap}</p>
</div>
</div>
<div className="lg:col-span-7 space-y-8">
<div>
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-4 tracking-[0.1em] flex items-center gap-2"><Activity size={14}/> A/B Başlık Alternatifleri & Neuro-Performans</h3>
<div className="grid gap-4">
{(strategy.seoAnalysis.alternativeTitles || []).map((alt, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] p-5 rounded-2xl border border-[var(--color-border-faint)] flex items-center justify-between hover:border-red-500/30 transition-all group shadow-sm">
<div className="pr-4 flex-1">
<p className="font-bold text-[var(--color-text-primary)] mb-2 text-sm md:text-base group-hover:text-red-400 transition-colors leading-tight">{alt.title}</p>
<span className="text-[10px] text-[var(--color-text-secondary)] uppercase font-black tracking-widest bg-[var(--color-bg-elevated)] px-2 py-1 rounded-md border border-[var(--color-border-default)]">{alt.psychologicalAngle}</span>
</div>
<div className="text-center bg-[var(--color-bg-elevated)] p-3 rounded-xl border border-[var(--color-border-default)] min-w-[80px] shadow-inner shrink-0">
<span className={`text-2xl font-black ${alt.neuroScore > 85 ? 'text-emerald-500' : alt.neuroScore > 70 ? 'text-orange-500' : 'text-[var(--color-text-secondary)]'}`}>{alt.neuroScore}</span>
<p className="text-[9px] text-[var(--color-text-ghost)] font-black uppercase tracking-widest mt-1">Neuro</p>
</div>
</div>
))}
</div>
</div>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] shadow-inner">
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-3 tracking-[0.1em] flex items-center gap-2"><FileText size={14}/> Meta Açıklama Projeksiyonu</h3>
<p className="text-sm text-[var(--color-text-primary)] leading-relaxed italic border-l-2 border-red-500/30 pl-4">"{strategy.seoAnalysis.metaDescription}"</p>
</div>
</div>
</div>
</div>
)}
</motion.div>
)}
{/* TAB: COMMERCIAL */}
{activeTab === 'commercial' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] rounded-[2rem] overflow-hidden shadow-2xl">
<div className="p-8 md:p-10 border-b border-[var(--color-border-default)] bg-[var(--color-bg-surface)]/50 flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
<div>
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-2 flex items-center gap-3"><Briefcase className="text-emerald-500" /> Sponsorluk & Marka İş Birlikleri</h2>
<p className="text-[var(--color-text-secondary)] text-sm font-medium tracking-tight">İçeriğe doğal entegrasyon ve yerel marka önerileri.</p>
</div>
{!strategy.commercialAnalysis.deepAnalysis && (
<button onClick={handleDeepCommercial} disabled={isGenerating} className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white px-8 py-3.5 rounded-xl font-black text-xs uppercase tracking-widest flex items-center gap-2 shadow-xl shadow-emerald-500/20 active:scale-95 transition-all disabled:opacity-50 shrink-0">
{isGenerating ? <Loader2 className="animate-spin" size={16}/> : <Plus size={16}/>} DERİN ANALİZ ÜRET
</button>
)}
</div>
<div className="grid lg:grid-cols-12 gap-0">
<div className="lg:col-span-4 border-b lg:border-b-0 lg:border-r border-[var(--color-border-default)] p-8 md:p-10 space-y-8 bg-[var(--color-bg-surface)]/30">
<div>
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-4 tracking-widest flex items-center gap-2"><Target size={14}/> Önerilen Türk Markaları</h3>
<div className="flex flex-wrap gap-2">
{(strategy.commercialAnalysis.suggestedBrands || []).map((brand, i) => (
<span key={i} className="px-4 py-2 bg-emerald-500/10 text-emerald-500 rounded-xl text-[11px] font-black border border-emerald-500/20 shadow-sm">{brand}</span>
))}
</div>
</div>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] shadow-inner">
<h3 className="text-[11px] font-black text-blue-500 uppercase mb-2 tracking-widest flex items-center gap-2"><Check size={14}/> Marka Güven Skoru</h3>
<p className="text-5xl font-[family-name:var(--font-display)] font-black text-[var(--color-text-primary)]">%{strategy.commercialAnalysis.brandSafetyScore}</p>
<div className="w-full bg-[var(--color-bg-elevated)] h-2 rounded-full mt-4 overflow-hidden border border-[var(--color-border-default)]">
<div className="bg-gradient-to-r from-blue-600 to-cyan-500 h-full transition-all duration-1000" style={{width: `${strategy.commercialAnalysis.brandSafetyScore}%`}}></div>
</div>
</div>
<div>
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-4 tracking-widest flex items-center gap-2"><Layers size={14}/> Uygun Sektörler</h3>
<div className="grid gap-3">
{(strategy.commercialAnalysis.suitableIndustries || []).map((ind, i) => (
<div key={i} className="flex items-center gap-3 text-sm text-[var(--color-text-primary)] font-medium bg-[var(--color-bg-surface)] px-4 py-3 rounded-xl border border-[var(--color-border-faint)]">
<ChevronRight size={14} className="text-emerald-500"/> {ind}
</div>
))}
</div>
</div>
</div>
<div className="lg:col-span-8 p-8 md:p-10 min-h-[400px]">
{strategy.commercialAnalysis.deepAnalysis ? (
<div className="space-y-6 animate-fade-in">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h3 className="text-xl font-bold text-[var(--color-text-primary)] flex items-center gap-3"><Mail className="text-emerald-500" /> Profesyonel Sponsorluk Maili</h3>
<button onClick={() => {
navigator.clipboard.writeText(strategy.commercialAnalysis.deepAnalysis?.emailDraft || '');
alert("Taslak kopyalandı!");
}} className="px-4 py-2 bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl text-xs font-black text-blue-500 hover:text-blue-400 hover:border-blue-500/50 flex items-center gap-2 uppercase tracking-widest transition-all active:scale-95 shadow-sm">
<Copy size={14}/> Taslağı Kopyala
</button>
</div>
<div className="bg-[var(--color-bg-surface)] p-8 rounded-2xl border border-[var(--color-border-default)] font-serif text-[var(--color-text-secondary)] whitespace-pre-wrap leading-relaxed shadow-inner max-h-[500px] overflow-y-auto custom-scrollbar border-t-4 border-t-emerald-500 text-sm md:text-base relative">
<div className="absolute top-4 right-4 opacity-5"><Mail size={64}/></div>
<div className="relative z-10">{strategy.commercialAnalysis.deepAnalysis.emailDraft}</div>
</div>
<div className="p-6 bg-[var(--color-bg-surface)] rounded-2xl border border-[var(--color-border-faint)] flex flex-col sm:flex-row justify-between items-center gap-4 text-[11px] font-black uppercase tracking-widest">
<span className="text-[var(--color-text-secondary)] flex items-center gap-2"><TrendingUp size={14}/> Tahmini Bütçe/Gelir Projeksiyonu:</span>
<span className="text-emerald-500 text-xl md:text-2xl drop-shadow-sm px-4 py-2 bg-emerald-500/10 rounded-xl border border-emerald-500/20">{strategy.commercialAnalysis.deepAnalysis.estimatedRevenue}</span>
</div>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-[var(--color-text-ghost)] italic animate-pulse gap-4 text-center">
<Briefcase size={48} className="opacity-20"/>
<p>Derin analiz butonuyla marka stratejisini ve mail taslaklarını detaylandırabilirsiniz.</p>
</div>
)}
</div>
</div>
</motion.div>
)}
</div>
<AnimatePresence>
{showLive && <LiveBrainstorm context={JSON.stringify(strategy)} onClose={() => setShowLive(false)} />}
</AnimatePresence>
</div>
);
};
export default StrategyView;
@@ -0,0 +1,59 @@
'use client';
import { useEffect } from 'react';
import { AlertTriangle } from 'lucide-react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error('TUBE STRATEGIST ERROR.TSX CAUGHT:', error);
}, [error]);
return (
<div className="flex h-screen flex-col items-center justify-center p-8 bg-[var(--color-bg-base)]">
<div className="max-w-2xl w-full p-8 bg-red-500/10 border border-red-500 rounded-2xl">
<div className="flex items-center gap-4 mb-6">
<AlertTriangle className="text-red-500 w-12 h-12" />
<h2 className="text-3xl font-bold text-red-500">Kritik Uygulama Hatası</h2>
</div>
<p className="text-[var(--color-text-secondary)] mb-6 text-lg">
Beklenmeyen bir hata oluştu. Lütfen aşağıdaki hata detayını kopyalayarak AI asistana iletin:
</p>
<div className="bg-black/50 p-6 rounded-xl overflow-x-auto mb-8 border border-red-500/30">
<p className="text-red-400 font-bold mb-2">Hata Mesajı:</p>
<pre className="text-red-300 font-mono text-sm whitespace-pre-wrap">
{error.message}
</pre>
{error.stack && (
<>
<p className="text-red-400 font-bold mt-4 mb-2">Stack Trace:</p>
<pre className="text-red-300/80 font-mono text-xs whitespace-pre-wrap max-h-60 overflow-y-auto">
{error.stack}
</pre>
</>
)}
{error.digest && (
<p className="text-gray-400 font-mono text-xs mt-4">Digest: {error.digest}</p>
)}
</div>
<button
onClick={() => reset()}
className="px-6 py-3 bg-red-500 text-white font-bold rounded-xl hover:bg-red-600 transition-colors"
>
Yeniden Dene
</button>
</div>
</div>
);
}
@@ -0,0 +1,369 @@
'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { getProjects, createProject, deleteProject, ProjectResponse } from './services/strategistApi';
import { useRouter, useParams } from 'next/navigation';
import { LegacyUploader } from './components/LegacyUploader';
import { MonitorPlay, Plus, FolderKanban, Loader2, Calendar, LayoutTemplate, X, TrendingUp, Sparkles, User, Target, Users, PlayCircle, History, Trash2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/tr';
import { TargetAudience, VideoDuration } from './types';
dayjs.extend(relativeTime);
dayjs.locale('tr');
export default function TubeStrategistDashboard() {
const router = useRouter();
const params = useParams();
const locale = params.locale as string;
const [projects, setProjects] = useState<ProjectResponse[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [viewMode, setViewMode] = useState<'projects' | 'legacy'>('projects');
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
// New Project Form State
const [name, setName] = useState('');
const [tone, setTone] = useState("Samimi, sıcak ve kalbe dokunan");
const [duration, setDuration] = useState<VideoDuration>('45-60min');
const [speakerName, setSpeakerName] = useState("");
const [topicFocus, setTopicFocus] = useState("");
const [targetAudience, setTargetAudience] = useState<TargetAudience>("Genel İzleyici (Basit, Anlaşılır)");
const [isCreating, setIsCreating] = useState(false);
const fetchProjects = async () => {
try {
setIsLoading(true);
const data = await getProjects();
setProjects(data);
} catch (err) {
console.error("Failed to fetch projects:", err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchProjects();
}, []);
const handleCreateProject = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) return;
try {
setIsCreating(true);
const newProj = await createProject(name, tone, duration, speakerName, topicFocus, targetAudience);
setIsCreateModalOpen(false);
router.push(`/dashboard/tools/tube-strategist/${newProj.id}`);
} catch (error) {
console.error("Proje oluşturulamadı:", error);
alert("Proje oluşturulurken bir hata oluştu.");
} finally {
setIsCreating(false);
}
};
const handleDeleteProject = async (e: React.MouseEvent, projectId: string) => {
e.stopPropagation();
if (!window.confirm("Bu projeyi silmek istediğinize emin misiniz? Bu işlem geri alınamaz ve proje içindeki tüm analiz, video ve bölümler silinecektir.")) {
return;
}
try {
await deleteProject(projectId);
setProjects(projects.filter(p => p.id !== projectId));
} catch (error) {
console.error("Proje silinirken hata:", error);
alert("Proje silinirken bir hata oluştu.");
}
};
return (
<div className="max-w-6xl mx-auto space-y-8 pb-24 font-sans px-4 sm:px-0">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col md:flex-row items-center justify-between gap-6 pt-8 pb-6 border-b border-[var(--color-border-faint)]"
>
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-tr from-red-500/20 to-orange-500/20 border border-red-500/30 flex items-center justify-center shadow-[0_0_30px_rgba(239,68,68,0.15)]">
<MonitorPlay className="text-red-500 w-8 h-8" />
</div>
<div>
<h1 className="font-[family-name:var(--font-display)] text-3xl font-bold tracking-tight text-[var(--color-text-primary)]">
TubeStrategist <span className="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-orange-500">AI</span>
</h1>
<p className="text-[var(--color-text-secondary)] text-sm font-medium">YouTube Kanal Zekası ve Viral İçerik Kurgulama Motoru</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="bg-[var(--color-bg-elevated)] p-1 rounded-xl border border-[var(--color-border-faint)] flex shadow-sm">
<button
onClick={() => setViewMode('projects')}
className={cn(
"px-4 py-2 rounded-lg text-sm font-bold flex items-center gap-2 transition-all",
viewMode === 'projects'
? "bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] shadow-sm"
: "text-[var(--color-text-ghost)] hover:text-[var(--color-text-secondary)]"
)}
>
<FolderKanban size={16} /> Projeler
</button>
<button
onClick={() => setViewMode('legacy')}
className={cn(
"px-4 py-2 rounded-lg text-sm font-bold flex items-center gap-2 transition-all",
viewMode === 'legacy'
? "bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] shadow-sm"
: "text-[var(--color-text-ghost)] hover:text-[var(--color-text-secondary)]"
)}
>
<History size={16} /> Legacy TXT Modu
</button>
</div>
</div>
</motion.div>
{viewMode === 'legacy' ? (
<LegacyUploader />
) : (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-[var(--color-text-primary)] flex items-center gap-2">
<FolderKanban className="text-red-500" size={20} /> Strateji Projeleri
</h2>
<button
onClick={() => setIsCreateModalOpen(true)}
className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-red-600 to-orange-500 text-white text-sm font-bold flex items-center gap-2 hover:shadow-[0_0_20px_rgba(239,68,68,0.3)] hover:scale-[1.02] transition-all"
>
<Plus size={18} /> Yeni Proje
</button>
</div>
{isLoading ? (
<div className="flex flex-col items-center justify-center py-20 text-[var(--color-text-ghost)]">
<Loader2 className="animate-spin w-8 h-8 text-red-500 mb-4" />
<p className="font-medium text-sm">Projeleriniz Yükleniyor...</p>
</div>
) : projects.length === 0 ? (
<div className="card p-12 flex flex-col items-center justify-center text-center border border-dashed border-[var(--color-border-default)]">
<div className="w-20 h-20 bg-[var(--color-bg-elevated)] rounded-full flex items-center justify-center mb-6">
<LayoutTemplate className="w-10 h-10 text-[var(--color-text-ghost)]" />
</div>
<h3 className="text-xl font-bold text-[var(--color-text-primary)] mb-2">Henüz Projeniz Yok</h3>
<p className="text-[var(--color-text-secondary)] text-sm max-w-md mb-8">
Hemen yeni bir Tube Strategist projesi oluşturun, rakiplerinizin veya kendi videolarınızın linklerini ekleyerek analizlere başlayın.
</p>
<button
onClick={() => setIsCreateModalOpen(true)}
className="px-6 py-3 rounded-xl bg-gradient-to-r from-red-600 to-orange-500 text-white font-bold flex items-center gap-2 shadow-sm"
>
<Plus size={18} /> Proje Oluştur
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<AnimatePresence>
{projects.map((proj) => (
<motion.div
key={proj.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
whileHover={{ y: -5 }}
onClick={() => router.push(`/${locale}/dashboard/tools/tube-strategist/${proj.id}`)}
className="card p-6 cursor-pointer hover:border-red-500/30 transition-all flex flex-col h-full relative overflow-hidden group"
>
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-red-500/5 to-orange-500/5 rounded-bl-full -z-10 group-hover:scale-110 transition-transform"></div>
<div className="flex items-start justify-between mb-4">
<h3 className="text-lg font-bold text-[var(--color-text-primary)] line-clamp-1 group-hover:text-red-500 transition-colors pr-2">{proj.name}</h3>
<div className="flex items-center gap-2">
<div className={cn(
"px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider",
proj.status === 'COMPLETED' ? "bg-emerald-500/10 text-emerald-500 border border-emerald-500/20" :
proj.status === 'ANALYZING' ? "bg-amber-500/10 text-amber-500 border border-amber-500/20 flex items-center gap-1" :
"bg-[var(--color-bg-elevated)] text-[var(--color-text-secondary)] border border-[var(--color-border-faint)]"
)}>
{proj.status === 'ANALYZING' && <Loader2 className="w-3 h-3 animate-spin" />}
{proj.status === 'COMPLETED' ? 'Tamamlandı' : proj.status === 'ANALYZING' ? 'Analiz Ediliyor' : 'Bekliyor'}
</div>
<button
onClick={(e) => handleDeleteProject(e, proj.id)}
className="p-1.5 rounded-md text-[var(--color-text-ghost)] hover:text-red-500 hover:bg-red-500/10 transition-colors opacity-0 group-hover:opacity-100"
title="Projeyi Sil"
>
<Trash2 size={14} />
</button>
</div>
</div>
<div className="space-y-3 mb-6 flex-1">
<div className="flex items-center gap-2 text-[12px] text-[var(--color-text-secondary)] font-medium">
<PlayCircle size={14} className="text-blue-500" />
<span>{proj._count?.videos ?? proj.videos?.length ?? 0} Yüklenmiş Video</span>
</div>
<div className="flex items-center gap-2 text-[12px] text-[var(--color-text-secondary)] font-medium">
<Target size={14} className="text-orange-500" />
<span className="line-clamp-1">{proj.formatDescription || proj.topicFocus || 'Format belirtilmedi'}</span>
</div>
</div>
<div className="pt-4 border-t border-[var(--color-border-faint)] flex items-center justify-between text-[11px] text-[var(--color-text-ghost)] font-medium">
<div className="flex items-center gap-1.5">
<Calendar size={12} />
{dayjs(proj.createdAt).fromNow()}
</div>
<div className="flex items-center gap-1 text-red-500 opacity-0 group-hover:opacity-100 transition-opacity">
<span>İncele</span>
<TrendingUp size={14} />
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
)}
</div>
)}
{/* Create Project Modal */}
<AnimatePresence>
{isCreateModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center px-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsCreateModalOpen(false)}
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="card relative w-full max-w-2xl overflow-hidden shadow-2xl p-0 border-[var(--color-border-default)]"
>
{/* Modal Header */}
<div className="px-6 py-4 border-b border-[var(--color-border-faint)] flex items-center justify-between bg-[var(--color-bg-elevated)]">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-red-500/10 text-red-500 flex items-center justify-center">
<Sparkles size={16} />
</div>
<h3 className="text-lg font-bold text-[var(--color-text-primary)]">Yeni Strateji Projesi</h3>
</div>
<button
onClick={() => setIsCreateModalOpen(false)}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] hover:text-[var(--color-text-primary)] transition-colors"
>
<X size={18} />
</button>
</div>
{/* Modal Body */}
<form onSubmit={handleCreateProject} className="p-6 overflow-y-auto max-h-[80vh]">
<div className="space-y-6">
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider">Proje Adı</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
placeholder="Örn: 2025 AI Trendleri Analizi"
required
/>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5"><User size={14}/> Sunucu Adı</label>
<input
type="text"
value={speakerName}
onChange={(e) => setSpeakerName(e.target.value)}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
placeholder="Örn: Barış Özcan"
/>
</div>
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5"><Calendar size={14}/> Hedef Süre</label>
<select
value={duration}
onChange={(e) => setDuration(e.target.value as any)}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
>
<option value="30-45min">30 - 45 Dakika</option>
<option value="45-60min">45 - 60 Dakika</option>
<option value="1-2hours">1 - 2 Saat</option>
</select>
</div>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5"><Target size={14}/> Format / Ana Konsept</label>
<textarea
value={topicFocus}
onChange={(e) => setTopicFocus(e.target.value)}
rows={3}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50 resize-none"
placeholder="Bu projenin genel formatını ve konseptini tanımlayın. Örn: Masa başı sohbet formatında, konukla birlikte güncel psikoloji araştırmalarını halkın anlayabileceği dilde tartışıyoruz..."
/>
</div>
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5"><Users size={14}/> Hedef Kitle</label>
<select
value={targetAudience}
onChange={(e) => setTargetAudience(e.target.value as any)}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
>
<option value="Genel İzleyici (Basit, Anlaşılır)">Genel İzleyici</option>
<option value="Gen Z (Hızlı, Argo, Samimi)">Gen Z</option>
<option value="Millennials (Nostaljik, Bilgi Odaklı)">Millennials</option>
<option value="Teknoloji Meraklıları (Jargonlu, Detaycı)">Teknoloji Meraklıları</option>
</select>
</div>
</div>
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider">İçerik Tonu</label>
<input
type="text"
value={tone}
onChange={(e) => setTone(e.target.value)}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
</div>
<div className="mt-8 pt-6 border-t border-[var(--color-border-faint)] flex items-center justify-end gap-3">
<button
type="button"
onClick={() => setIsCreateModalOpen(false)}
className="px-5 py-2.5 rounded-xl font-bold text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-colors"
>
İptal
</button>
<button
type="submit"
disabled={isCreating || !name.trim()}
className="px-6 py-2.5 rounded-xl bg-gradient-to-r from-red-600 to-orange-500 text-white font-bold text-sm flex items-center gap-2 hover:shadow-[0_0_20px_rgba(239,68,68,0.3)] disabled:opacity-50 transition-all"
>
{isCreating ? <Loader2 size={16} className="animate-spin" /> : <Plus size={16} />}
{isCreating ? 'Oluşturuluyor...' : 'Projeyi Başlat'}
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
}
@@ -0,0 +1,233 @@
import { apiClient } from '@/lib/api/api-service';
export interface VideoDetail {
id: string;
youtubeUrl: string;
videoId: string;
title: string;
thumbnail: string;
transcript?: string;
transcriptDuration?: number;
totalComments: number;
mainComments: number;
replyComments: number;
viewCount: string;
likeCount: string;
commentsJson?: any;
tier1Analysis?: any;
createdAt: string;
updatedAt: string;
}
export interface EpisodeResponse {
id: string;
projectId: string;
topic: string;
targetAudience: string;
duration: string;
format: string;
status: string;
masterAnalysis: any;
thumbnailMatrix?: any;
shortsConcepts?: any;
sponsorshipPitch?: any;
createdAt: string;
updatedAt: string;
}
export interface TopicSuggestion {
title: string;
description: string;
reasoning: string;
}
export interface ProjectResponse {
id: string;
name: string;
status: string;
tone: string;
targetDuration: string;
speakerName: string;
topicFocus: string;
targetAudience: string;
formatDescription?: string;
videos: VideoDetail[];
episodes?: EpisodeResponse[];
_count?: { videos: number };
masterAnalysis: any;
communityInsights?: any;
createdAt: string;
updatedAt: string;
}
export const getProjects = async (): Promise<ProjectResponse[]> => {
return apiClient.get<ProjectResponse[]>('/youtube-tools/strategist/projects').then(r => r.data as unknown as ProjectResponse[]);
};
export const createProject = async (
name: string,
tone: string,
duration: string,
speakerName: string,
formatDescription: string,
targetAudience: string
): Promise<ProjectResponse> => {
return apiClient.post<ProjectResponse>('/youtube-tools/strategist/projects', {
name, tone, targetDuration: duration, speakerName, formatDescription, targetAudience
}).then(r => r.data as unknown as ProjectResponse);
};
export const updateProject = async (
projectId: string,
data: {
name?: string;
tone?: string;
targetDuration?: string;
speakerName?: string;
targetAudience?: string;
formatDescription?: string;
}
): Promise<ProjectResponse> => {
return apiClient.put<ProjectResponse>(`/youtube-tools/strategist/projects/${projectId}`, data).then(r => r.data as unknown as ProjectResponse);
};
export const getProjectById = async (projectId: string): Promise<ProjectResponse> => {
return apiClient.get<ProjectResponse>(`/youtube-tools/strategist/projects/${projectId}`).then(r => r.data as unknown as ProjectResponse);
};
export const deleteProject = async (projectId: string): Promise<any> => {
return apiClient.delete<any>(`/youtube-tools/strategist/projects/${projectId}`).then(r => r.data);
};
export const addVideoToProject = async (projectId: string, url: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/video`, { youtubeUrl: url }).then(r => r.data);
};
export const addDocumentToProject = async (
projectId: string,
title: string,
content: string,
type: 'transcript' | 'comments'
): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/document`, { title, content, type }).then(r => r.data);
};
export const getTopicSuggestions = async (projectId: string): Promise<{ suggestions: TopicSuggestion[] }> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/topic-suggestions`).then(r => r.data);
};
export const createEpisode = async (
projectId: string,
topic: string,
format: string,
targetAudience: string,
duration: string
): Promise<EpisodeResponse> => {
return apiClient.post<EpisodeResponse>(`/youtube-tools/strategist/projects/${projectId}/episode`, {
topic, format, targetAudience, duration
}).then(r => r.data as unknown as EpisodeResponse);
};
export const getEpisodesByProject = async (projectId: string): Promise<EpisodeResponse[]> => {
return apiClient.get<EpisodeResponse[]>(`/youtube-tools/strategist/projects/${projectId}/episodes`).then(r => r.data as unknown as EpisodeResponse[]);
};
export const getEpisodeById = async (episodeId: string): Promise<EpisodeResponse> => {
return apiClient.get<EpisodeResponse>(`/youtube-tools/strategist/episodes/${episodeId}`).then(r => r.data as unknown as EpisodeResponse);
};
export const analyzeEpisode = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/analyze`).then(r => r.data);
};
export const generateMoreQuestions = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/generate-more-questions`).then(r => r.data);
};
export const generateCommunityIdeas = async (projectId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/community-ideas`).then(r => r.data);
};
export const generateThumbnailMatrix = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/thumbnail-matrix`).then(r => r.data);
};
export const generateEpisodeShorts = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/shorts-concepts`).then(r => r.data);
};
export const generateEpisodeSponsorship = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/sponsorship`).then(r => r.data);
};
export const generateEpisodeQuestions = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/generate-questions`).then(r => r.data);
};
export const generateEpisodeSeoMarketing = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/generate-seo`).then(r => r.data);
};
export const generateEpisodeCrisisSponsors = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/generate-crisis`).then(r => r.data);
};
export const generateThumbnail = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/generate-thumbnail`).then(r => r.data);
};
export const generateNeuroReport = async (projectId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/neuro`).then(r => r.data);
};
export const generateMarketingReport = async (projectId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/marketing`).then(r => r.data);
};
export const generateSeoReport = async (projectId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/seo`).then(r => r.data);
};
export const generateDeepCommercialAnalysis = async (projectId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/commercial`).then(r => r.data);
};
export const generateThumbnailImage = async (projectId: string, prompt: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/thumbnail`, { prompt }).then(r => r.data);
};
// --- Audio Utilities for LiveBrainstorm ---
export const audioContexts: { [key: string]: AudioContext } = {};
export const decode = async (base64Audio: string): Promise<ArrayBuffer> => {
const binaryString = window.atob(base64Audio);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
};
export const decodeAudioData = async (buffer: ArrayBuffer, context: AudioContext): Promise<AudioBuffer> => {
return new Promise((resolve, reject) => {
context.decodeAudioData(buffer, resolve, reject);
});
};
export const float32ToPcm16 = (float32Array: Float32Array): Int16Array => {
const pcm16 = new Int16Array(float32Array.length);
for (let i = 0; i < float32Array.length; i++) {
const s = Math.max(-1, Math.min(1, float32Array[i]));
pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
return pcm16;
};
export const encode = (pcm16Array: Int16Array): string => {
const bytes = new Uint8Array(pcm16Array.buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
};
@@ -0,0 +1,201 @@
export enum SegmentType {
INTRO = 'INTRO',
SKETCH = 'SKETCH',
NARRATION = 'NARRATION',
B_ROLL = 'B_ROLL',
Q_AND_A = 'Q_AND_A',
OUTRO = 'OUTRO',
DEEP_DIVE = 'DEEP_DIVE',
EMOTIONAL_PEAK = 'EMOTIONAL_PEAK',
PSYCH_TRICK = 'PSYCH_TRICK',
BRAND_INTEGRATION = 'BRAND_INTEGRATION',
NEURO_HOOK = 'NEURO_HOOK',
RE_ENGAGEMENT = 'RE_ENGAGEMENT'
}
export interface VideoSegment {
type: SegmentType;
duration: string;
description: string;
keyPoints: string[];
visualCues?: string;
neuroObjective?: string;
inspirationSource?: string;
}
export interface ChartDataPoint {
topic: string;
emotionalArousal: number;
}
export interface SelectedComment {
username: string;
text: string;
reason: string;
insightValue: string;
sourceFile: string;
}
export interface AlternativeTitle {
title: string;
thumbnailOverlay: string;
neuroScore: number;
psychologicalAngle: string;
}
export interface NeuroReport {
eyeTrackingFocus: string;
colorPsychology: string;
dopamineTriggers: string[];
limbicSystemGoal: string;
attentionSpans: { phase: string; score: number }[];
}
export interface MarketingInsights {
targetPersonas: string[];
socialMediaHooks: { platform: string; text: string }[];
emailSubjectLines: string[];
viralHooks: string[];
}
export interface SeoAnalysis {
mainKeywords: string[];
secondaryKeywords: string[];
competitorGap: string;
optimizedTitle: string;
metaDescription: string;
tags: string[];
alternativeTitles: AlternativeTitle[];
}
export interface DeepCommercialAnalysis {
targetBrands: string[];
emailDraft: string;
estimatedRevenue: string;
affiliateIdeas: string[];
negotiationTip: string;
}
export interface CommercialAnalysis {
suitableIndustries: string[];
brandSafetyScore: number;
integrationIdeas: string[];
monetizationPotential: 'High' | 'Medium' | 'Low';
suggestedBrands: string[];
deepAnalysis?: DeepCommercialAnalysis;
}
export interface TrendAnalysisPoint {
videoIndex: number;
videoTitle: string;
sentimentScore: number;
arousalScore: number;
}
export interface ComboShort {
title: string;
description: string;
timecodes: string[];
}
export interface ProjectDNA {
tone: string;
audiencePersona: string;
coreMessage: string;
}
export interface CrisisManagement {
potentialBacklash: string;
prStrategy: string;
}
export interface StrategyResult {
title: string;
thumbnailConcept: string;
generatedThumbnail?: string; // AI tarafından üretilen base64 görsel verisi
hook: string;
segments: VideoSegment[];
chartData: ChartDataPoint[];
selectedComments: SelectedComment[];
interviewQuestions: string[];
wowFactor: string;
psychologicalTheme: string;
commercialAnalysis: CommercialAnalysis;
inspiredByGap?: string;
provenanceNotes?: string;
neuroReport?: NeuroReport;
marketingInsights?: MarketingInsights;
seoAnalysis?: SeoAnalysis;
projectDNA?: ProjectDNA;
trendAnalysis?: TrendAnalysisPoint[];
comboShorts?: ComboShort[];
crisisManagement?: CrisisManagement;
bRollSuggestions?: string[];
communityHooks?: string[];
sponsorIntegration?: string;
// 5 New Pre-Production Fields
gapAnalysis?: string;
segmentArchetypes?: string;
frictionPoints?: string[];
visualDna?: { timestamp: string; suggestion: string }[];
guestBriefing?: string;
}
export interface TubeStrategistVideo {
id: string;
youtubeUrl: string;
videoId: string;
title: string;
thumbnail?: string;
viewCount?: string;
likeCount?: string;
totalComments: number;
createdAt: string;
}
export interface TubeStrategistEpisode {
id: string;
projectId: string;
topic: string;
targetAudience?: string;
duration?: string;
format?: string;
status: 'PENDING' | 'ANALYZING' | 'COMPLETED' | 'FAILED';
masterAnalysis?: StrategyResult;
createdAt: string;
updatedAt: string;
}
export interface TubeStrategistProject {
id: string;
userId: string;
name: string;
tone?: string;
duration?: string;
speakerName?: string;
targetAudience?: string;
topicFocus?: string;
status: 'PENDING' | 'ANALYZING' | 'COMPLETED' | 'FAILED';
masterAnalysis?: StrategyResult; // Legacy support or project-level DNA
createdAt: string;
updatedAt: string;
videos: TubeStrategistVideo[];
episodes: TubeStrategistEpisode[];
}
export interface UploadedFile {
name: string;
content: string;
type: 'transcript' | 'comments';
}
export type VideoDuration = '30-45min' | '45-60min' | '1-2hours' | '2hours+';
export type TargetAudience =
| 'Gen Z (Hızlı, Argo, Samimi)'
| 'Millennials (Nostaljik, Bilgi Odaklı)'
| 'Gen X / Boomers (Ciddi, TV Tadında)'
| 'Teknoloji Meraklıları (Jargonlu, Detaycı)'
| 'Genel İzleyici (Basit, Anlaşılır)';
@@ -0,0 +1,421 @@
'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { voiceboxApi } from '@/services/voiceboxApi';
import { Play, Download, Mic, Settings2, Loader2, Sparkles, Volume2, AlertTriangle, History, Clock, ChevronDown, ChevronUp, Trash2 } from 'lucide-react';
export default function VoiceBoxStudio() {
const [text, setText] = useState('');
const [profiles, setProfiles] = useState<any[]>([]);
const [selectedProfile, setSelectedProfile] = useState('');
const [historyItems, setHistoryItems] = useState<any[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
const [audioUrl, setAudioUrl] = useState<string | null>(null);
// Advanced settings
const [engine, setEngine] = useState('kokoro');
const [language, setLanguage] = useState('tr');
const [modelSize, setModelSize] = useState('1.7B');
const [instruct, setInstruct] = useState('');
const [seed, setSeed] = useState<number | ''>('');
const [showAdvanced, setShowAdvanced] = useState(false);
// Derive if current profile is a preset
const currentProfile = profiles.find(p => p.id === selectedProfile);
const isPresetProfile = currentProfile?.voice_type === 'preset';
useEffect(() => {
const fetchInitialData = async () => {
try {
const profileData = await voiceboxApi.getProfiles();
const fetchedProfiles = Array.isArray(profileData) ? profileData : (profileData?.profiles || []);
if (fetchedProfiles && fetchedProfiles.length > 0) {
setProfiles(fetchedProfiles);
setSelectedProfile(fetchedProfiles[0].id);
}
} catch (error) {
console.error('Failed to load profiles', error);
}
try {
const historyData = await voiceboxApi.getHistory();
setHistoryItems(historyData?.items || []);
} catch (error) {
console.error('Failed to load history', error);
}
};
fetchInitialData();
}, []);
// Sync engine when profile changes
useEffect(() => {
if (currentProfile) {
if (currentProfile.voice_type === 'preset' && currentProfile.preset_engine) {
setEngine(currentProfile.preset_engine);
} else if (currentProfile.default_engine) {
setEngine(currentProfile.default_engine);
} else {
setEngine('qwen'); // default for cloned voices if not specified
}
}
}, [selectedProfile, currentProfile]);
const handleGenerate = async () => {
if (!text.trim()) {
alert('Lütfen dönüştürülecek metni girin.');
return;
}
setIsGenerating(true);
setAudioUrl(null);
try {
const options = {
language,
engine,
modelSize: engine === 'qwen' ? modelSize : undefined,
instruct: instruct.trim() || undefined,
seed: seed !== '' ? Number(seed) : undefined,
};
const audioBlob = await voiceboxApi.generateSpeech(text, selectedProfile, options);
const blob = new Blob([audioBlob], { type: 'audio/wav' });
const url = URL.createObjectURL(blob);
setAudioUrl(url);
// Refresh history after generation
const historyData = await voiceboxApi.getHistory();
setHistoryItems(historyData?.items || []);
} catch (error: any) {
alert(`Ses üretilirken bir hata oluştu: ${error.message || 'Bilinmeyen hata'}\n\nAyarları (özellikle ağır modelleri) kontrol edin.`);
} finally {
setIsGenerating(false);
}
};
const handleDownload = (url: string, filename: string) => {
if (!url) return;
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
const handleDeleteHistory = async (id: string) => {
if (!confirm('Bu geçmiş kaydını silmek istediğinize emin misiniz?')) return;
try {
await voiceboxApi.deleteHistory(id);
setHistoryItems(prev => prev.filter(item => item.id !== id));
} catch (error) {
alert('Kayıt silinirken bir hata oluştu.');
}
};
const insertTag = (tag: string) => {
setText((prev) => (prev ? `${prev} ${tag}` : tag));
};
const formatDate = (dateString: string) => {
const d = new Date(dateString);
return new Intl.DateTimeFormat('tr-TR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }).format(d);
};
return (
<div className="container mx-auto p-4 md:p-8 space-y-8 max-w-7xl">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-purple-400 to-blue-500 bg-clip-text text-transparent flex items-center gap-2">
<Mic className="w-8 h-8 text-purple-500" />
VoiceBox Studio <span className="text-xs bg-purple-500/20 text-purple-400 px-2 py-1 rounded-full align-top">Pro</span>
</h1>
<p className="text-muted-foreground mt-2">
Gelişmiş AI Ses Sentezi ve Klonlama Arayüzü
</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* LEFT COLUMN: Prompt & Results */}
<div className="lg:col-span-8 space-y-6">
<div className="rounded-xl border border-border/50 shadow-xl bg-card/50 backdrop-blur-sm text-card-foreground">
<div className="flex flex-col space-y-1.5 p-6 border-b border-border/50 bg-background/30 rounded-t-xl">
<h3 className="font-semibold leading-none tracking-tight">Senaryo Girişi</h3>
</div>
<div className="p-6 space-y-4">
<textarea
placeholder="Seslendirilmesini istediğiniz metni buraya yazın... [laugh]"
className="w-full rounded-xl border border-input/50 px-4 py-3 text-sm shadow-inner placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-purple-500 min-h-[200px] resize-y bg-background text-base transition-all"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex flex-wrap gap-2">
{['[laugh]', '[sigh]', '[breath]', '[pause]'].map((tag) => (
<button
key={tag}
type="button"
onClick={() => insertTag(tag)}
className="inline-flex items-center rounded-md border border-border/50 px-2.5 py-1 text-xs font-medium transition-colors hover:bg-purple-500/20 hover:border-purple-500/50 hover:text-purple-400 bg-background/50 text-muted-foreground relative z-10 cursor-pointer"
>
{tag}
</button>
))}
</div>
<button
onClick={handleGenerate}
disabled={isGenerating || !text.trim()}
className="inline-flex items-center justify-center rounded-lg text-sm font-semibold transition-all focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 h-11 px-8 bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 text-white shadow-[0_0_20px_rgba(168,85,247,0.3)] hover:shadow-[0_0_25px_rgba(168,85,247,0.5)] w-full sm:w-auto"
>
{isGenerating ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Ses Üretiliyor...
</>
) : (
<>
<Volume2 className="mr-2 h-5 w-5" />
Sesi Sentezle
</>
)}
</button>
</div>
</div>
</div>
<AnimatePresence>
{audioUrl && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="rounded-xl border border-purple-500/30 shadow-2xl bg-gradient-to-br from-card to-purple-900/10 relative overflow-hidden"
>
<div className="absolute top-0 left-0 w-1.5 h-full bg-gradient-to-b from-purple-500 to-blue-500 shadow-[0_0_15px_rgba(168,85,247,0.5)]"></div>
<div className="p-6 flex flex-col sm:flex-row items-center gap-6 relative z-10">
<div className="flex-1 w-full space-y-3">
<h3 className="font-semibold flex items-center gap-2 text-purple-400">
<Sparkles className="w-5 h-5" />
Üretim Başarılı
</h3>
<audio controls src={audioUrl} className="w-full h-12" autoPlay />
</div>
<button
onClick={() => handleDownload(audioUrl, `voicebox_${new Date().getTime()}.wav`)}
className="inline-flex items-center justify-center rounded-lg text-sm font-medium transition-colors h-11 px-6 w-full sm:w-auto border border-purple-500/30 hover:bg-purple-500/20 text-purple-300 bg-background/50 backdrop-blur-sm"
>
<Download className="w-4 h-4 mr-2" />
İndir
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* RIGHT COLUMN: Settings & History */}
<div className="lg:col-span-4 space-y-6">
{/* Settings Card */}
<div className="rounded-xl border border-border/50 shadow-lg bg-card/50 backdrop-blur-sm overflow-hidden">
<div className="p-5 border-b border-border/50 bg-background/30 flex items-center gap-2">
<Settings2 className="w-5 h-5 text-purple-400" />
<h3 className="font-semibold">Temel Ayarlar</h3>
</div>
<div className="p-5 space-y-4">
<div className="space-y-2 relative z-20">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Ses Profili</label>
<select
value={selectedProfile}
onChange={(e) => setSelectedProfile(e.target.value)}
className="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:ring-1 focus:ring-purple-500 focus:border-purple-500 outline-none transition-all cursor-pointer"
>
{profiles.map((p) => (
<option key={p.id} value={p.id} className="bg-background">
{p.name} {p.voice_type === 'preset' ? '(Hazır)' : '(Klon)'}
</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2 relative z-20">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider flex items-center justify-between">
Motor (Engine)
{isPresetProfile && <span className="text-[10px] text-purple-400 bg-purple-500/10 px-1.5 py-0.5 rounded ml-2">Sabit</span>}
</label>
<select
value={engine}
onChange={(e) => setEngine(e.target.value)}
disabled={isPresetProfile}
className="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:ring-1 focus:ring-purple-500 focus:border-purple-500 outline-none transition-all cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<option value="kokoro">Kokoro (Hızlı, CPU)</option>
<option value="edge_tts">Edge TTS (Türkçe İçin En İyisi)</option>
<option value="qwen">Qwen</option>
<option value="qwen_custom_voice">Qwen CustomVoice</option>
<option value="chatterbox">Chatterbox</option>
<option value="luxtts">LuxTTS</option>
</select>
{isPresetProfile && (
<p className="text-[10px] text-muted-foreground">Hazır profillerin motoru değiştirilemez.</p>
)}
</div>
<div className="space-y-2 relative z-20">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Dil</label>
<select
value={language}
onChange={(e) => setLanguage(e.target.value)}
className="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:ring-1 focus:ring-purple-500 focus:border-purple-500 outline-none transition-all cursor-pointer"
>
<option value="tr">Türkçe (TR)</option>
<option value="en">İngilizce (EN)</option>
<option value="zh">Çince (ZH)</option>
<option value="ja">Japonca (JA)</option>
<option value="ko">Korece (KO)</option>
<option value="de">Almanca (DE)</option>
<option value="fr">Fransızca (FR)</option>
<option value="es">İspanyolca (ES)</option>
</select>
</div>
</div>
</div>
{/* Advanced Settings Accordion */}
<div className="border-t border-border/50">
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className="w-full flex items-center justify-between p-4 hover:bg-background/50 transition-colors text-sm font-medium"
>
Gelişmiş Ayarlar
{showAdvanced ? <ChevronUp className="w-4 h-4 text-muted-foreground" /> : <ChevronDown className="w-4 h-4 text-muted-foreground" />}
</button>
<AnimatePresence>
{showAdvanced && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden"
>
<div className="p-5 pt-0 space-y-4 bg-background/20">
{engine === 'qwen' && (
<div className="rounded-lg bg-orange-500/10 border border-orange-500/20 p-3 flex gap-3 text-orange-400">
<AlertTriangle className="w-5 h-5 shrink-0" />
<p className="text-xs">
<strong>Uyarı:</strong> Qwen veya büyük boyutlu (1.7B, 4B) modeller, sınırlı RAM'e sahip ortamlarda (Raspberry Pi vb.) stabilite sorunları ve bellek taşması (Out of Memory) yaratabilir. Varsayılan Kokoro motoru daha verimlidir.
</p>
</div>
)}
<div className="space-y-2">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Model Boyutu</label>
<select
value={modelSize}
onChange={(e) => setModelSize(e.target.value)}
disabled={engine !== 'qwen'}
className="w-full rounded-lg border border-input bg-background/50 px-3 py-2 text-sm focus:ring-1 focus:ring-purple-500 disabled:opacity-50"
>
<option value="0.6B">0.6B (Hızlı/Düşük RAM)</option>
<option value="1.7B">1.7B (Dengeli)</option>
<option value="3B">3B (Gelişmiş)</option>
<option value="4B">4B (Ağır/Yüksek RAM)</option>
</select>
</div>
<div className="space-y-2">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Talimat (Instruct)</label>
<input
type="text"
value={instruct}
onChange={(e) => setInstruct(e.target.value)}
placeholder="Örn: Fısıldayarak konuş..."
className="w-full rounded-lg border border-input bg-background/50 px-3 py-2 text-sm focus:ring-1 focus:ring-purple-500"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Tohum (Seed)</label>
<input
type="number"
value={seed}
onChange={(e) => setSeed(e.target.value ? Number(e.target.value) : '')}
placeholder="Boş bırakırsanız rastgele"
className="w-full rounded-lg border border-input bg-background/50 px-3 py-2 text-sm focus:ring-1 focus:ring-purple-500"
/>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* History Card */}
<div className="rounded-xl border border-border/50 shadow-lg bg-card/50 backdrop-blur-sm overflow-hidden flex flex-col h-[400px]">
<div className="p-4 border-b border-border/50 bg-background/30 flex items-center justify-between">
<div className="flex items-center gap-2">
<History className="w-5 h-5 text-blue-400" />
<h3 className="font-semibold">Üretim Geçmişi</h3>
</div>
<span className="bg-blue-500/10 text-blue-400 text-xs px-2 py-1 rounded-full font-medium">
{historyItems.length} Kayıt
</span>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-2">
{historyItems.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground p-6 text-center">
<History className="w-10 h-10 mb-3 opacity-20" />
<p className="text-sm">Henüz bir ses üretmediniz.</p>
</div>
) : (
historyItems.map((item) => (
<div key={item.id} className="group rounded-lg border border-border/50 bg-background/40 hover:bg-background/80 p-3 transition-colors">
<div className="flex items-start justify-between mb-2">
<p className="text-xs font-medium line-clamp-2 pr-4">{item.text}</p>
</div>
<div className="flex items-center gap-3 text-[10px] text-muted-foreground mb-3">
<span className="flex items-center gap-1 bg-muted px-1.5 py-0.5 rounded">
<Clock className="w-3 h-3" /> {formatDate(item.created_at)}
</span>
<span className="uppercase font-semibold tracking-wider">{item.engine}</span>
<span className="uppercase">{item.language}</span>
</div>
<div className="flex items-center gap-2">
<audio controls src={voiceboxApi.getAudioUrl(item.id)} className="h-7 w-full [&::-webkit-media-controls-panel]:bg-background" />
<button
onClick={() => handleDownload(voiceboxApi.getAudioUrl(item.id), `history_${item.id}.wav`)}
className="p-1.5 rounded-md border border-border/50 hover:bg-purple-500/20 hover:text-purple-400 transition-colors"
title="İndir"
>
<Download className="w-3.5 h-3.5" />
</button>
<button
onClick={() => handleDeleteHistory(item.id)}
className="p-1.5 rounded-md border border-border/50 hover:bg-red-500/20 hover:text-red-400 transition-colors"
title="Sil"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,566 @@
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Video, Search, AlertCircle, CheckCircle2, Clock, MessageSquare, Play, BarChart2, TrendingUp, HelpCircle, ExternalLink, History, ThumbsUp, MessageCircle } from "lucide-react";
import { toolsApi } from "@/lib/api/api-service";
import { useToast } from "@/components/ui/toast";
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip as RechartsTooltip, Legend } from "recharts";
export default function YoutubeAnalyzerPage() {
const [url, setUrl] = useState("");
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [activeTab, setActiveTab] = useState("summary");
const [result, setResult] = useState<any>(null);
const [history, setHistory] = useState<any[]>([]);
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
const toast = useToast();
useEffect(() => {
if (isHistoryOpen && history.length === 0) {
loadHistory();
}
}, [isHistoryOpen]);
const loadHistory = async () => {
setIsLoadingHistory(true);
try {
const data = await toolsApi.getYoutubeAnalysisHistory();
setHistory(data);
} catch (e) {
console.error(e);
} finally {
setIsLoadingHistory(false);
}
};
const handleLoadAnalysis = async (id: string) => {
setIsAnalyzing(true);
setResult(null);
setActiveTab("summary");
try {
const data = await toolsApi.getYoutubeAnalysisById(id);
let parsedData = data.analysisData;
while (typeof parsedData === 'string') {
try {
const nextParse = JSON.parse(parsedData);
if (typeof nextParse === 'string' && nextParse === parsedData) break;
parsedData = nextParse;
} catch(e) {
console.error("JSON parse error:", e);
break;
}
}
setResult(parsedData);
setUrl(data.videoUrl);
setIsHistoryOpen(false);
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (error: any) {
toast.error("Geçmiş analiz yüklenirken hata oluştu.");
} finally {
setIsAnalyzing(false);
}
};
const handleAnalyze = async (e: React.FormEvent) => {
e.preventDefault();
if (!url.trim()) return;
setIsAnalyzing(true);
setResult(null);
setActiveTab("summary");
try {
const data = await toolsApi.analyzeYoutubeVideo(url);
setResult(data);
toast.success("Analiz başarıyla tamamlandı!");
} catch (error: any) {
toast.error(error?.response?.data?.message || "Analiz sırasında bir hata oluştu.");
} finally {
setIsAnalyzing(false);
}
};
const COLORS = ['#22c55e', '#ef4444', '#64748b'];
const sentimentData = result?.commentsAnalysis?.sentiment ? [
{ name: 'Pozitif', value: result.commentsAnalysis.sentiment.positive },
{ name: 'Negatif', value: result.commentsAnalysis.sentiment.negative },
{ name: 'Nötr', value: result.commentsAnalysis.sentiment.neutral },
] : [];
return (
<div className="max-w-7xl mx-auto pb-20">
{/* Header */}
<div className="mb-10 text-center max-w-3xl mx-auto">
<div className="inline-flex items-center justify-center p-3 rounded-2xl bg-red-500/10 text-red-500 mb-6">
<Video size={32} />
</div>
<h1 className="text-4xl md:text-5xl font-[family-name:var(--font-display)] font-bold text-white mb-4">
YouTube Analiz Aracı
</h1>
<p className="text-[var(--color-text-muted)] text-lg">
Uzun videoların transkriptini çıkarın, binlerce yorumu yapay zekayla analiz edin ve kitlenizin nabzını tutun.
</p>
</div>
{/* Input Section */}
<div className="max-w-3xl mx-auto mb-4">
<form onSubmit={handleAnalyze} className="relative group">
<div className="absolute -inset-1 bg-gradient-to-r from-red-500 to-purple-500 rounded-2xl blur opacity-20 group-hover:opacity-40 transition duration-1000 group-hover:duration-200"></div>
<div className="relative flex items-center bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] rounded-2xl p-2 shadow-2xl">
<div className="pl-4 pr-2 text-[var(--color-text-ghost)]">
<LinkIcon />
</div>
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="YouTube video linkini yapıştırın..."
className="w-full bg-transparent border-none text-white focus:ring-0 placeholder-[var(--color-text-ghost)] h-12 text-lg"
required
disabled={isAnalyzing}
/>
<button
type="submit"
disabled={isAnalyzing || !url}
className="ml-2 px-8 h-12 bg-white text-black rounded-xl font-bold flex items-center gap-2 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{isAnalyzing ? (
<>
<div className="w-5 h-5 border-2 border-black border-t-transparent rounded-full animate-spin" />
<span>Analiz Ediliyor...</span>
</>
) : (
<>
<Search size={18} />
<span>Analiz Et</span>
</>
)}
</button>
</div>
</form>
</div>
<div className="max-w-3xl mx-auto flex justify-end mb-8">
<button
onClick={() => setIsHistoryOpen(!isHistoryOpen)}
className="flex items-center gap-2 text-[var(--color-text-muted)] hover:text-white transition-colors text-sm font-medium bg-[var(--color-bg-elevated)] px-4 py-2 rounded-xl border border-[var(--color-border-faint)]"
>
<History size={16} />
<span>{isHistoryOpen ? 'Geçmişi Gizle' : 'Geçmiş Analizlerim'}</span>
</button>
</div>
<div className="max-w-3xl mx-auto mb-12">
{/* History Section */}
<AnimatePresence>
{isHistoryOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mb-8 overflow-hidden"
>
<div className="glass rounded-2xl p-6 border border-[var(--color-border-faint)]">
<h3 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
<History size={20} /> Geçmiş Analizler
</h3>
{isLoadingHistory ? (
<div className="flex justify-center p-8">
<div className="w-8 h-8 border-2 border-red-500 border-t-transparent rounded-full animate-spin"></div>
</div>
) : history.length === 0 ? (
<p className="text-center text-[var(--color-text-muted)] py-8">Henüz analiz edilmiş bir video bulunmuyor.</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{history.map((item) => (
<button
key={item.id}
onClick={() => handleLoadAnalysis(item.id)}
className="text-left bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] rounded-xl p-4 hover:border-red-500/50 hover:bg-white/5 transition-all group flex gap-4"
>
<img
src={item.thumbnail}
alt=""
className="w-24 h-16 object-cover rounded-lg shrink-0"
/>
<div className="overflow-hidden">
<h4 className="font-bold text-white text-sm line-clamp-2 mb-1 group-hover:text-red-400 transition-colors">
{item.title}
</h4>
<div className="flex items-center gap-3 text-xs text-[var(--color-text-ghost)]">
<span className="flex items-center gap-1"><MessageSquare size={12} /> {item.commentCount}</span>
<span>{new Date(item.createdAt).toLocaleDateString('tr-TR')}</span>
</div>
</div>
</button>
))}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Loading Steps */}
<AnimatePresence>
{isAnalyzing && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, height: 0 }}
className="mt-8 glass rounded-2xl p-6 border border-[var(--color-border-faint)]"
>
<div className="space-y-4">
<LoadingStep icon={<Play size={18} />} text="YouTube videosu bulunuyor..." delay={0} />
<LoadingStep icon={<FileTextIcon />} text="Transkript çekiliyor ve özetleniyor..." delay={2} />
<LoadingStep icon={<MessageSquare size={18} />} text="Yorumlar toplanıyor ve kümeleniyor..." delay={4} />
<LoadingStep icon={<ActivityIcon />} text="Yapay zeka çapraz analizi gerçekleştiriyor..." delay={6} />
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Results Section */}
<AnimatePresence>
{result && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-8"
>
{/* Video Info Card */}
{result.commentsAnalysis?.videoDetails && (
<div className="glass rounded-3xl p-6 border border-[var(--color-border-faint)] flex flex-col md:flex-row gap-6 items-center md:items-start bg-gradient-to-br from-[var(--color-bg-deep)] to-transparent">
<img
src={result.commentsAnalysis.videoDetails.thumbnail}
alt={result.commentsAnalysis.videoDetails.title}
className="w-full md:w-72 rounded-xl object-cover aspect-video shadow-2xl border border-[var(--color-border-faint)]"
/>
<div className="flex-1 space-y-4 w-full">
<h2 className="text-2xl md:text-3xl font-bold text-white leading-tight line-clamp-2 font-[family-name:var(--font-display)]">
{result.commentsAnalysis.videoDetails.title}
</h2>
<div className="flex flex-wrap gap-3 text-sm font-medium">
<div className="flex items-center gap-1.5 text-[var(--color-text-muted)] bg-[var(--color-bg-elevated)] px-4 py-2 rounded-xl border border-[var(--color-border-faint)] shadow-sm">
<Play size={16} className="text-red-500" />
<span>{new Intl.NumberFormat('tr-TR').format(result.commentsAnalysis.videoDetails.viewCount || 0)} İzlenme</span>
</div>
<div className="flex items-center gap-1.5 text-[var(--color-text-muted)] bg-[var(--color-bg-elevated)] px-4 py-2 rounded-xl border border-[var(--color-border-faint)] shadow-sm">
<TrendingUp size={16} className="text-blue-500" />
<span>{new Intl.NumberFormat('tr-TR').format(result.commentsAnalysis.videoDetails.likeCount || 0)} Beğeni</span>
</div>
<a href={result.url} target="_blank" rel="noreferrer" className="flex items-center gap-1.5 text-white bg-red-600 hover:bg-red-700 px-4 py-2 rounded-xl transition-colors shadow-lg shadow-red-500/20">
<ExternalLink size={16} />
<span>YouTube'da Aç</span>
</a>
</div>
</div>
</div>
)}
{/* Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatCard icon={<Clock />} title="Video Uzunluğu" value={result.transcriptAnalysis?.chapters?.length ? `${result.transcriptAnalysis.chapters.length} Bölüm` : 'Bilinmiyor'} />
<StatCard icon={<MessageSquare />} title="İncelenen Yorum" value={result.commentsAnalysis?.commentCount || 0} />
<StatCard icon={<TrendingUp />} title="Genel Duygu" value={result.commentsAnalysis?.sentiment?.positive > 50 ? 'Pozitif 🟢' : 'Karışık 🟡'} />
<StatCard icon={<CheckCircle2 />} title="Durum" value="Başarılı" />
</div>
{/* Tabs */}
<div className="flex overflow-x-auto hide-scrollbar gap-2 p-1 bg-[var(--color-bg-elevated)] rounded-xl border border-[var(--color-border-faint)] w-fit mx-auto">
<TabButton active={activeTab === 'summary'} onClick={() => setActiveTab('summary')} icon={<BarChart2 size={16} />} label="Genel Özet" />
<TabButton active={activeTab === 'chapters'} onClick={() => setActiveTab('chapters')} icon={<Play size={16} />} label="Bölümler" />
<TabButton active={activeTab === 'comments'} onClick={() => setActiveTab('comments')} icon={<MessageSquare size={16} />} label="Yorum Analizi" />
<TabButton active={activeTab === 'cross'} onClick={() => setActiveTab('cross')} icon={<ActivityIcon />} label="Çapraz Analiz" />
<TabButton active={activeTab === 'ideas'} onClick={() => setActiveTab('ideas')} icon={<HelpCircle size={16} />} label="Yeni Fikirler" />
</div>
{/* Tab Content */}
<div className="glass rounded-3xl p-6 md:p-10 border border-[var(--color-border-faint)] min-h-[400px]">
{/* Summary Tab */}
{activeTab === 'summary' && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-8">
<div>
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
<FileTextIcon /> Transkript Özeti
</h3>
<div className="prose prose-invert max-w-none text-[var(--color-text-muted)] leading-relaxed bg-[var(--color-bg-deep)] p-6 rounded-2xl border border-[var(--color-border-faint)]">
{result.transcriptAnalysis?.overallSummary ? (
result.transcriptAnalysis.overallSummary.split('\n').map((p: string, i: number) => (
<p key={i}>{p}</p>
))
) : (
<p>Transkript özeti bulunamadı.</p>
)}
</div>
</div>
<div>
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
<MessageSquare size={20} /> Genel Yankı (Yorumlar)
</h3>
<div className="bg-[var(--color-bg-deep)] p-6 rounded-2xl border border-[var(--color-border-faint)]">
<p className="text-[var(--color-text-muted)] leading-relaxed">
{result.commentsAnalysis?.generalResonance || "Yorum verisi bulunamadı."}
</p>
</div>
</div>
</motion.div>
)}
{/* Chapters Tab */}
{activeTab === 'chapters' && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-6">
<h3 className="text-xl font-bold text-white mb-6">Bölüm Bazlı Analiz</h3>
<div className="grid gap-4">
{result.transcriptAnalysis?.chapters?.map((chapter: any, i: number) => (
<div key={i} className="bg-[var(--color-bg-deep)] p-5 rounded-2xl border border-[var(--color-border-faint)]">
<h4 className="font-bold text-lg text-white mb-2">{chapter.title}</h4>
<p className="text-[var(--color-text-muted)] mb-4 text-sm">{chapter.summary}</p>
{chapter.points && chapter.points.length > 0 && (
<ul className="space-y-1">
{chapter.points.map((pt: string, j: number) => (
<li key={j} className="text-sm text-[var(--color-text-ghost)] flex items-start gap-2">
<span className="text-blue-500 mt-1">•</span>
<span>{pt}</span>
</li>
))}
</ul>
)}
</div>
))}
</div>
</motion.div>
)}
{/* Comments Tab */}
{activeTab === 'comments' && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-8">
<h3 className="text-xl font-bold text-white mb-6">Yorum & Duygu Analizi</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
<div className="h-64 bg-[var(--color-bg-deep)] p-4 rounded-2xl border border-[var(--color-border-faint)] flex items-center justify-center">
{sentimentData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={sentimentData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={5}
dataKey="value"
>
{sentimentData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<RechartsTooltip
contentStyle={{ backgroundColor: '#111', border: '1px solid #333', borderRadius: '8px' }}
itemStyle={{ color: '#fff' }}
/>
<Legend />
</PieChart>
</ResponsiveContainer>
) : (
<span className="text-[var(--color-text-ghost)]">Grafik verisi yok</span>
)}
</div>
<div className="space-y-4">
<div className="bg-[var(--color-bg-deep)] p-5 rounded-2xl border border-green-500/20">
<h4 className="font-bold text-green-500 mb-1">Pozitif Etkileşim (%{result.commentsAnalysis?.sentiment?.positive || 0})</h4>
<p className="text-sm text-[var(--color-text-muted)]">Kitle genel olarak videonun değerinden ve anlatımından memnun.</p>
</div>
<div className="bg-[var(--color-bg-deep)] p-5 rounded-2xl border border-red-500/20">
<h4 className="font-bold text-red-500 mb-1">Negatif Etkileşim (%{result.commentsAnalysis?.sentiment?.negative || 0})</h4>
<p className="text-sm text-[var(--color-text-muted)]">Eleştiriler veya videodaki eksik bulunan noktalar.</p>
</div>
</div>
</div>
<div className="mt-8">
<h4 className="font-bold text-lg text-white mb-4 flex items-center gap-2">
<HelpCircle size={18} /> Sık Sorulan Sorular (FAQ)
</h4>
<div className="grid gap-4">
{result.commentsAnalysis?.faq?.map((f: any, i: number) => (
<div key={i} className="bg-[var(--color-bg-deep)] p-5 md:p-6 rounded-2xl border border-[var(--color-border-faint)] hover:border-[var(--color-border-hover)] transition-colors">
<p className="font-bold text-white text-base md:text-lg mb-2 leading-relaxed">
<span className="text-blue-400 mr-2">Soru:</span>
{f.question}
</p>
<p className="text-sm md:text-base text-[var(--color-text-muted)] leading-relaxed">
<span className="text-[var(--color-text-ghost)] mr-2 font-medium">Bağlam:</span>
{f.context}
</p>
</div>
))}
</div>
</div>
{result.commentsAnalysis?.topComments && result.commentsAnalysis.topComments.length > 0 && (
<div className="mt-8">
<h4 className="font-bold text-lg text-white mb-4 flex items-center gap-2">
<MessageSquare size={18} /> En Yüksek Etkileşim Alan 10 Yorum
</h4>
<div className="grid gap-4">
{result.commentsAnalysis.topComments.map((c: any, i: number) => (
<div key={i} className="bg-[var(--color-bg-deep)] p-5 md:p-6 rounded-2xl border border-[var(--color-border-faint)] hover:border-[var(--color-border-hover)] transition-colors flex flex-col gap-3">
<div className="flex items-center justify-between">
<span className="font-bold text-blue-400">{c.author}</span>
<div className="flex items-center gap-4 text-xs font-medium text-[var(--color-text-muted)]">
<span className="flex items-center gap-1"><ThumbsUp size={14} className="text-green-500" /> {c.likes}</span>
<span className="flex items-center gap-1"><MessageCircle size={14} className="text-blue-500" /> {c.replies}</span>
</div>
</div>
<p className="text-sm md:text-base text-white leading-relaxed whitespace-pre-wrap">
{c.text}
</p>
</div>
))}
</div>
</div>
)}
</motion.div>
)}
{/* Cross Analysis Tab */}
{activeTab === 'cross' && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-6">
<h3 className="text-xl font-bold text-white mb-6">İçerik - Yorum Çapraz Analizi</h3>
{result.crossAnalysis && !result.crossAnalysis.error ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-[var(--color-bg-deep)] p-6 rounded-2xl border border-[var(--color-border-faint)] md:col-span-2">
<h4 className="font-bold text-white mb-2">Örtüşme (Alignment)</h4>
<p className="text-[var(--color-text-muted)] text-sm">{result.crossAnalysis.alignment}</p>
</div>
<div className="bg-red-500/5 p-6 rounded-2xl border border-red-500/20">
<h4 className="font-bold text-red-400 mb-4">Yanlış Anlaşılan / Atlanan Noktalar</h4>
<ul className="space-y-2">
{result.crossAnalysis.misunderstoodPoints?.map((pt: string, i: number) => (
<li key={i} className="text-sm text-[var(--color-text-muted)] flex items-start gap-2">
<span className="text-red-500 mt-0.5">•</span>
<span>{pt}</span>
</li>
))}
</ul>
</div>
<div className="bg-green-500/5 p-6 rounded-2xl border border-green-500/20">
<h4 className="font-bold text-green-400 mb-4">Öne Çıkan Güçlü Yönler</h4>
<ul className="space-y-2">
{result.crossAnalysis.highlightedStrengths?.map((pt: string, i: number) => (
<li key={i} className="text-sm text-[var(--color-text-muted)] flex items-start gap-2">
<span className="text-green-500 mt-0.5">•</span>
<span>{pt}</span>
</li>
))}
</ul>
</div>
<div className="bg-blue-500/5 p-6 rounded-2xl border border-blue-500/20 md:col-span-2">
<h4 className="font-bold text-blue-400 mb-4">İçerik Boşlukları (Content Gaps)</h4>
<ul className="space-y-2">
{result.crossAnalysis.contentGaps?.map((pt: string, i: number) => (
<li key={i} className="text-sm text-[var(--color-text-muted)] flex items-start gap-2">
<span className="text-blue-500 mt-0.5">•</span>
<span>{pt}</span>
</li>
))}
</ul>
</div>
</div>
) : (
<div className="p-6 text-center text-[var(--color-text-ghost)]">Çapraz analiz verisi bulunamadı.</div>
)}
</motion.div>
)}
{/* Ideas Tab */}
{activeTab === 'ideas' && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-6">
<h3 className="text-xl font-bold text-white mb-6">Gelecek İçerik Fikirleri</h3>
<p className="text-[var(--color-text-ghost)] text-sm mb-6">
Kitle tepkilerine ve eksik bırakılan noktalara dayalı video fikirleri:
</p>
<div className="grid gap-4">
{result.commentsAnalysis?.suggestions?.map((idea: string, i: number) => (
<div key={i} className="bg-[var(--color-bg-deep)] p-5 rounded-2xl border border-[var(--color-border-faint)] flex items-start gap-4">
<div className="w-8 h-8 rounded-full bg-blue-500/20 text-blue-500 flex items-center justify-center font-bold shrink-0">
{i + 1}
</div>
<p className="text-[var(--color-text-muted)] mt-1">{idea}</p>
</div>
))}
</div>
</motion.div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
function TabButton({ active, onClick, icon, label }: { active: boolean; onClick: () => void; icon: React.ReactNode; label: string }) {
return (
<button
onClick={onClick}
className={`flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-semibold transition-all whitespace-nowrap ${
active
? 'bg-white text-black shadow-md'
: 'text-[var(--color-text-ghost)] hover:text-white hover:bg-white/5'
}`}
>
{icon}
<span>{label}</span>
</button>
);
}
function StatCard({ icon, title, value }: { icon: React.ReactNode; title: string; value: string | number }) {
return (
<div className="glass p-5 rounded-2xl border border-[var(--color-border-faint)] flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-white/5 flex items-center justify-center text-[var(--color-text-muted)]">
{icon}
</div>
<div>
<p className="text-xs text-[var(--color-text-ghost)] uppercase tracking-wider font-semibold mb-1">{title}</p>
<p className="text-xl font-bold text-white font-[family-name:var(--font-display)]">{value}</p>
</div>
</div>
);
}
function LoadingStep({ icon, text, delay }: { icon: React.ReactNode; text: string; delay: number }) {
return (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: delay * 0.5, duration: 0.5 }}
className="flex items-center gap-3 text-[var(--color-text-muted)]"
>
<div className="w-8 h-8 rounded-full bg-[var(--color-bg-elevated)] flex items-center justify-center">
{icon}
</div>
<span className="text-sm font-medium">{text}</span>
</motion.div>
);
}
const LinkIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>;
const FileTextIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><line x1="10" y1="9" x2="8" y2="9"></line></svg>;
const ActivityIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>;
@@ -0,0 +1,573 @@
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Search, History, Target, TrendingUp, ImageIcon, Lightbulb, PenTool, HelpCircle, Copy, Check, Loader2, Download } from "lucide-react";
import { toolsApi } from "@/lib/api/api-service";
import { useToast } from "@/components/ui/toast";
const YoutubeIcon = ({ size = 20, className = "" }: { size?: number; className?: string }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</svg>
);
const CopyButton = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<button onClick={(e) => { e.preventDefault(); handleCopy(); }} className="p-1.5 bg-white/5 hover:bg-white/10 rounded-lg transition-colors flex-shrink-0" title="Kopyala">
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-white/40 hover:text-white" />}
</button>
);
};
const ThumbnailCard = ({ idea, index }: { idea: any, index: number }) => {
const [loading, setLoading] = useState(false);
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const toast = useToast();
const handleGenerate = async () => {
setLoading(true);
try {
const res = await toolsApi.generateYoutubeSeoImage(idea.midjourneyPrompt);
setImageUrl(res.url);
toast.success("Kapak görseli başarıyla üretildi!");
} catch(err: any) {
toast.error(err.message || "Görsel üretilemedi.");
} finally {
setLoading(false);
}
};
const handleDownload = async (e: React.MouseEvent) => {
e.stopPropagation();
if (!imageUrl) return;
try {
const response = await fetch(imageUrl);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = `thumbnail-concept-${index + 1}.jpg`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (err) {
toast.error("Görsel indirilirken hata oluştu.");
}
};
return (
<>
<div className="bg-black/40 border border-white/10 rounded-2xl p-4 flex flex-col relative">
<h4 className="font-medium text-pink-300 mb-2">Konsept {index + 1}</h4>
<p className="text-sm text-white/80 mb-4">{idea.concept}</p>
{imageUrl ? (
<div
className="mb-4 rounded-xl overflow-hidden border border-white/10 cursor-pointer relative group"
onClick={() => setIsFullscreen(true)}
>
<img src={imageUrl} alt={`Thumbnail ${index + 1}`} className="w-full aspect-video object-cover transition-transform duration-300 group-hover:scale-105" />
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<Search className="w-8 h-8 text-white" />
</div>
</div>
) : (
<button
onClick={handleGenerate}
disabled={loading}
className="mb-4 w-full py-2 bg-pink-500/10 hover:bg-pink-500/20 text-pink-400 border border-pink-500/20 rounded-xl transition-all text-sm font-medium flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <ImageIcon className="w-4 h-4" />}
{loading ? "Görsel Üretiliyor..." : "Görseli Oluştur"}
</button>
)}
<div className="mt-auto">
<div className="text-xs text-white/50 mb-1 flex justify-between items-center">
<span>AI Prompt:</span>
<CopyButton text={idea.midjourneyPrompt} />
</div>
<div className="bg-white/5 border border-white/10 p-2 rounded-lg text-xs text-white/70 font-mono break-all mb-3">
{idea.midjourneyPrompt}
</div>
<div className="flex items-center gap-2 text-xs">
<span className="text-white/40">Renk Paleti:</span>
<span className="text-white font-medium">{idea.colorPalette}</span>
</div>
</div>
</div>
<AnimatePresence>
{isFullscreen && imageUrl && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsFullscreen(false)}
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/95 backdrop-blur-md cursor-zoom-out"
>
<div className="relative w-full h-full p-4 md:p-8 flex items-center justify-center" onClick={e => e.stopPropagation()}>
<img
src={imageUrl}
alt={`Thumbnail Fullscreen ${index + 1}`}
className="w-full h-full object-contain rounded-xl"
/>
<button
onClick={handleDownload}
className="absolute top-6 right-6 bg-black/60 hover:bg-black border border-white/20 p-3 rounded-xl backdrop-blur-md transition-all text-white flex items-center gap-2"
title="Görseli İndir"
>
<Download className="w-5 h-5" />
<span className="text-sm font-medium hidden sm:block">İndir</span>
</button>
<button
onClick={() => setIsFullscreen(false)}
className="absolute top-6 right-36 md:right-40 text-white/60 hover:text-white transition-colors bg-black/40 hover:bg-black/60 px-4 py-3 rounded-xl border border-transparent hover:border-white/20"
title="Kapat"
>
Kapat
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
};
export default function YoutubeSeoPage() {
const [url, setUrl] = useState("");
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [result, setResult] = useState<any>(null);
const [history, setHistory] = useState<any[]>([]);
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
const toast = useToast();
useEffect(() => {
if (isHistoryOpen && history.length === 0) {
loadHistory();
}
}, [isHistoryOpen]);
const loadHistory = async () => {
setIsLoadingHistory(true);
try {
const data = await toolsApi.getYoutubeSeoHistory();
setHistory(data);
} catch (e) {
console.error(e);
} finally {
setIsLoadingHistory(false);
}
};
const handleLoadAnalysis = async (id: string) => {
setIsAnalyzing(true);
setResult(null);
try {
const data = await toolsApi.getYoutubeSeoAnalysisById(id);
if (data && data.seoAnalysis) {
while (typeof data.seoAnalysis === 'string') {
try {
const nextParse = JSON.parse(data.seoAnalysis);
if (typeof nextParse === 'string' && nextParse === data.seoAnalysis) break;
data.seoAnalysis = nextParse;
} catch(e) {
console.error("JSON parse error:", e);
break;
}
}
}
setResult(data);
setUrl(data.videoUrl);
setIsHistoryOpen(false);
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (error: any) {
toast.error("Geçmiş analiz yüklenirken hata oluştu.");
} finally {
setIsAnalyzing(false);
}
};
const handleAnalyze = async (e: React.FormEvent) => {
e.preventDefault();
if (!url.trim()) return;
setIsAnalyzing(true);
setResult(null);
setIsHistoryOpen(false);
try {
const data = await toolsApi.analyzeYoutubeSEO(url);
setResult(data);
} catch (error: any) {
toast.error(error.message || "Analiz sırasında bir hata oluştu.");
} finally {
setIsAnalyzing(false);
}
};
const ScoreCircle = ({ score }: { score: number }) => {
const color = score >= 80 ? "text-green-500" : score >= 50 ? "text-yellow-500" : "text-red-500";
const strokeColor = score >= 80 ? "#22c55e" : score >= 50 ? "#eab308" : "#ef4444";
return (
<div className="relative w-24 h-24 flex items-center justify-center">
<svg className="w-full h-full transform -rotate-90">
<circle cx="48" cy="48" r="40" stroke="currentColor" strokeWidth="8" fill="transparent" className="text-white/10" />
<circle
cx="48" cy="48" r="40" stroke={strokeColor} strokeWidth="8" fill="transparent"
strokeDasharray={251.2}
strokeDashoffset={251.2 - (251.2 * score) / 100}
className="transition-all duration-1000 ease-out"
/>
</svg>
<span className={`absolute text-2xl font-bold ${color}`}>{score}</span>
</div>
);
};
return (
<div className="min-h-screen bg-black text-white p-4 md:p-8 font-sans">
<div className="max-w-6xl mx-auto space-y-8">
{/* Header */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className="p-3 bg-red-500/20 rounded-2xl border border-red-500/30">
<YoutubeIcon className="w-8 h-8 text-red-500" />
</div>
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-red-400 to-orange-400 bg-clip-text text-transparent">
YouTube SEO Power Engine
</h1>
<p className="text-white/60 mt-1">
Videolarınızı sıralamada zirveye taşıyacak premium analiz aracı
</p>
</div>
</div>
<button
onClick={() => setIsHistoryOpen(!isHistoryOpen)}
className="flex items-center gap-2 px-4 py-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl transition-colors"
>
<History className="w-4 h-4" />
Geçmiş Analizlerim
</button>
</div>
{/* URL Input Form */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white/5 border border-white/10 rounded-3xl p-6 backdrop-blur-xl relative overflow-hidden"
>
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-red-500 via-orange-500 to-yellow-500"></div>
<form onSubmit={handleAnalyze} className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<div className="absolute inset-y-0 left-4 flex items-center pointer-events-none">
<Search className="h-5 w-5 text-white/40" />
</div>
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="YouTube video linkini yapıştırın (örn: https://youtube.com/watch?v=...)"
className="w-full bg-black/50 border border-white/10 rounded-2xl pl-12 pr-4 py-4 text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-red-500/50 transition-all"
required
/>
</div>
<button
type="submit"
disabled={isAnalyzing}
className="bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white px-8 py-4 rounded-2xl font-semibold transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isAnalyzing ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white/20 border-t-white"></div>
Analiz Ediliyor...
</>
) : (
<>
<Target className="w-5 h-5" />
SEO Analizi Başlat
</>
)}
</button>
</form>
</motion.div>
{/* History Dropdown */}
<AnimatePresence>
{isHistoryOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<div className="bg-white/5 border border-white/10 rounded-3xl p-6 backdrop-blur-xl mb-8">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
<History className="w-5 h-5 text-white/60" /> Geçmiş Analizler
</h3>
{isLoadingHistory ? (
<div className="text-center text-white/50 py-4">Yükleniyor...</div>
) : history.length === 0 ? (
<div className="text-center text-white/50 py-4">Henüz geçmiş analiz bulunmuyor.</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{history.map((item) => (
<button
key={item.id}
onClick={() => handleLoadAnalysis(item.id)}
className="text-left bg-black/40 hover:bg-white/10 border border-white/10 rounded-xl p-4 transition-all group flex gap-4 items-center"
>
{item.thumbnail ? (
<img src={item.thumbnail} alt={item.title || 'Video'} className="w-24 h-16 object-cover rounded-lg" />
) : (
<div className="w-24 h-16 bg-white/5 rounded-lg flex items-center justify-center">
<YoutubeIcon className="w-6 h-6 text-white/20" />
</div>
)}
<div className="flex-1 overflow-hidden">
<p className="font-medium text-sm text-white truncate group-hover:text-red-400 transition-colors">
{item.title || 'İsimsiz Video'}
</p>
<div className="flex items-center gap-2 mt-2 text-xs text-white/50">
<span className="bg-white/10 px-2 py-1 rounded">Skor: {item.seoScore}</span>
<span>{new Date(item.createdAt).toLocaleDateString()}</span>
</div>
</div>
</button>
))}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Results */}
{result && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="grid grid-cols-1 lg:grid-cols-3 gap-8"
>
{/* Left Column: Video Info & Scores */}
<div className="lg:col-span-1 space-y-6">
<div className="bg-white/5 border border-white/10 rounded-3xl p-6 backdrop-blur-xl">
{result.videoDetails?.thumbnail && (
<img src={result.videoDetails.thumbnail} alt="Thumbnail" className="w-full aspect-video object-cover rounded-2xl mb-4 border border-white/10" />
)}
<h2 className="text-lg font-semibold text-white mb-2 leading-tight">
{result.videoDetails?.title}
</h2>
<div className="flex gap-4 text-sm text-white/60 mb-6">
<span suppressHydrationWarning>{Number(result.videoDetails?.viewCount || 0).toLocaleString()} İzl.</span>
<span suppressHydrationWarning>{Number(result.videoDetails?.likeCount || 0).toLocaleString()} Beğeni</span>
</div>
<div className="border-t border-white/10 pt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-medium text-white/80">Genel SEO Skoru</h3>
<ScoreCircle score={result.seoAnalysis?.currentStatus?.seoScore || 0} />
</div>
<div className="space-y-4 text-sm">
<div className="bg-white/5 p-3 rounded-xl border border-white/5">
<span className="text-white/40 block mb-1">Başlık Durumu</span>
<p className="text-white/80">{result.seoAnalysis?.currentStatus?.titleFeedback}</p>
</div>
<div className="bg-white/5 p-3 rounded-xl border border-white/5">
<span className="text-white/40 block mb-1">Açıklama Durumu</span>
<p className="text-white/80">{result.seoAnalysis?.currentStatus?.descriptionFeedback}</p>
</div>
<div className="bg-white/5 p-3 rounded-xl border border-white/5">
<span className="text-white/40 block mb-1">Etiket (Keyword) Durumu</span>
<p className="text-white/80">{result.seoAnalysis?.currentStatus?.keywordsFeedback}</p>
</div>
</div>
</div>
<div className="border-t border-white/10 pt-6 mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-medium text-white/80 flex items-center gap-2">
<Target className="w-4 h-4 text-orange-400" /> Kanca (Hook) Skoru
</h3>
<ScoreCircle score={result.seoAnalysis?.hookAnalysis?.score || 0} />
</div>
<div className="space-y-4 text-sm">
<div className="bg-white/5 p-3 rounded-xl border border-white/5">
<p className="text-white/80">{result.seoAnalysis?.hookAnalysis?.feedback}</p>
<div className="mt-2 text-orange-300 font-medium bg-orange-500/10 p-2 rounded border border-orange-500/20">
💡 Öneri: {result.seoAnalysis?.hookAnalysis?.suggestion}
</div>
</div>
</div>
</div>
</div>
</div>
{/* Right Column: SEO Assets */}
<div className="lg:col-span-2 space-y-6">
{/* A/B Test Titles */}
<div className="bg-white/5 border border-white/10 rounded-3xl p-6 backdrop-blur-xl">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2 text-white">
<Lightbulb className="w-5 h-5 text-yellow-400" /> A/B Test Başlık Stratejileri
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{result.seoAnalysis?.abTestTitles?.map((titleObj: any, i: number) => (
<div key={i} className="bg-gradient-to-br from-black/60 to-black/40 border border-white/10 p-5 rounded-2xl relative group flex flex-col">
<div className="absolute top-0 left-0 w-full h-1 bg-yellow-500/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-t-2xl"></div>
<div className="flex justify-between items-start mb-3">
<span className="inline-block px-2 py-1 bg-white/10 rounded text-xs font-medium text-yellow-300">
{titleObj.type}
</span>
{titleObj.seoScore && (
<span className="inline-block px-2 py-1 bg-green-500/10 border border-green-500/20 rounded text-xs font-bold text-green-400" title="Tahmini SEO Skoru">
SEO: {titleObj.seoScore}
</span>
)}
</div>
<div className="flex justify-between items-start gap-2 mb-3">
<p className="font-semibold text-white text-lg leading-tight flex-1">
{titleObj.title}
</p>
<CopyButton text={titleObj.title} />
</div>
<p className="text-xs text-white/50 border-t border-white/10 pt-3 mt-auto">
<span className="text-white/70 block mb-1">Neden Çalışır?</span>
{titleObj.reason}
</p>
</div>
))}
</div>
</div>
{/* Keywords & FAQ */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white/5 border border-white/10 rounded-3xl p-6 backdrop-blur-xl flex flex-col">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-semibold flex items-center gap-2 text-white">
<Search className="w-5 h-5 text-blue-400" /> Long-tail Keywords
</h3>
<CopyButton text={result.seoAnalysis?.suggestedKeywords?.join(", ") || ""} />
</div>
<div className="flex flex-wrap gap-2">
{result.seoAnalysis?.suggestedKeywords?.map((kw: string, i: number) => (
<span key={i} className="bg-blue-500/10 border border-blue-500/20 text-blue-300 px-3 py-1.5 rounded-lg text-sm flex items-center gap-1">
<TrendingUp className="w-3 h-3" /> {kw}
</span>
))}
</div>
</div>
<div className="bg-white/5 border border-white/10 rounded-3xl p-6 backdrop-blur-xl flex flex-col">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-semibold flex items-center gap-2 text-white">
<HelpCircle className="w-5 h-5 text-purple-400" /> Sık Sorulan Sorular (SSS)
</h3>
<CopyButton text={result.seoAnalysis?.faqQuestions?.map((q: any) => `Q: ${q.question || q}\nA: ${q.answer || ''}`).join("\n\n") || ""} />
</div>
<ul className="space-y-3">
{result.seoAnalysis?.faqQuestions?.map((q: any, i: number) => (
<li key={i} className="flex flex-col gap-1 text-sm text-white/80 bg-black/30 p-3 rounded-xl border border-white/5">
<div className="flex gap-2">
<span className="text-purple-400 font-bold">Q:</span>
<span className="font-semibold">{q.question || q}</span>
</div>
{q.answer && (
<div className="flex gap-2 pl-6 mt-1 text-white/60">
<span className="text-blue-400 font-bold">A:</span>
<span>{q.answer}</span>
</div>
)}
</li>
))}
</ul>
</div>
</div>
{/* Suggested Description */}
<div className="bg-white/5 border border-white/10 rounded-3xl p-6 backdrop-blur-xl">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-semibold flex items-center gap-2 text-white">
<PenTool className="w-5 h-5 text-green-400" /> SEO Uyumlu Açıklama Şablonu
</h3>
<CopyButton text={result.seoAnalysis?.suggestedDescriptionTemplate || ""} />
</div>
<div className="bg-black/50 border border-white/10 p-4 rounded-2xl">
<pre className="text-sm text-white/70 whitespace-pre-wrap font-sans">
{result.seoAnalysis?.suggestedDescriptionTemplate}
</pre>
</div>
</div>
{/* Thumbnail Concepts */}
<div className="bg-white/5 border border-white/10 rounded-3xl p-6 backdrop-blur-xl">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2 text-white">
<ImageIcon className="w-5 h-5 text-pink-400" /> Kapak Görseli (Thumbnail) Konseptleri
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{result.seoAnalysis?.thumbnailIdeas?.map((idea: any, i: number) => (
<ThumbnailCard key={i} idea={idea} index={i} />
))}
</div>
</div>
{/* Shorts Ideas */}
{result.seoAnalysis?.shortsIdeas && result.seoAnalysis.shortsIdeas.length > 0 && (
<div className="bg-white/5 border border-white/10 rounded-3xl p-6 backdrop-blur-xl">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2 text-white">
<YoutubeIcon className="w-5 h-5 text-red-500" /> Shorts Fikirleri
</h3>
<div className="space-y-4">
{result.seoAnalysis.shortsIdeas.map((idea: any, i: number) => (
<div key={i} className="bg-black/30 border border-white/5 p-4 rounded-xl flex items-start gap-4">
<div className="bg-red-500/20 p-2 rounded-lg text-red-400 flex-shrink-0 w-8 h-8 flex items-center justify-center font-bold">
{i+1}
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-start gap-2 mb-1">
<h4 className="font-semibold text-white">{idea.title || idea.topic}</h4>
{(idea.timestamp || idea.timecode) && (
<span className="inline-flex items-center px-2 py-1 bg-red-500/10 border border-red-500/20 text-red-400 text-xs rounded font-medium whitespace-nowrap">
{idea.timestamp || idea.timecode}
</span>
)}
</div>
<p className="text-sm text-white/60 mb-2">{idea.context || idea.description}</p>
{idea.hook && (
<div className="bg-black/30 rounded-xl p-3 mt-2">
<p className="text-xs text-white/40 mb-1">Önerilen Kanca (Hook):</p>
<p className="text-sm text-white/80 font-medium">{idea.hook}</p>
</div>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
</motion.div>
)}
</div>
</div>
);
}
@@ -1,322 +0,0 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import {
AtSign,
Link2,
Loader2,
ArrowRight,
Sparkles,
Wand2,
MessageSquare,
Heart,
Repeat2,
Eye,
Image as ImageIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useTweetPreview, useCreateFromTweet } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
import {
LanguageSelector,
StyleSelector,
DurationSelector,
AspectRatioSelector,
} from "@/components/projects/ProjectConfiguration";
export default function XToVideoPage() {
const router = useRouter();
const { toast } = useToast();
const tweetPreview = useTweetPreview();
const createFromTweet = useCreateFromTweet();
const [tweetUrl, setTweetUrl] = useState("");
const [style, setStyle] = useState("CINEMATIC");
const [cinematicReference, setCinematicReference] = useState("");
const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
const [language, setLanguage] = useState("tr");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [previewData, setPreviewData] = useState<any>(null);
const isValidUrl = /https?:\/\/(x\.com|twitter\.com)\/\w+\/status\/\d+/.test(tweetUrl);
const handlePreview = async () => {
if (!isValidUrl) {
toast("error", "Geçerli bir X/Twitter URL'si girin (https://x.com/...)");
return;
}
try {
const result = await tweetPreview.mutateAsync(tweetUrl);
setPreviewData(result);
toast("success", "Tweet başarıyla yüklendi!");
} catch {
toast("error", "Tweet yüklenemedi. URL'yi kontrol edin.");
}
};
const handleGenerate = async () => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = await createFromTweet.mutateAsync({
tweetUrl,
language,
aspectRatio,
videoStyle: style,
cinematicReference: cinematicReference ? cinematicReference : undefined,
targetDuration: duration,
});
toast("success", "Tweet → Video projesi oluşturuldu!");
const projectId = result?.id;
if (projectId) {
router.push(`/dashboard/projects/${projectId}`);
} else {
router.push("/dashboard/projects");
}
} catch {
toast("error", "Proje oluşturulurken bir hata oluştu.");
}
};
return (
<div className="max-w-3xl mx-auto space-y-6 pb-24">
{/* Header */}
<div className="text-center space-y-3 pb-4">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
<AtSign size={32} />
</div>
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
Tweet'ten Video Oluştur
</h1>
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
X/Twitter yazılarını AI ile kısa videolara dönüştürün
</p>
</div>
{/* URL Input */}
<div className="card p-5 space-y-4">
<label className="text-sm font-medium text-[var(--color-text-secondary)] block">
<Link2 size={14} className="inline mr-1.5 text-cyan-400" />
Tweet URL
</label>
<div className="flex gap-2">
<input
type="url"
value={tweetUrl}
onChange={(e) => {
setTweetUrl(e.target.value);
setPreviewData(null);
}}
placeholder="https://x.com/username/status/123456..."
className="flex-1 px-4 py-2.5 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-[var(--color-bg-inverted)]/40 focus:ring-1 focus:ring-[var(--color-bg-inverted)]/20 transition-all"
/>
<button
onClick={handlePreview}
disabled={!isValidUrl || tweetPreview.isPending}
className={cn(
"px-4 py-2.5 rounded-xl text-sm font-semibold flex items-center gap-2 transition-all shrink-0",
isValidUrl
? "btn-primary"
: "bg-[var(--color-bg-elevated)] text-[var(--color-text-ghost)] cursor-not-allowed",
)}
>
{tweetPreview.isPending ? (
<Loader2 size={16} className="animate-spin" />
) : (
<>
<Eye size={16} />
Önizle
</>
)}
</button>
</div>
<p className="text-[11px] text-[var(--color-text-ghost)]">
Thread desteği: Çoklu tweet zincirleri de otomatik olarak algılanır
</p>
</div>
{/* Tweet Preview */}
<AnimatePresence>
{previewData && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="card p-5 space-y-3"
>
<h3 className="text-sm font-semibold text-[var(--color-text-secondary)]">
<MessageSquare
size={14}
className="inline mr-1.5 text-violet-400"
/>
Tweet Önizleme
</h3>
<div className="p-4 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] space-y-3">
{/* Author */}
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-violet-500/20 to-cyan-500/20 flex items-center justify-center text-sm font-bold text-violet-300">
{(previewData.tweet?.author?.name ?? "X")?.[0]}
</div>
<div>
<p className="text-sm font-bold text-[var(--color-text-primary)]">
{previewData.tweet?.author?.name ?? "Kullanıcı"}
</p>
<p className="text-[11px] text-[var(--color-text-ghost)]">
@{previewData.tweet?.author?.username ?? "handle"}
</p>
</div>
</div>
{/* Content */}
<p className="text-sm text-[var(--color-text-secondary)] whitespace-pre-line">
{previewData.tweet?.text ?? ""}
</p>
{/* Images */}
{(previewData.tweet?.media?.length > 0) && (
<div className="flex gap-2 overflow-x-auto">
{(previewData.tweet.media ?? [])
.filter((m: any) => m.type === 'photo')
.map(
(m: any, i: number) => (
<div
key={i}
className="w-20 h-20 rounded-lg bg-[var(--color-bg-elevated)] overflow-hidden shrink-0"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={m.url}
alt={`Media ${i + 1}`}
className="w-full h-full object-cover"
/>
</div>
),
)}
</div>
)}
{/* Stats */}
<div className="flex items-center gap-4 text-[11px] text-[var(--color-text-ghost)]">
<span className="flex items-center gap-1">
<Heart size={12} />
{previewData.tweet?.metrics?.likes ?? 0}
</span>
<span className="flex items-center gap-1">
<Repeat2 size={12} />
{previewData.tweet?.metrics?.retweets ?? 0}
</span>
<span className="flex items-center gap-1">
<Eye size={12} />
{previewData.tweet?.metrics?.views ?? 0}
</span>
{previewData.tweet?.isThread && (
<span className="flex items-center gap-1 text-[var(--color-bg-inverted)]">
<MessageSquare size={12} />
{previewData.tweet?.threadTweets?.length ?? 0} tweet thread
</span>
)}
</div>
</div>
{/* Suggested info */}
{previewData.suggestedTitle && (
<div className="flex items-center gap-1.5 text-[11px] text-[var(--color-text-secondary)]">
<Sparkles size={12} />
Önerilen başlık: {previewData.suggestedTitle} · Tahmini süre: {previewData.estimatedDuration}sn · Viral skoru: {previewData.viralScore}/100
</div>
)}
{/* Images tag */}
{(previewData.tweet?.media?.filter((m: any) => m.type === 'photo')?.length > 0) && (
<div className="flex items-center gap-1.5 text-[11px] text-[var(--color-text-secondary)]">
<ImageIcon size={12} />
{previewData.tweet.media.filter((m: any) => m.type === 'photo').length} görsel referans olarak kullanılacak + AI görsel üretilecek
</div>
)}
</motion.div>
)}
</AnimatePresence>
{/* Video Settings */}
<AnimatePresence>
{previewData && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<div className="card p-6 space-y-8">
<LanguageSelector value={language} onChange={setLanguage} />
<StyleSelector
value={style}
onChange={setStyle}
cinematicReference={cinematicReference}
onCinematicReferenceChange={setCinematicReference}
/>
<DurationSelector value={duration} onChange={setDuration} />
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
</div>
{/* Generate Button */}
<div className="flex justify-end">
<button
onClick={handleGenerate}
disabled={createFromTweet.isPending}
className={cn(
"group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
"bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
"disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
)}
>
{createFromTweet.isPending ? (
<>
<Loader2 size={20} className="animate-spin" />
<span>Video Projesi Oluşturuluyor...</span>
</>
) : (
<>
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
<span>Tweet → Video Oluştur</span>
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</>
)}
</button>
</div>
<p className="text-center text-[11px] text-[var(--color-text-ghost)]">
Bu işlem 1 kredi kullanır • AI senaryo + görsel üretim dahil
</p>
</motion.div>
)}
</AnimatePresence>
{/* Info Box */}
{!previewData && (
<div className="card p-5 bg-[var(--color-bg-surface)]">
<div className="flex items-start gap-3">
<Sparkles size={20} className="text-[var(--color-text-secondary)] shrink-0 mt-0.5" />
<div className="space-y-2">
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
Nasıl Çalışır?
</h3>
<ol className="text-xs text-[var(--color-text-muted)] space-y-1.5 list-decimal list-inside">
<li>X/Twitter URL'sini yapıştırın ve "Önizle" butonuna tıklayın</li>
<li>Tweet içeriği otomatik olarak çekilir (thread desteği dahil)</li>
<li>Video stilini, süresini ve dilini seçin</li>
<li>AI otomatik olarak senaryo yazar ve görseller üretir</li>
<li>Video render edilir ve indirilmeye hazır hale gelir</li>
</ol>
</div>
</div>
</div>
)}
</div>
);
}
@@ -1,157 +0,0 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { useCreateFromYoutube } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
import {
PlaySquare,
Link2,
Loader2,
ArrowRight,
Sparkles,
Wand2,
} from "lucide-react";
import {
LanguageSelector,
StyleSelector,
DurationSelector,
AspectRatioSelector,
} from "@/components/projects/ProjectConfiguration";
export default function YoutubeToVideoPage() {
const router = useRouter();
const { toast } = useToast();
const createFromYoutube = useCreateFromYoutube();
const [youtubeUrl, setYoutubeUrl] = useState("");
const [style, setStyle] = useState("CINEMATIC");
const [cinematicReference, setCinematicReference] = useState("");
const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
const [language, setLanguage] = useState("tr");
const isValidUrl = /https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/.+/.test(youtubeUrl);
const handleGenerate = async () => {
if (!isValidUrl) {
toast("error", "Lütfen geçerli bir YouTube URL'si girin.");
return;
}
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = await createFromYoutube.mutateAsync({
youtubeUrl,
language,
aspectRatio,
videoStyle: style,
cinematicReference: cinematicReference ? cinematicReference : undefined,
targetDuration: duration,
});
toast("success", "YouTube → Video projesi oluşturuldu!");
router.push(`/dashboard/projects/${result.id}`);
} catch {
toast("error", "Proje oluşturulurken bir hata oluştu.");
}
};
return (
<div className="max-w-3xl mx-auto space-y-8 pb-24">
{/* Header */}
<div className="text-center space-y-3 pb-4">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
<PlaySquare size={32} />
</div>
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
YouTube'dan Video Oluştur
</h1>
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
YouTube videolarını veya Shorts içeriklerini kendi tarzınızda yeniden üretin
</p>
</div>
{/* Main Form */}
<div className="card p-6 md:p-8 space-y-6">
{/* Input */}
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block flex items-center gap-1.5">
<Link2 size={14} className="text-red-500" />
YouTube Video URL
</label>
<input
type="url"
value={youtubeUrl}
onChange={(e) => setYoutubeUrl(e.target.value)}
placeholder="https://youtube.com/watch?v=... veya https://youtu.be/..."
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl px-4 py-3.5 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-bg-inverted)]/50 transition-all placeholder:text-[var(--color-text-ghost)]"
/>
</div>
{/* Configurations */}
<div className="space-y-8 pt-6 border-t border-[var(--color-border-faint)]">
<LanguageSelector value={language} onChange={setLanguage} />
<StyleSelector
value={style}
onChange={setStyle}
cinematicReference={cinematicReference}
onCinematicReferenceChange={setCinematicReference}
/>
<DurationSelector value={duration} onChange={setDuration} />
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
</div>
</div>
{/* Action Button */}
<div className="flex justify-end">
<button
onClick={handleGenerate}
disabled={createFromYoutube.isPending || !isValidUrl}
className={cn(
"group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
"bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
"disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
)}
>
{createFromYoutube.isPending ? (
<>
<Loader2 size={20} className="animate-spin" />
<span>Yapay Zeka Videoyu İşliyor...</span>
</>
) : (
<>
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
<span>YouTube → Video Üret</span>
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</>
)}
</button>
</div>
{/* Info Box */}
<div className="card p-5 bg-[var(--color-bg-surface)]">
<div className="flex items-start gap-3">
<Sparkles size={20} className="text-[var(--color-text-secondary)] shrink-0 mt-0.5" />
<div className="space-y-2">
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
Nasıl Çalışır?
</h3>
<ol className="text-xs text-[var(--color-text-muted)] space-y-1.5 list-decimal list-inside">
<li>Uzun bir YouTube videosu veya Shorts URL'si yapıştırın</li>
<li>Video otomatik olarak indirilir ve deşifre edilir (transkript)</li>
<li>Belirttiğiniz süreye ve tarza göre yepyeni bir senaryo çıkarılır</li>
<li>Orijinal video kullanılmaz, referans olarak alınıp yeni görseller + ses üretilir</li>
</ol>
</div>
</div>
</div>
</div>
);
}
+1 -1
View File
@@ -251,7 +251,7 @@ body {
/* ── Page Transition ── */ /* ── Page Transition ── */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); } from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: none; }
} }
.page-enter { .page-enter {
animation: fadeIn var(--duration-slow) var(--ease-out-expo) forwards; animation: fadeIn var(--duration-slow) var(--ease-out-expo) forwards;
+6
View File
@@ -71,12 +71,18 @@ const handler = NextAuth({
token.refreshToken = user.refreshToken; token.refreshToken = user.refreshToken;
token.id = user.id; token.id = user.id;
token.roles = user.roles; token.roles = user.roles;
token.name = user.name;
token.email = user.email;
} }
return token; return token;
}, },
async session({ session, token }: any) { async session({ session, token }: any) {
if (session.user) {
session.user.id = token.id; session.user.id = token.id;
session.user.roles = token.roles; session.user.roles = token.roles;
session.user.name = token.name;
session.user.email = token.email;
}
session.accessToken = token.accessToken; session.accessToken = token.accessToken;
session.refreshToken = token.refreshToken; session.refreshToken = token.refreshToken;
return session; return session;
+3
View File
@@ -23,6 +23,7 @@ import { useState } from "react";
import { MdMail } from "react-icons/md"; import { MdMail } from "react-icons/md";
import { BiLock } from "react-icons/bi"; import { BiLock } from "react-icons/bi";
import { Link } from "@/i18n/navigation"; import { Link } from "@/i18n/navigation";
import { useQueryClient } from "@tanstack/react-query";
const schema = yup.object({ const schema = yup.object({
email: yup.string().email().required(), email: yup.string().email().required(),
@@ -39,6 +40,7 @@ interface LoginModalProps {
export function LoginModal({ open, onOpenChange }: LoginModalProps) { export function LoginModal({ open, onOpenChange }: LoginModalProps) {
const t = useTranslations(); const t = useTranslations();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const queryClient = useQueryClient();
const { const {
handleSubmit, handleSubmit,
@@ -62,6 +64,7 @@ export function LoginModal({ open, onOpenChange }: LoginModalProps) {
throw new Error(res.error); throw new Error(res.error);
} }
queryClient.clear();
onOpenChange(false); onOpenChange(false);
toaster.success({ toaster.success({
title: t("auth.login-success") || "Login successful!", title: t("auth.login-success") || "Login successful!",
+32 -23
View File
@@ -14,7 +14,7 @@ import {
} from "recharts"; } from "recharts";
import { useDashboardStats } from "@/hooks/use-api"; import { useDashboardStats } from "@/hooks/use-api";
const COLORS = ["#ffffff", "#a3a3a3", "#525252", "#262626"]; const COLORS = ["#06b6d4", "#8b5cf6", "#3b82f6", "#6366f1"];
function formatWeekData(stats: Record<string, unknown> | undefined) { function formatWeekData(stats: Record<string, unknown> | undefined) {
if (!stats) { if (!stats) {
@@ -96,12 +96,12 @@ export function DashboardCharts() {
<AreaChart data={weekData}> <AreaChart data={weekData}>
<defs> <defs>
<linearGradient id="colorProjects" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorProjects" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ffffff" stopOpacity={0.2} /> <stop offset="5%" stopColor="#06b6d4" stopOpacity={0.3} />
<stop offset="95%" stopColor="#ffffff" stopOpacity={0} /> <stop offset="95%" stopColor="#06b6d4" stopOpacity={0} />
</linearGradient> </linearGradient>
<linearGradient id="colorVideos" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorVideos" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#737373" stopOpacity={0.2} /> <stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#737373" stopOpacity={0} /> <stop offset="95%" stopColor="#8b5cf6" stopOpacity={0} />
</linearGradient> </linearGradient>
</defs> </defs>
<XAxis <XAxis
@@ -123,16 +123,16 @@ export function DashboardCharts() {
<Area <Area
type="monotone" type="monotone"
dataKey="projects" dataKey="projects"
stroke="#ffffff" stroke="#06b6d4"
strokeWidth={2} strokeWidth={3}
fill="url(#colorProjects)" fill="url(#colorProjects)"
name="Projeler" name="Projeler"
/> />
<Area <Area
type="monotone" type="monotone"
dataKey="videos" dataKey="videos"
stroke="#737373" stroke="#8b5cf6"
strokeWidth={2} strokeWidth={3}
fill="url(#colorVideos)" fill="url(#colorVideos)"
name="Videolar" name="Videolar"
/> />
@@ -141,27 +141,29 @@ export function DashboardCharts() {
</div> </div>
{/* Proje Durumu */} {/* Proje Durumu */}
<div className="card-surface p-6 md:p-8"> <div className="card-surface p-6 md:p-8 flex flex-col">
<h3 className="text-base font-semibold text-[var(--color-text-secondary)] mb-6"> <h3 className="text-base font-semibold text-[var(--color-text-secondary)] mb-2">
Proje Durumu Proje Durumu
</h3> </h3>
{pieData.length === 0 ? ( {pieData.length === 0 ? (
<div className="flex items-center justify-center h-[220px] text-sm text-[var(--color-text-ghost)]"> <div className="flex flex-1 items-center justify-center text-sm text-[var(--color-text-ghost)]">
Henüz proje verisi yok Henüz proje verisi yok
</div> </div>
) : ( ) : (
<div className="flex items-center gap-6"> <div className="flex flex-1 flex-row items-center justify-center gap-4 sm:gap-8 min-h-[220px]">
<ResponsiveContainer width="50%" height={220}> <div className="w-[160px] h-[160px] sm:w-[200px] sm:h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart> <PieChart>
<Pie <Pie
data={pieData} data={pieData}
cx="50%" cx="50%"
cy="50%" cy="50%"
outerRadius={80} outerRadius="85%"
innerRadius={50} innerRadius="65%"
dataKey="value" dataKey="value"
stroke="var(--color-bg-surface)" stroke="var(--color-bg-surface)"
strokeWidth={2} strokeWidth={3}
paddingAngle={4}
> >
{pieData.map((_: unknown, index: number) => ( {pieData.map((_: unknown, index: number) => (
<Cell key={index} fill={COLORS[index % COLORS.length]} /> <Cell key={index} fill={COLORS[index % COLORS.length]} />
@@ -174,24 +176,31 @@ export function DashboardCharts() {
borderRadius: 12, borderRadius: 12,
fontSize: 13, fontSize: 13,
color: "#fff", color: "#fff",
boxShadow: "0 4px 20px rgba(0,0,0,0.3)"
}}
itemStyle={{
color: "#e5e5e5"
}} }}
/> />
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
<div className="space-y-2"> </div>
<div className="flex flex-col justify-center space-y-3">
{pieData.map((item: { name: string; value: number }, idx: number) => ( {pieData.map((item: { name: string; value: number }, idx: number) => (
<div key={item.name} className="flex items-center gap-2"> <div key={item.name} className="flex items-center gap-3">
<div <div
className="w-2.5 h-2.5 rounded-full" className="w-3 h-3 rounded-full shadow-sm"
style={{ backgroundColor: COLORS[idx % COLORS.length] }} style={{ backgroundColor: COLORS[idx % COLORS.length], boxShadow: `0 0 8px ${COLORS[idx % COLORS.length]}80` }}
/> />
<span className="text-xs text-[var(--color-text-muted)]"> <div className="flex flex-col">
<span className="text-sm text-[var(--color-text-muted)] font-medium leading-none mb-1">
{item.name} {item.name}
</span> </span>
<span className="text-xs font-bold text-[var(--color-text-secondary)] ml-auto"> <span className="text-base font-bold text-[var(--color-text-primary)] leading-none">
{item.value} {item.value}
</span> </span>
</div> </div>
</div>
))} ))}
</div> </div>
</div> </div>
@@ -0,0 +1,171 @@
"use client";
import { useState, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Loader2,
CheckCircle2,
XCircle,
Link2,
Sparkles,
} from "lucide-react";
import { useCreateFromYoutube } from "@/hooks/use-api";
import { cn } from "@/lib/utils";
const YoutubeIcon = ({ size = 20, className = "" }: { size?: number; className?: string }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</svg>
);
interface YoutubeImportCardProps {
onProjectCreated?: (projectId: string) => void;
}
export function YoutubeImportCard({ onProjectCreated }: YoutubeImportCardProps) {
const [youtubeUrl, setYoutubeUrl] = useState("");
const [error, setError] = useState<string | null>(null);
const [createdProject, setCreatedProject] = useState<{ id: string; title: string } | null>(null);
const createFromYoutube = useCreateFromYoutube();
const isValidUrl = /https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/.+/.test(youtubeUrl);
const handleCreate = useCallback(async () => {
if (!youtubeUrl.trim() || !isValidUrl) return;
setError(null);
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const project: any = await createFromYoutube.mutateAsync({
youtubeUrl: youtubeUrl.trim(),
language: "tr",
aspectRatio: "PORTRAIT_9_16",
videoStyle: "CINEMATIC",
targetDuration: 60,
});
setCreatedProject({ id: project.id, title: project.title || "YouTube Projesi" });
if (onProjectCreated) {
onProjectCreated(project.id);
}
} catch (err: any) {
setError(err?.response?.data?.message || "YouTube videosu işlenirken bir hata oluştu.");
}
}, [youtubeUrl, isValidUrl, createFromYoutube, onProjectCreated]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleCreate();
}
};
return (
<div className="card-surface overflow-hidden">
{/* Header */}
<div className="flex items-center gap-3 p-4 md:p-5 border-b border-[var(--color-border-faint)]">
<div className="w-9 h-9 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] flex items-center justify-center">
<YoutubeIcon size={18} className="text-red-500" />
</div>
<div>
<h3 className="font-[family-name:var(--font-display)] text-sm font-semibold">
YouTube Import
</h3>
<p className="text-[11px] text-[var(--color-text-ghost)]">
YouTube Video pipeline
</p>
</div>
</div>
{/* URL Input */}
<div className="p-4 md:p-5 space-y-4">
<div className="relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]">
<Link2 size={16} />
</div>
<input
type="url"
value={youtubeUrl}
onChange={(e) => {
setYoutubeUrl(e.target.value);
setError(null);
}}
onKeyDown={handleKeyDown}
placeholder="https://youtube.com/watch?v=... veya youtu.be/..."
className="w-full h-11 pl-10 pr-28 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:border-red-500/50 focus:ring-1 focus:ring-red-500/25 outline-none transition-all"
/>
<button
onClick={handleCreate}
disabled={!isValidUrl || createFromYoutube.isPending}
className={cn(
"absolute right-1.5 top-1/2 -translate-y-1/2 h-8 px-3 rounded-lg text-xs font-medium transition-all flex items-center gap-1.5",
isValidUrl && !createFromYoutube.isPending
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] hover:bg-neutral-800 shadow-sm"
: "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] cursor-not-allowed"
)}
>
{createFromYoutube.isPending ? (
<>
<Loader2 size={13} className="animate-spin" />
Üretiliyor
</>
) : (
<>
<Sparkles size={13} />
Oluştur
</>
)}
</button>
</div>
{/* Error */}
<AnimatePresence mode="wait">
{error && (
<motion.div
initial={{ opacity: 0, y: -8, height: 0 }}
animate={{ opacity: 1, y: 0, height: "auto" }}
exit={{ opacity: 0, y: -8, height: 0 }}
className="flex items-center gap-2 text-xs text-rose-400 bg-rose-500/10 border border-rose-500/20 rounded-lg px-3 py-2 mt-2"
>
<XCircle size={14} />
{error}
</motion.div>
)}
</AnimatePresence>
{/* Success */}
<AnimatePresence>
{createdProject && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="flex items-center gap-3 p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/25 mt-2"
>
<CheckCircle2 size={18} className="text-emerald-400 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-emerald-300">
Proje oluşturuldu!
</p>
<p className="text-[11px] text-emerald-400/70 truncate">
{createdProject.title}
</p>
</div>
<button
onClick={() => {
if (onProjectCreated) onProjectCreated(createdProject.id);
}}
className="text-[11px] text-emerald-400 hover:text-emerald-300 font-medium transition-colors whitespace-nowrap"
>
Görüntüle
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}
+3 -5
View File
@@ -2,7 +2,7 @@
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { Home, FolderOpen, LayoutGrid, Settings, Sparkles, AtSign, ShieldCheck, LogOut, Link2, FileText, Activity } from "lucide-react"; import { Home, FolderOpen, LayoutGrid, Settings, Sparkles, AtSign, ShieldCheck, LogOut, Link2, FileText, Activity, Wrench } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useCreditBalance, useCurrentUser } from "@/hooks/use-api"; import { useCreditBalance, useCurrentUser } from "@/hooks/use-api";
@@ -11,13 +11,11 @@ import { signOut } from "next-auth/react";
const navItems = [ const navItems = [
{ href: "/dashboard", icon: Home, label: "Ana Sayfa" }, { href: "/dashboard", icon: Home, label: "Ana Sayfa" },
{ href: "/dashboard/create-project", icon: Sparkles, label: "Yeni Proje Üret" },
{ href: "/dashboard/projects", icon: FolderOpen, label: "Projeler" }, { href: "/dashboard/projects", icon: FolderOpen, label: "Projeler" },
{ href: "/dashboard/render-queue", icon: Activity, label: "Render Monitör" }, { href: "/dashboard/render-queue", icon: Activity, label: "Render Monitör" },
{ href: "/dashboard/text-to-video", icon: FileText, label: "Metin → Video" },
{ href: "/dashboard/x-to-video", icon: AtSign, label: "X → Video" },
{ href: "/dashboard/youtube-to-video", icon: Link2, label: "YT → Video" },
{ href: "/dashboard/document-to-video", icon: FileText, label: "Belge → Video" },
{ href: "/dashboard/templates", icon: LayoutGrid, label: "Şablonlar" }, { href: "/dashboard/templates", icon: LayoutGrid, label: "Şablonlar" },
{ href: "/dashboard/tools", icon: Wrench, label: "Araçlar" },
{ href: "/dashboard/settings", icon: Settings, label: "Ayarlar" }, { href: "/dashboard/settings", icon: Settings, label: "Ayarlar" },
]; ];
+12 -2
View File
@@ -38,6 +38,7 @@ import { signOut, useSession } from "next-auth/react";
import { authConfig } from "@/config/auth"; import { authConfig } from "@/config/auth";
import { LoginModal } from "@/components/auth/login-modal"; import { LoginModal } from "@/components/auth/login-modal";
import { LuLogIn } from "react-icons/lu"; import { LuLogIn } from "react-icons/lu";
import { useQueryClient } from "@tanstack/react-query";
export default function Header() { export default function Header() {
const t = useTranslations(); const t = useTranslations();
@@ -45,6 +46,7 @@ export default function Header() {
const [loginModalOpen, setLoginModalOpen] = useState(false); const [loginModalOpen, setLoginModalOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const queryClient = useQueryClient();
const isAuthenticated = !!session; const isAuthenticated = !!session;
const isLoading = status === "loading"; const isLoading = status === "loading";
@@ -61,6 +63,7 @@ export default function Header() {
}, []); }, []);
const handleLogout = async () => { const handleLogout = async () => {
queryClient.clear();
await signOut({ redirect: false }); await signOut({ redirect: false });
if (authConfig.isAuthRequired) { if (authConfig.isAuthRequired) {
router.replace("/signin"); router.replace("/signin");
@@ -77,7 +80,10 @@ export default function Header() {
return ( return (
<MenuRoot positioning={{ placement: "bottom-start" }}> <MenuRoot positioning={{ placement: "bottom-start" }}>
<MenuTrigger rounded="full" focusRing="none"> <MenuTrigger rounded="full" focusRing="none">
<Avatar name={session?.user?.name || "User"} variant="solid" /> <Avatar
name={session?.user?.name || (session?.user?.email ? session.user.email.split('@')[0] : "Kullanıcı")}
variant="solid"
/>
</MenuTrigger> </MenuTrigger>
<MenuContent> <MenuContent>
<MenuItem onClick={handleLogout} value="sign-out"> <MenuItem onClick={handleLogout} value="sign-out">
@@ -109,9 +115,13 @@ export default function Header() {
} }
if (isAuthenticated) { if (isAuthenticated) {
const displayInitial = session?.user?.name
? session.user.name
: (session?.user?.email ? session.user.email.split('@')[0] : "Kullanıcı");
return ( return (
<> <>
<Avatar name={session?.user?.name || "User"} variant="solid" /> <Avatar name={displayInitial} variant="solid" />
<Button <Button
variant="surface" variant="surface"
size="sm" size="sm"
+67 -50
View File
@@ -74,23 +74,23 @@ export function SceneCard({
transition={{ delay: scene.order * 0.05, duration: 0.4 }} transition={{ delay: scene.order * 0.05, duration: 0.4 }}
className="relative group" className="relative group"
> >
<div className="card-surface p-4 md:p-5 hover:border-neutral-500/20 transition-all duration-300"> <div className="card-surface p-5 md:p-6 hover:border-neutral-500/30 transition-all duration-300 shadow-sm">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-[var(--color-bg-elevated)] flex items-center justify-center border border-[var(--color-border-faint)]"> <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500/20 to-cyan-500/20 flex items-center justify-center border border-white/10 shadow-sm">
<span className="text-xs font-bold text-neutral-400">{scene.order}</span> <span className="text-sm font-bold text-[var(--color-text-primary)]">{scene.order}</span>
</div> </div>
<div> <div>
<h4 className="text-sm font-semibold text-[var(--color-text-primary)]"> <h4 className="text-base font-bold text-[var(--color-text-primary)]">
{scene.title || `Sahne ${scene.order}`} {scene.title || `Sahne ${scene.order}`}
</h4> </h4>
<div className="flex items-center gap-2 mt-0.5"> <div className="flex items-center gap-3 mt-1">
<span className="flex items-center gap-1 text-[10px] text-[var(--color-text-ghost)]"> <span className="flex items-center gap-1 text-xs font-medium text-[var(--color-text-muted)] bg-[var(--color-bg-elevated)] px-2 py-0.5 rounded-md">
<Clock size={10} /> {scene.duration}s <Clock size={12} className="text-violet-400" /> {scene.duration}s
</span> </span>
<span className="flex items-center gap-1 text-[10px] text-[var(--color-text-ghost)]"> <span className="flex items-center gap-1 text-xs font-medium text-[var(--color-text-muted)] bg-[var(--color-bg-elevated)] px-2 py-0.5 rounded-md">
<ArrowRight size={10} /> {scene.transitionType.toLowerCase()} <ArrowRight size={12} className="text-cyan-400" /> {scene.transitionType.toLowerCase()}
</span> </span>
</div> </div>
</div> </div>
@@ -98,21 +98,21 @@ export function SceneCard({
{/* Aksiyon butonları */} {/* Aksiyon butonları */}
{!isEditing && ( {!isEditing && (
<div className="flex items-center gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity"> <div className="flex items-center gap-1.5 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
<button <button
onClick={() => setIsEditing(true)} onClick={() => setIsEditing(true)}
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800 transition-colors" className="w-8 h-8 rounded-xl flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-elevated)] transition-all"
title="Düzenle" title="Düzenle"
> >
<Pencil size={13} /> <Pencil size={14} />
</button> </button>
<button <button
onClick={() => onRegenerate?.(scene.id)} onClick={() => onRegenerate?.(scene.id)}
disabled={!isEditable || isRendering || isRegenerating} disabled={!isEditable || isRendering || isRegenerating}
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed" className="w-8 h-8 rounded-xl flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-elevated)] transition-all disabled:opacity-40 disabled:cursor-not-allowed"
title="AI ile yeniden üret" title="AI ile yeniden üret"
> >
<RefreshCw size={13} className={isRegenerating ? 'animate-spin' : ''} /> <RefreshCw size={14} className={isRegenerating ? 'animate-spin' : ''} />
</button> </button>
</div> </div>
)} )}
@@ -125,76 +125,92 @@ export function SceneCard({
initial={{ opacity: 0, height: 0 }} initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }} animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }} exit={{ opacity: 0, height: 0 }}
className="space-y-3" className="space-y-4"
> >
{/* Narrasyon düzenleme */} {/* Narrasyon düzenleme */}
<div> <div>
<label className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-muted)] mb-1.5"> <label className="flex items-center gap-1.5 text-sm font-medium text-violet-400 mb-2">
<Mic size={12} /> Narrasyon <Mic size={14} /> Narrasyon
</label> </label>
<textarea <textarea
value={editNarration} value={editNarration}
onChange={(e) => setEditNarration(e.target.value)} onChange={(e) => setEditNarration(e.target.value)}
rows={3} rows={3}
className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] resize-none focus:outline-none focus:ring-1 focus:ring-neutral-500/40 transition-all" className="w-full px-4 py-3 rounded-xl bg-[var(--color-bg-deep)] border border-violet-500/30 text-base font-medium text-[var(--color-text-primary)] resize-none focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/60 transition-all shadow-inner"
/> />
</div> </div>
{/* Görsel prompt düzenleme */} {/* Görsel prompt düzenleme */}
<div> <div>
<label className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-muted)] mb-1.5"> <label className="flex items-center gap-1.5 text-sm font-medium text-cyan-400 mb-2">
<ImageIcon size={12} /> Görsel Prompt <ImageIcon size={14} /> Görsel Prompt
</label> </label>
<textarea <textarea
value={editVisual} value={editVisual}
onChange={(e) => setEditVisual(e.target.value)} onChange={(e) => setEditVisual(e.target.value)}
rows={2} rows={3}
className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-secondary)] resize-none focus:outline-none focus:ring-1 focus:ring-neutral-500/40 transition-all" className="w-full px-4 py-3 rounded-xl bg-[var(--color-bg-deep)] border border-cyan-500/30 text-sm font-medium text-cyan-50 resize-none focus:outline-none focus:border-cyan-500/60 focus:ring-1 focus:ring-cyan-500/60 transition-all shadow-inner"
/> />
</div> </div>
{/* Kaydet/İptal */} {/* Kaydet/İptal */}
<div className="flex items-center gap-2 pt-1"> <div className="flex items-center gap-3 pt-2">
<button <button
onClick={handleSave} onClick={handleSave}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] text-xs font-medium hover:bg-neutral-800 transition-colors" className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-gradient-to-r from-violet-600 to-cyan-500 text-white text-sm font-medium hover:shadow-[0_0_15px_rgba(34,211,238,0.4)] hover:scale-[1.02] transition-all"
> >
<Check size={13} /> Kaydet <Check size={14} /> Kaydet
</button> </button>
<button <button
onClick={handleCancel} onClick={handleCancel}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[var(--color-bg-elevated)] text-[var(--color-text-muted)] text-xs font-medium hover:text-[var(--color-text-secondary)] transition-colors" className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-[var(--color-bg-elevated)] text-[var(--color-text-muted)] text-sm font-medium hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-all"
> >
<X size={13} /> İptal <X size={14} /> İptal
</button> </button>
</div> </div>
</motion.div> </motion.div>
) : ( ) : (
<motion.div key="viewing" className="space-y-2.5"> <motion.div key="viewing" className="space-y-4">
{/* Narrasyon */} {/* Narrasyon */}
<div className="flex gap-2"> <div className="flex gap-3">
<div className="w-5 h-5 rounded-md bg-[var(--color-bg-elevated)] flex items-center justify-center shrink-0 mt-0.5 border border-[var(--color-border-faint)]"> <div className="w-6 h-6 rounded-md bg-violet-500/10 flex items-center justify-center shrink-0 mt-1 border border-violet-500/20">
<Mic size={11} className="text-neutral-400" /> <Mic size={14} className="text-violet-400" />
</div> </div>
<p className="text-sm text-[var(--color-text-secondary)] leading-relaxed"> <div className="flex-1 group/narration relative bg-violet-900/10 p-3.5 md:p-5 rounded-xl border border-violet-500/20 hover:border-violet-500/40 transition-colors">
<p className="text-lg md:text-xl font-[family-name:var(--font-display)] text-[var(--color-text-primary)] font-medium leading-relaxed tracking-wide pr-8">
{scene.narrationText} {scene.narrationText}
</p> </p>
<button
onClick={() => {
navigator.clipboard.writeText(scene.narrationText);
}}
className="absolute top-2 right-2 p-1.5 opacity-0 group-hover/narration:opacity-100 transition-opacity bg-violet-500/20 rounded-md text-violet-300 hover:text-violet-100 hover:bg-violet-500/40"
title="Metni Kopyala"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
</button>
</div>
</div> </div>
{/* Görsel Prompt */} {/* Görsel Prompt ve Görsel Alanı */}
<div className="flex gap-2"> <div className="flex flex-col md:flex-row gap-4 pt-2">
<div className="w-5 h-5 rounded-md bg-[var(--color-bg-elevated)] flex items-center justify-center shrink-0 mt-0.5 border border-[var(--color-border-faint)]"> {/* Sol: Prompt */}
<ImageIcon size={11} className="text-neutral-400" /> <div className="flex gap-3 flex-1">
<div className="w-6 h-6 rounded-md bg-cyan-500/10 flex items-center justify-center shrink-0 mt-1 border border-cyan-500/20">
<ImageIcon size={14} className="text-cyan-400" />
</div> </div>
<div className="flex-1 group/prompt relative"> <div className="flex-1 group/prompt relative bg-cyan-900/10 p-3.5 rounded-xl border border-cyan-500/20 hover:border-cyan-500/40 transition-colors">
<p className="text-xs text-[var(--color-text-ghost)] leading-relaxed italic pr-6"> <p className="text-sm font-medium text-cyan-50 leading-relaxed pr-8">
{scene.visualPrompt} {scene.visualPrompt}
</p> </p>
<button <button
onClick={() => { onClick={() => {
navigator.clipboard.writeText(scene.visualPrompt); navigator.clipboard.writeText(scene.visualPrompt);
}} }}
className="absolute top-0 right-0 p-1 opacity-0 group-hover/prompt:opacity-100 transition-opacity bg-[var(--color-bg-elevated)] rounded-md text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800" className="absolute top-2 right-2 p-1.5 opacity-0 group-hover/prompt:opacity-100 transition-opacity bg-cyan-500/20 rounded-md text-cyan-300 hover:text-cyan-100 hover:bg-cyan-500/40"
title="Prompt'u Kopyala" title="Prompt'u Kopyala"
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
@@ -205,10 +221,10 @@ export function SceneCard({
</div> </div>
</div> </div>
{/* Görsel / Upscale Alanı */} {/* Sağ: Görsel Önizleme ve Butonlar */}
<div className="flex flex-col gap-2 pt-2"> <div className="flex flex-col gap-2 w-full md:w-64 shrink-0">
{thumbnailAsset?.url && !isGeneratingImage ? ( {thumbnailAsset?.url && !isGeneratingImage ? (
<div className="relative group/thumb rounded-lg overflow-hidden border border-[var(--color-border-faint)] aspect-video max-w-sm"> <div className="relative group/thumb rounded-lg overflow-hidden border border-[var(--color-border-faint)] aspect-video w-full">
<img <img
src={thumbnailAsset.url} src={thumbnailAsset.url}
alt="Scene Thumbnail" alt="Scene Thumbnail"
@@ -220,7 +236,7 @@ export function SceneCard({
</div> </div>
</div> </div>
) : ( ) : (
<div className="rounded-lg border border-dashed border-[var(--color-border-faint)] bg-[var(--color-bg-deep)] aspect-video max-w-sm flex flex-col items-center justify-center p-4 relative overflow-hidden"> <div className="rounded-lg border border-dashed border-[var(--color-border-faint)] bg-[var(--color-bg-deep)] aspect-video w-full flex flex-col items-center justify-center p-4 relative overflow-hidden">
{isGeneratingImage ? ( {isGeneratingImage ? (
<div className="flex flex-col items-center justify-center animate-in fade-in zoom-in duration-300"> <div className="flex flex-col items-center justify-center animate-in fade-in zoom-in duration-300">
<div className="relative w-12 h-12 mb-3"> <div className="relative w-12 h-12 mb-3">
@@ -241,21 +257,21 @@ export function SceneCard({
</div> </div>
)} )}
{/* Görsel üretim butonları — tüm projelerde her zaman göster, render sürecinde disable et */} {/* Görsel üretim butonları */}
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1 flex-wrap">
<button <button
onClick={() => onGenerateImage?.(scene.id, scene.visualPrompt)} onClick={() => onGenerateImage?.(scene.id, scene.visualPrompt)}
disabled={isGeneratingImage || isUpscalingImage} disabled={isGeneratingImage || isUpscalingImage}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-500/15 text-emerald-400 text-xs font-medium hover:bg-emerald-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" className="flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-500/15 text-emerald-400 text-xs font-medium hover:bg-emerald-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
> >
{isGeneratingImage ? <RefreshCw size={13} className="animate-spin" /> : <ImageIcon size={13} />} {isGeneratingImage ? <RefreshCw size={13} className="animate-spin" /> : <ImageIcon size={13} />}
{thumbnailAsset ? "Görseli Yeniden Üret" : "Görsel Üret"} {thumbnailAsset ? "Yeniden Üret" : "Görsel Üret"}
</button> </button>
{thumbnailAsset?.url && ( {thumbnailAsset?.url && (
<button <button
onClick={() => onUpscaleImage?.(scene.id)} onClick={() => onUpscaleImage?.(scene.id)}
disabled={isUpscalingImage || isGeneratingImage} disabled={isUpscalingImage || isGeneratingImage}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-orange-500/15 text-orange-400 text-xs font-medium hover:bg-orange-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" className="flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg bg-orange-500/15 text-orange-400 text-xs font-medium hover:bg-orange-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
> >
{isUpscalingImage ? <RefreshCw size={13} className="animate-spin" /> : <Wand2 size={13} />} {isUpscalingImage ? <RefreshCw size={13} className="animate-spin" /> : <Wand2 size={13} />}
Upscale (4K) Upscale (4K)
@@ -263,6 +279,7 @@ export function SceneCard({
)} )}
</div> </div>
</div> </div>
</div>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
@@ -25,6 +25,18 @@ export const languages = [
{ code: "ar", label: "العربية", flag: "🇸🇦" }, { code: "ar", label: "العربية", flag: "🇸🇦" },
{ code: "pt", label: "Português", flag: "🇧🇷" }, { code: "pt", label: "Português", flag: "🇧🇷" },
{ code: "ja", label: "日本語", flag: "🇯🇵" }, { code: "ja", label: "日本語", flag: "🇯🇵" },
{ code: "hi", label: "हिन्दी", flag: "🇮🇳" },
{ code: "ru", label: "Русский", flag: "🇷🇺" },
{ code: "ko", label: "한국어", flag: "🇰🇷" },
{ code: "it", label: "Italiano", flag: "🇮🇹" },
{ code: "id", label: "Bahasa Indonesia", flag: "🇮🇩" },
{ code: "vi", label: "Tiếng Việt", flag: "🇻🇳" },
{ code: "zh", label: "简体中文", flag: "🇨🇳" },
{ code: "pl", label: "Polski", flag: "🇵🇱" },
{ code: "th", label: "ภาษาไทย", flag: "🇹🇭" },
{ code: "nl", label: "Nederlands", flag: "🇳🇱" },
{ code: "tl", label: "Tagalog", flag: "🇵🇭" },
{ code: "uk", label: "Українська", flag: "🇺🇦" },
]; ];
export const videoStyles = [ export const videoStyles = [
@@ -292,25 +304,26 @@ export function DurationSelector({
<input <input
type="range" type="range"
min={15} min={15}
max={180} max={900}
step={5} step={5}
value={value} value={value}
onChange={(e) => onChange(Number(e.target.value))} onChange={(e) => onChange(Number(e.target.value))}
className={cn( className={cn(
"w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer", "w-full h-2 rounded-full bg-neutral-200 dark:bg-neutral-700 border border-neutral-300 dark:border-neutral-600 appearance-none cursor-pointer outline-none",
"[&::-webkit-slider-thumb]:appearance-none", "[&::-webkit-slider-thumb]:appearance-none",
"[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5", "[&::-webkit-slider-thumb]:w-6 [&::-webkit-slider-thumb]:h-6",
"[&::-webkit-slider-thumb]:rounded-full", "[&::-webkit-slider-thumb]:rounded-full",
"[&::-webkit-slider-thumb]:bg-[var(--color-bg-inverted)]", "[&::-webkit-slider-thumb]:bg-cyan-400",
"[&::-webkit-slider-thumb]:shadow-md", "[&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white",
"[&::-webkit-slider-thumb]:shadow-[0_0_10px_rgba(34,211,238,0.6)]",
"[&::-webkit-slider-thumb]:cursor-grab" "[&::-webkit-slider-thumb]:cursor-grab"
)} )}
/> />
<div className="flex justify-between text-[10px] text-[var(--color-text-ghost)] mt-1"> <div className="flex justify-between text-[10px] text-[var(--color-text-ghost)] mt-2">
<span>15s</span> <span>15s</span>
<span>60s</span> <span>60s</span>
<span>120s</span> <span>120s</span>
<span>180s</span> <span>900s</span>
</div> </div>
</div> </div>
); );
@@ -336,19 +349,19 @@ export function AspectRatioSelector({
key={ar.id} key={ar.id}
onClick={() => onChange(ar.id)} onClick={() => onChange(ar.id)}
className={cn( className={cn(
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all", "flex-1 flex flex-col items-center gap-1.5 py-4 rounded-xl text-xs transition-all duration-300 relative overflow-hidden",
value === ar.id value === ar.id
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]" ? "bg-gradient-to-b from-cyan-500/10 to-transparent border border-cyan-400/50 shadow-[0_0_20px_rgba(34,211,238,0.15)] text-cyan-400"
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-[var(--color-border-default)]" : "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-[var(--color-border-default)]"
)} )}
> >
<Icon size={20} /> <Icon size={24} className={value === ar.id ? "drop-shadow-[0_0_8px_rgba(34,211,238,0.5)]" : ""} />
<span className="font-semibold">{ar.label}</span> <span className="font-semibold text-[13px]">{ar.label}</span>
<span <span
className={cn( className={cn(
"text-[10px]", "text-[10px]",
value === ar.id value === ar.id
? "text-[var(--color-text-inverted)]/70" ? "text-cyan-400/70"
: "text-[var(--color-text-ghost)]" : "text-[var(--color-text-ghost)]"
)} )}
> >
+46 -3
View File
@@ -160,9 +160,10 @@ export function useGenerateScript() {
export function useApproveAndQueue() { export function useApproveAndQueue() {
const qc = useQueryClient(); const qc = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (projectId: string) => projectsApi.approveAndQueue(projectId), mutationFn: ({ projectId, data }: { projectId: string; data?: { ttsProvider?: string; visualEffect?: string } }) =>
onSuccess: (_data, projectId) => { projectsApi.approveAndQueue(projectId, data),
qc.invalidateQueries({ queryKey: queryKeys.projects.detail(projectId) }); onSuccess: (_data, variables) => {
qc.invalidateQueries({ queryKey: queryKeys.projects.detail(variables.projectId) });
qc.invalidateQueries({ queryKey: queryKeys.projects.all }); qc.invalidateQueries({ queryKey: queryKeys.projects.all });
qc.invalidateQueries({ queryKey: queryKeys.credits.balance }); qc.invalidateQueries({ queryKey: queryKeys.credits.balance });
qc.invalidateQueries({ queryKey: queryKeys.dashboard.stats }); qc.invalidateQueries({ queryKey: queryKeys.dashboard.stats });
@@ -182,6 +183,48 @@ 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) });
},
});
}
/** AI ile tüm SEO ve Sosyal Medya içeriklerini (caption, description vs) üret */
export function useGenerateSocialContent() {
const qc = useQueryClient();
return useMutation({
mutationFn: (projectId: string) =>
projectsApi.generateSocialContent(projectId),
onSuccess: (_data, projectId) => {
qc.invalidateQueries({ queryKey: queryKeys.projects.detail(projectId) });
toast.success({ title: 'SEO ve Sosyal Medya içerikleri başarıyla üretildi' });
},
onError: (error: any) => {
console.error('İçerik üretme hatası:', error);
toast.error({ title: 'Hata', description: error.response?.data?.message || 'İçerikler üretilemedi' });
},
});
}
/** 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) */ /** Sahne güncelleme (narrasyon, prompt) */
export function useUpdateScene() { export function useUpdateScene() {
const qc = useQueryClient(); const qc = useQueryClient();
+53 -2
View File
@@ -30,6 +30,19 @@ export interface Project {
renderJobs?: RenderJob[]; renderJobs?: RenderJob[];
sourceType?: 'MANUAL' | 'X_TWEET' | 'YOUTUBE'; sourceType?: 'MANUAL' | 'X_TWEET' | 'YOUTUBE';
sourceTweetData?: Record<string, unknown>; sourceTweetData?: Record<string, unknown>;
// 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; createdAt: string;
updatedAt: string; updatedAt: string;
completedAt?: string; completedAt?: string;
@@ -99,13 +112,17 @@ export interface ScriptJson {
language: string; language: string;
hashtags: string[]; hashtags: string[];
}; };
seo?: { seo: {
title: string; title: string;
description: string; description: string;
keywords: string[]; keywords: string[];
hashtags: string[]; hashtags: string[];
trendingHashtags?: string[];
estimatedSearchVolume?: string;
schemaMarkup: Record<string, unknown>; schemaMarkup: Record<string, unknown>;
}; };
seoTitleAlternatives?: string[];
seoScore?: number;
scenes: Array<{ scenes: Array<{
order: number; order: number;
title?: string; title?: string;
@@ -353,9 +370,10 @@ export const projectsApi = {
generateScript: (id: string) => generateScript: (id: string) =>
apiClient.post<Project>(`/projects/${id}/generate-script`).then((r) => r.data), apiClient.post<Project>(`/projects/${id}/generate-script`).then((r) => r.data),
approveAndQueue: (id: string) => approveAndQueue: (id: string, data?: { ttsProvider?: string; visualEffect?: string }) =>
apiClient.post<{ projectId: string; renderJobId: string; bullJobId: string }>( apiClient.post<{ projectId: string; renderJobId: string; bullJobId: string }>(
`/projects/${id}/approve`, `/projects/${id}/approve`,
data || {}
).then((r) => r.data), ).then((r) => r.data),
cancelRender: (id: string) => cancelRender: (id: string) =>
@@ -363,6 +381,20 @@ export const projectsApi = {
`/projects/${id}/cancel-render`, `/projects/${id}/cancel-render`,
).then((r) => r.data), ).then((r) => r.data),
generateSeoTitles: (id: string) =>
apiClient.post<{ titles: string[]; seoScore: number; currentTitle: string }>(
`/projects/${id}/generate-seo-titles`,
).then((r) => r.data),
generateSocialContent: (id: string) =>
apiClient.post<any>(`/projects/${id}/generate-social-content`).then((r) => r.data),
selectSeoTitle: (id: string, title: string) =>
apiClient.patch<Project>(
`/projects/${id}/select-title`,
{ title },
).then((r) => r.data),
getRenderQueue: () => getRenderQueue: () =>
apiClient.get<any>('/projects/render-queue').then((r) => r.data), apiClient.get<any>('/projects/render-queue').then((r) => r.data),
@@ -417,6 +449,25 @@ export const projectsApi = {
apiClient.post<Scene>(`/projects/${projectId}/scenes/${sceneId}/upscale-image`).then((r) => r.data), apiClient.post<Scene>(`/projects/${projectId}/scenes/${sceneId}/upscale-image`).then((r) => r.data),
}; };
export const toolsApi = {
analyzeYoutubeVideo: (url: string) =>
apiClient.post<any>('/youtube-tools/analyze', { url }).then((r) => r.data),
getYoutubeAnalysisHistory: () =>
apiClient.get<any[]>('/youtube-tools/history').then((r) => r.data),
getYoutubeAnalysisById: (id: string) =>
apiClient.get<any>(`/youtube-tools/analyze/${id}`).then((r) => r.data),
// SEO
analyzeYoutubeSEO: (url: string) =>
apiClient.post<any>('/youtube-tools/seo/analyze', { url }).then((r) => r.data),
getYoutubeSeoHistory: () =>
apiClient.get<any[]>('/youtube-tools/seo/history').then((r) => r.data),
getYoutubeSeoAnalysisById: (id: string) =>
apiClient.get<any>(`/youtube-tools/seo/analyze/${id}`).then((r) => r.data),
generateYoutubeSeoImage: (prompt: string) =>
apiClient.post<{ url: string }>('/youtube-tools/seo/generate-image', { prompt }).then((r) => r.data),
};
// Backend path: /billing/credits/balance (billing controller prefix) // Backend path: /billing/credits/balance (billing controller prefix)
export const creditsApi = { export const creditsApi = {
getBalance: () => getBalance: () =>
+15 -3
View File
@@ -55,15 +55,27 @@ export function createApiClient(baseURL: string): AxiosInstance {
client.interceptors.response.use( client.interceptors.response.use(
(response) => { (response) => {
// Backend ResponseInterceptor tüm yanıtları { success, status, message, data } ile sarıyor. // Backend ResponseInterceptor tüm yanıtları { success, status, message, data } ile sarıyor.
// Frontend'in her yerde .data.data yazmasına gerek kalmadan otomatik unwrap yapıyoruz. // GlobalExceptionFilter hataları da HTTP 200 ile dönüyor, bu yüzden success bayrağını kontrol etmeliyiz.
if ( if (
response.data && response.data &&
typeof response.data === 'object' && typeof response.data === 'object' &&
'success' in response.data && 'success' in response.data
'data' in response.data
) { ) {
if (response.data.success === false) {
if (response.data.status === 401) {
const isAuthPath =
typeof window !== 'undefined' &&
(window.location.pathname.includes('/api/auth') || window.location.pathname === '/');
if (!isAuthPath && typeof window !== 'undefined') {
signOut({ redirect: true, callbackUrl: '/' });
}
}
return Promise.reject(new Error(response.data.message || 'Bir hata oluştu'));
}
if ('data' in response.data) {
response.data = response.data.data; response.data = response.data.data;
} }
}
return response; return response;
}, },
async (error) => { async (error) => {
+89
View File
@@ -0,0 +1,89 @@
import { clientMap } from '@/lib/api/client-map';
export const voiceboxApi = {
getProfiles: async () => {
try {
const response = await clientMap.core.get('/voicebox/profiles');
return response.data;
} catch (error) {
console.error('Error fetching VoiceBox profiles:', error);
throw error;
}
},
generateSpeech: async (text: string, profileId: string, options: { language?: string, engine?: string, modelSize?: string, instruct?: string, seed?: number } = {}): Promise<Blob> => {
try {
const response = await clientMap.core.post(
'/voicebox/generate',
{ text, profileId, language: options.language || 'tr', engine: options.engine, modelSize: options.modelSize, instruct: options.instruct, seed: options.seed },
{ responseType: 'blob' }
);
const data = response.data;
// Eğer backend JSON döndüyse (hata durumu) ama responseType blob olduğu için blob olarak geldiyse
if (data instanceof Blob) {
// Content-Type kontrolü her zaman güvenilir olmayabilir (özellikle hata durumlarında interceptor'lar değiştirebilir).
// Eğer boyut çok küçükse (örneğin < 1000 byte) ve type json içeriyorsa veya hiç type yoksa kontrol et:
if (data.type.includes('application/json') || data.size < 2000) {
const textData = await data.text();
try {
const json = JSON.parse(textData);
if (!json.success) {
throw new Error(json.message || 'Ses üretilirken bir hata oluştu.');
}
} catch (e) {
// Eğer JSON parse hatası verdiyse, demek ki gerçekten küçük bir ses dosyası (ya da geçersiz json).
// Eğer orijinal hata fırlatıldıysa (Error instance'ı), onu yukarı taşı:
if (e instanceof Error && e.message !== 'Unexpected token' && !e.message.includes('JSON')) {
throw e;
}
}
}
}
return data;
} catch (error) {
console.error('Error generating VoiceBox speech:', error);
throw error;
}
},
getHistory: async () => {
try {
const response = await clientMap.core.get('/voicebox/history');
return response.data;
} catch (error) {
console.error('Error fetching VoiceBox history:', error);
throw error;
}
},
deleteHistory: async (id: string) => {
try {
const response = await clientMap.core.delete(`/voicebox/history/${id}`);
return response.data;
} catch (error) {
console.error(`Error deleting VoiceBox history for ${id}:`, error);
throw error;
}
},
getAudioUrl: (generationId: string) => {
// API endpoint for `<audio src="...">` tag
return `${process.env.NEXT_PUBLIC_CORE_API_URL || 'http://localhost:3000/api'}/voicebox/audio/${generationId}`;
},
speak: async (text: string, profile: string, personality: boolean = false) => {
try {
const response = await clientMap.core.post('/voicebox/speak', {
text,
profile,
personality,
});
return response.data;
} catch (error) {
console.error('Error triggering VoiceBox speak:', error);
throw error;
}
},
};
+1 -1
View File
File diff suppressed because one or more lines are too long