Compare commits

..

2 Commits

Author SHA1 Message Date
Harun CAN 5144ee4d9a main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
2026-04-30 13:25:43 +02:00
Harun CAN 1b69eaf219 feat: Implement text-to-video and fix hydration UI issues 2026-04-28 09:48:43 +02:00
28 changed files with 1231 additions and 1283 deletions
@@ -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} />}
@@ -1,53 +1,34 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useRef } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useExtractDocumentTopics, useCreateFromExtractedText } from "@/hooks/use-api"; import { useCreateFromDocument } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast"; import { useToast } from "@/components/ui/toast";
import { import {
FileText, FileText,
Upload,
Loader2, Loader2,
ArrowRight, ArrowRight,
Clock, Sparkles,
Palette,
Monitor,
Smartphone,
Square,
Wand2, Wand2,
X,
File,
} from "lucide-react"; } from "lucide-react";
import {
const videoStyles = [ LanguageSelector,
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬" }, StyleSelector,
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹" }, DurationSelector,
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚" }, AspectRatioSelector,
{ id: "STORYTELLING", label: "Hikâye", emoji: "📖" }, } from "@/components/projects/ProjectConfiguration";
{ 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() { export default function DocumentToVideoPage() {
const router = useRouter(); const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const createFromDoc = useCreateFromDocument();
const extractDocumentTopics = useExtractDocumentTopics(); const fileInputRef = useRef<HTMLInputElement>(null);
const createFromExtractedText = useCreateFromExtractedText();
const [file, setFile] = useState<File | null>(null); 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 [style, setStyle] = useState("CINEMATIC");
const [cinematicReference, setCinematicReference] = useState(""); const [cinematicReference, setCinematicReference] = useState("");
@@ -57,41 +38,34 @@ export default function DocumentToVideoPage() {
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
setFile(e.target.files[0]); const selectedFile = e.target.files[0];
setExtractedData(null);
setSelectedTopic(null); // Boyut kontrolü (örn: 10MB)
if (selectedFile.size > 10 * 1024 * 1024) {
toast("error", "Dosya boyutu 10MB'dan küçük olmalıdır.");
return;
}
setFile(selectedFile);
} }
}; };
const handleExtractTopics = async () => { const clearFile = () => {
if (!file) { setFile(null);
toast("error", "Lütfen bir belge seçin."); if (fileInputRef.current) {
return; fileInputRef.current.value = "";
}
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 () => { const handleGenerate = async () => {
if (!extractedData || !selectedTopic) { if (!file) {
toast("error", "Lütfen bir belge yükleyip konu seçin."); toast("error", "Lütfen bir belge yükleyin.");
return; return;
} }
try { try {
const result: any = await createFromExtractedText.mutateAsync({ const result: any = await createFromDoc.mutateAsync({
text: extractedData.text, file,
topic: selectedTopic,
originalFilename: extractedData.originalFilename,
language, language,
aspectRatio, aspectRatio,
videoStyle: style, videoStyle: style,
@@ -99,10 +73,10 @@ export default function DocumentToVideoPage() {
targetDuration: duration, targetDuration: duration,
}); });
toast("success", "Video projesi oluşturuldu!"); toast("success", "Belge → Video projesi oluşturuldu!");
router.push(`/dashboard/projects/${result.id}`); router.push(`/dashboard/projects/${result.id}`);
} catch (error) { } catch {
toast("error", "Proje oluşturulurken hata oluştu."); toast("error", "Proje oluşturulurken bir hata oluştu.");
} }
}; };
@@ -110,230 +84,127 @@ export default function DocumentToVideoPage() {
<div className="max-w-3xl mx-auto space-y-8 pb-24"> <div className="max-w-3xl mx-auto space-y-8 pb-24">
{/* Header */} {/* Header */}
<div className="text-center space-y-3 pb-4"> <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)]"> <div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
<FileText size={32} /> <FileText size={32} />
</div> </div>
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]"> <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 Belgeden Video Üret
</h1> </h1>
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto"> <p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
PDF, Word, TXT vb. belgenizi yükleyin, yapay zeka eriği tarayıp senaryolastirsin. PDF, Word veya Text dosyalarınızı yükleyin, yapay zeka sizin in profesyonel bir videoya dönüştürsün
</p> </p>
</div> </div>
{/* Input */} {/* Main Form */}
<div className="card p-6 md:p-8 space-y-4"> <div className="card p-6 md:p-8 space-y-6">
{/* File Upload */}
<div> <div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block"> <label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
Belge Yükle (PDF, DOCX, TXT) Belge Yükle (.pdf, .docx, .txt, vb.)
</label> </label>
<div className="relative">
<input {!file ? (
type="file" <div
onChange={handleFileChange} onClick={() => fileInputRef.current?.click()}
accept=".pdf,.docx,.txt,.csv,.xlsx,.pptx" className="w-full h-32 border-2 border-dashed border-[var(--color-border-default)] rounded-xl flex flex-col items-center justify-center gap-3 bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-elevated)] transition-colors cursor-pointer"
className="block w-full text-sm text-[var(--color-text-muted)] >
file:mr-4 file:py-3 file:px-4 <div className="w-10 h-10 rounded-full bg-[var(--color-bg-elevated)] flex items-center justify-center text-[var(--color-text-muted)]">
file:rounded-xl file:border-0 <Upload size={20} />
file:text-sm file:font-semibold </div>
file:bg-blue-500/10 file:text-blue-500 <div className="text-center">
hover:file:bg-blue-500/20 cursor-pointer" <p className="text-sm font-medium text-[var(--color-text-primary)]">Tıklayın veya sürükleyin</p>
/> <p className="text-xs text-[var(--color-text-ghost)] mt-1">Maksimum 10MB</p>
</div> </div>
</div>
) : (
<div className="w-full p-4 border border-[var(--color-border-default)] rounded-xl flex items-center justify-between bg-[var(--color-bg-elevated)]">
<div className="flex items-center gap-3 overflow-hidden">
<div className="w-10 h-10 rounded-lg bg-[var(--color-bg-surface)] flex items-center justify-center text-[var(--color-text-secondary)] shrink-0">
<File size={20} />
</div>
<div className="truncate">
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate">{file.name}</p>
<p className="text-xs text-[var(--color-text-ghost)]">{(file.size / 1024 / 1024).toFixed(2)} MB</p>
</div>
</div>
<button
onClick={clearFile}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-[var(--color-bg-surface)] text-[var(--color-text-muted)] transition-colors shrink-0"
>
<X size={16} />
</button>
</div>
)}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept=".pdf,.doc,.docx,.txt,.csv"
className="hidden"
/>
</div> </div>
{!extractedData && file && ( {/* Configurations */}
<button <div className="space-y-8 pt-6 border-t border-[var(--color-border-faint)]">
onClick={handleExtractTopics} <LanguageSelector value={language} onChange={setLanguage} />
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" <StyleSelector
> value={style}
{extractDocumentTopics.isPending ? ( onChange={setStyle}
<> cinematicReference={cinematicReference}
<Loader2 size={18} className="animate-spin" /> onCinematicReferenceChange={setCinematicReference}
Belge İnceleniyor... />
</>
) : ( <DurationSelector value={duration} onChange={setDuration} />
<>
<FileText size={18} /> <AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
Konu Çıkar </div>
</>
)}
</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> </div>
{/* Video Settings */} {/* Action Button */}
<div className="space-y-6"> <div className="flex justify-end">
<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 <button
onClick={handleGenerate} onClick={handleGenerate}
disabled={createFromExtractedText.isPending || !selectedTopic} disabled={createFromDoc.isPending || !file}
className={cn( className={cn(
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all", "group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
createFromExtractedText.isPending "bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
? "bg-blue-500/20 text-blue-400 cursor-wait" "disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
: "bg-blue-500 hover:bg-blue-600 text-white shadow-lg shadow-blue-500/20",
!selectedTopic && "opacity-50 cursor-not-allowed"
)} )}
> >
{createFromExtractedText.isPending ? ( {createFromDoc.isPending ? (
<> <>
<Loader2 size={20} className="animate-spin" /> <Loader2 size={20} className="animate-spin" />
<span>Video Projesi Oluşturuluyor... (Bu işlem uzun sürebilir)</span> <span>Yapay Zeka Belgeyi Okuyor...</span>
</> </>
) : ( ) : (
<> <>
<Wand2 size={20} /> <Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
<span>Konudan Video Oluştur</span> <span>Belgeden Video Üret</span>
<ArrowRight size={16} /> <ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</> </>
)} )}
</button> </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>
{/* Info Box */}
<div className="card p-5 bg-[var(--color-bg-surface)]">
<div className="flex items-start gap-3">
<Sparkles size={20} className="text-[var(--color-text-secondary)] shrink-0 mt-0.5" />
<div className="space-y-2">
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
Nasıl Çalışır?
</h3>
<ol className="text-xs text-[var(--color-text-muted)] space-y-1.5 list-decimal list-inside">
<li>PDF, Word veya TXT formatında bir metin dosyası yükleyin</li>
<li>MarkItDown AI teknolojisiyle belgeniz analiz edilip özetlenir</li>
<li>Belgenin içeriğine en uygun görseller ve anlatım senaryosu çıkarılır</li>
<li>Sizin seçtiğiniz dil, stil ve süreye göre yepyeni bir video oluşturulur</li>
</ol>
</div>
</div>
</div>
</div> </div>
); );
} }
+40 -25
View File
@@ -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,
@@ -60,41 +61,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 +113,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}
@@ -136,7 +151,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) => {
@@ -169,10 +184,10 @@ export default function DashboardPage() {
<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/projects/new"
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 +197,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 +210,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 +223,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>
@@ -45,13 +45,13 @@ const XIcon = ({ size = 16 }: { size?: number }) => (
); );
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 = [
@@ -240,7 +240,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>
@@ -379,7 +379,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 +393,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 +438,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 +470,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 +534,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 +554,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)]">
@@ -585,8 +585,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 +595,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 +612,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 +632,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 +658,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" />
@@ -1,105 +1,33 @@
"use client"; "use client";
import { useState, useMemo } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { import {
ArrowLeft, ArrowLeft,
ArrowRight, ArrowRight,
Sparkles, Sparkles,
Languages,
Clock,
Palette,
Monitor,
Smartphone,
Square,
Loader2, Loader2,
Check, Check,
Wand2, Wand2,
Search,
ChevronDown,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useCreateProject } from "@/hooks/use-api"; import { useCreateProject } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast"; import { useToast } from "@/components/ui/toast";
import { projectsApi } from "@/lib/api/api-service"; import { projectsApi } from "@/lib/api/api-service";
import {
LanguageSelector,
StyleSelector,
DurationSelector,
AspectRatioSelector,
languages,
videoStyles,
aspectRatios,
} from "@/components/projects/ProjectConfiguration";
const steps = ["Konu & Dil", "Stil & Süre", "AI Senaryo"]; 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() { export default function NewProjectPage() {
const router = useRouter(); const router = useRouter();
const createProject = useCreateProject(); const createProject = useCreateProject();
@@ -114,26 +42,6 @@ export default function NewProjectPage() {
const [duration, setDuration] = useState(60); const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16"); 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 canProceed = currentStep === 0 ? topic.trim().length >= 5 : true;
const isGenerating = createProject.isPending; const isGenerating = createProject.isPending;
@@ -142,9 +50,9 @@ export default function NewProjectPage() {
// Backend DTO alanları: prompt (zorunlu), videoStyle, title, language, aspectRatio, targetDuration // Backend DTO alanları: prompt (zorunlu), videoStyle, title, language, aspectRatio, targetDuration
const result = await createProject.mutateAsync({ const result = await createProject.mutateAsync({
title: topic.slice(0, 80), title: topic.slice(0, 80),
prompt: topic, // ← topic → prompt (backend alanı) prompt: topic,
language, language,
videoStyle: style, // ← style → videoStyle (backend alanı) videoStyle: style,
cinematicReference: cinematicReference ? cinematicReference : undefined, cinematicReference: cinematicReference ? cinematicReference : undefined,
targetDuration: duration, targetDuration: duration,
aspectRatio, aspectRatio,
@@ -190,9 +98,9 @@ export default function NewProjectPage() {
className={cn( className={cn(
"flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium transition-all", "flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium transition-all",
i === currentStep i === currentStep
? "bg-violet-500/15 text-violet-400 border border-violet-500/25" ? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
: i < currentStep : i < currentStep
? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 cursor-pointer" ? "bg-[var(--color-bg-inverted)]/10 text-[var(--color-text-primary)] border border-[var(--color-bg-inverted)]/20 cursor-pointer"
: "text-[var(--color-text-ghost)] border border-[var(--color-border-faint)]" : "text-[var(--color-text-ghost)] border border-[var(--color-border-faint)]"
)} )}
> >
@@ -208,7 +116,7 @@ export default function NewProjectPage() {
{i < steps.length - 1 && ( {i < steps.length - 1 && (
<div className={cn( <div className={cn(
"w-6 h-px", "w-6 h-px",
i < currentStep ? "bg-emerald-500/40" : "bg-[var(--color-border-faint)]" i < currentStep ? "bg-[var(--color-bg-inverted)]/40" : "bg-[var(--color-border-faint)]"
)} /> )} />
)} )}
</div> </div>
@@ -229,14 +137,14 @@ export default function NewProjectPage() {
{/* Konu */} {/* Konu */}
<div> <div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block"> <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" /> <Sparkles size={14} className="inline mr-1.5 text-[var(--color-text-primary)]" />
Videonun Konusu Videonun Konusu
</label> </label>
<textarea <textarea
value={topic} value={topic}
onChange={(e) => setTopic(e.target.value)} onChange={(e) => setTopic(e.target.value)}
placeholder="Örn: Boötes Boşluğu — evrendeki en büyük boşluk ve gizemi..." 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" className="w-full h-32 px-4 py-3 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-primary)] text-sm placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-[var(--color-border-default)] transition-all resize-none"
/> />
<p className="text-[11px] text-[var(--color-text-ghost)] mt-1.5"> <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) Ne kadar detaylı yazarsan, AI o kadar iyi senaryo üretir ({topic.length} karakter)
@@ -244,29 +152,7 @@ export default function NewProjectPage() {
</div> </div>
{/* Dil Seçimi */} {/* Dil Seçimi */}
<div> <LanguageSelector value={language} onChange={setLanguage} />
<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> </motion.div>
)} )}
@@ -280,154 +166,18 @@ export default function NewProjectPage() {
className="space-y-6" className="space-y-6"
> >
{/* Video Stili */} {/* Video Stili */}
<div> <StyleSelector
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3"> value={style}
<label className="text-sm font-medium text-[var(--color-text-secondary)] block"> onChange={setStyle}
<Palette size={14} className="inline mr-1.5 text-violet-400" /> cinematicReference={cinematicReference}
Video Stili onCinematicReferenceChange={setCinematicReference}
</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 */} {/* Süre */}
<div> <DurationSelector value={duration} onChange={setDuration} />
<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ı */} {/* En-Boy Oranı */}
<div> <AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
<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> </motion.div>
)} )}
@@ -476,7 +226,7 @@ export default function NewProjectPage() {
className={cn( className={cn(
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all", "w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
isGenerating isGenerating
? "bg-violet-500/20 text-violet-300 cursor-wait" ? "bg-[var(--color-bg-inverted)]/20 text-[var(--color-text-primary)] cursor-wait"
: "btn-primary text-lg" : "btn-primary text-lg"
)} )}
> >
@@ -40,20 +40,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: {
@@ -136,7 +136,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 +163,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 +177,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 +191,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,7 +221,7 @@ 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>
@@ -234,7 +234,7 @@ export default function ProjectsPage() {
return ( return (
<div <div
key={project.id} key={project.id}
className="flex items-center rounded-xl card hover:border-violet-500/20 transition-all group relative" className="flex items-center rounded-xl card hover:border-neutral-400 dark:hover:border-neutral-600 transition-all group relative"
> >
<Link <Link
href={`/dashboard/projects/${project.id}`} href={`/dashboard/projects/${project.id}`}
@@ -246,7 +246,7 @@ export default function ProjectsPage() {
<StIcon size={18} /> <StIcon size={18} />
</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-[var(--color-text-secondary)] transition-colors">
{project.title} {project.title}
</p> </p>
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]"> <div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]">
@@ -274,7 +274,7 @@ export default function ProjectsPage() {
</span> </span>
<ExternalLink <ExternalLink
size={14} size={14}
className="text-[var(--color-text-ghost)] group-hover:text-violet-400 transition-colors shrink-0" className="text-[var(--color-text-ghost)] group-hover:text-[var(--color-text-primary)] transition-colors shrink-0"
/> />
</Link> </Link>
@@ -0,0 +1,22 @@
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('TEXT TO VIDEO ERROR:', error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<pre>{error.message}</pre>
<pre>{error.stack}</pre>
</div>
)
}
@@ -0,0 +1,130 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { useCreateFromText } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
import { Loader2, ArrowRight, Wand2, Type } from "lucide-react";
import {
LanguageSelector,
StyleSelector,
DurationSelector,
AspectRatioSelector,
} from "@/components/projects/ProjectConfiguration";
export default function TextToVideoPage() {
const router = useRouter();
const { toast } = useToast();
const createFromText = useCreateFromText();
const [textInput, setTextInput] = useState("");
const [style, setStyle] = useState("CINEMATIC");
const [cinematicReference, setCinematicReference] = useState("");
const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
const [language, setLanguage] = useState("tr");
const handleGenerate = async () => {
if (!textInput.trim()) {
toast("error", "Lütfen bir konu, hikaye veya metin girin.");
return;
}
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = await createFromText.mutateAsync({
text: textInput,
language,
aspectRatio,
videoStyle: style,
cinematicReference: cinematicReference ? cinematicReference : undefined,
targetDuration: duration,
});
toast("success", "Video projesi başarıyla oluşturuldu!");
router.push(`/dashboard/projects/${result.id}`);
} catch (error) {
toast("error", "Proje oluşturulurken bir hata oluştu.");
}
};
return (
<div className="max-w-3xl mx-auto space-y-8 pb-24">
{/* Header */}
<div className="text-center space-y-3 pb-4">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
<Type size={32} />
</div>
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
Metinden Video Üret
</h1>
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
İstediğiniz konuyu, bir hikayeyi veya makaleyi kopyalayıp yapıştırın; yapay zeka sizin için detaylı bir video senaryosu üretsin.
</p>
</div>
{/* Input */}
<div className="card p-6 md:p-8 space-y-6">
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
Fikriniz veya Metniniz
</label>
<textarea
value={textInput}
onChange={(e) => setTextInput(e.target.value)}
placeholder="Örn: Evrenin başlangıcı ve kara deliklerin sırları hakkında detaylı ve sürükleyici bir anlatım..."
rows={6}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-bg-inverted)]/50 resize-none transition-all placeholder:text-[var(--color-text-ghost)]"
/>
</div>
{/* Configurations */}
<div className="space-y-8 pt-6 border-t border-[var(--color-border-faint)]">
<LanguageSelector value={language} onChange={setLanguage} />
<StyleSelector
value={style}
onChange={setStyle}
cinematicReference={cinematicReference}
onCinematicReferenceChange={setCinematicReference}
/>
<DurationSelector value={duration} onChange={setDuration} />
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
</div>
</div>
{/* Action Button */}
<div className="flex justify-end">
<button
onClick={handleGenerate}
disabled={createFromText.isPending || !textInput.trim()}
className={cn(
"group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
"bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
"disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
)}
>
{createFromText.isPending ? (
<>
<Loader2 size={20} className="animate-spin" />
<span>Yapay Zeka Senaryoyu Yazıyor...</span>
</>
) : (
<>
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
<span>Video Projesi Oluştur</span>
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</>
)}
</button>
</div>
</div>
);
}
@@ -8,11 +8,6 @@ import {
Link2, Link2,
Loader2, Loader2,
ArrowRight, ArrowRight,
Clock,
Palette,
Monitor,
Smartphone,
Square,
Sparkles, Sparkles,
Wand2, Wand2,
MessageSquare, MessageSquare,
@@ -24,31 +19,16 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTweetPreview, useCreateFromTweet } from "@/hooks/use-api"; import { useTweetPreview, useCreateFromTweet } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast"; import { useToast } from "@/components/ui/toast";
import {
const videoStyles = [ LanguageSelector,
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬" }, StyleSelector,
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹" }, DurationSelector,
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚" }, AspectRatioSelector,
{ id: "STORYTELLING", label: "Hikâye", emoji: "📖" }, } from "@/components/projects/ProjectConfiguration";
{ 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() { export default function XToVideoPage() {
const router = useRouter(); const router = useRouter();
const toast = useToast(); const { toast } = useToast();
const tweetPreview = useTweetPreview(); const tweetPreview = useTweetPreview();
const createFromTweet = useCreateFromTweet(); const createFromTweet = useCreateFromTweet();
@@ -66,15 +46,15 @@ export default function XToVideoPage() {
const handlePreview = async () => { const handlePreview = async () => {
if (!isValidUrl) { if (!isValidUrl) {
toast.error("Geçerli bir X/Twitter URL'si girin (https://x.com/...)"); toast("error", "Geçerli bir X/Twitter URL'si girin (https://x.com/...)");
return; return;
} }
try { try {
const result = await tweetPreview.mutateAsync(tweetUrl); const result = await tweetPreview.mutateAsync(tweetUrl);
setPreviewData(result); setPreviewData(result);
toast.success("Tweet başarıyla yüklendi!"); toast("success", "Tweet başarıyla yüklendi!");
} catch { } catch {
toast.error("Tweet yüklenemedi. URL'yi kontrol edin."); toast("error", "Tweet yüklenemedi. URL'yi kontrol edin.");
} }
}; };
@@ -89,7 +69,7 @@ export default function XToVideoPage() {
cinematicReference: cinematicReference ? cinematicReference : undefined, cinematicReference: cinematicReference ? cinematicReference : undefined,
targetDuration: duration, targetDuration: duration,
}); });
toast.success("Tweet → Video projesi oluşturuldu!"); toast("success", "Tweet → Video projesi oluşturuldu!");
const projectId = result?.id; const projectId = result?.id;
if (projectId) { if (projectId) {
router.push(`/dashboard/projects/${projectId}`); router.push(`/dashboard/projects/${projectId}`);
@@ -97,22 +77,21 @@ export default function XToVideoPage() {
router.push("/dashboard/projects"); router.push("/dashboard/projects");
} }
} catch { } catch {
toast.error("Proje oluşturulurken bir hata oluştu."); toast("error", "Proje oluşturulurken bir hata oluştu.");
} }
}; };
return ( return (
<div className="max-w-3xl mx-auto space-y-6"> <div className="max-w-3xl mx-auto space-y-6 pb-24">
{/* Header */} {/* Header */}
<div> <div className="text-center space-y-3 pb-4">
<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"> <div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
<AtSign size={12} /> <AtSign size={32} />
X Video
</div> </div>
<h1 className="font-[family-name:var(--font-display)] text-2xl md:text-3xl font-bold"> <h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
Tweet'ten Video Oluştur Tweet'ten Video Oluştur
</h1> </h1>
<p className="text-sm text-[var(--color-text-muted)] mt-1"> <p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
X/Twitter yazılarını AI ile kısa videolara dönüştürün X/Twitter yazılarını AI ile kısa videolara dönüştürün
</p> </p>
</div> </div>
@@ -132,7 +111,7 @@ export default function XToVideoPage() {
setPreviewData(null); setPreviewData(null);
}} }}
placeholder="https://x.com/username/status/123456..." 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" className="flex-1 px-4 py-2.5 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-[var(--color-bg-inverted)]/40 focus:ring-1 focus:ring-[var(--color-bg-inverted)]/20 transition-all"
/> />
<button <button
onClick={handlePreview} onClick={handlePreview}
@@ -235,7 +214,7 @@ export default function XToVideoPage() {
{previewData.tweet?.metrics?.views ?? 0} {previewData.tweet?.metrics?.views ?? 0}
</span> </span>
{previewData.tweet?.isThread && ( {previewData.tweet?.isThread && (
<span className="flex items-center gap-1 text-violet-400"> <span className="flex items-center gap-1 text-[var(--color-bg-inverted)]">
<MessageSquare size={12} /> <MessageSquare size={12} />
{previewData.tweet?.threadTweets?.length ?? 0} tweet thread {previewData.tweet?.threadTweets?.length ?? 0} tweet thread
</span> </span>
@@ -245,7 +224,7 @@ export default function XToVideoPage() {
{/* Suggested info */} {/* Suggested info */}
{previewData.suggestedTitle && ( {previewData.suggestedTitle && (
<div className="flex items-center gap-1.5 text-[11px] text-cyan-400"> <div className="flex items-center gap-1.5 text-[11px] text-[var(--color-text-secondary)]">
<Sparkles size={12} /> <Sparkles size={12} />
Önerilen başlık: {previewData.suggestedTitle} · Tahmini süre: {previewData.estimatedDuration}sn · Viral skoru: {previewData.viralScore}/100 Önerilen başlık: {previewData.suggestedTitle} · Tahmini süre: {previewData.estimatedDuration}sn · Viral skoru: {previewData.viralScore}/100
</div> </div>
@@ -253,7 +232,7 @@ export default function XToVideoPage() {
{/* Images tag */} {/* Images tag */}
{(previewData.tweet?.media?.filter((m: any) => m.type === 'photo')?.length > 0) && ( {(previewData.tweet?.media?.filter((m: any) => m.type === 'photo')?.length > 0) && (
<div className="flex items-center gap-1.5 text-[11px] text-cyan-400"> <div className="flex items-center gap-1.5 text-[11px] text-[var(--color-text-secondary)]">
<ImageIcon size={12} /> <ImageIcon size={12} />
{previewData.tweet.media.filter((m: any) => m.type === 'photo').length} görsel referans olarak kullanılacak + AI görsel üretilecek {previewData.tweet.media.filter((m: any) => m.type === 'photo').length} görsel referans olarak kullanılacak + AI görsel üretilecek
</div> </div>
@@ -270,147 +249,46 @@ export default function XToVideoPage() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="space-y-6" className="space-y-6"
> >
{/* Language */} <div className="card p-6 space-y-8">
<div className="card p-5 space-y-3"> <LanguageSelector value={language} onChange={setLanguage} />
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
Video Dili <StyleSelector
</label> value={style}
<div className="flex gap-2"> onChange={setStyle}
{languages.map((l) => ( cinematicReference={cinematicReference}
<button onCinematicReferenceChange={setCinematicReference}
key={l.code} />
onClick={() => setLanguage(l.code)}
className={cn( <DurationSelector value={duration} onChange={setDuration} />
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
language === l.code <AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
? "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> </div>
{/* Generate Button */} {/* Generate Button */}
<button <div className="flex justify-end">
onClick={handleGenerate} <button
disabled={createFromTweet.isPending} onClick={handleGenerate}
className={cn( disabled={createFromTweet.isPending}
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all", className={cn(
createFromTweet.isPending "group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
? "bg-violet-500/20 text-violet-300 cursor-wait" "bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
: "btn-primary text-lg", "disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
)} )}
> >
{createFromTweet.isPending ? ( {createFromTweet.isPending ? (
<> <>
<Loader2 size={20} className="animate-spin" /> <Loader2 size={20} className="animate-spin" />
<span>Video Projesi Oluşturuluyor...</span> <span>Video Projesi Oluşturuluyor...</span>
</> </>
) : ( ) : (
<> <>
<Wand2 size={20} /> <Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
<span>Tweet → Video Oluştur</span> <span>Tweet → Video Oluştur</span>
<ArrowRight size={16} /> <ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</> </>
)} )}
</button> </button>
</div>
<p className="text-center text-[11px] text-[var(--color-text-ghost)]"> <p className="text-center text-[11px] text-[var(--color-text-ghost)]">
Bu işlem 1 kredi kullanır • AI senaryo + görsel üretim dahil Bu işlem 1 kredi kullanır • AI senaryo + görsel üretim dahil
@@ -421,9 +299,9 @@ export default function XToVideoPage() {
{/* Info Box */} {/* Info Box */}
{!previewData && ( {!previewData && (
<div className="card p-5 bg-gradient-to-br from-violet-500/5 to-cyan-500/5"> <div className="card p-5 bg-[var(--color-bg-surface)]">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Sparkles size={20} className="text-violet-400 shrink-0 mt-0.5" /> <Sparkles size={20} className="text-[var(--color-text-secondary)] shrink-0 mt-0.5" />
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]"> <h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
Nasıl Çalışır? Nasıl Çalışır?
@@ -2,64 +2,47 @@
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation"; 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 { cn } from "@/lib/utils";
import { useCreateFromYoutube } from "@/hooks/use-api"; import { useCreateFromYoutube } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast"; import { useToast } from "@/components/ui/toast";
import {
const videoStyles = [ PlaySquare,
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬" }, Link2,
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹" }, Loader2,
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚" }, ArrowRight,
{ id: "STORYTELLING", label: "Hikâye", emoji: "📖" }, Sparkles,
{ id: "NEWS", label: "Haber", emoji: "📰" }, Wand2,
]; } from "lucide-react";
import {
const aspectRatios = [ LanguageSelector,
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" }, StyleSelector,
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" }, DurationSelector,
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" }, AspectRatioSelector,
]; } from "@/components/projects/ProjectConfiguration";
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() { export default function YoutubeToVideoPage() {
const router = useRouter(); const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const createFromYoutube = useCreateFromYoutube(); const createFromYoutube = useCreateFromYoutube();
const [youtubeUrl, setYoutubeUrl] = useState(""); const [youtubeUrl, setYoutubeUrl] = useState("");
const [style, setStyle] = useState("CINEMATIC"); const [style, setStyle] = useState("CINEMATIC");
const [cinematicReference, setCinematicReference] = useState(""); const [cinematicReference, setCinematicReference] = useState("");
const [duration, setDuration] = useState(60); const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16"); const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
const [language, setLanguage] = useState("tr"); const [language, setLanguage] = useState("tr");
const isValidUrl = /https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/.+/.test(youtubeUrl);
const handleGenerate = async () => { const handleGenerate = async () => {
if (!youtubeUrl.includes("youtube.com") && !youtubeUrl.includes("youtu.be")) { if (!isValidUrl) {
toast("error", "Lütfen geçerli bir YouTube tam linki girin."); toast("error", "Lütfen geçerli bir YouTube URL'si girin.");
return; return;
} }
try { try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = await createFromYoutube.mutateAsync({ const result: any = await createFromYoutube.mutateAsync({
youtubeUrl, youtubeUrl,
language, language,
@@ -71,8 +54,8 @@ export default function YoutubeToVideoPage() {
toast("success", "YouTube → Video projesi oluşturuldu!"); toast("success", "YouTube → Video projesi oluşturuldu!");
router.push(`/dashboard/projects/${result.id}`); router.push(`/dashboard/projects/${result.id}`);
} catch (error) { } catch {
toast("error", "Proje oluşturulurken hata oluştu."); toast("error", "Proje oluşturulurken bir hata oluştu.");
} }
}; };
@@ -80,186 +63,95 @@ export default function YoutubeToVideoPage() {
<div className="max-w-3xl mx-auto space-y-8 pb-24"> <div className="max-w-3xl mx-auto space-y-8 pb-24">
{/* Header */} {/* Header */}
<div className="text-center space-y-3 pb-4"> <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)]"> <div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
<Link2 size={32} /> <PlaySquare size={32} />
</div> </div>
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]"> <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 YouTube'dan Video Oluştur
</h1> </h1>
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto"> <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. YouTube videolarını veya Shorts içeriklerini kendi tarzınızda yeniden üretin
</p> </p>
</div> </div>
{/* Input */} {/* Main Form */}
<div className="card p-6 md:p-8 space-y-4"> <div className="card p-6 md:p-8 space-y-6">
{/* Input */}
<div> <div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block"> <label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block flex items-center gap-1.5">
YouTube URL <Link2 size={14} className="text-red-500" />
YouTube Video URL
</label> </label>
<div className="relative"> <input
<input type="url"
type="text" value={youtubeUrl}
placeholder="https://www.youtube.com/watch?v=..." onChange={(e) => setYoutubeUrl(e.target.value)}
value={youtubeUrl} placeholder="https://youtube.com/watch?v=... veya https://youtu.be/..."
onChange={(e) => setYoutubeUrl(e.target.value)} className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl px-4 py-3.5 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-bg-inverted)]/50 transition-all placeholder:text-[var(--color-text-ghost)]"
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" </div>
/>
<Link2 {/* Configurations */}
size={18} <div className="space-y-8 pt-6 border-t border-[var(--color-border-faint)]">
className="absolute left-4 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]" <LanguageSelector value={language} onChange={setLanguage} />
/>
</div> <StyleSelector
value={style}
onChange={setStyle}
cinematicReference={cinematicReference}
onCinematicReferenceChange={setCinematicReference}
/>
<DurationSelector value={duration} onChange={setDuration} />
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
</div> </div>
</div> </div>
{/* Video Settings */} {/* Action Button */}
<div className="space-y-6"> <div className="flex justify-end">
<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 <button
onClick={handleGenerate} onClick={handleGenerate}
disabled={createFromYoutube.isPending || !youtubeUrl} disabled={createFromYoutube.isPending || !isValidUrl}
className={cn( className={cn(
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all", "group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
createFromYoutube.isPending "bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
? "bg-red-500/20 text-red-400 cursor-wait" "disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
: "bg-red-500 hover:bg-red-600 text-white shadow-lg shadow-red-500/20",
!youtubeUrl && "opacity-50 cursor-not-allowed"
)} )}
> >
{createFromYoutube.isPending ? ( {createFromYoutube.isPending ? (
<> <>
<Loader2 size={20} className="animate-spin" /> <Loader2 size={20} className="animate-spin" />
<span>Video Projesi Oluşturuluyor... (Bu işlem uzun sürebilir)</span> <span>Yapay Zeka Videoyu İşliyor...</span>
</> </>
) : ( ) : (
<> <>
<Wand2 size={20} /> <Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
<span>YouTube Video Oluştur</span> <span>YouTube → Video Üret</span>
<ArrowRight size={16} /> <ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</> </>
)} )}
</button> </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>
{/* Info Box */}
<div className="card p-5 bg-[var(--color-bg-surface)]">
<div className="flex items-start gap-3">
<Sparkles size={20} className="text-[var(--color-text-secondary)] shrink-0 mt-0.5" />
<div className="space-y-2">
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
Nasıl Çalışır?
</h3>
<ol className="text-xs text-[var(--color-text-muted)] space-y-1.5 list-decimal list-inside">
<li>Uzun bir YouTube videosu veya Shorts URL'si yapıştırın</li>
<li>Video otomatik olarak indirilir ve deşifre edilir (transkript)</li>
<li>Belirttiğiniz süreye ve tarza göre yepyeni bir senaryo çıkarılır</li>
<li>Orijinal video kullanılmaz, referans olarak alınıp yeni görseller + ses üretilir</li>
</ol>
</div>
</div>
</div>
</div> </div>
); );
} }
+70 -116
View File
@@ -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);
} }
+35 -27
View File
@@ -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 = ["#ffffff", "#a3a3a3", "#525252", "#262626"];
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,42 +88,42 @@ 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="5%" stopColor="#ffffff" stopOpacity={0.2} />
<stop offset="95%" stopColor="#8b5cf6" stopOpacity={0} /> <stop offset="95%" stopColor="#ffffff" stopOpacity={0} />
</linearGradient> </linearGradient>
<linearGradient id="colorVideos" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorVideos" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#06b6d4" stopOpacity={0.3} /> <stop offset="5%" stopColor="#737373" stopOpacity={0.2} />
<stop offset="95%" stopColor="#06b6d4" stopOpacity={0} /> <stop offset="95%" stopColor="#737373" stopOpacity={0} />
</linearGradient> </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="#ffffff"
strokeWidth={2} strokeWidth={2}
fill="url(#colorProjects)" fill="url(#colorProjects)"
name="Projeler" name="Projeler"
@@ -124,7 +131,7 @@ export function DashboardCharts() {
<Area <Area
type="monotone" type="monotone"
dataKey="videos" dataKey="videos"
stroke="#06b6d4" stroke="#737373"
strokeWidth={2} strokeWidth={2}
fill="url(#colorVideos)" fill="url(#colorVideos)"
name="Videolar" name="Videolar"
@@ -134,26 +141,27 @@ export function DashboardCharts() {
</div> </div>
{/* Proje Durumu */} {/* Proje Durumu */}
<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">
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 items-center justify-center h-[220px] 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 items-center gap-6">
<ResponsiveContainer width="50%" height={200}> <ResponsiveContainer width="50%" height={220}>
<PieChart> <PieChart>
<Pie <Pie
data={pieData} data={pieData}
cx="50%" cx="50%"
cy="50%" cy="50%"
outerRadius={70} outerRadius={80}
innerRadius={40} innerRadius={50}
dataKey="value" dataKey="value"
stroke="none" stroke="var(--color-bg-surface)"
strokeWidth={2}
> >
{pieData.map((_: unknown, index: number) => ( {pieData.map((_: unknown, index: number) => (
<Cell key={index} fill={COLORS[index % COLORS.length]} /> <Cell key={index} fill={COLORS[index % COLORS.length]} />
@@ -161,10 +169,10 @@ export function DashboardCharts() {
</Pie> </Pie>
<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",
}} }}
/> />
+3 -3
View File
@@ -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)]">
+12 -12
View File
@@ -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 ? (
+39 -38
View File
@@ -13,6 +13,7 @@ const navItems = [
{ href: "/dashboard", icon: Home, label: "Ana Sayfa" }, { href: "/dashboard", icon: Home, label: "Ana Sayfa" },
{ href: "/dashboard/projects", icon: FolderOpen, label: "Projeler" }, { href: "/dashboard/projects", icon: FolderOpen, label: "Projeler" },
{ href: "/dashboard/render-queue", icon: Activity, label: "Render Monitör" }, { href: "/dashboard/render-queue", icon: Activity, label: "Render Monitör" },
{ href: "/dashboard/text-to-video", icon: FileText, label: "Metin → Video" },
{ href: "/dashboard/x-to-video", icon: AtSign, label: "X → Video" }, { href: "/dashboard/x-to-video", icon: AtSign, label: "X → Video" },
{ href: "/dashboard/youtube-to-video", icon: Link2, label: "YT → Video" }, { href: "/dashboard/youtube-to-video", icon: Link2, label: "YT → Video" },
{ href: "/dashboard/document-to-video", icon: FileText, label: "Belge → Video" }, { href: "/dashboard/document-to-video", icon: FileText, label: "Belge → Video" },
@@ -40,23 +41,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 +80,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 +94,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 +108,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 +116,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 +145,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 +170,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 +208,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 +221,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>
+42 -13
View File
@@ -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>
); );
})} })}
+13 -13
View File
@@ -74,12 +74,12 @@ 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-4 md:p-5 hover:border-neutral-500/20 transition-all duration-300">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<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-8 h-8 rounded-lg bg-[var(--color-bg-elevated)] flex items-center justify-center border border-[var(--color-border-faint)]">
<span className="text-xs font-bold text-violet-400">{scene.order}</span> <span className="text-xs font-bold text-neutral-400">{scene.order}</span>
</div> </div>
<div> <div>
<h4 className="text-sm font-semibold text-[var(--color-text-primary)]"> <h4 className="text-sm font-semibold text-[var(--color-text-primary)]">
@@ -101,7 +101,7 @@ export function SceneCard({
<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 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-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800 transition-colors"
title="Düzenle" title="Düzenle"
> >
<Pencil size={13} /> <Pencil size={13} />
@@ -109,7 +109,7 @@ export function SceneCard({
<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-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
title="AI ile yeniden üret" title="AI ile yeniden üret"
> >
<RefreshCw size={13} className={isRegenerating ? 'animate-spin' : ''} /> <RefreshCw size={13} className={isRegenerating ? 'animate-spin' : ''} />
@@ -136,7 +136,7 @@ export function SceneCard({
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-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] resize-none focus:outline-none focus:ring-1 focus:ring-neutral-500/40 transition-all"
/> />
</div> </div>
@@ -149,7 +149,7 @@ export function SceneCard({
value={editVisual} value={editVisual}
onChange={(e) => setEditVisual(e.target.value)} onChange={(e) => setEditVisual(e.target.value)}
rows={2} rows={2}
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-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-secondary)] resize-none focus:outline-none focus:ring-1 focus:ring-neutral-500/40 transition-all"
/> />
</div> </div>
@@ -157,7 +157,7 @@ export function SceneCard({
<div className="flex items-center gap-2 pt-1"> <div className="flex items-center gap-2 pt-1">
<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-3 py-1.5 rounded-lg bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] text-xs font-medium hover:bg-neutral-800 transition-colors"
> >
<Check size={13} /> Kaydet <Check size={13} /> Kaydet
</button> </button>
@@ -173,8 +173,8 @@ export function SceneCard({
<motion.div key="viewing" className="space-y-2.5"> <motion.div key="viewing" className="space-y-2.5">
{/* Narrasyon */} {/* Narrasyon */}
<div className="flex gap-2"> <div className="flex gap-2">
<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-5 h-5 rounded-md bg-[var(--color-bg-elevated)] flex items-center justify-center shrink-0 mt-0.5 border border-[var(--color-border-faint)]">
<Mic size={11} className="text-violet-400" /> <Mic size={11} className="text-neutral-400" />
</div> </div>
<p className="text-sm text-[var(--color-text-secondary)] leading-relaxed"> <p className="text-sm text-[var(--color-text-secondary)] leading-relaxed">
{scene.narrationText} {scene.narrationText}
@@ -183,8 +183,8 @@ export function SceneCard({
{/* Görsel Prompt */} {/* Görsel Prompt */}
<div className="flex gap-2"> <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"> <div className="w-5 h-5 rounded-md bg-[var(--color-bg-elevated)] flex items-center justify-center shrink-0 mt-0.5 border border-[var(--color-border-faint)]">
<ImageIcon size={11} className="text-cyan-400" /> <ImageIcon size={11} className="text-neutral-400" />
</div> </div>
<div className="flex-1 group/prompt relative"> <div className="flex-1 group/prompt relative">
<p className="text-xs text-[var(--color-text-ghost)] leading-relaxed italic pr-6"> <p className="text-xs text-[var(--color-text-ghost)] leading-relaxed italic pr-6">
@@ -194,7 +194,7 @@ export function SceneCard({
onClick={() => { onClick={() => {
navigator.clipboard.writeText(scene.visualPrompt); navigator.clipboard.writeText(scene.visualPrompt);
}} }}
className="absolute top-0 right-0 p-1 opacity-0 group-hover/prompt:opacity-100 transition-opacity bg-[var(--color-bg-elevated)] rounded-md text-[var(--color-text-muted)] hover:text-cyan-400 hover:bg-cyan-500/10" className="absolute top-0 right-0 p-1 opacity-0 group-hover/prompt:opacity-100 transition-opacity bg-[var(--color-bg-elevated)] rounded-md text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800"
title="Prompt'u Kopyala" title="Prompt'u Kopyala"
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
@@ -0,0 +1,363 @@
"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: "🇯🇵" },
];
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={180}
step={5}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className={cn(
"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-[var(--color-bg-inverted)]",
"[&::-webkit-slider-thumb]:shadow-md",
"[&::-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>
);
}
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-3 rounded-xl text-xs transition-all",
value === ar.id
? "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)]"
)}
>
<Icon size={20} />
<span className="font-semibold">{ar.label}</span>
<span
className={cn(
"text-[10px]",
value === ar.id
? "text-[var(--color-text-inverted)]/70"
: "text-[var(--color-text-ghost)]"
)}
>
{ar.desc}
</span>
</button>
);
})}
</div>
</div>
);
}
+16 -12
View File
@@ -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>
); );
} }
+13
View File
@@ -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,
@@ -434,6 +435,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ı
// ═══════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════
+13
View File
@@ -271,6 +271,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;
} }
@@ -378,6 +388,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);
+17 -16
View File
@@ -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: {
+13
View File
@@ -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();
})();
+1 -1
View File
File diff suppressed because one or more lines are too long