generated from fahricansecer/boilerplate-fe
Compare commits
13 Commits
89eb9d4dfd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ecf0612205 | |||
| fc5ceeebb6 | |||
| 4b1abf1996 | |||
| 1f8f24fcf5 | |||
| d3a83bf901 | |||
| ff76ead6d4 | |||
| 565a7ba3b9 | |||
| bc9a1587a8 | |||
| 7d161fdb3d | |||
| 51ec6bd0fd | |||
| 1b980f637b | |||
| 5144ee4d9a | |||
| 1b69eaf219 |
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -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",
|
||||||
|
|||||||
Generated
+28
@@ -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
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ const navLinks = [
|
|||||||
|
|
||||||
function colorClass(color: string, type: "bg" | "text" | "icon") {
|
function colorClass(color: string, type: "bg" | "text" | "icon") {
|
||||||
const map: Record<string, Record<string, string>> = {
|
const map: Record<string, Record<string, string>> = {
|
||||||
violet: { bg: "from-violet-500/12 to-violet-600/5", text: "text-violet-400", icon: "bg-violet-500/12" },
|
violet: { bg: "from-neutral-500/12 to-neutral-600/5", text: "text-neutral-400", icon: "bg-neutral-500/12" },
|
||||||
cyan: { bg: "from-cyan-500/12 to-cyan-600/5", text: "text-cyan-400", icon: "bg-cyan-500/12" },
|
cyan: { bg: "from-neutral-500/12 to-neutral-600/5", text: "text-neutral-400", icon: "bg-neutral-500/12" },
|
||||||
emerald: { bg: "from-emerald-500/12 to-emerald-600/5", text: "text-emerald-400", icon: "bg-emerald-500/12" },
|
emerald: { bg: "from-neutral-500/12 to-neutral-600/5", text: "text-neutral-400", icon: "bg-neutral-500/12" },
|
||||||
amber: { bg: "from-amber-500/12 to-amber-600/5", text: "text-amber-400", icon: "bg-amber-500/12" },
|
amber: { bg: "from-neutral-500/12 to-neutral-600/5", text: "text-neutral-400", icon: "bg-neutral-500/12" },
|
||||||
rose: { bg: "from-rose-500/12 to-rose-600/5", text: "text-rose-400", icon: "bg-rose-500/12" },
|
rose: { bg: "from-neutral-500/12 to-neutral-600/5", text: "text-neutral-400", icon: "bg-neutral-500/12" },
|
||||||
};
|
};
|
||||||
return map[color]?.[type] ?? "";
|
return map[color]?.[type] ?? "";
|
||||||
}
|
}
|
||||||
@@ -110,14 +110,14 @@ export default function AdminPage() {
|
|||||||
Sistem genelinde yönetim ve izleme
|
Sistem genelinde yönetim ve izleme
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="badge badge-violet text-xs px-3 py-1">Süper Yönetici</span>
|
<span className="bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] rounded-md text-xs px-3 py-1 font-medium">Süper Yönetici</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* İstatistik Kartları */}
|
{/* İstatistik Kartları */}
|
||||||
<motion.div variants={fadeUp} className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3">
|
<motion.div variants={fadeUp} className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="col-span-6 flex justify-center py-12">
|
<div className="col-span-6 flex justify-center py-12">
|
||||||
<Loader2 size={28} className="animate-spin text-violet-400" />
|
<Loader2 size={28} className="animate-spin text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
statCards.map((card) => {
|
statCards.map((card) => {
|
||||||
@@ -154,15 +154,10 @@ export default function AdminPage() {
|
|||||||
<Link
|
<Link
|
||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className="group card-surface p-5 flex items-center gap-4 hover:border-violet-500/30 transition-colors"
|
className="group card-surface p-5 flex items-center gap-4 hover:border-neutral-500/30 transition-colors"
|
||||||
>
|
>
|
||||||
<div className={`w-11 h-11 rounded-xl bg-gradient-to-br ${
|
<div className={`w-11 h-11 rounded-xl bg-[var(--color-bg-inverted)] flex items-center justify-center shadow-lg`}>
|
||||||
link.color === "violet" ? "from-violet-500 to-violet-700" :
|
<Icon size={20} className="text-[var(--color-text-inverted)]" />
|
||||||
link.color === "cyan" ? "from-cyan-500 to-cyan-700" :
|
|
||||||
link.color === "emerald" ? "from-emerald-500 to-emerald-700" :
|
|
||||||
"from-amber-500 to-amber-700"
|
|
||||||
} flex items-center justify-center shadow-lg`}>
|
|
||||||
<Icon size={20} className="text-white" />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold">{link.label}</h3>
|
<h3 className="text-sm font-semibold">{link.label}</h3>
|
||||||
@@ -178,7 +173,7 @@ export default function AdminPage() {
|
|||||||
<motion.div variants={fadeUp} className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<motion.div variants={fadeUp} className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
<div className="card-surface p-5">
|
<div className="card-surface p-5">
|
||||||
<h2 className="text-sm font-semibold mb-4 flex items-center gap-2">
|
<h2 className="text-sm font-semibold mb-4 flex items-center gap-2">
|
||||||
<TrendingUp size={14} className="text-violet-400" /> Proje Durumları
|
<TrendingUp size={14} className="text-neutral-400" /> Proje Durumları
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Object.entries(projectsByStatus as Record<string, number>).map(([status, count]) => (
|
{Object.entries(projectsByStatus as Record<string, number>).map(([status, count]) => (
|
||||||
@@ -192,14 +187,14 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
<div className="card-surface p-5">
|
<div className="card-surface p-5">
|
||||||
<h2 className="text-sm font-semibold mb-4 flex items-center gap-2">
|
<h2 className="text-sm font-semibold mb-4 flex items-center gap-2">
|
||||||
<Cpu size={14} className="text-emerald-400" /> Render Job Durumları
|
<Cpu size={14} className="text-neutral-400" /> Render Job Durumları
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{[
|
{[
|
||||||
{ key: "QUEUED", icon: Clock, color: "text-amber-400", label: "Beklemede" },
|
{ key: "QUEUED", icon: Clock, color: "text-neutral-400", label: "Beklemede" },
|
||||||
{ key: "PROCESSING", icon: Loader2, color: "text-cyan-400", label: "İşleniyor" },
|
{ key: "PROCESSING", icon: Loader2, color: "text-neutral-400", label: "İşleniyor" },
|
||||||
{ key: "COMPLETED", icon: CheckCircle2, color: "text-emerald-400", label: "Tamamlandı" },
|
{ key: "COMPLETED", icon: CheckCircle2, color: "text-neutral-400", label: "Tamamlandı" },
|
||||||
{ key: "FAILED", icon: XCircle, color: "text-rose-400", label: "Başarısız" },
|
{ key: "FAILED", icon: XCircle, color: "text-neutral-400", label: "Başarısız" },
|
||||||
].map(({ key, icon: Icon, color, label }) => (
|
].map(({ key, icon: Icon, color, label }) => (
|
||||||
<div key={key} className="flex items-center justify-between text-sm">
|
<div key={key} className="flex items-center justify-between text-sm">
|
||||||
<span className={`flex items-center gap-2 ${color}`}>
|
<span className={`flex items-center gap-2 ${color}`}>
|
||||||
@@ -219,9 +214,9 @@ export default function AdminPage() {
|
|||||||
<motion.div variants={fadeUp} className="card-surface p-5">
|
<motion.div variants={fadeUp} className="card-surface p-5">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-sm font-semibold flex items-center gap-2">
|
<h2 className="text-sm font-semibold flex items-center gap-2">
|
||||||
<Users size={14} className="text-violet-400" /> Son Kayıt Kullanıcılar
|
<Users size={14} className="text-neutral-400" /> Son Kayıt Kullanıcılar
|
||||||
</h2>
|
</h2>
|
||||||
<Link href="/dashboard/admin/users" className="text-xs text-violet-400 hover:underline">
|
<Link href="/dashboard/admin/users" className="text-xs text-neutral-400 hover:underline">
|
||||||
Tümünü Gör
|
Tümünü Gör
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,7 +231,7 @@ export default function AdminPage() {
|
|||||||
<p className="text-xs text-[var(--color-text-muted)]">{user.email}</p>
|
<p className="text-xs text-[var(--color-text-muted)]">{user.email}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`text-xs px-2 py-0.5 rounded-full ${user.isActive ? "bg-emerald-500/10 text-emerald-400" : "bg-rose-500/10 text-rose-400"}`}>
|
<span className={`text-xs px-2 py-0.5 rounded-full border ${user.isActive ? "bg-neutral-500/10 text-neutral-300 border-neutral-500/20" : "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] border-[var(--color-border-faint)]"}`}>
|
||||||
{user.isActive ? "Aktif" : "Pasif"}
|
{user.isActive ? "Aktif" : "Pasif"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-[var(--color-text-ghost)]">
|
<span className="text-xs text-[var(--color-text-ghost)]">
|
||||||
|
|||||||
@@ -66,9 +66,9 @@ export default function AdminPlansPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PLAN_COLORS: Record<string, string> = {
|
const PLAN_COLORS: Record<string, string> = {
|
||||||
free: "from-gray-500/10 to-gray-600/5 border-gray-500/20",
|
free: "from-neutral-500/10 to-neutral-600/5 border-neutral-500/20",
|
||||||
pro: "from-violet-500/10 to-violet-600/5 border-violet-500/20",
|
pro: "from-[var(--color-bg-inverted)] to-neutral-800 border-[var(--color-border-faint)] text-[var(--color-text-inverted)]",
|
||||||
business: "from-amber-500/10 to-amber-600/5 border-amber-500/20",
|
business: "from-neutral-500/20 to-neutral-600/10 border-neutral-500/30",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -81,7 +81,7 @@ export default function AdminPlansPage() {
|
|||||||
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold flex items-center gap-2">
|
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold flex items-center gap-2">
|
||||||
<LayoutGrid size={22} className="text-amber-400" /> Plan Yönetimi
|
<LayoutGrid size={22} className="text-neutral-400" /> Plan Yönetimi
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
||||||
Abonelik planlarını düzenle ve fiyatları güncelle
|
Abonelik planlarını düzenle ve fiyatları güncelle
|
||||||
@@ -94,7 +94,7 @@ export default function AdminPlansPage() {
|
|||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex justify-center py-20">
|
<div className="flex justify-center py-20">
|
||||||
<Loader2 size={28} className="animate-spin text-amber-400" />
|
<Loader2 size={28} className="animate-spin text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||||||
@@ -129,8 +129,8 @@ export default function AdminPlansPage() {
|
|||||||
{field.type === "toggle" ? (
|
{field.type === "toggle" ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => setEdit(plan.id, field.key, !val)}
|
onClick={() => setEdit(plan.id, field.key, !val)}
|
||||||
className={`flex items-center gap-2 text-sm px-3 py-1.5 rounded-xl ${
|
className={`flex items-center gap-2 text-sm px-3 py-1.5 rounded-xl border ${
|
||||||
val ? "bg-emerald-500/10 text-emerald-400" : "bg-rose-500/10 text-rose-400"
|
val ? "bg-neutral-500/10 text-neutral-300 border-neutral-500/20" : "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] border-[var(--color-border-faint)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{val ? "✓ Aktif" : "✗ Pasif"}
|
{val ? "✓ Aktif" : "✗ Pasif"}
|
||||||
@@ -160,7 +160,7 @@ export default function AdminPlansPage() {
|
|||||||
disabled={!hasEdits || updatePlan.isPending}
|
disabled={!hasEdits || updatePlan.isPending}
|
||||||
className={`w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-sm font-medium transition-all ${
|
className={`w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-sm font-medium transition-all ${
|
||||||
savedIds.has(plan.id)
|
savedIds.has(plan.id)
|
||||||
? "bg-emerald-500/10 text-emerald-400"
|
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] opacity-80"
|
||||||
: hasEdits
|
: hasEdits
|
||||||
? "btn-primary"
|
? "btn-primary"
|
||||||
: "opacity-40 cursor-not-allowed bg-[var(--color-bg-elevated)] text-[var(--color-text-muted)]"
|
: "opacity-40 cursor-not-allowed bg-[var(--color-bg-elevated)] text-[var(--color-text-muted)]"
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ import Link from "next/link";
|
|||||||
import { useAdminProjects, useAdminDeleteProject } from "@/hooks/use-api";
|
import { useAdminProjects, useAdminDeleteProject } from "@/hooks/use-api";
|
||||||
|
|
||||||
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
||||||
DRAFT: { label: "Taslak", color: "bg-gray-500/10 text-gray-400" },
|
DRAFT: { label: "Taslak", color: "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] border border-[var(--color-border-faint)]" },
|
||||||
GENERATING_SCRIPT: { label: "Senaryo Üretiliyor", color: "bg-blue-500/10 text-blue-400" },
|
GENERATING_SCRIPT: { label: "Senaryo Üretiliyor", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||||
PENDING: { label: "Kuyrukta", color: "bg-amber-500/10 text-amber-400" },
|
PENDING: { label: "Kuyrukta", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||||
GENERATING_MEDIA: { label: "Medya Üretiliyor", color: "bg-cyan-500/10 text-cyan-400" },
|
GENERATING_MEDIA: { label: "Medya Üretiliyor", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||||
RENDERING: { label: "Render", color: "bg-violet-500/10 text-violet-400" },
|
RENDERING: { label: "Render", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||||
COMPLETED: { label: "Tamamlandı", color: "bg-emerald-500/10 text-emerald-400" },
|
COMPLETED: { label: "Tamamlandı", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||||
FAILED: { label: "Başarısız", color: "bg-rose-500/10 text-rose-400" },
|
FAILED: { label: "Başarısız", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const fadeUp = {
|
const fadeUp = {
|
||||||
@@ -66,7 +66,7 @@ export default function AdminProjectsPage() {
|
|||||||
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold flex items-center gap-2">
|
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold flex items-center gap-2">
|
||||||
<FolderOpen size={22} className="text-cyan-400" /> Proje Yönetimi
|
<FolderOpen size={22} className="text-neutral-400" /> Proje Yönetimi
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
||||||
Toplam {meta.total ?? "—"} proje
|
Toplam {meta.total ?? "—"} proje
|
||||||
@@ -104,7 +104,7 @@ export default function AdminProjectsPage() {
|
|||||||
<motion.div variants={fadeUp} className="card-surface overflow-hidden">
|
<motion.div variants={fadeUp} className="card-surface overflow-hidden">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex justify-center py-16">
|
<div className="flex justify-center py-16">
|
||||||
<Loader2 size={28} className="animate-spin text-cyan-400" />
|
<Loader2 size={28} className="animate-spin text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -141,7 +141,7 @@ export default function AdminProjectsPage() {
|
|||||||
</span>
|
</span>
|
||||||
{project.status !== "DRAFT" && project.status !== "COMPLETED" && project.status !== "FAILED" && (
|
{project.status !== "DRAFT" && project.status !== "COMPLETED" && project.status !== "FAILED" && (
|
||||||
<div className="w-16 h-1 rounded-full bg-[var(--color-border-faint)] mt-1">
|
<div className="w-16 h-1 rounded-full bg-[var(--color-border-faint)] mt-1">
|
||||||
<div className="h-full rounded-full bg-violet-500" style={{ width: `${project.progress}%` }} />
|
<div className="h-full rounded-full bg-neutral-400" style={{ width: `${project.progress}%` }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
@@ -156,7 +156,7 @@ export default function AdminProjectsPage() {
|
|||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Link
|
<Link
|
||||||
href={`/dashboard/projects/${project.id}`}
|
href={`/dashboard/projects/${project.id}`}
|
||||||
className="p-1.5 rounded-lg hover:bg-cyan-500/10 text-cyan-400 transition-colors"
|
className="p-1.5 rounded-lg hover:bg-neutral-500/10 text-neutral-400 transition-colors"
|
||||||
title="Detay"
|
title="Detay"
|
||||||
>
|
>
|
||||||
<ExternalLink size={13} />
|
<ExternalLink size={13} />
|
||||||
@@ -168,7 +168,7 @@ export default function AdminProjectsPage() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={deleteProject.isPending}
|
disabled={deleteProject.isPending}
|
||||||
className="p-1.5 rounded-lg hover:bg-rose-500/10 text-rose-400 transition-colors"
|
className="p-1.5 rounded-lg hover:bg-neutral-500/10 text-neutral-400 transition-colors"
|
||||||
title="Sil"
|
title="Sil"
|
||||||
>
|
>
|
||||||
<Trash2 size={13} />
|
<Trash2 size={13} />
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ import {
|
|||||||
import { useAdminRenderJobs } from "@/hooks/use-api";
|
import { useAdminRenderJobs } from "@/hooks/use-api";
|
||||||
|
|
||||||
const STATUS_CONFIG: Record<string, { label: string; color: string; icon: React.ElementType }> = {
|
const STATUS_CONFIG: Record<string, { label: string; color: string; icon: React.ElementType }> = {
|
||||||
QUEUED: { label: "Beklemede", color: "text-amber-400 bg-amber-500/10", icon: Clock },
|
QUEUED: { label: "Beklemede", color: "text-neutral-400 bg-neutral-500/10 border border-neutral-500/20", icon: Clock },
|
||||||
PROCESSING: { label: "İşleniyor", color: "text-cyan-400 bg-cyan-500/10", icon: Loader2 },
|
PROCESSING: { label: "İşleniyor", color: "text-neutral-400 bg-neutral-500/10 border border-neutral-500/20", icon: Loader2 },
|
||||||
COMPLETED: { label: "Tamamlandı", color: "text-emerald-400 bg-emerald-500/10", icon: CheckCircle2 },
|
COMPLETED: { label: "Tamamlandı", color: "text-neutral-400 bg-neutral-500/10 border border-neutral-500/20", icon: CheckCircle2 },
|
||||||
FAILED: { label: "Başarısız", color: "text-rose-400 bg-rose-500/10", icon: XCircle },
|
FAILED: { label: "Başarısız", color: "text-neutral-400 bg-neutral-500/10 border border-neutral-500/20", icon: XCircle },
|
||||||
CANCELLED: { label: "İptal", color: "text-gray-400 bg-gray-500/10", icon: AlertTriangle },
|
CANCELLED: { label: "İptal", color: "text-[var(--color-text-ghost)] bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)]", icon: AlertTriangle },
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatMs(ms?: number) {
|
function formatMs(ms?: number) {
|
||||||
@@ -62,7 +62,7 @@ export default function AdminRenderJobsPage() {
|
|||||||
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold flex items-center gap-2">
|
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold flex items-center gap-2">
|
||||||
<Cpu size={22} className="text-emerald-400" /> Render İş Takibi
|
<Cpu size={22} className="text-neutral-400" /> Render İş Takibi
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
||||||
Toplam {meta.total ?? "—"} render işi (15 saniyede bir güncellenir)
|
Toplam {meta.total ?? "—"} render işi (15 saniyede bir güncellenir)
|
||||||
@@ -81,8 +81,8 @@ export default function AdminRenderJobsPage() {
|
|||||||
onClick={() => { setStatusFilter(value); setPage(1); }}
|
onClick={() => { setStatusFilter(value); setPage(1); }}
|
||||||
className={`text-xs px-3 py-1.5 rounded-full transition-colors border ${
|
className={`text-xs px-3 py-1.5 rounded-full transition-colors border ${
|
||||||
statusFilter === value
|
statusFilter === value
|
||||||
? "border-violet-500 bg-violet-500/10 text-violet-400"
|
? "border-neutral-500 bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
|
||||||
: "border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-violet-500/40"
|
: "border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-neutral-500/40"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@@ -93,7 +93,7 @@ export default function AdminRenderJobsPage() {
|
|||||||
<motion.div variants={fadeUp} className="card-surface overflow-hidden">
|
<motion.div variants={fadeUp} className="card-surface overflow-hidden">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex justify-center py-16">
|
<div className="flex justify-center py-16">
|
||||||
<Loader2 size={28} className="animate-spin text-emerald-400" />
|
<Loader2 size={28} className="animate-spin text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function AdminUsersPage() {
|
|||||||
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold flex items-center gap-2">
|
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold flex items-center gap-2">
|
||||||
<Users size={22} className="text-violet-400" /> Kullanıcı Yönetimi
|
<Users size={22} className="text-neutral-400" /> Kullanıcı Yönetimi
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
||||||
Toplam {meta.total ?? "—"} kullanıcı
|
Toplam {meta.total ?? "—"} kullanıcı
|
||||||
@@ -92,7 +92,7 @@ export default function AdminUsersPage() {
|
|||||||
<motion.div variants={fadeUp} className="card-surface overflow-hidden">
|
<motion.div variants={fadeUp} className="card-surface overflow-hidden">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex justify-center py-16">
|
<div className="flex justify-center py-16">
|
||||||
<Loader2 size={28} className="animate-spin text-violet-400" />
|
<Loader2 size={28} className="animate-spin text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -127,7 +127,7 @@ export default function AdminUsersPage() {
|
|||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{roleNames.length > 0 ? (
|
{roleNames.length > 0 ? (
|
||||||
roleNames.map((role) => (
|
roleNames.map((role) => (
|
||||||
<span key={role} className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/10 text-violet-400 flex items-center gap-1">
|
<span key={role} className="text-[10px] px-2 py-0.5 rounded-full bg-neutral-500/10 text-neutral-400 border border-neutral-500/20 flex items-center gap-1">
|
||||||
<Shield size={8} /> {role}
|
<Shield size={8} /> {role}
|
||||||
</span>
|
</span>
|
||||||
))
|
))
|
||||||
@@ -138,10 +138,10 @@ export default function AdminUsersPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span
|
<span
|
||||||
className={`text-xs px-2 py-0.5 rounded-full ${
|
className={`text-xs px-2 py-0.5 rounded-full border ${
|
||||||
user.isActive
|
user.isActive
|
||||||
? "bg-emerald-500/10 text-emerald-400"
|
? "bg-neutral-500/10 text-neutral-300 border-neutral-500/20"
|
||||||
: "bg-rose-500/10 text-rose-400"
|
: "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] border-[var(--color-border-faint)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{user.isActive ? "Aktif" : "Pasif"}
|
{user.isActive ? "Aktif" : "Pasif"}
|
||||||
@@ -154,7 +154,7 @@ export default function AdminUsersPage() {
|
|||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setGrantModal({ userId: user.id, email: user.email })}
|
onClick={() => setGrantModal({ userId: user.id, email: user.email })}
|
||||||
className="p-1.5 rounded-lg hover:bg-amber-500/10 text-amber-400 transition-colors"
|
className="p-1.5 rounded-lg hover:bg-neutral-500/10 text-neutral-400 transition-colors"
|
||||||
title="Kredi Yükle"
|
title="Kredi Yükle"
|
||||||
>
|
>
|
||||||
<Coins size={14} />
|
<Coins size={14} />
|
||||||
@@ -162,11 +162,7 @@ export default function AdminUsersPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => toggleActive.mutate(user.id)}
|
onClick={() => toggleActive.mutate(user.id)}
|
||||||
disabled={toggleActive.isPending}
|
disabled={toggleActive.isPending}
|
||||||
className={`p-1.5 rounded-lg transition-colors ${
|
className={`p-1.5 rounded-lg transition-colors hover:bg-neutral-500/10 text-neutral-400`}
|
||||||
user.isActive
|
|
||||||
? "hover:bg-rose-500/10 text-rose-400"
|
|
||||||
: "hover:bg-emerald-500/10 text-emerald-400"
|
|
||||||
}`}
|
|
||||||
title={user.isActive ? "Pasif Yap" : "Aktif Yap"}
|
title={user.isActive ? "Pasif Yap" : "Aktif Yap"}
|
||||||
>
|
>
|
||||||
{user.isActive ? <UserX size={14} /> : <UserCheck size={14} />}
|
{user.isActive ? <UserX size={14} /> : <UserCheck size={14} />}
|
||||||
|
|||||||
@@ -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,339 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { useExtractDocumentTopics, useCreateFromExtractedText } from "@/hooks/use-api";
|
|
||||||
import { useToast } from "@/components/ui/toast";
|
|
||||||
import {
|
|
||||||
FileText,
|
|
||||||
Loader2,
|
|
||||||
ArrowRight,
|
|
||||||
Clock,
|
|
||||||
Palette,
|
|
||||||
Monitor,
|
|
||||||
Smartphone,
|
|
||||||
Square,
|
|
||||||
Wand2,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
const videoStyles = [
|
|
||||||
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬" },
|
|
||||||
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹" },
|
|
||||||
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚" },
|
|
||||||
{ id: "STORYTELLING", label: "Hikâye", emoji: "📖" },
|
|
||||||
{ id: "NEWS", label: "Haber", emoji: "📰" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const aspectRatios = [
|
|
||||||
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
|
|
||||||
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
|
|
||||||
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const languages = [
|
|
||||||
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
|
|
||||||
{ code: "en", label: "English", flag: "🇺🇸" },
|
|
||||||
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
|
||||||
{ code: "es", label: "Español", flag: "🇪🇸" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function DocumentToVideoPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const extractDocumentTopics = useExtractDocumentTopics();
|
|
||||||
const createFromExtractedText = useCreateFromExtractedText();
|
|
||||||
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
const [extractedData, setExtractedData] = useState<{text: string; topics: string[]; originalFilename: string} | null>(null);
|
|
||||||
const [selectedTopic, setSelectedTopic] = useState<string | 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) {
|
|
||||||
setFile(e.target.files[0]);
|
|
||||||
setExtractedData(null);
|
|
||||||
setSelectedTopic(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExtractTopics = async () => {
|
|
||||||
if (!file) {
|
|
||||||
toast("error", "Lütfen bir belge seçin.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await extractDocumentTopics.mutateAsync({ file });
|
|
||||||
setExtractedData(result);
|
|
||||||
if (result.topics.length > 0) {
|
|
||||||
setSelectedTopic(result.topics[0]);
|
|
||||||
}
|
|
||||||
toast("success", "Belge incelendi ve konular çıkarıldı!");
|
|
||||||
} catch (error) {
|
|
||||||
toast("error", "Konu çıkarılırken hata oluştu. Belki belge okunamıyor veya çok büyük.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
|
||||||
if (!extractedData || !selectedTopic) {
|
|
||||||
toast("error", "Lütfen bir belge yükleyip konu seçin.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result: any = await createFromExtractedText.mutateAsync({
|
|
||||||
text: extractedData.text,
|
|
||||||
topic: selectedTopic,
|
|
||||||
originalFilename: extractedData.originalFilename,
|
|
||||||
language,
|
|
||||||
aspectRatio,
|
|
||||||
videoStyle: style,
|
|
||||||
cinematicReference: cinematicReference ? cinematicReference : undefined,
|
|
||||||
targetDuration: duration,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast("success", "Video projesi oluşturuldu!");
|
|
||||||
router.push(`/dashboard/projects/${result.id}`);
|
|
||||||
} catch (error) {
|
|
||||||
toast("error", "Proje oluşturulurken 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-blue-500/10 text-blue-500 mb-2 ring-1 ring-blue-500/20 shadow-[0_0_30px_rgba(59,130,246,0.15)]">
|
|
||||||
<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, TXT vb. belgenizi yükleyin, yapay zeka içeriği tarayıp senaryolastirsin.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Input */}
|
|
||||||
<div className="card p-6 md:p-8 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
|
|
||||||
Belge Yükle (PDF, DOCX, TXT)
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
accept=".pdf,.docx,.txt,.csv,.xlsx,.pptx"
|
|
||||||
className="block w-full text-sm text-[var(--color-text-muted)]
|
|
||||||
file:mr-4 file:py-3 file:px-4
|
|
||||||
file:rounded-xl file:border-0
|
|
||||||
file:text-sm file:font-semibold
|
|
||||||
file:bg-blue-500/10 file:text-blue-500
|
|
||||||
hover:file:bg-blue-500/20 cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!extractedData && file && (
|
|
||||||
<button
|
|
||||||
onClick={handleExtractTopics}
|
|
||||||
disabled={extractDocumentTopics.isPending}
|
|
||||||
className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl font-medium text-white shadow-lg bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{extractDocumentTopics.isPending ? (
|
|
||||||
<>
|
|
||||||
<Loader2 size={18} className="animate-spin" />
|
|
||||||
Belge İnceleniyor...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FileText size={18} />
|
|
||||||
Konu Çıkar
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{extractedData && (
|
|
||||||
<div className="mt-6 p-4 rounded-xl bg-blue-500/5 border border-blue-500/20">
|
|
||||||
<h3 className="font-medium text-blue-400 mb-3 text-sm flex items-center gap-2">
|
|
||||||
<Wand2 size={16} />
|
|
||||||
Şu Konulardan Birini Seçin:
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{extractedData.topics.map((topic, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
onClick={() => setSelectedTopic(topic)}
|
|
||||||
className={cn(
|
|
||||||
"p-3 rounded-lg border text-sm cursor-pointer transition-all",
|
|
||||||
selectedTopic === topic
|
|
||||||
? "bg-blue-500/20 border-blue-500 text-blue-400"
|
|
||||||
: "bg-[var(--color-bg-surface)] border-[var(--color-border-faint)] text-[var(--color-text-secondary)] hover:border-blue-500/50"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{topic}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Video Settings */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="card p-5 space-y-3">
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
|
||||||
Video Dili
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{languages.map((l) => (
|
|
||||||
<button
|
|
||||||
key={l.code}
|
|
||||||
onClick={() => setLanguage(l.code)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
|
||||||
language === l.code
|
|
||||||
? "bg-blue-500/12 border border-blue-500/30 text-blue-400"
|
|
||||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{l.flag}</span>
|
|
||||||
{l.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card p-5 space-y-3">
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
|
||||||
<Palette size={14} className="inline mr-1.5 text-blue-400" />
|
|
||||||
Video Stili
|
|
||||||
</label>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{videoStyles.map((s) => (
|
|
||||||
<button
|
|
||||||
key={s.id}
|
|
||||||
onClick={() => setStyle(s.id)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
|
||||||
style === s.id
|
|
||||||
? "bg-blue-500/12 border border-blue-500/30 text-blue-400"
|
|
||||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{s.emoji}</span>
|
|
||||||
{s.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{style === "CINEMATIC" && (
|
|
||||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
|
||||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
|
||||||
Özel Sinematik Referans (Opsiyonel)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
|
||||||
value={cinematicReference}
|
|
||||||
onChange={(e) => setCinematicReference(e.target.value)}
|
|
||||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-xl py-2 px-3 text-sm focus:border-blue-500/50 outline-none transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card p-5 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
|
||||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
|
||||||
Hedef Süre: <span className="text-blue-400">{duration}s</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={15}
|
|
||||||
max={120}
|
|
||||||
step={5}
|
|
||||||
value={duration}
|
|
||||||
onChange={(e) => setDuration(Number(e.target.value))}
|
|
||||||
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
|
|
||||||
[&::-webkit-slider-thumb]:appearance-none
|
|
||||||
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
|
|
||||||
[&::-webkit-slider-thumb]:rounded-full
|
|
||||||
[&::-webkit-slider-thumb]:bg-blue-500
|
|
||||||
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(59,130,246,0.4)]
|
|
||||||
[&::-webkit-slider-thumb]:cursor-grab"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
|
||||||
En-Boy Oranı
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{aspectRatios.map((ar) => {
|
|
||||||
const Icon = ar.icon;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={ar.id}
|
|
||||||
onClick={() => setAspectRatio(ar.id)}
|
|
||||||
className={cn(
|
|
||||||
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all",
|
|
||||||
aspectRatio === ar.id
|
|
||||||
? "bg-blue-500/12 border border-blue-500/30 text-blue-400"
|
|
||||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon size={20} />
|
|
||||||
<span className="font-semibold">{ar.label}</span>
|
|
||||||
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
|
||||||
{ar.desc}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleGenerate}
|
|
||||||
disabled={createFromExtractedText.isPending || !selectedTopic}
|
|
||||||
className={cn(
|
|
||||||
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
|
|
||||||
createFromExtractedText.isPending
|
|
||||||
? "bg-blue-500/20 text-blue-400 cursor-wait"
|
|
||||||
: "bg-blue-500 hover:bg-blue-600 text-white shadow-lg shadow-blue-500/20",
|
|
||||||
!selectedTopic && "opacity-50 cursor-not-allowed"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{createFromExtractedText.isPending ? (
|
|
||||||
<>
|
|
||||||
<Loader2 size={20} className="animate-spin" />
|
|
||||||
<span>Video Projesi Oluşturuluyor... (Bu işlem uzun sürebilir)</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Wand2 size={20} />
|
|
||||||
<span>Konudan Video Oluştur</span>
|
|
||||||
<ArrowRight size={16} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
@@ -24,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 = {
|
||||||
@@ -60,41 +62,47 @@ function getStatCards(data?: typeof MOCK_STATS, creditBalance?: { balance: numbe
|
|||||||
value: String(stats.totalProjects),
|
value: String(stats.totalProjects),
|
||||||
change: `${stats.completedVideos} tamamlandı`,
|
change: `${stats.completedVideos} tamamlandı`,
|
||||||
icon: FolderOpen,
|
icon: FolderOpen,
|
||||||
gradient: "from-violet-500/12 to-violet-600/5",
|
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||||||
iconBg: "bg-violet-500/12",
|
iconBg: "bg-neutral-500/12",
|
||||||
iconColor: "text-violet-400",
|
iconColor: "text-neutral-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Devam Eden",
|
label: "Devam Eden",
|
||||||
value: String(stats.activeRenderJobs),
|
value: String(stats.activeRenderJobs),
|
||||||
change: "İşleniyor",
|
change: "İşleniyor",
|
||||||
icon: PlayCircle,
|
icon: PlayCircle,
|
||||||
gradient: "from-cyan-500/12 to-cyan-600/5",
|
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||||||
iconBg: "bg-cyan-500/12",
|
iconBg: "bg-neutral-500/12",
|
||||||
iconColor: "text-cyan-400",
|
iconColor: "text-neutral-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Tamamlanan",
|
label: "Tamamlanan",
|
||||||
value: String(stats.completedVideos),
|
value: String(stats.completedVideos),
|
||||||
change: "Bu ay",
|
change: "Bu ay",
|
||||||
icon: CheckCircle2,
|
icon: CheckCircle2,
|
||||||
gradient: "from-emerald-500/12 to-emerald-600/5",
|
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||||||
iconBg: "bg-emerald-500/12",
|
iconBg: "bg-neutral-500/12",
|
||||||
iconColor: "text-emerald-400",
|
iconColor: "text-neutral-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kalan Kredi",
|
label: "Kalan Kredi",
|
||||||
value: String(credits.balance),
|
value: String(credits.balance),
|
||||||
change: `${credits.monthlyLimit} üzerinden`,
|
change: `${credits.monthlyLimit} üzerinden`,
|
||||||
icon: Coins,
|
icon: Coins,
|
||||||
gradient: "from-amber-500/12 to-amber-600/5",
|
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||||||
iconBg: "bg-amber-500/12",
|
iconBg: "bg-neutral-500/12",
|
||||||
iconColor: "text-amber-400",
|
iconColor: "text-neutral-400",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Real API hook'ları — mock modunda çağrılmaz
|
// Real API hook'ları — mock modunda çağrılmaz
|
||||||
const statsQuery = useDashboardStats();
|
const statsQuery = useDashboardStats();
|
||||||
const creditQuery = useCreditBalance();
|
const creditQuery = useCreditBalance();
|
||||||
@@ -106,6 +114,14 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
const statCards = getStatCards(statsData, creditData);
|
const statCards = getStatCards(statsData, creditData);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<Loader2 size={32} className="animate-spin text-neutral-500" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={stagger}
|
variants={stagger}
|
||||||
@@ -124,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} />
|
||||||
@@ -136,7 +152,7 @@ export default function DashboardPage() {
|
|||||||
<motion.div variants={fadeUp} className="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-4">
|
<motion.div variants={fadeUp} className="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-4">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="col-span-4 flex items-center justify-center py-12">
|
<div className="col-span-4 flex items-center justify-center py-12">
|
||||||
<Loader2 size={28} className="animate-spin text-violet-400" />
|
<Loader2 size={28} className="animate-spin text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
statCards.map((stat) => {
|
statCards.map((stat) => {
|
||||||
@@ -168,11 +184,11 @@ 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-violet-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-gradient-to-br from-violet-500 to-violet-700 flex items-center justify-center shrink-0 shadow-lg group-hover:shadow-violet-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">
|
||||||
<Sparkles size={20} className="text-white" />
|
<Sparkles size={20} className="text-[var(--color-text-inverted)]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold">AI ile Video Oluştur</h3>
|
<h3 className="text-sm font-semibold">AI ile Video Oluştur</h3>
|
||||||
@@ -182,10 +198,10 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/templates"
|
href="/dashboard/templates"
|
||||||
className="group card-surface p-4 flex items-center gap-4 hover:border-cyan-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-gradient-to-br from-cyan-500 to-cyan-700 flex items-center justify-center shrink-0 shadow-lg group-hover:shadow-cyan-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">
|
||||||
<TrendingUp size={20} className="text-white" />
|
<TrendingUp size={20} className="text-[var(--color-text-inverted)]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold">Şablon Keşfet</h3>
|
<h3 className="text-sm font-semibold">Şablon Keşfet</h3>
|
||||||
@@ -195,10 +211,10 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/projects"
|
href="/dashboard/projects"
|
||||||
className="group card-surface p-4 flex items-center gap-4 hover:border-emerald-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-gradient-to-br from-emerald-500 to-emerald-700 flex items-center justify-center shrink-0 shadow-lg group-hover:shadow-emerald-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">
|
||||||
<Clock size={20} className="text-white" />
|
<Clock size={20} className="text-[var(--color-text-inverted)]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold">Devam Eden İşler</h3>
|
<h3 className="text-sm font-semibold">Devam Eden İşler</h3>
|
||||||
@@ -208,14 +224,14 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="#tweet-import"
|
href="#tweet-import"
|
||||||
className="group card-surface p-4 flex items-center gap-4 hover:border-sky-500/30"
|
className="group card-surface p-4 flex items-center gap-4 hover:border-neutral-500/30"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
document.getElementById('tweet-import')?.scrollIntoView({ behavior: 'smooth' });
|
document.getElementById('tweet-import')?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-sky-500 to-sky-700 flex items-center justify-center shrink-0 shadow-lg group-hover:shadow-sky-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">
|
||||||
<XIcon size={20} className="text-white" />
|
<XIcon size={20} className="text-[var(--color-text-inverted)]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold">Tweet → Video</h3>
|
<h3 className="text-sm font-semibold">Tweet → Video</h3>
|
||||||
@@ -226,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,14 +59,26 @@ 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-slate-400', icon: FileText, bgClass: 'bg-slate-500/10 border-slate-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-violet-400', icon: Sparkles, bgClass: 'bg-violet-500/10 border-violet-500/20' },
|
GENERATING_SCRIPT: { label: 'Senaryo Üretiliyor', color: 'text-neutral-300', icon: Sparkles, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
|
||||||
PENDING: { label: 'Kuyrukta', color: 'text-amber-400', icon: Clock, bgClass: 'bg-amber-500/10 border-amber-500/20' },
|
PENDING: { label: 'Kuyrukta', color: 'text-neutral-400', icon: Clock, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
|
||||||
GENERATING_MEDIA: { label: 'Medya Üretiliyor', color: 'text-cyan-400', icon: Sparkles, bgClass: 'bg-cyan-500/10 border-cyan-500/20' },
|
GENERATING_MEDIA: { label: 'Medya Üretiliyor', color: 'text-neutral-300', icon: Sparkles, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
|
||||||
RENDERING: { label: 'Video İşleniyor', color: 'text-blue-400', icon: Film, bgClass: 'bg-blue-500/10 border-blue-500/20' },
|
RENDERING: { label: 'Video İşleniyor', color: 'text-neutral-300', icon: Film, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
|
||||||
COMPLETED: { label: 'Tamamlandı', color: 'text-emerald-400', icon: CheckCircle2, bgClass: 'bg-emerald-500/10 border-emerald-500/20' },
|
COMPLETED: { label: 'Tamamlandı', color: 'text-neutral-100', icon: CheckCircle2, bgClass: 'bg-neutral-500/20 border-neutral-500/30' },
|
||||||
FAILED: { label: 'Başarısız', color: 'text-red-400', icon: AlertCircle, bgClass: 'bg-red-500/10 border-red-500/20' },
|
FAILED: { label: 'Başarısız', color: 'text-neutral-500', icon: AlertCircle, bgClass: 'bg-neutral-800/50 border-neutral-800' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const videoStyles = [
|
const videoStyles = [
|
||||||
@@ -126,6 +153,27 @@ export default function ProjectDetailPage() {
|
|||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
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(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -240,7 +335,7 @@ export default function ProjectDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-[60vh]">
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<Loader2 size={32} className="animate-spin text-violet-400" />
|
<Loader2 size={32} className="animate-spin text-neutral-400" />
|
||||||
<p className="text-sm text-[var(--color-text-muted)]">Proje yükleniyor...</p>
|
<p className="text-sm text-[var(--color-text-muted)]">Proje yükleniyor...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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"
|
||||||
@@ -379,7 +480,7 @@ export default function ProjectDetailPage() {
|
|||||||
value={project.videoStyle}
|
value={project.videoStyle}
|
||||||
onChange={(e) => handleStyleChange(e.target.value)}
|
onChange={(e) => handleStyleChange(e.target.value)}
|
||||||
disabled={isRendering}
|
disabled={isRendering}
|
||||||
className="bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] rounded-md px-2 py-0.5 text-xs text-[var(--color-text-secondary)] focus:outline-none focus:border-violet-500/50 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
className="bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] rounded-md px-2 py-0.5 text-xs text-[var(--color-text-secondary)] focus:outline-none focus:border-neutral-500/50 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
||||||
>
|
>
|
||||||
{videoStyles.map((s) => (
|
{videoStyles.map((s) => (
|
||||||
<option key={s.id} value={s.id}>
|
<option key={s.id} value={s.id}>
|
||||||
@@ -393,7 +494,7 @@ export default function ProjectDetailPage() {
|
|||||||
value={project.cinematicReference || ''}
|
value={project.cinematicReference || ''}
|
||||||
onChange={(e) => handleCinematicReferenceChange(e.target.value)}
|
onChange={(e) => handleCinematicReferenceChange(e.target.value)}
|
||||||
disabled={isRendering}
|
disabled={isRendering}
|
||||||
className="bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] rounded-md px-2 py-0.5 text-xs text-[var(--color-text-secondary)] focus:outline-none focus:border-violet-500/50 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer w-44 truncate"
|
className="bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] rounded-md px-2 py-0.5 text-xs text-[var(--color-text-secondary)] focus:outline-none focus:border-neutral-500/50 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer w-44 truncate"
|
||||||
>
|
>
|
||||||
<option value="">🎬 Sinematik Yönetmen/Film...</option>
|
<option value="">🎬 Sinematik Yönetmen/Film...</option>
|
||||||
{CINEMATIC_REFERENCES.map(ref => (
|
{CINEMATIC_REFERENCES.map(ref => (
|
||||||
@@ -438,7 +539,7 @@ export default function ProjectDetailPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleGenerateScript}
|
onClick={handleGenerateScript}
|
||||||
disabled={isRendering || generateScriptMutation.isPending}
|
disabled={isRendering || generateScriptMutation.isPending}
|
||||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-violet-500 to-violet-600 text-white text-sm font-medium shadow-lg shadow-violet-500/20 hover:shadow-violet-500/30 transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] text-sm font-medium hover:bg-neutral-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{generateScriptMutation.isPending ? (
|
{generateScriptMutation.isPending ? (
|
||||||
<Loader2 size={15} className="animate-spin" />
|
<Loader2 size={15} className="animate-spin" />
|
||||||
@@ -470,7 +571,7 @@ export default function ProjectDetailPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleApprove}
|
onClick={handleApprove}
|
||||||
disabled={isRendering || approveMutation.isPending}
|
disabled={isRendering || approveMutation.isPending}
|
||||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-emerald-500 to-emerald-600 text-white text-sm font-medium shadow-lg shadow-emerald-500/20 hover:shadow-emerald-500/30 transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] text-sm font-medium hover:bg-neutral-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{approveMutation.isPending ? (
|
{approveMutation.isPending ? (
|
||||||
<Loader2 size={15} className="animate-spin" />
|
<Loader2 size={15} className="animate-spin" />
|
||||||
@@ -534,7 +635,7 @@ export default function ProjectDetailPage() {
|
|||||||
{/* ── Render Progress (WebSocket) ── */}
|
{/* ── Render Progress (WebSocket) ── */}
|
||||||
{isRendering && (
|
{isRendering && (
|
||||||
<motion.div variants={fadeUp}>
|
<motion.div variants={fadeUp}>
|
||||||
<RenderProgress renderState={renderState} />
|
<RenderProgress renderState={renderState} projectStatus={project.status} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -554,7 +655,7 @@ export default function ProjectDetailPage() {
|
|||||||
<motion.div variants={fadeUp}>
|
<motion.div variants={fadeUp}>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h2 className="text-sm font-semibold text-[var(--color-text-primary)] flex items-center gap-2">
|
<h2 className="text-sm font-semibold text-[var(--color-text-primary)] flex items-center gap-2">
|
||||||
<Film size={15} className="text-violet-400" />
|
<Film size={15} className="text-neutral-400" />
|
||||||
Senaryo — {project.scenes!.length} sahne
|
Senaryo — {project.scenes!.length} sahne
|
||||||
</h2>
|
</h2>
|
||||||
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
||||||
@@ -562,22 +663,341 @@ export default function ProjectDetailPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div
|
||||||
{project.scenes!.map((scene) => (
|
ref={parentRef}
|
||||||
<SceneCard
|
className="w-full h-[800px] overflow-auto pr-2 rounded-xl"
|
||||||
key={scene.id}
|
>
|
||||||
scene={scene}
|
<div
|
||||||
isEditable={isEditable}
|
style={{
|
||||||
isRendering={isRendering}
|
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||||
onUpdate={handleSceneUpdate}
|
width: '100%',
|
||||||
onRegenerate={handleSceneRegenerate}
|
position: 'relative',
|
||||||
onGenerateImage={handleGenerateImage}
|
}}
|
||||||
onUpscaleImage={handleUpscaleImage}
|
>
|
||||||
isRegenerating={regeneratingSceneId === scene.id}
|
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||||
isGeneratingImage={generatingImageId === scene.id}
|
const scene = project.scenes![virtualRow.index];
|
||||||
isUpscalingImage={upscalingImageId === scene.id}
|
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
|
||||||
|
scene={scene}
|
||||||
|
isEditable={isEditable}
|
||||||
|
isRendering={isRendering}
|
||||||
|
onUpdate={handleSceneUpdate}
|
||||||
|
onRegenerate={handleSceneRegenerate}
|
||||||
|
onGenerateImage={handleGenerateImage}
|
||||||
|
onUpscaleImage={handleUpscaleImage}
|
||||||
|
isRegenerating={regeneratingSceneId === scene.id}
|
||||||
|
isGeneratingImage={generatingImageId === 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>
|
||||||
|
|
||||||
|
{/* ─── 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'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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -585,8 +1005,8 @@ export default function ProjectDetailPage() {
|
|||||||
{/* ── Boş durum (senaryo yok) ── */}
|
{/* ── Boş durum (senaryo yok) ── */}
|
||||||
{!hasScript && isEditable && (
|
{!hasScript && isEditable && (
|
||||||
<motion.div variants={fadeUp} className="card-surface p-8 text-center">
|
<motion.div variants={fadeUp} className="card-surface p-8 text-center">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-500/15 to-cyan-400/10 mx-auto mb-4 flex items-center justify-center">
|
<div className="w-16 h-16 rounded-2xl bg-[var(--color-bg-elevated)] mx-auto mb-4 flex items-center justify-center">
|
||||||
<Sparkles size={28} className="text-violet-400" />
|
<Sparkles size={28} className="text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-base font-semibold mb-1.5">Henüz senaryo üretilmedi</h3>
|
<h3 className="text-base font-semibold mb-1.5">Henüz senaryo üretilmedi</h3>
|
||||||
<p className="text-sm text-[var(--color-text-muted)] mb-4 max-w-sm mx-auto">
|
<p className="text-sm text-[var(--color-text-muted)] mb-4 max-w-sm mx-auto">
|
||||||
@@ -595,7 +1015,7 @@ export default function ProjectDetailPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleGenerateScript}
|
onClick={handleGenerateScript}
|
||||||
disabled={generateScriptMutation.isPending}
|
disabled={generateScriptMutation.isPending}
|
||||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-gradient-to-r from-violet-500 to-violet-600 text-white text-sm font-medium shadow-lg shadow-violet-500/20 hover:shadow-violet-500/30 transition-shadow disabled:opacity-50"
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] text-sm font-medium hover:bg-neutral-800 transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{generateScriptMutation.isPending ? (
|
{generateScriptMutation.isPending ? (
|
||||||
<Loader2 size={15} className="animate-spin" />
|
<Loader2 size={15} className="animate-spin" />
|
||||||
@@ -612,7 +1032,7 @@ export default function ProjectDetailPage() {
|
|||||||
<motion.div variants={fadeUp}>
|
<motion.div variants={fadeUp}>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h2 className="text-sm font-semibold text-[var(--color-text-primary)] flex items-center gap-2">
|
<h2 className="text-sm font-semibold text-[var(--color-text-primary)] flex items-center gap-2">
|
||||||
<Clock size={15} className="text-cyan-400" />
|
<Clock size={15} className="text-neutral-400" />
|
||||||
Render Geçmişi
|
Render Geçmişi
|
||||||
</h2>
|
</h2>
|
||||||
{project.status === 'GENERATING_MEDIA' || project.renderJobs.some((j: any) => j.status === 'QUEUED' || j.status === 'PROCESSING') ? (
|
{project.status === 'GENERATING_MEDIA' || project.renderJobs.some((j: any) => j.status === 'QUEUED' || j.status === 'PROCESSING') ? (
|
||||||
@@ -632,10 +1052,10 @@ export default function ProjectDetailPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className={`w-2 h-2 rounded-full ${
|
<div className={`w-2 h-2 rounded-full ${
|
||||||
job.status === 'COMPLETED' ? 'bg-emerald-400' :
|
job.status === 'COMPLETED' ? 'bg-neutral-100' :
|
||||||
job.status === 'FAILED' ? 'bg-red-400' :
|
job.status === 'FAILED' ? 'bg-neutral-500' :
|
||||||
job.status === 'CANCELLED' ? 'bg-slate-400' :
|
job.status === 'CANCELLED' ? 'bg-neutral-600' :
|
||||||
'bg-amber-400 animate-pulse'
|
'bg-neutral-300 animate-pulse'
|
||||||
}`} />
|
}`} />
|
||||||
<span className="text-xs text-[var(--color-text-secondary)]">
|
<span className="text-xs text-[var(--color-text-secondary)]">
|
||||||
Deneme #{job.attemptNumber}
|
Deneme #{job.attemptNumber}
|
||||||
@@ -658,9 +1078,9 @@ export default function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(job.status === 'QUEUED' || job.status === 'PROCESSING') && (
|
{(job.status === 'QUEUED' || job.status === 'PROCESSING') && (
|
||||||
<div className="w-full bg-slate-800 rounded-full h-1.5 mt-1 overflow-hidden">
|
<div className="w-full bg-[var(--color-bg-surface)] rounded-full h-1.5 mt-1 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="bg-amber-400 h-1.5 rounded-full transition-all duration-1000 ease-out relative"
|
className="bg-neutral-300 h-1.5 rounded-full transition-all duration-1000 ease-out relative"
|
||||||
style={{ width: `${job.progress || (job.status === 'QUEUED' ? 5 : 50)}%` }}
|
style={{ width: `${job.progress || (job.status === 'QUEUED' ? 5 : 50)}%` }}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-white/20 animate-pulse" />
|
<div className="absolute inset-0 bg-white/20 animate-pulse" />
|
||||||
@@ -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,536 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import {
|
|
||||||
ArrowLeft,
|
|
||||||
ArrowRight,
|
|
||||||
Sparkles,
|
|
||||||
Languages,
|
|
||||||
Clock,
|
|
||||||
Palette,
|
|
||||||
Monitor,
|
|
||||||
Smartphone,
|
|
||||||
Square,
|
|
||||||
Loader2,
|
|
||||||
Check,
|
|
||||||
Wand2,
|
|
||||||
Search,
|
|
||||||
ChevronDown,
|
|
||||||
} 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";
|
|
||||||
|
|
||||||
const steps = ["Konu & Dil", "Stil & Süre", "AI Senaryo"];
|
|
||||||
|
|
||||||
const languages = [
|
|
||||||
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
|
|
||||||
{ code: "en", label: "English", flag: "🇺🇸" },
|
|
||||||
{ code: "es", label: "Español", flag: "🇪🇸" },
|
|
||||||
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
|
||||||
{ code: "fr", label: "Français", flag: "🇫🇷" },
|
|
||||||
{ code: "ar", label: "العربية", flag: "🇸🇦" },
|
|
||||||
{ code: "pt", label: "Português", flag: "🇧🇷" },
|
|
||||||
{ code: "ja", label: "日本語", flag: "🇯🇵" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const videoStyles = [
|
|
||||||
// Film & Sinema
|
|
||||||
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller", category: "Film & Sinema" },
|
|
||||||
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton", category: "Film & Sinema" },
|
|
||||||
{ id: "STORYTELLING", label: "Hikâye Anlatımı", emoji: "📖", desc: "Anlatı odaklı, sürükleyici", category: "Film & Sinema" },
|
|
||||||
{ id: "NEWS", label: "Haber", emoji: "📰", desc: "Güncel ve bilgilendirici", category: "Film & Sinema" },
|
|
||||||
{ id: "ARTISTIC", label: "Sanatsal", emoji: "🎨", desc: "Yaratıcı ve sıra dışı", category: "Film & Sinema" },
|
|
||||||
{ id: "NOIR", label: "Film Noir", emoji: "🖤", desc: "Karanlık, dramatik", category: "Film & Sinema" },
|
|
||||||
{ id: "VLOG", label: "Vlog", emoji: "📱", desc: "Günlük, samimi", category: "Film & Sinema" },
|
|
||||||
// Animasyon
|
|
||||||
{ id: "ANIME", label: "Anime", emoji: "⛩️", desc: "Japon animasyon stili", category: "Animasyon" },
|
|
||||||
{ id: "ANIMATION_3D", label: "3D Animasyon", emoji: "🧊", desc: "Pixar kalitesi", category: "Animasyon" },
|
|
||||||
{ id: "ANIMATION_2D", label: "2D Animasyon", emoji: "✏️", desc: "Klasik el çizimi", category: "Animasyon" },
|
|
||||||
{ id: "STOP_MOTION", label: "Stop Motion", emoji: "🧸", desc: "Kare kare animasyon", category: "Animasyon" },
|
|
||||||
{ id: "MOTION_COMIC", label: "Hareketli Çizgi Roman", emoji: "💥", desc: "Panel bazlı anlatım", category: "Animasyon" },
|
|
||||||
{ id: "CARTOON", label: "Karikatür", emoji: "🎭", desc: "Çizgi film stili", category: "Animasyon" },
|
|
||||||
{ id: "CLAYMATION", label: "Claymation", emoji: "🏺", desc: "Kil animasyon", category: "Animasyon" },
|
|
||||||
{ id: "PIXEL_ART", label: "Pixel Art", emoji: "👾", desc: "8-bit retro oyun", category: "Animasyon" },
|
|
||||||
{ id: "ISOMETRIC", label: "İzometrik", emoji: "🔷", desc: "İzometrik animasyon", category: "Animasyon" },
|
|
||||||
// Eğitim & Bilgi
|
|
||||||
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "🎓", desc: "Öğretici ve açıklayıcı", category: "Eğitim & Bilgi" },
|
|
||||||
{ id: "INFOGRAPHIC", label: "İnfografik", emoji: "📊", desc: "Veri görselleştirme", category: "Eğitim & Bilgi" },
|
|
||||||
{ id: "WHITEBOARD", label: "Whiteboard", emoji: "📝", desc: "Tahta animasyonu", category: "Eğitim & Bilgi" },
|
|
||||||
{ id: "EXPLAINER", label: "Explainer", emoji: "💡", desc: "Ürün/konsept anlatımı", category: "Eğitim & Bilgi" },
|
|
||||||
{ id: "DATA_VIZ", label: "Veri Görselleştirme", emoji: "📈", desc: "Grafikler ve tablolar", category: "Eğitim & Bilgi" },
|
|
||||||
// Retro & Nostaljik
|
|
||||||
{ id: "RETRO_80S", label: "Retro 80s", emoji: "🕹️", desc: "Synthwave estetik", category: "Retro & Nostaljik" },
|
|
||||||
{ id: "VINTAGE_FILM", label: "Vintage Film", emoji: "📽️", desc: "Super 8 filmi", category: "Retro & Nostaljik" },
|
|
||||||
{ id: "VHS", label: "VHS", emoji: "📼", desc: "Kaset estetik", category: "Retro & Nostaljik" },
|
|
||||||
{ id: "POLAROID", label: "Polaroid", emoji: "📸", desc: "Analog fotoğraf", category: "Retro & Nostaljik" },
|
|
||||||
{ id: "RETRO_90S", label: "Retro 90s Y2K", emoji: "💿", desc: "Y2K & internet", category: "Retro & Nostaljik" },
|
|
||||||
// Sanat Akımları
|
|
||||||
{ id: "WATERCOLOR", label: "Suluboya", emoji: "🎨", desc: "Suluboya resim", category: "Sanat Akımları" },
|
|
||||||
{ id: "OIL_PAINTING", label: "Yağlı Boya", emoji: "🖌️", desc: "Klasik tuval", category: "Sanat Akımları" },
|
|
||||||
{ id: "IMPRESSIONIST", label: "Empresyonist", emoji: "🌅", desc: "Monet tarzı", category: "Sanat Akımları" },
|
|
||||||
{ id: "POP_ART", label: "Pop Art", emoji: "🎯", desc: "Warhol stili", category: "Sanat Akımları" },
|
|
||||||
{ id: "UKIYO_E", label: "Ukiyo-e", emoji: "🏯", desc: "Japon gravür", category: "Sanat Akımları" },
|
|
||||||
{ id: "ART_DECO", label: "Art Deco", emoji: "✨", desc: "1920s zarafet", category: "Sanat Akımları" },
|
|
||||||
{ id: "SURREAL", label: "Sürrealist", emoji: "🌀", desc: "Dalí tarzı", category: "Sanat Akımları" },
|
|
||||||
{ id: "COMIC_BOOK", label: "Çizgi Roman", emoji: "💬", desc: "Marvel/DC stili", category: "Sanat Akımları" },
|
|
||||||
{ id: "SKETCH", label: "Karakalem", emoji: "✍️", desc: "Kalem çizim", category: "Sanat Akımları" },
|
|
||||||
// Modern & Minimal
|
|
||||||
{ id: "MINIMALIST", label: "Minimalist", emoji: "⚪", desc: "Apple estetiği", category: "Modern & Minimal" },
|
|
||||||
{ id: "GLASSMORPHISM", label: "Glassmorphism", emoji: "🔮", desc: "Cam efekti", category: "Modern & Minimal" },
|
|
||||||
{ id: "NEON", label: "Neon Glow", emoji: "💜", desc: "Neon ışıkları", category: "Modern & Minimal" },
|
|
||||||
{ id: "CYBERPUNK", label: "Cyberpunk", emoji: "🤖", desc: "Gelecek distopya", category: "Modern & Minimal" },
|
|
||||||
{ id: "STEAMPUNK", label: "Steampunk", emoji: "⚙️", desc: "Viktoryan mekanik", category: "Modern & Minimal" },
|
|
||||||
{ id: "ABSTRACT", label: "Soyut", emoji: "🔵", desc: "Abstract sanat", category: "Modern & Minimal" },
|
|
||||||
// Fotoğrafik
|
|
||||||
{ id: "PRODUCT", label: "Ürün Fotoğrafı", emoji: "📦", desc: "Studio çekim", category: "Fotoğrafik" },
|
|
||||||
{ id: "FASHION", label: "Moda", emoji: "👗", desc: "Editöryal çekim", category: "Fotoğrafik" },
|
|
||||||
{ id: "AERIAL", label: "Havadan", emoji: "🚁", desc: "Drone görüntüsü", category: "Fotoğrafik" },
|
|
||||||
{ id: "MACRO", label: "Makro", emoji: "🔬", desc: "Yakın çekim", category: "Fotoğrafik" },
|
|
||||||
{ id: "PORTRAIT", label: "Portre", emoji: "🧑", desc: "Portre fotoğraf", category: "Fotoğrafik" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const aspectRatios = [
|
|
||||||
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
|
|
||||||
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
|
|
||||||
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
|
|
||||||
];
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
// Search & Category state
|
|
||||||
const [styleSearch, setStyleSearch] = useState("");
|
|
||||||
const [expandedCategory, setExpandedCategory] = useState<string | null>("Film & Sinema");
|
|
||||||
|
|
||||||
const filteredStyles = useMemo(() => {
|
|
||||||
return videoStyles.filter(s =>
|
|
||||||
s.label.toLowerCase().includes(styleSearch.toLowerCase()) ||
|
|
||||||
s.desc.toLowerCase().includes(styleSearch.toLowerCase())
|
|
||||||
);
|
|
||||||
}, [styleSearch]);
|
|
||||||
|
|
||||||
const groupedStyles = useMemo(() => {
|
|
||||||
return filteredStyles.reduce((acc, curr) => {
|
|
||||||
const cat = curr.category || "Diğer";
|
|
||||||
if (!acc[cat]) acc[cat] = [];
|
|
||||||
acc[cat].push(curr);
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, typeof videoStyles>);
|
|
||||||
}, [filteredStyles]);
|
|
||||||
|
|
||||||
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, // ← topic → prompt (backend alanı)
|
|
||||||
language,
|
|
||||||
videoStyle: style, // ← style → videoStyle (backend alanı)
|
|
||||||
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-violet-500/15 text-violet-400 border border-violet-500/25"
|
|
||||||
: i < currentStep
|
|
||||||
? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/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-emerald-500/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-violet-400" />
|
|
||||||
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-violet-500/40 focus:ring-1 focus:ring-violet-500/20 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 */}
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
|
||||||
<Languages size={14} className="inline mr-1.5 text-cyan-400" />
|
|
||||||
Video Dili
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
|
||||||
{languages.map((lang) => (
|
|
||||||
<button
|
|
||||||
key={lang.code}
|
|
||||||
onClick={() => setLanguage(lang.code)}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col items-center gap-1 py-3 px-2 rounded-xl text-xs transition-all",
|
|
||||||
language === lang.code
|
|
||||||
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
|
|
||||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-[var(--color-border-default)]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="text-lg">{lang.flag}</span>
|
|
||||||
<span className="font-medium">{lang.label}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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 */}
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3">
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] block">
|
|
||||||
<Palette size={14} className="inline mr-1.5 text-violet-400" />
|
|
||||||
Video Stili
|
|
||||||
</label>
|
|
||||||
<div className="relative w-full sm:w-56">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none">
|
|
||||||
<Search size={14} className="text-[var(--color-text-ghost)]" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Stil ara..."
|
|
||||||
value={styleSearch}
|
|
||||||
onChange={(e) => setStyleSearch(e.target.value)}
|
|
||||||
className="w-full pl-8 pr-3 py-1.5 bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-md text-xs text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-violet-500/50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1 custom-scrollbar">
|
|
||||||
{Object.entries(groupedStyles).map(([category, items]) => {
|
|
||||||
const isExpanded = styleSearch ? true : expandedCategory === category;
|
|
||||||
return (
|
|
||||||
<div key={category} className="bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl overflow-hidden">
|
|
||||||
<button
|
|
||||||
onClick={() => !styleSearch && setExpandedCategory(isExpanded ? null : category)}
|
|
||||||
className="w-full flex items-center justify-between p-3 bg-[var(--color-bg-elevated)] hover:bg-[var(--color-bg-surface-hover)] transition-colors"
|
|
||||||
>
|
|
||||||
<span className="text-sm font-medium text-[var(--color-text-secondary)]">{category} <span className="text-[11px] text-[var(--color-text-ghost)] ml-1">({items.length})</span></span>
|
|
||||||
{!styleSearch && (
|
|
||||||
<ChevronDown
|
|
||||||
size={16}
|
|
||||||
className={cn("text-[var(--color-text-ghost)] transition-transform duration-200", isExpanded && "rotate-180")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{isExpanded && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ height: 0, opacity: 0 }}
|
|
||||||
animate={{ height: "auto", opacity: 1 }}
|
|
||||||
exit={{ height: 0, opacity: 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="p-3 pt-0 mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2">
|
|
||||||
{items.map((s) => (
|
|
||||||
<button
|
|
||||||
key={s.id}
|
|
||||||
onClick={() => setStyle(s.id)}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col items-start gap-1 p-2.5 rounded-xl text-left transition-all",
|
|
||||||
style === s.id
|
|
||||||
? "bg-violet-500/12 border border-violet-500/30 glow-violet"
|
|
||||||
: "bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="text-xl mb-0.5">{s.emoji}</span>
|
|
||||||
<span className="text-xs font-semibold leading-tight">{s.label}</span>
|
|
||||||
<span className="text-[10px] text-[var(--color-text-ghost)] leading-tight">{s.desc}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{Object.keys(groupedStyles).length === 0 && (
|
|
||||||
<div className="text-center py-8 text-[var(--color-text-ghost)] text-sm">
|
|
||||||
"{styleSearch}" için sonuç bulunamadı.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{style === "CINEMATIC" && (
|
|
||||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
|
||||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
|
||||||
Özel Sinematik Referans (Opsiyonel)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
|
||||||
value={cinematicReference}
|
|
||||||
onChange={(e) => setCinematicReference(e.target.value)}
|
|
||||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-md py-1.5 px-3 text-sm focus:border-violet-500/50 outline-none transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Süre */}
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
|
||||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
|
||||||
Hedef Süre: <span className="text-violet-400">{duration}s</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={15}
|
|
||||||
max={180}
|
|
||||||
step={5}
|
|
||||||
value={duration}
|
|
||||||
onChange={(e) => setDuration(Number(e.target.value))}
|
|
||||||
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
|
|
||||||
[&::-webkit-slider-thumb]:appearance-none
|
|
||||||
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
|
|
||||||
[&::-webkit-slider-thumb]:rounded-full
|
|
||||||
[&::-webkit-slider-thumb]:bg-violet-500
|
|
||||||
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(139,92,246,0.4)]
|
|
||||||
[&::-webkit-slider-thumb]:cursor-grab"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-between text-[10px] text-[var(--color-text-ghost)] mt-1">
|
|
||||||
<span>15s</span>
|
|
||||||
<span>60s</span>
|
|
||||||
<span>120s</span>
|
|
||||||
<span>180s</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* En-Boy Oranı */}
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
|
||||||
En-Boy Oranı
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{aspectRatios.map((ar) => {
|
|
||||||
const Icon = ar.icon;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={ar.id}
|
|
||||||
onClick={() => setAspectRatio(ar.id)}
|
|
||||||
className={cn(
|
|
||||||
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all",
|
|
||||||
aspectRatio === ar.id
|
|
||||||
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
|
|
||||||
: "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} />
|
|
||||||
<span className="font-semibold">{ar.label}</span>
|
|
||||||
<span className="text-[10px] text-[var(--color-text-ghost)]">{ar.desc}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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-violet-500/20 text-violet-300 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ü" },
|
||||||
@@ -40,20 +47,20 @@ const statusMap: Record<
|
|||||||
},
|
},
|
||||||
scripting: {
|
scripting: {
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
color: "text-blue-400",
|
color: "text-neutral-400",
|
||||||
bgColor: "bg-blue-500/10",
|
bgColor: "bg-neutral-200 dark:bg-neutral-800",
|
||||||
label: "Senaryo",
|
label: "Senaryo",
|
||||||
},
|
},
|
||||||
reviewing: {
|
reviewing: {
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
color: "text-purple-400",
|
color: "text-neutral-500",
|
||||||
bgColor: "bg-purple-500/10",
|
bgColor: "bg-neutral-200 dark:bg-neutral-800",
|
||||||
label: "İnceleme",
|
label: "İnceleme",
|
||||||
},
|
},
|
||||||
rendering: {
|
rendering: {
|
||||||
icon: Video,
|
icon: Video,
|
||||||
color: "text-cyan-400",
|
color: "text-[var(--color-text-primary)]",
|
||||||
bgColor: "bg-cyan-500/10",
|
bgColor: "bg-neutral-200 dark:bg-neutral-800",
|
||||||
label: "Render",
|
label: "Render",
|
||||||
},
|
},
|
||||||
completed: {
|
completed: {
|
||||||
@@ -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 */}
|
||||||
@@ -136,7 +197,7 @@ export default function ProjectsPage() {
|
|||||||
<p className="text-sm text-[var(--color-text-muted)] mt-0.5">
|
<p className="text-sm text-[var(--color-text-muted)] mt-0.5">
|
||||||
Tüm video projelerini yönet
|
Tüm video projelerini yönet
|
||||||
{projects.length > 0 && (
|
{projects.length > 0 && (
|
||||||
<span className="ml-1 text-violet-400">
|
<span className="ml-1 text-[var(--color-text-primary)] font-medium">
|
||||||
({projects.length} proje)
|
({projects.length} proje)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -163,7 +224,7 @@ export default function ProjectsPage() {
|
|||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder="Proje ara..."
|
placeholder="Proje ara..."
|
||||||
className="w-full pl-9 pr-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-violet-500/40 transition-colors"
|
className="w-full pl-9 pr-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-neutral-500 transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,7 +238,7 @@ export default function ProjectsPage() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"px-3.5 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-all",
|
"px-3.5 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-all",
|
||||||
activeFilter === filter.id
|
activeFilter === filter.id
|
||||||
? "bg-violet-500/15 text-violet-400 border border-violet-500/25"
|
? "bg-neutral-200 text-black border border-neutral-400 dark:bg-neutral-800 dark:text-white dark:border-neutral-600"
|
||||||
: "text-[var(--color-text-muted)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]",
|
: "text-[var(--color-text-muted)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -191,7 +252,7 @@ export default function ProjectsPage() {
|
|||||||
<div className="flex flex-col items-center justify-center py-16">
|
<div className="flex flex-col items-center justify-center py-16">
|
||||||
<Loader2
|
<Loader2
|
||||||
size={32}
|
size={32}
|
||||||
className="animate-spin text-violet-400 mb-3"
|
className="animate-spin text-[var(--color-text-primary)] mb-3"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-[var(--color-text-muted)]">
|
<p className="text-sm text-[var(--color-text-muted)]">
|
||||||
Projeler yükleniyor...
|
Projeler yükleniyor...
|
||||||
@@ -221,152 +282,292 @@ export default function ProjectsPage() {
|
|||||||
{!searchQuery && activeFilter === "all" && (
|
{!searchQuery && activeFilter === "all" && (
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/projects/new"
|
href="/dashboard/projects/new"
|
||||||
className="mt-3 text-xs text-violet-400 hover:text-violet-300"
|
className="mt-3 text-xs text-[var(--color-text-primary)] hover:text-[var(--color-text-secondary)] font-medium"
|
||||||
>
|
>
|
||||||
İlk projenizi oluşturun →
|
İlk projenizi oluşturun →
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filtered.map((project) => {
|
rootProjects.map((project) => {
|
||||||
const st = statusMap[project.status] ?? statusMap.draft;
|
const children = childrenMap[project.id] || [];
|
||||||
const StIcon = st.icon;
|
const hasChildren = children.length > 0;
|
||||||
return (
|
const isExpanded = expandedProjects[project.id];
|
||||||
<div
|
|
||||||
key={project.id}
|
const renderCard = (p: ProjectItem, isChild = false) => {
|
||||||
className="flex items-center rounded-xl card hover:border-violet-500/20 transition-all group relative"
|
const st = statusMap[p.status] ?? statusMap.draft;
|
||||||
>
|
const StIcon = st.icon;
|
||||||
<Link
|
return (
|
||||||
href={`/dashboard/projects/${project.id}`}
|
<div
|
||||||
className="flex items-center gap-4 p-4 flex-1 min-w-0"
|
key={p.id}
|
||||||
|
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)]"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<Link
|
||||||
className={`w-10 h-10 rounded-xl ${st.bgColor} flex items-center justify-center shrink-0 ${st.color}`}
|
href={`/dashboard/projects/${p.id}`}
|
||||||
|
className="flex items-center gap-4 p-4 flex-1 min-w-0"
|
||||||
>
|
>
|
||||||
<StIcon size={18} />
|
<div
|
||||||
</div>
|
className={`w-10 h-10 rounded-xl ${st.bgColor} flex items-center justify-center shrink-0 ${st.color}`}
|
||||||
<div className="flex-1 min-w-0">
|
>
|
||||||
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-violet-300 transition-colors">
|
<StIcon size={18} />
|
||||||
{project.title}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]">
|
|
||||||
<span>
|
|
||||||
{new Date(project.createdAt).toLocaleDateString(
|
|
||||||
"tr-TR",
|
|
||||||
{
|
|
||||||
day: "numeric",
|
|
||||||
month: "short",
|
|
||||||
year: "numeric",
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{project.language && <span>• {project.language}</span>}
|
|
||||||
{typeof project.creditsUsed === "number" &&
|
|
||||||
project.creditsUsed > 0 && (
|
|
||||||
<span>• {project.creditsUsed} kredi</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex-1 min-w-0">
|
||||||
<span
|
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-[var(--color-text-secondary)] transition-colors">
|
||||||
className={`text-[10px] font-medium px-2.5 py-1 rounded-full border ${st.color} border-current/20 ${st.bgColor} shrink-0 mr-2`}
|
{p.title}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]">
|
||||||
|
<span>
|
||||||
|
{new Date(p.createdAt).toLocaleDateString(
|
||||||
|
"tr-TR",
|
||||||
|
{
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{p.language && <span>• {p.language}</span>}
|
||||||
|
{typeof p.creditsUsed === "number" &&
|
||||||
|
p.creditsUsed > 0 && (
|
||||||
|
<span>• {p.creditsUsed} kredi</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-[10px] font-medium px-2.5 py-1 rounded-full border ${st.color} border-current/20 ${st.bgColor} shrink-0 mr-2`}
|
||||||
|
>
|
||||||
|
{st.label}
|
||||||
|
</span>
|
||||||
|
<ExternalLink
|
||||||
|
size={14}
|
||||||
|
className="text-[var(--color-text-ghost)] group-hover:text-[var(--color-text-primary)] transition-colors shrink-0"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
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"
|
||||||
>
|
>
|
||||||
{st.label}
|
<Languages size={16} />
|
||||||
</span>
|
</button>
|
||||||
<ExternalLink
|
|
||||||
size={14}
|
<button
|
||||||
className="text-[var(--color-text-ghost)] group-hover:text-violet-400 transition-colors shrink-0"
|
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"
|
||||||
</Link>
|
title="Projeyi Sil"
|
||||||
|
>
|
||||||
<button
|
<Trash2 size={16} />
|
||||||
onClick={(e) => openDeleteConfirm(e, project)}
|
</button>
|
||||||
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"
|
|
||||||
title="Projeyi Sil"
|
{!isChild && hasChildren && (
|
||||||
>
|
<button
|
||||||
<Trash2 size={16} />
|
onClick={(e) => toggleExpand(e, p.id)}
|
||||||
</button>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
};
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ─── Silme Onay Modal ─── */}
|
return (
|
||||||
<AnimatePresence>
|
<div key={project.id} className="space-y-2">
|
||||||
{deleteTarget && (
|
{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>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ─── Silme Onay Modal ─── */}
|
||||||
|
{mounted && createPortal(
|
||||||
|
<div className="portal-container">
|
||||||
|
<AnimatePresence>
|
||||||
|
{deleteTarget && (
|
||||||
|
<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={() => !deleteMutation.isPending && setDeleteTarget(null)}
|
||||||
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
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={() => !deleteMutation.isPending && setDeleteTarget(null)}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<motion.div
|
{/* Kapatma butonu */}
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
<button
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
onClick={() => setDeleteTarget(null)}
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
disabled={deleteMutation.isPending}
|
||||||
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
|
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"
|
||||||
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 */}
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-red-500/10 mb-4">
|
||||||
|
<Trash2 size={22} className="text-red-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* İçerik */}
|
||||||
|
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-1.5">
|
||||||
|
Projeyi Sil
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--color-text-muted)] mb-1">
|
||||||
|
Bu projeyi silmek istediğinize emin misiniz?
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--color-text-ghost)] mb-5 line-clamp-2 italic">
|
||||||
|
“{deleteTarget.title}”
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Butonlar */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setDeleteTarget(null)}
|
onClick={() => setDeleteTarget(null)}
|
||||||
disabled={deleteMutation.isPending}
|
disabled={deleteMutation.isPending}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
<X size={16} />
|
İptal
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
{/* Icon */}
|
onClick={confirmDelete}
|
||||||
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-red-500/10 mb-4">
|
disabled={deleteMutation.isPending}
|
||||||
<Trash2 size={22} className="text-red-400" />
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-red-500 hover:bg-red-600 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
</div>
|
>
|
||||||
|
{deleteMutation.isPending ? (
|
||||||
{/* İçerik */}
|
<>
|
||||||
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-1.5">
|
<Loader2 size={14} className="animate-spin" />
|
||||||
Projeyi Sil
|
Siliniyor...
|
||||||
</h3>
|
</>
|
||||||
<p className="text-sm text-[var(--color-text-muted)] mb-1">
|
) : (
|
||||||
Bu projeyi silmek istediğinize emin misiniz?
|
<>
|
||||||
</p>
|
<Trash2 size={14} />
|
||||||
<p className="text-xs text-[var(--color-text-ghost)] mb-5 line-clamp-2 italic">
|
Evet, Sil
|
||||||
“{deleteTarget.title}”
|
</>
|
||||||
</p>
|
)}
|
||||||
|
</button>
|
||||||
{/* Butonlar */}
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteTarget(null)}
|
|
||||||
disabled={deleteMutation.isPending}
|
|
||||||
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={confirmDelete}
|
|
||||||
disabled={deleteMutation.isPending}
|
|
||||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-red-500 hover:bg-red-600 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{deleteMutation.isPending ? (
|
|
||||||
<>
|
|
||||||
<Loader2 size={14} className="animate-spin" />
|
|
||||||
Siliniyor...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
Evet, Sil
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/* İç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>
|
</AnimatePresence>
|
||||||
</div>
|
</div>,
|
||||||
);
|
document.body
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1301
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+19
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+179
@@ -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;
|
||||||
+885
@@ -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">"{c.text}"</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+233
@@ -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,444 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import {
|
|
||||||
AtSign,
|
|
||||||
Link2,
|
|
||||||
Loader2,
|
|
||||||
ArrowRight,
|
|
||||||
Clock,
|
|
||||||
Palette,
|
|
||||||
Monitor,
|
|
||||||
Smartphone,
|
|
||||||
Square,
|
|
||||||
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";
|
|
||||||
|
|
||||||
const videoStyles = [
|
|
||||||
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬" },
|
|
||||||
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹" },
|
|
||||||
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚" },
|
|
||||||
{ id: "STORYTELLING", label: "Hikâye", emoji: "📖" },
|
|
||||||
{ id: "NEWS", label: "Haber", emoji: "📰" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const aspectRatios = [
|
|
||||||
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
|
|
||||||
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
|
|
||||||
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const languages = [
|
|
||||||
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
|
|
||||||
{ code: "en", label: "English", flag: "🇺🇸" },
|
|
||||||
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
|
||||||
{ code: "es", label: "Español", flag: "🇪🇸" },
|
|
||||||
];
|
|
||||||
|
|
||||||
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">
|
|
||||||
{/* Header */}
|
|
||||||
<div>
|
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-violet-500/10 border border-violet-500/20 text-violet-300 text-xs font-medium mb-3">
|
|
||||||
<AtSign size={12} />
|
|
||||||
X → Video
|
|
||||||
</div>
|
|
||||||
<h1 className="font-[family-name:var(--font-display)] text-2xl md:text-3xl font-bold">
|
|
||||||
Tweet'ten Video Oluştur
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
|
||||||
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-violet-500/40 focus:ring-1 focus:ring-violet-500/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-violet-400">
|
|
||||||
<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-cyan-400">
|
|
||||||
<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-cyan-400">
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
{/* Language */}
|
|
||||||
<div className="card p-5 space-y-3">
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
|
||||||
Video Dili
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{languages.map((l) => (
|
|
||||||
<button
|
|
||||||
key={l.code}
|
|
||||||
onClick={() => setLanguage(l.code)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
|
||||||
language === l.code
|
|
||||||
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
|
|
||||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{l.flag}</span>
|
|
||||||
{l.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Style */}
|
|
||||||
<div className="card p-5 space-y-3">
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
|
||||||
<Palette size={14} className="inline mr-1.5 text-violet-400" />
|
|
||||||
Video Stili
|
|
||||||
</label>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{videoStyles.map((s) => (
|
|
||||||
<button
|
|
||||||
key={s.id}
|
|
||||||
onClick={() => setStyle(s.id)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
|
||||||
style === s.id
|
|
||||||
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
|
|
||||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{s.emoji}</span>
|
|
||||||
{s.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{style === "CINEMATIC" && (
|
|
||||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
|
||||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
|
||||||
Özel Sinematik Referans (Opsiyonel)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
|
||||||
value={cinematicReference}
|
|
||||||
onChange={(e) => setCinematicReference(e.target.value)}
|
|
||||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-xl py-2 px-3 text-sm focus:border-violet-500/50 outline-none transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Duration + Aspect Ratio */}
|
|
||||||
<div className="card p-5 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
|
||||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
|
||||||
Hedef Süre: <span className="text-violet-400">{duration}s</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={15}
|
|
||||||
max={120}
|
|
||||||
step={5}
|
|
||||||
value={duration}
|
|
||||||
onChange={(e) => setDuration(Number(e.target.value))}
|
|
||||||
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
|
|
||||||
[&::-webkit-slider-thumb]:appearance-none
|
|
||||||
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
|
|
||||||
[&::-webkit-slider-thumb]:rounded-full
|
|
||||||
[&::-webkit-slider-thumb]:bg-violet-500
|
|
||||||
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(139,92,246,0.4)]
|
|
||||||
[&::-webkit-slider-thumb]:cursor-grab"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
|
||||||
En-Boy Oranı
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{aspectRatios.map((ar) => {
|
|
||||||
const Icon = ar.icon;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={ar.id}
|
|
||||||
onClick={() => setAspectRatio(ar.id)}
|
|
||||||
className={cn(
|
|
||||||
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all",
|
|
||||||
aspectRatio === ar.id
|
|
||||||
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
|
|
||||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon size={20} />
|
|
||||||
<span className="font-semibold">{ar.label}</span>
|
|
||||||
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
|
||||||
{ar.desc}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Generate Button */}
|
|
||||||
<button
|
|
||||||
onClick={handleGenerate}
|
|
||||||
disabled={createFromTweet.isPending}
|
|
||||||
className={cn(
|
|
||||||
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
|
|
||||||
createFromTweet.isPending
|
|
||||||
? "bg-violet-500/20 text-violet-300 cursor-wait"
|
|
||||||
: "btn-primary text-lg",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{createFromTweet.isPending ? (
|
|
||||||
<>
|
|
||||||
<Loader2 size={20} className="animate-spin" />
|
|
||||||
<span>Video Projesi Oluşturuluyor...</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Wand2 size={20} />
|
|
||||||
<span>Tweet → Video Oluştur</span>
|
|
||||||
<ArrowRight size={16} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<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-gradient-to-br from-violet-500/5 to-cyan-500/5">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<Sparkles size={20} className="text-violet-400 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,265 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import {
|
|
||||||
Link2,
|
|
||||||
Loader2,
|
|
||||||
ArrowRight,
|
|
||||||
Clock,
|
|
||||||
Palette,
|
|
||||||
Monitor,
|
|
||||||
Smartphone,
|
|
||||||
Square,
|
|
||||||
Sparkles,
|
|
||||||
Wand2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { useCreateFromYoutube } from "@/hooks/use-api";
|
|
||||||
import { useToast } from "@/components/ui/toast";
|
|
||||||
|
|
||||||
const videoStyles = [
|
|
||||||
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬" },
|
|
||||||
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹" },
|
|
||||||
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚" },
|
|
||||||
{ id: "STORYTELLING", label: "Hikâye", emoji: "📖" },
|
|
||||||
{ id: "NEWS", label: "Haber", emoji: "📰" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const aspectRatios = [
|
|
||||||
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
|
|
||||||
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
|
|
||||||
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const languages = [
|
|
||||||
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
|
|
||||||
{ code: "en", label: "English", flag: "🇺🇸" },
|
|
||||||
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
|
||||||
{ code: "es", label: "Español", flag: "🇪🇸" },
|
|
||||||
];
|
|
||||||
|
|
||||||
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 handleGenerate = async () => {
|
|
||||||
if (!youtubeUrl.includes("youtube.com") && !youtubeUrl.includes("youtu.be")) {
|
|
||||||
toast("error", "Lütfen geçerli bir YouTube tam linki girin.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
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 (error) {
|
|
||||||
toast("error", "Proje oluşturulurken 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-red-500/10 text-red-500 mb-2 ring-1 ring-red-500/20 shadow-[0_0_30px_rgba(239,68,68,0.15)]">
|
|
||||||
<Link2 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 Üret
|
|
||||||
</h1>
|
|
||||||
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
|
|
||||||
YouTube linkini yapıştırın, yapay zeka ana içeriği çıkartıp viral bir Reels/Shorts yaratsın.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Input */}
|
|
||||||
<div className="card p-6 md:p-8 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
|
|
||||||
YouTube URL
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="https://www.youtube.com/watch?v=..."
|
|
||||||
value={youtubeUrl}
|
|
||||||
onChange={(e) => setYoutubeUrl(e.target.value)}
|
|
||||||
className="w-full bg-[var(--color-bg-surface)] border-2 border-[var(--color-border-faint)] rounded-2xl py-4 pl-12 pr-4 text-sm
|
|
||||||
focus:border-red-500/50 focus:ring-4 focus:ring-red-500/10 transition-all outline-none"
|
|
||||||
/>
|
|
||||||
<Link2
|
|
||||||
size={18}
|
|
||||||
className="absolute left-4 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Video Settings */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="card p-5 space-y-3">
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
|
||||||
Video Dili
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{languages.map((l) => (
|
|
||||||
<button
|
|
||||||
key={l.code}
|
|
||||||
onClick={() => setLanguage(l.code)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
|
||||||
language === l.code
|
|
||||||
? "bg-red-500/12 border border-red-500/30 text-red-400"
|
|
||||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{l.flag}</span>
|
|
||||||
{l.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card p-5 space-y-3">
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
|
||||||
<Palette size={14} className="inline mr-1.5 text-red-400" />
|
|
||||||
Video Stili
|
|
||||||
</label>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{videoStyles.map((s) => (
|
|
||||||
<button
|
|
||||||
key={s.id}
|
|
||||||
onClick={() => setStyle(s.id)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
|
||||||
style === s.id
|
|
||||||
? "bg-red-500/12 border border-red-500/30 text-red-400"
|
|
||||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{s.emoji}</span>
|
|
||||||
{s.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{style === "CINEMATIC" && (
|
|
||||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
|
||||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
|
||||||
Özel Sinematik Referans (Opsiyonel)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
|
||||||
value={cinematicReference}
|
|
||||||
onChange={(e) => setCinematicReference(e.target.value)}
|
|
||||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-xl py-2 px-3 text-sm focus:border-red-500/50 outline-none transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card p-5 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
|
||||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
|
||||||
Hedef Süre: <span className="text-red-400">{duration}s</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={15}
|
|
||||||
max={120}
|
|
||||||
step={5}
|
|
||||||
value={duration}
|
|
||||||
onChange={(e) => setDuration(Number(e.target.value))}
|
|
||||||
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
|
|
||||||
[&::-webkit-slider-thumb]:appearance-none
|
|
||||||
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
|
|
||||||
[&::-webkit-slider-thumb]:rounded-full
|
|
||||||
[&::-webkit-slider-thumb]:bg-red-500
|
|
||||||
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(239,68,68,0.4)]
|
|
||||||
[&::-webkit-slider-thumb]:cursor-grab"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
|
||||||
En-Boy Oranı
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{aspectRatios.map((ar) => {
|
|
||||||
const Icon = ar.icon;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={ar.id}
|
|
||||||
onClick={() => setAspectRatio(ar.id)}
|
|
||||||
className={cn(
|
|
||||||
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all",
|
|
||||||
aspectRatio === ar.id
|
|
||||||
? "bg-red-500/12 border border-red-500/30 text-red-400"
|
|
||||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon size={20} />
|
|
||||||
<span className="font-semibold">{ar.label}</span>
|
|
||||||
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
|
||||||
{ar.desc}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleGenerate}
|
|
||||||
disabled={createFromYoutube.isPending || !youtubeUrl}
|
|
||||||
className={cn(
|
|
||||||
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
|
|
||||||
createFromYoutube.isPending
|
|
||||||
? "bg-red-500/20 text-red-400 cursor-wait"
|
|
||||||
: "bg-red-500 hover:bg-red-600 text-white shadow-lg shadow-red-500/20",
|
|
||||||
!youtubeUrl && "opacity-50 cursor-not-allowed"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{createFromYoutube.isPending ? (
|
|
||||||
<>
|
|
||||||
<Loader2 size={20} className="animate-spin" />
|
|
||||||
<span>Video Projesi Oluşturuluyor... (Bu işlem uzun sürebilir)</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Wand2 size={20} />
|
|
||||||
<span>YouTube → Video Oluştur</span>
|
|
||||||
<ArrowRight size={16} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+71
-117
@@ -2,65 +2,63 @@
|
|||||||
|
|
||||||
/* ================================================
|
/* ================================================
|
||||||
ContentGen AI — Design System
|
ContentGen AI — Design System
|
||||||
Aesthetic Direction: Cinematic Dark + Violet Neon
|
Aesthetic Direction: Premium Monochrome & High Contrast
|
||||||
Frontend Design Skill: Bold, intentional, unforgettable
|
Frontend Design Skill: Clean, spacious, highly legible
|
||||||
================================================ */
|
================================================ */
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* ── Color Palette ── */
|
/* ── Color Palette (Monochrome Focus) ── */
|
||||||
--color-bg-void: #000000;
|
--color-bg-void: #000000;
|
||||||
--color-bg-deep: #050509;
|
--color-bg-deep: #0a0a0a;
|
||||||
--color-bg-base: #0a0a12;
|
--color-bg-base: #111111;
|
||||||
--color-bg-surface: #111120;
|
--color-bg-surface: #1a1a1a;
|
||||||
--color-bg-elevated: #1a1a2e;
|
--color-bg-elevated: #222222;
|
||||||
--color-bg-subtle: #232340;
|
--color-bg-subtle: #2a2a2a;
|
||||||
|
|
||||||
--color-border-faint: #1e1e3a;
|
--color-border-faint: rgba(255, 255, 255, 0.05);
|
||||||
--color-border-default: #2a2a4a;
|
--color-border-default: rgba(255, 255, 255, 0.1);
|
||||||
--color-border-strong: #3a3a5a;
|
--color-border-strong: rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
--color-text-primary: #f0f0ff;
|
--color-text-primary: #ffffff;
|
||||||
--color-text-secondary: #a0a0c0;
|
--color-text-secondary: #a3a3a3;
|
||||||
--color-text-muted: #6a6a8a;
|
--color-text-muted: #737373;
|
||||||
--color-text-ghost: #4a4a6a;
|
--color-text-ghost: #525252;
|
||||||
|
|
||||||
/* Brand */
|
/* Accent: Clean White/Gray (Replaces Neon) */
|
||||||
--color-violet-400: #a78bfa;
|
--color-accent-400: #e5e5e5;
|
||||||
--color-violet-500: #8b5cf6;
|
--color-accent-500: #ffffff;
|
||||||
--color-violet-600: #7c3aed;
|
--color-accent-glow: rgba(255, 255, 255, 0.08);
|
||||||
--color-violet-700: #6d28d9;
|
|
||||||
--color-violet-glow: rgba(139, 92, 246, 0.15);
|
|
||||||
|
|
||||||
--color-cyan-400: #22d3ee;
|
|
||||||
--color-cyan-500: #06b6d4;
|
|
||||||
--color-cyan-glow: rgba(6, 182, 212, 0.12);
|
|
||||||
|
|
||||||
--color-emerald-400: #34d399;
|
|
||||||
--color-emerald-500: #10b981;
|
|
||||||
|
|
||||||
|
/* Status Colors (Subtle versions) */
|
||||||
|
--color-emerald-400: #4ade80;
|
||||||
|
--color-emerald-500: #22c55e;
|
||||||
|
|
||||||
--color-amber-400: #fbbf24;
|
--color-amber-400: #fbbf24;
|
||||||
--color-amber-500: #f59e0b;
|
--color-amber-500: #f59e0b;
|
||||||
|
|
||||||
--color-rose-400: #fb7185;
|
--color-rose-400: #fb7185;
|
||||||
--color-rose-500: #f43f5e;
|
--color-rose-500: #f43f5e;
|
||||||
|
|
||||||
/* ── Spacing ── */
|
--color-cyan-400: #38bdf8; /* Kept for processing, but subdued */
|
||||||
--spacing-page: clamp(1rem, 4vw, 2.5rem);
|
--color-cyan-500: #0ea5e9;
|
||||||
|
|
||||||
/* ── Radius ── */
|
/* ── Spacing (Increased for breathable UI) ── */
|
||||||
--radius-sm: 0.375rem;
|
--spacing-page: clamp(1.5rem, 5vw, 3rem);
|
||||||
--radius-md: 0.625rem;
|
|
||||||
--radius-lg: 0.875rem;
|
/* ── Radius (Sharper, more professional) ── */
|
||||||
--radius-xl: 1.25rem;
|
--radius-sm: 0.25rem;
|
||||||
|
--radius-md: 0.5rem;
|
||||||
|
--radius-lg: 0.75rem;
|
||||||
|
--radius-xl: 1rem;
|
||||||
--radius-2xl: 1.5rem;
|
--radius-2xl: 1.5rem;
|
||||||
--radius-full: 9999px;
|
--radius-full: 9999px;
|
||||||
|
|
||||||
/* ── Shadows ── */
|
/* ── Shadows (Soft, realistic depth instead of glows) ── */
|
||||||
--shadow-glow-sm: 0 0 12px rgba(139, 92, 246, 0.08);
|
--shadow-glow-sm: 0 4px 14px rgba(0, 0, 0, 0.5);
|
||||||
--shadow-glow-md: 0 0 24px rgba(139, 92, 246, 0.12);
|
--shadow-glow-md: 0 10px 30px rgba(0, 0, 0, 0.6);
|
||||||
--shadow-glow-lg: 0 0 48px rgba(139, 92, 246, 0.18);
|
--shadow-glow-lg: 0 20px 40px rgba(0, 0, 0, 0.8);
|
||||||
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.2);
|
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.8), 0 8px 16px rgba(0, 0, 0, 0.4);
|
||||||
--shadow-elevated: 0 8px 32px rgba(0, 0, 0, 0.5);
|
--shadow-elevated: 0 12px 32px rgba(0, 0, 0, 0.6);
|
||||||
|
|
||||||
/* ── Animations ── */
|
/* ── Animations ── */
|
||||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
@@ -82,11 +80,14 @@ body {
|
|||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
|
/* Slightly larger base text */
|
||||||
|
font-size: 1.05rem;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Selection ── */
|
/* ── Selection ── */
|
||||||
::selection {
|
::selection {
|
||||||
background-color: rgba(139, 92, 246, 0.3);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,55 +107,18 @@ body {
|
|||||||
background: var(--color-border-strong);
|
background: var(--color-border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Glass Effect ── */
|
/* ── Glass Effect (Desaturated, High Blur) ── */
|
||||||
.glass {
|
.glass {
|
||||||
background: rgba(17, 17, 32, 0.6);
|
background: rgba(10, 10, 10, 0.7);
|
||||||
backdrop-filter: blur(16px) saturate(180%);
|
backdrop-filter: blur(24px) saturate(100%);
|
||||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
-webkit-backdrop-filter: blur(24px) saturate(100%);
|
||||||
border: 1px solid var(--color-border-faint);
|
border: 1px solid var(--color-border-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-subtle {
|
.glass-subtle {
|
||||||
background: rgba(17, 17, 32, 0.35);
|
background: rgba(10, 10, 10, 0.4);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(8px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Glow Effects ── */
|
|
||||||
.glow-violet {
|
|
||||||
box-shadow: 0 0 20px rgba(139, 92, 246, 0.15),
|
|
||||||
0 0 40px rgba(139, 92, 246, 0.08),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-cyan {
|
|
||||||
box-shadow: 0 0 20px rgba(6, 182, 212, 0.15),
|
|
||||||
0 0 40px rgba(6, 182, 212, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Gradient Borders ── */
|
|
||||||
.gradient-border {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.gradient-border::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
border-radius: inherit;
|
|
||||||
padding: 1px;
|
|
||||||
background: linear-gradient(135deg, var(--color-violet-500), var(--color-cyan-400));
|
|
||||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
||||||
mask-composite: exclude;
|
|
||||||
-webkit-mask-composite: xor;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Animated Gradient Background ── */
|
|
||||||
.gradient-mesh {
|
|
||||||
background-image:
|
|
||||||
radial-gradient(ellipse at 20% 50%, var(--color-violet-glow) 0%, transparent 50%),
|
|
||||||
radial-gradient(ellipse at 80% 20%, var(--color-cyan-glow) 0%, transparent 50%),
|
|
||||||
radial-gradient(ellipse at 50% 80%, rgba(16, 185, 129, 0.06) 0%, transparent 50%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Card Styles ── */
|
/* ── Card Styles ── */
|
||||||
@@ -163,58 +127,48 @@ body {
|
|||||||
border: 1px solid var(--color-border-faint);
|
border: 1px solid var(--color-border-faint);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius-xl);
|
||||||
transition: all var(--duration-normal) var(--ease-out-expo);
|
transition: all var(--duration-normal) var(--ease-out-expo);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
}
|
}
|
||||||
.card-surface:hover {
|
.card-surface:hover {
|
||||||
border-color: var(--color-border-default);
|
border-color: var(--color-border-default);
|
||||||
box-shadow: var(--shadow-glow-sm);
|
box-shadow: var(--shadow-glow-sm);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Badge Styles ── */
|
/* ── Badge Styles (Minimalist) ── */
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.125rem 0.625rem;
|
padding: 0.25rem 0.75rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
border-radius: var(--radius-full);
|
border-radius: var(--radius-full);
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.03em;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.badge-violet {
|
.badge-violet, .badge-cyan {
|
||||||
background: rgba(139, 92, 246, 0.12);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
color: var(--color-violet-400);
|
color: var(--color-text-primary);
|
||||||
border: 1px solid rgba(139, 92, 246, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
}
|
|
||||||
.badge-cyan {
|
|
||||||
background: rgba(6, 182, 212, 0.12);
|
|
||||||
color: var(--color-cyan-400);
|
|
||||||
border: 1px solid rgba(6, 182, 212, 0.2);
|
|
||||||
}
|
}
|
||||||
.badge-emerald {
|
.badge-emerald {
|
||||||
background: rgba(16, 185, 129, 0.12);
|
background: rgba(74, 222, 128, 0.1);
|
||||||
color: var(--color-emerald-400);
|
color: var(--color-emerald-400);
|
||||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
border: 1px solid rgba(74, 222, 128, 0.2);
|
||||||
}
|
}
|
||||||
.badge-amber {
|
.badge-amber {
|
||||||
background: rgba(245, 158, 11, 0.12);
|
background: rgba(251, 191, 36, 0.1);
|
||||||
color: var(--color-amber-400);
|
color: var(--color-amber-400);
|
||||||
border: 1px solid rgba(245, 158, 11, 0.2);
|
border: 1px solid rgba(251, 191, 36, 0.2);
|
||||||
}
|
}
|
||||||
.badge-rose {
|
.badge-rose {
|
||||||
background: rgba(244, 63, 94, 0.12);
|
background: rgba(251, 113, 133, 0.1);
|
||||||
color: var(--color-rose-400);
|
color: var(--color-rose-400);
|
||||||
border: 1px solid rgba(244, 63, 94, 0.2);
|
border: 1px solid rgba(251, 113, 133, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Button Styles ── */
|
/* ── Button Styles ── */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, var(--color-violet-600), var(--color-violet-500));
|
|
||||||
color: white;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: 0.625rem 1.25rem;
|
|
||||||
transition: all var(--duration-normal) var(--ease-out-expo);
|
|
||||||
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.25);
|
|
||||||
}
|
}
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.4);
|
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.4);
|
||||||
@@ -246,7 +200,7 @@ body {
|
|||||||
right: 0;
|
right: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
padding: 0.5rem 0 calc(0.5rem + env(safe-area-inset-bottom));
|
padding: 0.5rem 0 calc(0.5rem + env(safe-area-inset-bottom));
|
||||||
background: rgba(5, 5, 9, 0.85);
|
background: rgba(10, 10, 10, 0.85);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
-webkit-backdrop-filter: blur(20px);
|
||||||
border-top: 1px solid var(--color-border-faint);
|
border-top: 1px solid var(--color-border-faint);
|
||||||
@@ -262,7 +216,7 @@ body {
|
|||||||
.progress-bar-fill {
|
.progress-bar-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: var(--radius-full);
|
border-radius: var(--radius-full);
|
||||||
background: linear-gradient(90deg, var(--color-violet-500), var(--color-cyan-400));
|
background: var(--color-text-primary);
|
||||||
transition: width var(--duration-slow) var(--ease-out-expo);
|
transition: width var(--duration-slow) var(--ease-out-expo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
session.user.id = token.id;
|
if (session.user) {
|
||||||
session.user.roles = token.roles;
|
session.user.id = token.id;
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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!",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
AreaChart,
|
AreaChart,
|
||||||
Area,
|
Area,
|
||||||
@@ -13,7 +14,7 @@ import {
|
|||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { useDashboardStats } from "@/hooks/use-api";
|
import { useDashboardStats } from "@/hooks/use-api";
|
||||||
|
|
||||||
const COLORS = ["#8b5cf6", "#06b6d4", "#f59e0b", "#ef4444", "#10b981"];
|
const COLORS = ["#06b6d4", "#8b5cf6", "#3b82f6", "#6366f1"];
|
||||||
|
|
||||||
function formatWeekData(stats: Record<string, unknown> | undefined) {
|
function formatWeekData(stats: Record<string, unknown> | undefined) {
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
@@ -59,19 +60,25 @@ function formatPieData(stats: Record<string, unknown> | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardCharts() {
|
export function DashboardCharts() {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
const { data, isLoading } = useDashboardStats();
|
const { data, isLoading } = useDashboardStats();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const stats = (data as any)?.data ?? data;
|
const stats = (data as any)?.data ?? data;
|
||||||
const weekData = formatWeekData(stats);
|
const weekData = formatWeekData(stats);
|
||||||
const pieData = formatPieData(stats);
|
const pieData = formatPieData(stats);
|
||||||
|
|
||||||
if (isLoading) {
|
if (!mounted || isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
||||||
{[1, 2].map((i) => (
|
{[1, 2].map((i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="card p-5 h-[280px] animate-pulse bg-[var(--color-bg-surface)]"
|
className="card p-6 md:p-8 h-[300px] animate-pulse bg-[var(--color-bg-surface)] rounded-2xl border border-[var(--color-border-faint)]"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -81,51 +88,51 @@ export function DashboardCharts() {
|
|||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
||||||
{/* Haftalik Aktivite */}
|
{/* Haftalik Aktivite */}
|
||||||
<div className="card p-5">
|
<div className="card-surface p-6 md:p-8">
|
||||||
<h3 className="text-sm font-semibold text-[var(--color-text-secondary)] mb-4">
|
<h3 className="text-base font-semibold text-[var(--color-text-secondary)] mb-6">
|
||||||
Haftalık Aktivite
|
Haftalık Aktivite
|
||||||
</h3>
|
</h3>
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
<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="#8b5cf6" stopOpacity={0.3} />
|
|
||||||
<stop offset="95%" stopColor="#8b5cf6" stopOpacity={0} />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="colorVideos" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="5%" stopColor="#06b6d4" stopOpacity={0.3} />
|
<stop offset="5%" stopColor="#06b6d4" stopOpacity={0.3} />
|
||||||
<stop offset="95%" stopColor="#06b6d4" stopOpacity={0} />
|
<stop offset="95%" stopColor="#06b6d4" stopOpacity={0} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
<linearGradient id="colorVideos" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.3} />
|
||||||
|
<stop offset="95%" stopColor="#8b5cf6" stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="name"
|
dataKey="name"
|
||||||
tick={{ fontSize: 11, fill: "var(--color-text-ghost)" }}
|
tick={{ fontSize: 12, fill: "var(--color-text-ghost)" }}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
/>
|
/>
|
||||||
<YAxis hide />
|
<YAxis hide />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
backgroundColor: "rgba(15,15,30,0.9)",
|
backgroundColor: "rgba(10,10,10,0.95)",
|
||||||
border: "1px solid rgba(139,92,246,0.2)",
|
border: "1px solid rgba(255,255,255,0.1)",
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
fontSize: 12,
|
fontSize: 13,
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="projects"
|
dataKey="projects"
|
||||||
stroke="#8b5cf6"
|
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="#06b6d4"
|
stroke="#8b5cf6"
|
||||||
strokeWidth={2}
|
strokeWidth={3}
|
||||||
fill="url(#colorVideos)"
|
fill="url(#colorVideos)"
|
||||||
name="Videolar"
|
name="Videolar"
|
||||||
/>
|
/>
|
||||||
@@ -134,55 +141,65 @@ export function DashboardCharts() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Proje Durumu */}
|
{/* Proje Durumu */}
|
||||||
<div className="card p-5">
|
<div className="card-surface p-6 md:p-8 flex flex-col">
|
||||||
<h3 className="text-sm font-semibold text-[var(--color-text-secondary)] mb-4">
|
<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-[200px] 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-4">
|
<div className="flex flex-1 flex-row items-center justify-center gap-4 sm:gap-8 min-h-[220px]">
|
||||||
<ResponsiveContainer width="50%" height={200}>
|
<div className="w-[160px] h-[160px] sm:w-[200px] sm:h-[200px]">
|
||||||
<PieChart>
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<Pie
|
<PieChart>
|
||||||
data={pieData}
|
<Pie
|
||||||
cx="50%"
|
data={pieData}
|
||||||
cy="50%"
|
cx="50%"
|
||||||
outerRadius={70}
|
cy="50%"
|
||||||
innerRadius={40}
|
outerRadius="85%"
|
||||||
dataKey="value"
|
innerRadius="65%"
|
||||||
stroke="none"
|
dataKey="value"
|
||||||
>
|
stroke="var(--color-bg-surface)"
|
||||||
{pieData.map((_: unknown, index: number) => (
|
strokeWidth={3}
|
||||||
<Cell key={index} fill={COLORS[index % COLORS.length]} />
|
paddingAngle={4}
|
||||||
))}
|
>
|
||||||
</Pie>
|
{pieData.map((_: unknown, index: number) => (
|
||||||
<Tooltip
|
<Cell key={index} fill={COLORS[index % COLORS.length]} />
|
||||||
contentStyle={{
|
))}
|
||||||
backgroundColor: "rgba(15,15,30,0.9)",
|
</Pie>
|
||||||
border: "1px solid rgba(139,92,246,0.2)",
|
<Tooltip
|
||||||
borderRadius: 12,
|
contentStyle={{
|
||||||
fontSize: 12,
|
backgroundColor: "rgba(10,10,10,0.95)",
|
||||||
color: "#fff",
|
border: "1px solid rgba(255,255,255,0.1)",
|
||||||
}}
|
borderRadius: 12,
|
||||||
/>
|
fontSize: 13,
|
||||||
</PieChart>
|
color: "#fff",
|
||||||
</ResponsiveContainer>
|
boxShadow: "0 4px 20px rgba(0,0,0,0.3)"
|
||||||
<div className="space-y-2">
|
}}
|
||||||
{pieData.map((item: { name: string; value: number }, idx: number) => (
|
itemStyle={{
|
||||||
<div key={item.name} className="flex items-center gap-2">
|
color: "#e5e5e5"
|
||||||
<div
|
}}
|
||||||
className="w-2.5 h-2.5 rounded-full"
|
|
||||||
style={{ backgroundColor: COLORS[idx % COLORS.length] }}
|
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-[var(--color-text-muted)]">
|
</PieChart>
|
||||||
{item.name}
|
</ResponsiveContainer>
|
||||||
</span>
|
</div>
|
||||||
<span className="text-xs font-bold text-[var(--color-text-secondary)] ml-auto">
|
<div className="flex flex-col justify-center space-y-3">
|
||||||
{item.value}
|
{pieData.map((item: { name: string; value: number }, idx: number) => (
|
||||||
</span>
|
<div key={item.name} className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full shadow-sm"
|
||||||
|
style={{ backgroundColor: COLORS[idx % COLORS.length], boxShadow: `0 0 8px ${COLORS[idx % COLORS.length]}80` }}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm text-[var(--color-text-muted)] font-medium leading-none mb-1">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-bold text-[var(--color-text-primary)] leading-none">
|
||||||
|
{item.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function RecentProjects() {
|
|||||||
</h3>
|
</h3>
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/projects"
|
href="/dashboard/projects"
|
||||||
className="text-xs text-violet-400 hover:text-violet-300 flex items-center gap-1 transition-colors"
|
className="text-xs text-neutral-400 hover:text-neutral-300 flex items-center gap-1 transition-colors"
|
||||||
>
|
>
|
||||||
Tümü <ExternalLink size={12} />
|
Tümü <ExternalLink size={12} />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -61,7 +61,7 @@ export function RecentProjects() {
|
|||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/projects/new"
|
href="/dashboard/projects/new"
|
||||||
className="mt-3 text-xs text-violet-400 hover:text-violet-300"
|
className="mt-3 text-xs text-neutral-400 hover:text-neutral-300"
|
||||||
>
|
>
|
||||||
İlk projenizi oluşturun →
|
İlk projenizi oluşturun →
|
||||||
</Link>
|
</Link>
|
||||||
@@ -89,7 +89,7 @@ export function RecentProjects() {
|
|||||||
<StIcon size={14} />
|
<StIcon size={14} />
|
||||||
</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-violet-300 transition-colors">
|
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-neutral-300 transition-colors">
|
||||||
{project.title}
|
{project.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] text-[var(--color-text-ghost)]">
|
<p className="text-[10px] text-[var(--color-text-ghost)]">
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
|||||||
<div className="card-surface overflow-hidden">
|
<div className="card-surface overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3 p-4 md:p-5 border-b border-[var(--color-border-faint)]">
|
<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-sky-500/15 flex items-center justify-center">
|
<div className="w-9 h-9 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] flex items-center justify-center">
|
||||||
<XIcon size={18} className="text-sky-400" />
|
<XIcon size={18} className="text-neutral-300" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-[family-name:var(--font-display)] text-sm font-semibold">
|
<h3 className="font-[family-name:var(--font-display)] text-sm font-semibold">
|
||||||
@@ -128,7 +128,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
|||||||
}}
|
}}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="https://x.com/user/status/123..."
|
placeholder="https://x.com/user/status/123..."
|
||||||
className="w-full h-11 pl-10 pr-24 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-sky-500/50 focus:ring-1 focus:ring-sky-500/25 outline-none transition-all"
|
className="w-full h-11 pl-10 pr-24 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-neutral-500/50 focus:ring-1 focus:ring-neutral-500/25 outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={preview ? handleCreate : handlePreview}
|
onClick={preview ? handleCreate : handlePreview}
|
||||||
@@ -136,7 +136,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
|||||||
className={cn(
|
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",
|
"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",
|
||||||
isUrlValid && !isLoadingPreview && !isCreatingProject
|
isUrlValid && !isLoadingPreview && !isCreatingProject
|
||||||
? "bg-sky-500 text-white hover:bg-sky-400 shadow-sm"
|
? "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"
|
: "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] cursor-not-allowed"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -200,7 +200,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
|||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<XIcon size={16} className="text-sky-400" />
|
<XIcon size={16} className="text-neutral-400" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -211,7 +211,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
|||||||
{preview.tweet.author.verified && (
|
{preview.tweet.author.verified && (
|
||||||
<CheckCircle2
|
<CheckCircle2
|
||||||
size={13}
|
size={13}
|
||||||
className="text-sky-400 fill-sky-400"
|
className="text-neutral-400 fill-neutral-400"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -304,18 +304,18 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
|||||||
|
|
||||||
{/* Info badges */}
|
{/* Info badges */}
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
<span className="badge badge-cyan text-[10px]">
|
<span className="bg-neutral-500/10 text-neutral-400 border border-neutral-500/20 px-2 py-0.5 rounded-md text-[10px]">
|
||||||
{preview.contentType === "thread"
|
{preview.contentType === "thread"
|
||||||
? "Thread"
|
? "Thread"
|
||||||
: preview.contentType === "quote_tweet"
|
: preview.contentType === "quote_tweet"
|
||||||
? "Alıntı"
|
? "Alıntı"
|
||||||
: "Tweet"}
|
: "Tweet"}
|
||||||
</span>
|
</span>
|
||||||
<span className="badge badge-violet text-[10px]">
|
<span className="bg-neutral-500/10 text-neutral-400 border border-neutral-500/20 px-2 py-0.5 rounded-md text-[10px]">
|
||||||
~{preview.estimatedDuration}s video
|
~{preview.estimatedDuration}s video
|
||||||
</span>
|
</span>
|
||||||
{preview.tweet.media.length > 0 && (
|
{preview.tweet.media.length > 0 && (
|
||||||
<span className="badge badge-amber text-[10px]">
|
<span className="bg-neutral-500/10 text-neutral-400 border border-neutral-500/20 px-2 py-0.5 rounded-md text-[10px]">
|
||||||
{preview.tweet.media.length} medya
|
{preview.tweet.media.length} medya
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -327,7 +327,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
|||||||
value={customTitle}
|
value={customTitle}
|
||||||
onChange={(e) => setCustomTitle(e.target.value)}
|
onChange={(e) => setCustomTitle(e.target.value)}
|
||||||
placeholder={preview.suggestedTitle}
|
placeholder={preview.suggestedTitle}
|
||||||
className="w-full h-10 px-3 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)]/50 focus:border-violet-500/50 focus:ring-1 focus:ring-violet-500/25 outline-none transition-all"
|
className="w-full h-10 px-3 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)]/50 focus:border-neutral-500/50 focus:ring-1 focus:ring-neutral-500/25 outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Create Button */}
|
{/* Create Button */}
|
||||||
@@ -337,8 +337,8 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"w-full h-11 rounded-xl text-sm font-semibold shadow-sm transition-all flex items-center justify-center gap-2",
|
"w-full h-11 rounded-xl text-sm font-semibold shadow-sm transition-all flex items-center justify-center gap-2",
|
||||||
isCreatingProject
|
isCreatingProject
|
||||||
? "bg-violet-600/50 text-violet-200 cursor-not-allowed"
|
? "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] cursor-not-allowed"
|
||||||
: "bg-gradient-to-r from-violet-600 to-purple-600 text-white hover:from-violet-500 hover:to-purple-500 hover:shadow-lg hover:shadow-violet-500/25 active:scale-[0.98]"
|
: "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] hover:bg-neutral-800 active:scale-[0.98]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isCreatingProject ? (
|
{isCreatingProject ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,12 +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/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" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -40,23 +39,23 @@ export function MobileNav() {
|
|||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex flex-col items-center gap-0.5 py-1.5 px-3 rounded-xl min-w-[4rem] transition-colors",
|
"relative flex flex-col items-center gap-1 py-2 px-3 rounded-xl min-w-[4.5rem] transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "text-violet-400"
|
? "text-white"
|
||||||
: "text-[var(--color-text-muted)] active:text-[var(--color-text-secondary)]"
|
: "text-[var(--color-text-muted)] active:text-[var(--color-text-primary)]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon size={22} strokeWidth={isActive ? 2.2 : 1.8} />
|
<Icon size={24} strokeWidth={isActive ? 2.5 : 1.8} />
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId="nav-indicator"
|
layoutId="nav-indicator"
|
||||||
className="absolute -inset-2 rounded-xl bg-violet-500/10"
|
className="absolute -inset-2 rounded-xl bg-white/10"
|
||||||
transition={{ type: "spring", bounce: 0.2, duration: 0.5 }}
|
transition={{ type: "spring", bounce: 0.2, duration: 0.5 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] font-medium tracking-wide">
|
<span className="text-xs font-semibold tracking-wide mt-1">
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -79,12 +78,12 @@ function CreditCard() {
|
|||||||
const pct = isAdmin ? 100 : (total > 0 ? Math.round((remaining / total) * 100) : 0);
|
const pct = isAdmin ? 100 : (total > 0 ? Math.round((remaining / total) * 100) : 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-3 mb-4 p-4 rounded-xl bg-gradient-to-br from-violet-500/8 to-cyan-400/5 border border-[var(--color-border-faint)]">
|
<div className="mx-4 mb-6 p-5 rounded-2xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] shadow-sm">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<span className="text-xs text-[var(--color-text-muted)]">Kalan Kredi</span>
|
<span className="text-sm font-medium text-[var(--color-text-muted)]">Kalan Kredi</span>
|
||||||
<span className="badge badge-violet">{planName}</span>
|
<span className="badge bg-white/10 text-white border border-white/20">{planName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold font-[family-name:var(--font-display)] text-[var(--color-text-primary)]">
|
<div className="text-3xl font-bold font-[family-name:var(--font-display)] text-[var(--color-text-primary)]">
|
||||||
{isLoading ? "..." : isAdmin ? "∞" : remaining}
|
{isLoading ? "..." : isAdmin ? "∞" : remaining}
|
||||||
</div>
|
</div>
|
||||||
<div className="progress-bar mt-2">
|
<div className="progress-bar mt-2">
|
||||||
@@ -93,7 +92,7 @@ function CreditCard() {
|
|||||||
style={{ width: `${pct}%` }}
|
style={{ width: `${pct}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-[var(--color-text-ghost)] mt-1.5">
|
<p className="text-xs text-[var(--color-text-ghost)] mt-2">
|
||||||
{isAdmin ? "Sınırsız admin erişimi" : `${total} kredilik planınızın ${remaining}'${remaining === 1 ? "i" : "si"} kaldı`}
|
{isAdmin ? "Sınırsız admin erişimi" : `${total} kredilik planınızın ${remaining}'${remaining === 1 ? "i" : "si"} kaldı`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,7 +106,7 @@ export function DesktopSidebar() {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const user = (data as any)?.data ?? data;
|
const user = (data as any)?.data ?? data;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const roles: string[] = (user?.roles ?? []).map((r: any) => r?.role?.name ?? r?.name ?? "");
|
const roles: string[] = (user?.roles ?? []).map((r: any) => typeof r === "string" ? r : (r?.role?.name ?? r?.name ?? ""));
|
||||||
const isAdmin = roles.includes("admin") || roles.includes("superadmin");
|
const isAdmin = roles.includes("admin") || roles.includes("superadmin");
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
@@ -115,24 +114,24 @@ export function DesktopSidebar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="hidden md:flex md:w-64 lg:w-72 flex-col h-screen sticky top-0 border-r border-[var(--color-border-faint)] bg-[var(--color-bg-deep)]">
|
<aside className="hidden md:flex md:w-72 lg:w-80 flex-col h-screen sticky top-0 border-r border-[var(--color-border-faint)] bg-[var(--color-bg-deep)]">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex items-center gap-3 px-6 py-5 border-b border-[var(--color-border-faint)]">
|
<div className="flex items-center gap-4 px-8 py-6 border-b border-[var(--color-border-faint)]">
|
||||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-cyan-400 flex items-center justify-center shadow-lg">
|
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center shadow-md">
|
||||||
<Sparkles size={18} className="text-white" />
|
<Sparkles size={20} className="text-black" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-[family-name:var(--font-display)] text-lg font-bold tracking-tight text-[var(--color-text-primary)]">
|
<h1 className="font-[family-name:var(--font-display)] text-xl font-bold tracking-tight text-[var(--color-text-primary)]">
|
||||||
ContentGen
|
ContentGen
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-[10px] text-[var(--color-text-muted)] uppercase tracking-widest">
|
<p className="text-xs font-semibold text-[var(--color-text-muted)] uppercase tracking-[0.2em]">
|
||||||
AI Studio
|
AI Studio
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className="flex-1 px-3 py-4 space-y-1">
|
<nav className="flex-1 px-4 py-6 space-y-1.5">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
localePath === item.href ||
|
localePath === item.href ||
|
||||||
@@ -144,20 +143,20 @@ export function DesktopSidebar() {
|
|||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-200",
|
"relative flex items-center gap-4 px-4 py-3 rounded-xl text-base font-semibold transition-all duration-200",
|
||||||
isActive
|
isActive
|
||||||
? "text-white bg-violet-500/12 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]"
|
? "text-white bg-white/5"
|
||||||
: "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)]"
|
: "text-[var(--color-text-muted)] hover:text-white hover:bg-[var(--color-bg-surface)]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId="sidebar-active"
|
layoutId="sidebar-active"
|
||||||
className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-6 rounded-r-full bg-gradient-to-b from-violet-400 to-violet-600"
|
className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 rounded-r-full bg-white"
|
||||||
transition={{ type: "spring", bounce: 0.2, duration: 0.5 }}
|
transition={{ type: "spring", bounce: 0.2, duration: 0.5 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Icon size={18} strokeWidth={isActive ? 2.2 : 1.6} />
|
<Icon size={20} strokeWidth={isActive ? 2.5 : 2} />
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -169,29 +168,29 @@ export function DesktopSidebar() {
|
|||||||
|
|
||||||
{/* Admin Panel Linki (sadece admin) */}
|
{/* Admin Panel Linki (sadece admin) */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className="px-3 pb-3">
|
<div className="px-4 pb-4">
|
||||||
<Link
|
<Link
|
||||||
href={adminNavItem.href}
|
href={adminNavItem.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-200",
|
"relative flex items-center gap-4 px-4 py-3 rounded-xl text-base font-semibold transition-all duration-200",
|
||||||
localePath.startsWith("/dashboard/admin")
|
localePath.startsWith("/dashboard/admin")
|
||||||
? "text-rose-300 bg-rose-500/10"
|
? "text-white bg-white/10"
|
||||||
: "text-[var(--color-text-muted)] hover:text-rose-300 hover:bg-rose-500/8"
|
: "text-[var(--color-text-ghost)] hover:text-white hover:bg-white/5"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ShieldCheck size={18} strokeWidth={1.8} />
|
<ShieldCheck size={20} strokeWidth={2} />
|
||||||
<span>{adminNavItem.label}</span>
|
<span>{adminNavItem.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Çıkış Butonu */}
|
{/* Çıkış Butonu */}
|
||||||
<div className="px-3 pb-4">
|
<div className="px-4 pb-6">
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-[var(--color-text-muted)] hover:text-red-400 hover:bg-red-500/8 transition-all duration-200"
|
className="w-full flex items-center gap-4 px-4 py-3 rounded-xl text-base font-semibold text-[var(--color-text-ghost)] hover:text-white hover:bg-white/5 transition-all duration-200"
|
||||||
>
|
>
|
||||||
<LogOut size={18} strokeWidth={1.6} />
|
<LogOut size={20} strokeWidth={2} />
|
||||||
<span>Çıkış Yap</span>
|
<span>Çıkış Yap</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +206,7 @@ function UserAvatar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-600 to-cyan-500 flex items-center justify-center text-white text-sm font-semibold shadow-md"
|
className="w-10 h-10 rounded-full bg-white flex items-center justify-center text-black text-sm font-bold shadow-sm border border-white/20"
|
||||||
aria-label="Profil"
|
aria-label="Profil"
|
||||||
>
|
>
|
||||||
{initial}
|
{initial}
|
||||||
@@ -220,11 +219,11 @@ export function TopBar() {
|
|||||||
<header className="sticky top-0 z-40 glass">
|
<header className="sticky top-0 z-40 glass">
|
||||||
<div className="flex items-center justify-between px-4 md:px-6 h-14 md:h-16">
|
<div className="flex items-center justify-between px-4 md:px-6 h-14 md:h-16">
|
||||||
{/* Mobil logo */}
|
{/* Mobil logo */}
|
||||||
<div className="flex items-center gap-2.5 md:hidden">
|
<div className="flex items-center gap-3 md:hidden">
|
||||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-cyan-400 flex items-center justify-center">
|
<div className="w-8 h-8 rounded-lg bg-white flex items-center justify-center">
|
||||||
<Sparkles size={16} className="text-white" />
|
<Sparkles size={16} className="text-black" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-[family-name:var(--font-display)] font-bold text-base">
|
<span className="font-[family-name:var(--font-display)] font-bold text-lg">
|
||||||
ContentGen
|
ContentGen
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -7,21 +7,50 @@ import type { RenderProgressState } from '@/hooks/use-render-progress';
|
|||||||
const STAGE_ORDER = ['tts', 'image_generation', 'music_generation', 'compositing', 'encoding'];
|
const STAGE_ORDER = ['tts', 'image_generation', 'music_generation', 'compositing', 'encoding'];
|
||||||
|
|
||||||
const STAGE_DETAILS: Record<string, { label: string; icon: string; color: string }> = {
|
const STAGE_DETAILS: Record<string, { label: string; icon: string; color: string }> = {
|
||||||
tts: { label: 'Seslendirme', icon: '🔊', color: 'from-violet-500 to-violet-600' },
|
tts: { label: 'Seslendirme', icon: '🔊', color: 'from-neutral-500 to-neutral-600' },
|
||||||
image_generation: { label: 'Görsel Üretim', icon: '🎨', color: 'from-cyan-500 to-cyan-600' },
|
image_generation: { label: 'Görsel Üretim', icon: '🎨', color: 'from-neutral-400 to-neutral-500' },
|
||||||
music_generation: { label: 'Müzik Üretim', icon: '🎵', color: 'from-amber-500 to-amber-600' },
|
music_generation: { label: 'Müzik Üretim', icon: '🎵', color: 'from-neutral-600 to-neutral-700' },
|
||||||
compositing: { label: 'Birleştirme', icon: '🎬', color: 'from-emerald-500 to-emerald-600' },
|
compositing: { label: 'Birleştirme', icon: '🎬', color: 'from-neutral-500 to-neutral-600' },
|
||||||
encoding: { label: 'Kodlama', icon: '📦', color: 'from-rose-500 to-rose-600' },
|
encoding: { label: 'Kodlama', icon: '📦', color: 'from-neutral-400 to-neutral-500' },
|
||||||
};
|
};
|
||||||
|
|
||||||
interface RenderProgressProps {
|
interface RenderProgressProps {
|
||||||
renderState: RenderProgressState;
|
renderState: RenderProgressState;
|
||||||
|
projectStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RenderProgress({ renderState }: RenderProgressProps) {
|
export function RenderProgress({ renderState, projectStatus }: RenderProgressProps) {
|
||||||
const { progress, stage, stageLabel, currentScene, totalScenes, eta, status, isConnected } = renderState;
|
const { progress, stage, stageLabel, currentScene, totalScenes, eta, status, isConnected } = renderState;
|
||||||
|
|
||||||
if (status === 'idle') return null;
|
if (status === 'idle') {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="card-surface p-5 md:p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<Loader2 size={18} className="animate-spin text-amber-400" />
|
||||||
|
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||||
|
{projectStatus === 'GENERATING_SCRIPT' ? 'AI Senaryo Üretiyor...' : 'İşlem kuyrukta veya başlatılıyor...'}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{isConnected ? (
|
||||||
|
<Wifi size={13} className="text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<WifiOff size={13} className="text-red-400" />
|
||||||
|
)}
|
||||||
|
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
||||||
|
{isConnected ? 'Canlı' : 'Bağlantı koptu'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -33,7 +62,7 @@ export function RenderProgress({ renderState }: RenderProgressProps) {
|
|||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
{status === 'rendering' && (
|
{status === 'rendering' && (
|
||||||
<Loader2 size={18} className="animate-spin text-violet-400" />
|
<Loader2 size={18} className="animate-spin text-neutral-400" />
|
||||||
)}
|
)}
|
||||||
{status === 'completed' && (
|
{status === 'completed' && (
|
||||||
<CheckCircle2 size={18} className="text-emerald-400" />
|
<CheckCircle2 size={18} className="text-emerald-400" />
|
||||||
@@ -71,7 +100,7 @@ export function RenderProgress({ renderState }: RenderProgressProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-2.5 w-full rounded-full bg-[var(--color-bg-deep)] overflow-hidden">
|
<div className="h-2.5 w-full rounded-full bg-[var(--color-bg-deep)] overflow-hidden">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="h-full rounded-full bg-gradient-to-r from-violet-500 via-cyan-400 to-emerald-400"
|
className="h-full rounded-full bg-gradient-to-r from-neutral-500 via-neutral-400 to-neutral-300"
|
||||||
initial={{ width: 0 }}
|
initial={{ width: 0 }}
|
||||||
animate={{ width: `${progress}%` }}
|
animate={{ width: `${progress}%` }}
|
||||||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||||
@@ -94,9 +123,9 @@ export function RenderProgress({ renderState }: RenderProgressProps) {
|
|||||||
key={s}
|
key={s}
|
||||||
className={`flex flex-col items-center gap-1 py-2 px-1 rounded-lg transition-all ${
|
className={`flex flex-col items-center gap-1 py-2 px-1 rounded-lg transition-all ${
|
||||||
isCurrent
|
isCurrent
|
||||||
? 'bg-violet-500/10 border border-violet-500/20'
|
? 'bg-[var(--color-bg-elevated)] border border-neutral-500/20'
|
||||||
: isDone
|
: isDone
|
||||||
? 'bg-emerald-500/5'
|
? 'bg-neutral-500/10'
|
||||||
: 'opacity-40'
|
: 'opacity-40'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -104,8 +133,8 @@ export function RenderProgress({ renderState }: RenderProgressProps) {
|
|||||||
<span className="text-[9px] text-center text-[var(--color-text-ghost)] leading-tight">
|
<span className="text-[9px] text-center text-[var(--color-text-ghost)] leading-tight">
|
||||||
{detail.label}
|
{detail.label}
|
||||||
</span>
|
</span>
|
||||||
{isDone && <CheckCircle2 size={10} className="text-emerald-400" />}
|
{isDone && <CheckCircle2 size={10} className="text-neutral-300" />}
|
||||||
{isCurrent && <Loader2 size={10} className="animate-spin text-violet-400" />}
|
{isCurrent && <Loader2 size={10} className="animate-spin text-neutral-400" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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-violet-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-gradient-to-br from-violet-500/20 to-violet-600/10 flex items-center justify-center">
|
<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-violet-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-violet-400 hover:bg-violet-500/10 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-cyan-400 hover:bg-cyan-500/10 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,77 +125,67 @@ 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-violet-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-cyan-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-violet-500/15 text-violet-400 text-xs font-medium hover:bg-violet-500/25 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-violet-500/10 flex items-center justify-center shrink-0 mt-0.5">
|
<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-violet-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">
|
||||||
{scene.narrationText}
|
<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">
|
||||||
</p>
|
{scene.narrationText}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Görsel Prompt */}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="w-5 h-5 rounded-md bg-cyan-500/10 flex items-center justify-center shrink-0 mt-0.5">
|
|
||||||
<ImageIcon size={11} className="text-cyan-400" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 group/prompt relative">
|
|
||||||
<p className="text-xs text-[var(--color-text-ghost)] leading-relaxed italic pr-6">
|
|
||||||
{scene.visualPrompt}
|
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(scene.visualPrompt);
|
navigator.clipboard.writeText(scene.narrationText);
|
||||||
}}
|
}}
|
||||||
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-cyan-400 hover:bg-cyan-500/10"
|
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="Prompt'u Kopyala"
|
title="Metni 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">
|
||||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||||
@@ -205,62 +195,89 @@ export function SceneCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Görsel / Upscale Alanı */}
|
{/* Görsel Prompt ve Görsel Alanı */}
|
||||||
<div className="flex flex-col gap-2 pt-2">
|
<div className="flex flex-col md:flex-row gap-4 pt-2">
|
||||||
{thumbnailAsset?.url && !isGeneratingImage ? (
|
{/* Sol: Prompt */}
|
||||||
<div className="relative group/thumb rounded-lg overflow-hidden border border-[var(--color-border-faint)] aspect-video max-w-sm">
|
<div className="flex gap-3 flex-1">
|
||||||
<img
|
<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">
|
||||||
src={thumbnailAsset.url}
|
<ImageIcon size={14} className="text-cyan-400" />
|
||||||
alt="Scene Thumbnail"
|
|
||||||
className="w-full h-full object-cover cursor-pointer hover:scale-105 transition-transform duration-500"
|
|
||||||
onClick={() => setLightboxOpen(true)}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover/thumb:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
|
|
||||||
<Maximize2 size={24} className="text-white" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<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">
|
||||||
<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">
|
<p className="text-sm font-medium text-cyan-50 leading-relaxed pr-8">
|
||||||
{isGeneratingImage ? (
|
{scene.visualPrompt}
|
||||||
<div className="flex flex-col items-center justify-center animate-in fade-in zoom-in duration-300">
|
</p>
|
||||||
<div className="relative w-12 h-12 mb-3">
|
<button
|
||||||
<div className="absolute inset-0 rounded-full border-2 border-emerald-500/20"></div>
|
onClick={() => {
|
||||||
<div className="absolute inset-0 rounded-full border-2 border-emerald-500 border-t-transparent animate-spin"></div>
|
navigator.clipboard.writeText(scene.visualPrompt);
|
||||||
<Sparkles size={16} className="absolute inset-0 m-auto text-emerald-400 animate-pulse" />
|
}}
|
||||||
</div>
|
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"
|
||||||
<p className="text-xs font-medium text-emerald-400 text-center animate-pulse">
|
title="Prompt'u Kopyala"
|
||||||
AI Görsel Üretiyor...
|
>
|
||||||
</p>
|
<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>
|
||||||
|
|
||||||
|
{/* Sağ: Görsel Önizleme ve Butonlar */}
|
||||||
|
<div className="flex flex-col gap-2 w-full md:w-64 shrink-0">
|
||||||
|
{thumbnailAsset?.url && !isGeneratingImage ? (
|
||||||
|
<div className="relative group/thumb rounded-lg overflow-hidden border border-[var(--color-border-faint)] aspect-video w-full">
|
||||||
|
<img
|
||||||
|
src={thumbnailAsset.url}
|
||||||
|
alt="Scene Thumbnail"
|
||||||
|
className="w-full h-full object-cover cursor-pointer hover:scale-105 transition-transform duration-500"
|
||||||
|
onClick={() => setLightboxOpen(true)}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover/thumb:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
|
||||||
|
<Maximize2 size={24} className="text-white" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<>
|
) : (
|
||||||
<ImageIcon size={24} className="text-[var(--color-text-ghost)] mb-2" />
|
<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">
|
||||||
<p className="text-xs text-[var(--color-text-muted)] text-center">Görsel Henüz Üretilmedi</p>
|
{isGeneratingImage ? (
|
||||||
</>
|
<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="absolute inset-0 rounded-full border-2 border-emerald-500/20"></div>
|
||||||
|
<div className="absolute inset-0 rounded-full border-2 border-emerald-500 border-t-transparent animate-spin"></div>
|
||||||
|
<Sparkles size={16} className="absolute inset-0 m-auto text-emerald-400 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<p className="text-xs font-medium text-emerald-400 text-center animate-pulse">
|
||||||
|
AI Görsel Üretiyor...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ImageIcon size={24} className="text-[var(--color-text-ghost)] mb-2" />
|
||||||
|
<p className="text-xs text-[var(--color-text-muted)] text-center">Görsel Henüz Üretilmedi</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Görsel üretim butonları */}
|
||||||
|
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||||
|
<button
|
||||||
|
onClick={() => onGenerateImage?.(scene.id, scene.visualPrompt)}
|
||||||
|
disabled={isGeneratingImage || isUpscalingImage}
|
||||||
|
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} />}
|
||||||
|
{thumbnailAsset ? "Yeniden Üret" : "Görsel Üret"}
|
||||||
|
</button>
|
||||||
|
{thumbnailAsset?.url && (
|
||||||
|
<button
|
||||||
|
onClick={() => onUpscaleImage?.(scene.id)}
|
||||||
|
disabled={isUpscalingImage || isGeneratingImage}
|
||||||
|
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} />}
|
||||||
|
Upscale (4K)
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Görsel üretim butonları — tüm projelerde her zaman göster, render sürecinde disable et */}
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<button
|
|
||||||
onClick={() => onGenerateImage?.(scene.id, scene.visualPrompt)}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
{isGeneratingImage ? <RefreshCw size={13} className="animate-spin" /> : <ImageIcon size={13} />}
|
|
||||||
{thumbnailAsset ? "Görseli Yeniden Üret" : "Görsel Üret"}
|
|
||||||
</button>
|
|
||||||
{thumbnailAsset?.url && (
|
|
||||||
<button
|
|
||||||
onClick={() => onUpscaleImage?.(scene.id)}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
{isUpscalingImage ? <RefreshCw size={13} className="animate-spin" /> : <Wand2 size={13} />}
|
|
||||||
Upscale (4K)
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -0,0 +1,376 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
Monitor,
|
||||||
|
Smartphone,
|
||||||
|
Square,
|
||||||
|
Search,
|
||||||
|
ChevronDown,
|
||||||
|
Clock,
|
||||||
|
Palette,
|
||||||
|
Languages,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
// --- CONSTANTS ---
|
||||||
|
|
||||||
|
export const languages = [
|
||||||
|
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
|
||||||
|
{ code: "en", label: "English", flag: "🇺🇸" },
|
||||||
|
{ code: "es", label: "Español", flag: "🇪🇸" },
|
||||||
|
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
||||||
|
{ code: "fr", label: "Français", flag: "🇫🇷" },
|
||||||
|
{ code: "ar", label: "العربية", flag: "🇸🇦" },
|
||||||
|
{ code: "pt", label: "Português", 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 = [
|
||||||
|
// Film & Sinema
|
||||||
|
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller", category: "Film & Sinema" },
|
||||||
|
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton", category: "Film & Sinema" },
|
||||||
|
{ id: "STORYTELLING", label: "Hikâye Anlatımı", emoji: "📖", desc: "Anlatı odaklı, sürükleyici", category: "Film & Sinema" },
|
||||||
|
{ id: "NEWS", label: "Haber", emoji: "📰", desc: "Güncel ve bilgilendirici", category: "Film & Sinema" },
|
||||||
|
{ id: "ARTISTIC", label: "Sanatsal", emoji: "🎨", desc: "Yaratıcı ve sıra dışı", category: "Film & Sinema" },
|
||||||
|
{ id: "NOIR", label: "Film Noir", emoji: "🖤", desc: "Karanlık, dramatik", category: "Film & Sinema" },
|
||||||
|
{ id: "VLOG", label: "Vlog", emoji: "📱", desc: "Günlük, samimi", category: "Film & Sinema" },
|
||||||
|
// Animasyon
|
||||||
|
{ id: "ANIME", label: "Anime", emoji: "⛩️", desc: "Japon animasyon stili", category: "Animasyon" },
|
||||||
|
{ id: "ANIMATION_3D", label: "3D Animasyon", emoji: "🧊", desc: "Pixar kalitesi", category: "Animasyon" },
|
||||||
|
{ id: "ANIMATION_2D", label: "2D Animasyon", emoji: "✏️", desc: "Klasik el çizimi", category: "Animasyon" },
|
||||||
|
{ id: "STOP_MOTION", label: "Stop Motion", emoji: "🧸", desc: "Kare kare animasyon", category: "Animasyon" },
|
||||||
|
{ id: "MOTION_COMIC", label: "Hareketli Çizgi Roman", emoji: "💥", desc: "Panel bazlı anlatım", category: "Animasyon" },
|
||||||
|
{ id: "CARTOON", label: "Karikatür", emoji: "🎭", desc: "Çizgi film stili", category: "Animasyon" },
|
||||||
|
{ id: "CLAYMATION", label: "Claymation", emoji: "🏺", desc: "Kil animasyon", category: "Animasyon" },
|
||||||
|
{ id: "PIXEL_ART", label: "Pixel Art", emoji: "👾", desc: "8-bit retro oyun", category: "Animasyon" },
|
||||||
|
{ id: "ISOMETRIC", label: "İzometrik", emoji: "🔷", desc: "İzometrik animasyon", category: "Animasyon" },
|
||||||
|
// Eğitim & Bilgi
|
||||||
|
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "🎓", desc: "Öğretici ve açıklayıcı", category: "Eğitim & Bilgi" },
|
||||||
|
{ id: "INFOGRAPHIC", label: "İnfografik", emoji: "📊", desc: "Veri görselleştirme", category: "Eğitim & Bilgi" },
|
||||||
|
{ id: "WHITEBOARD", label: "Whiteboard", emoji: "📝", desc: "Tahta animasyonu", category: "Eğitim & Bilgi" },
|
||||||
|
{ id: "EXPLAINER", label: "Explainer", emoji: "💡", desc: "Ürün/konsept anlatımı", category: "Eğitim & Bilgi" },
|
||||||
|
{ id: "DATA_VIZ", label: "Veri Görselleştirme", emoji: "📈", desc: "Grafikler ve tablolar", category: "Eğitim & Bilgi" },
|
||||||
|
// Retro & Nostaljik
|
||||||
|
{ id: "RETRO_80S", label: "Retro 80s", emoji: "🕹️", desc: "Synthwave estetik", category: "Retro & Nostaljik" },
|
||||||
|
{ id: "VINTAGE_FILM", label: "Vintage Film", emoji: "📽️", desc: "Super 8 filmi", category: "Retro & Nostaljik" },
|
||||||
|
{ id: "VHS", label: "VHS", emoji: "📼", desc: "Kaset estetik", category: "Retro & Nostaljik" },
|
||||||
|
{ id: "POLAROID", label: "Polaroid", emoji: "📸", desc: "Analog fotoğraf", category: "Retro & Nostaljik" },
|
||||||
|
{ id: "RETRO_90S", label: "Retro 90s Y2K", emoji: "💿", desc: "Y2K & internet", category: "Retro & Nostaljik" },
|
||||||
|
// Sanat Akımları
|
||||||
|
{ id: "WATERCOLOR", label: "Suluboya", emoji: "🎨", desc: "Suluboya resim", category: "Sanat Akımları" },
|
||||||
|
{ id: "OIL_PAINTING", label: "Yağlı Boya", emoji: "🖌️", desc: "Klasik tuval", category: "Sanat Akımları" },
|
||||||
|
{ id: "IMPRESSIONIST", label: "Empresyonist", emoji: "🌅", desc: "Monet tarzı", category: "Sanat Akımları" },
|
||||||
|
{ id: "POP_ART", label: "Pop Art", emoji: "🎯", desc: "Warhol stili", category: "Sanat Akımları" },
|
||||||
|
{ id: "UKIYO_E", label: "Ukiyo-e", emoji: "🏯", desc: "Japon gravür", category: "Sanat Akımları" },
|
||||||
|
{ id: "ART_DECO", label: "Art Deco", emoji: "✨", desc: "1920s zarafet", category: "Sanat Akımları" },
|
||||||
|
{ id: "SURREAL", label: "Sürrealist", emoji: "🌀", desc: "Dalí tarzı", category: "Sanat Akımları" },
|
||||||
|
{ id: "COMIC_BOOK", label: "Çizgi Roman", emoji: "💬", desc: "Marvel/DC stili", category: "Sanat Akımları" },
|
||||||
|
{ id: "SKETCH", label: "Karakalem", emoji: "✍️", desc: "Kalem çizim", category: "Sanat Akımları" },
|
||||||
|
// Modern & Minimal
|
||||||
|
{ id: "MINIMALIST", label: "Minimalist", emoji: "⚪", desc: "Apple estetiği", category: "Modern & Minimal" },
|
||||||
|
{ id: "GLASSMORPHISM", label: "Glassmorphism", emoji: "🔮", desc: "Cam efekti", category: "Modern & Minimal" },
|
||||||
|
{ id: "NEON", label: "Neon Glow", emoji: "💜", desc: "Neon ışıkları", category: "Modern & Minimal" },
|
||||||
|
{ id: "CYBERPUNK", label: "Cyberpunk", emoji: "🤖", desc: "Gelecek distopya", category: "Modern & Minimal" },
|
||||||
|
{ id: "STEAMPUNK", label: "Steampunk", emoji: "⚙️", desc: "Viktoryan mekanik", category: "Modern & Minimal" },
|
||||||
|
{ id: "ABSTRACT", label: "Soyut", emoji: "🔵", desc: "Abstract sanat", category: "Modern & Minimal" },
|
||||||
|
// Fotoğrafik
|
||||||
|
{ id: "PRODUCT", label: "Ürün Fotoğrafı", emoji: "📦", desc: "Studio çekim", category: "Fotoğrafik" },
|
||||||
|
{ id: "FASHION", label: "Moda", emoji: "👗", desc: "Editöryal çekim", category: "Fotoğrafik" },
|
||||||
|
{ id: "AERIAL", label: "Havadan", emoji: "🚁", desc: "Drone görüntüsü", category: "Fotoğrafik" },
|
||||||
|
{ id: "MACRO", label: "Makro", emoji: "🔬", desc: "Yakın çekim", category: "Fotoğrafik" },
|
||||||
|
{ id: "PORTRAIT", label: "Portre", emoji: "🧑", desc: "Portre fotoğraf", category: "Fotoğrafik" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const aspectRatios = [
|
||||||
|
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
|
||||||
|
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
|
||||||
|
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- COMPONENTS ---
|
||||||
|
|
||||||
|
export function LanguageSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||||
|
<Languages size={14} className="inline mr-1.5 text-cyan-400" />
|
||||||
|
Video Dili
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<button
|
||||||
|
key={lang.code}
|
||||||
|
onClick={() => onChange(lang.code)}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center gap-1 py-3 px-2 rounded-xl text-xs transition-all",
|
||||||
|
value === lang.code
|
||||||
|
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
|
||||||
|
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-[var(--color-border-default)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-lg">{lang.flag}</span>
|
||||||
|
<span className="font-medium">{lang.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StyleSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
cinematicReference,
|
||||||
|
onCinematicReferenceChange,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
cinematicReference: string;
|
||||||
|
onCinematicReferenceChange: (val: string) => void;
|
||||||
|
}) {
|
||||||
|
const [styleSearch, setStyleSearch] = useState("");
|
||||||
|
const [expandedCategory, setExpandedCategory] = useState<string | null>("Film & Sinema");
|
||||||
|
|
||||||
|
const filteredStyles = useMemo(() => {
|
||||||
|
return videoStyles.filter(
|
||||||
|
(s) =>
|
||||||
|
s.label.toLowerCase().includes(styleSearch.toLowerCase()) ||
|
||||||
|
s.desc.toLowerCase().includes(styleSearch.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [styleSearch]);
|
||||||
|
|
||||||
|
const groupedStyles = useMemo(() => {
|
||||||
|
return filteredStyles.reduce((acc, curr) => {
|
||||||
|
const cat = curr.category || "Diğer";
|
||||||
|
if (!acc[cat]) acc[cat] = [];
|
||||||
|
acc[cat].push(curr);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, typeof videoStyles>);
|
||||||
|
}, [filteredStyles]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3">
|
||||||
|
<label className="text-sm font-medium text-[var(--color-text-secondary)] block">
|
||||||
|
<Palette size={14} className="inline mr-1.5 text-violet-400" />
|
||||||
|
Video Stili
|
||||||
|
</label>
|
||||||
|
<div className="relative w-full sm:w-56">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none">
|
||||||
|
<Search size={14} className="text-[var(--color-text-ghost)]" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Stil ara..."
|
||||||
|
value={styleSearch}
|
||||||
|
onChange={(e) => setStyleSearch(e.target.value)}
|
||||||
|
className="w-full pl-8 pr-3 py-1.5 bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-md text-xs text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-[var(--color-border-default)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1 custom-scrollbar">
|
||||||
|
{Object.entries(groupedStyles).map(([category, items]) => {
|
||||||
|
const isExpanded = styleSearch ? true : expandedCategory === category;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={category}
|
||||||
|
className="bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl overflow-hidden"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
!styleSearch && setExpandedCategory(isExpanded ? null : category)
|
||||||
|
}
|
||||||
|
className="w-full flex items-center justify-between p-3 bg-[var(--color-bg-elevated)] hover:bg-[var(--color-bg-surface-hover)] transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||||
|
{category}{" "}
|
||||||
|
<span className="text-[11px] text-[var(--color-text-ghost)] ml-1">
|
||||||
|
({items.length})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{!styleSearch && (
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className={cn(
|
||||||
|
"text-[var(--color-text-ghost)] transition-transform duration-200",
|
||||||
|
isExpanded && "rotate-180"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{isExpanded && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="p-3 pt-0 mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
|
{items.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.id}
|
||||||
|
onClick={() => onChange(s.id)}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-start gap-1 p-2.5 rounded-xl text-left transition-all",
|
||||||
|
value === s.id
|
||||||
|
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
|
||||||
|
: "bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-xl mb-0.5">{s.emoji}</span>
|
||||||
|
<span className="text-xs font-semibold leading-tight">
|
||||||
|
{s.label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-[10px] leading-tight",
|
||||||
|
value === s.id
|
||||||
|
? "text-[var(--color-text-inverted)]/70"
|
||||||
|
: "text-[var(--color-text-ghost)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{s.desc}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{Object.keys(groupedStyles).length === 0 && (
|
||||||
|
<div className="text-center py-8 text-[var(--color-text-ghost)] text-sm">
|
||||||
|
"{styleSearch}" için sonuç bulunamadı.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{value === "CINEMATIC" && (
|
||||||
|
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
||||||
|
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||||
|
Özel Sinematik Referans (Opsiyonel)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
||||||
|
value={cinematicReference}
|
||||||
|
onChange={(e) => onCinematicReferenceChange(e.target.value)}
|
||||||
|
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-md py-1.5 px-3 text-sm focus:border-[var(--color-border-default)] outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DurationSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
onChange: (val: number) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||||
|
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
||||||
|
Hedef Süre:{" "}
|
||||||
|
<span className="text-[var(--color-text-primary)] font-bold">{value}s</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={15}
|
||||||
|
max={900}
|
||||||
|
step={5}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
className={cn(
|
||||||
|
"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]:w-6 [&::-webkit-slider-thumb]:h-6",
|
||||||
|
"[&::-webkit-slider-thumb]:rounded-full",
|
||||||
|
"[&::-webkit-slider-thumb]:bg-cyan-400",
|
||||||
|
"[&::-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"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-[10px] text-[var(--color-text-ghost)] mt-2">
|
||||||
|
<span>15s</span>
|
||||||
|
<span>60s</span>
|
||||||
|
<span>120s</span>
|
||||||
|
<span>900s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AspectRatioSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||||
|
En-Boy Oranı
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{aspectRatios.map((ar) => {
|
||||||
|
const Icon = ar.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={ar.id}
|
||||||
|
onClick={() => onChange(ar.id)}
|
||||||
|
className={cn(
|
||||||
|
"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
|
||||||
|
? "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)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon size={24} className={value === ar.id ? "drop-shadow-[0_0_8px_rgba(34,211,238,0.5)]" : ""} />
|
||||||
|
<span className="font-semibold text-[13px]">{ar.label}</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-[10px]",
|
||||||
|
value === ar.id
|
||||||
|
? "text-cyan-400/70"
|
||||||
|
: "text-[var(--color-text-ghost)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{ar.desc}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ChakraProvider } from "@chakra-ui/react";
|
||||||
|
import { system } from "@/theme/theme";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
import ReactQueryProvider from "@/provider/react-query-provider";
|
import ReactQueryProvider from "@/provider/react-query-provider";
|
||||||
@@ -7,17 +9,19 @@ import { ToastProvider } from "@/components/ui/toast";
|
|||||||
|
|
||||||
export function Provider({ children }: { children: React.ReactNode }) {
|
export function Provider({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<SessionProvider>
|
<ChakraProvider value={system}>
|
||||||
<ReactQueryProvider>
|
<SessionProvider>
|
||||||
<ThemeProvider
|
<ReactQueryProvider>
|
||||||
attribute="class"
|
<ThemeProvider
|
||||||
defaultTheme="dark"
|
attribute="class"
|
||||||
enableSystem={false}
|
defaultTheme="dark"
|
||||||
disableTransitionOnChange
|
enableSystem={false}
|
||||||
>
|
disableTransitionOnChange
|
||||||
<ToastProvider>{children}</ToastProvider>
|
>
|
||||||
</ThemeProvider>
|
<ToastProvider>{children}</ToastProvider>
|
||||||
</ReactQueryProvider>
|
</ThemeProvider>
|
||||||
</SessionProvider>
|
</ReactQueryProvider>
|
||||||
|
</SessionProvider>
|
||||||
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-3
@@ -18,6 +18,7 @@ import {
|
|||||||
type CreateFromDocumentPayload,
|
type CreateFromDocumentPayload,
|
||||||
type ExtractDocumentTopicsPayload,
|
type ExtractDocumentTopicsPayload,
|
||||||
type CreateFromExtractedTextPayload,
|
type CreateFromExtractedTextPayload,
|
||||||
|
type CreateFromTextPayload,
|
||||||
type ExtractDocumentTopicsResponse,
|
type ExtractDocumentTopicsResponse,
|
||||||
type Template,
|
type Template,
|
||||||
type PaginatedResponse,
|
type PaginatedResponse,
|
||||||
@@ -159,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 });
|
||||||
@@ -181,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();
|
||||||
@@ -434,6 +478,18 @@ export function useCreateFromExtractedText() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Serbest metin veya fikir üzerinden proje üret */
|
||||||
|
export function useCreateFromText() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: CreateFromTextPayload) => projectsApi.createFromText(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: queryKeys.projects.all });
|
||||||
|
qc.invalidateQueries({ queryKey: queryKeys.dashboard.stats });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
// NOTIFICATIONS — Bildirim hook'ları
|
// NOTIFICATIONS — Bildirim hook'ları
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -271,6 +288,16 @@ export interface CreateFromDocumentPayload {
|
|||||||
targetDuration?: number;
|
targetDuration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateFromTextPayload {
|
||||||
|
text: string;
|
||||||
|
title?: string;
|
||||||
|
language?: string;
|
||||||
|
aspectRatio?: string;
|
||||||
|
videoStyle?: string;
|
||||||
|
cinematicReference?: string;
|
||||||
|
targetDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExtractDocumentTopicsPayload {
|
export interface ExtractDocumentTopicsPayload {
|
||||||
file: File;
|
file: File;
|
||||||
}
|
}
|
||||||
@@ -343,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) =>
|
||||||
@@ -353,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),
|
||||||
|
|
||||||
@@ -378,6 +420,9 @@ export const projectsApi = {
|
|||||||
}).then((r) => r.data);
|
}).then((r) => r.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createFromText: (data: CreateFromTextPayload) =>
|
||||||
|
apiClient.post<Project>('/projects/from-text', data).then((r) => r.data),
|
||||||
|
|
||||||
extractDocumentTopics: (data: ExtractDocumentTopicsPayload) => {
|
extractDocumentTopics: (data: ExtractDocumentTopicsPayload) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', data.file);
|
formData.append('file', data.file);
|
||||||
@@ -404,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: () =>
|
||||||
|
|||||||
@@ -55,14 +55,26 @@ 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
|
|
||||||
) {
|
) {
|
||||||
response.data = response.data.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
+17
-16
@@ -1,6 +1,7 @@
|
|||||||
import { createSystem, defaultConfig, SystemConfig } from '@chakra-ui/react';
|
import { createSystem, defaultConfig, SystemConfig } from '@chakra-ui/react';
|
||||||
|
|
||||||
const customConfig: SystemConfig = {
|
const customConfig: SystemConfig = {
|
||||||
|
preflight: false,
|
||||||
theme: {
|
theme: {
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
tablet: '768px',
|
tablet: '768px',
|
||||||
@@ -15,17 +16,17 @@ const customConfig: SystemConfig = {
|
|||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
50: { value: '#E6FFFA' },
|
50: { value: '#fafafa' },
|
||||||
100: { value: '#B2F5EA' },
|
100: { value: '#f5f5f5' },
|
||||||
200: { value: '#81E6D9' },
|
200: { value: '#e5e5e5' },
|
||||||
300: { value: '#4FD1C5' },
|
300: { value: '#d4d4d4' },
|
||||||
400: { value: '#38B2AC' },
|
400: { value: '#a3a3a3' },
|
||||||
500: { value: '#319795' },
|
500: { value: '#737373' },
|
||||||
600: { value: '#2C7A7B' },
|
600: { value: '#525252' },
|
||||||
700: { value: '#285E61' },
|
700: { value: '#404040' },
|
||||||
800: { value: '#234E52' },
|
800: { value: '#262626' },
|
||||||
900: { value: '#1D4044' },
|
900: { value: '#171717' },
|
||||||
950: { value: '#132E30' },
|
950: { value: '#0a0a0a' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -34,20 +35,20 @@ const customConfig: SystemConfig = {
|
|||||||
primary: {
|
primary: {
|
||||||
solid: {
|
solid: {
|
||||||
value: {
|
value: {
|
||||||
_light: '{colors.primary.600}',
|
_light: '{colors.primary.900}',
|
||||||
_dark: '{colors.primary.600}',
|
_dark: '{colors.primary.100}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contrast: {
|
contrast: {
|
||||||
value: {
|
value: {
|
||||||
_light: '{colors.white}',
|
_light: '{colors.white}',
|
||||||
_dark: '{colors.white}',
|
_dark: '{colors.black}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fg: {
|
fg: {
|
||||||
value: {
|
value: {
|
||||||
_light: '{colors.primary.700}',
|
_light: '{colors.primary.900}',
|
||||||
_dark: '{colors.primary.300}',
|
_dark: '{colors.primary.100}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
const puppeteer = require('puppeteer');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const browser = await puppeteer.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
|
||||||
|
page.on('pageerror', error => console.log('PAGE ERROR:', error.message));
|
||||||
|
page.on('requestfailed', request => console.log('REQUEST FAILED:', request.url(), request.failure().errorText));
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3001/tr/dashboard/text-to-video');
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
await browser.close();
|
||||||
|
})();
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user