generated from fahricansecer/boilerplate-fe
This commit is contained in:
@@ -36,11 +36,11 @@ const navLinks = [
|
||||
|
||||
function colorClass(color: string, type: "bg" | "text" | "icon") {
|
||||
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" },
|
||||
cyan: { bg: "from-cyan-500/12 to-cyan-600/5", text: "text-cyan-400", icon: "bg-cyan-500/12" },
|
||||
emerald: { bg: "from-emerald-500/12 to-emerald-600/5", text: "text-emerald-400", icon: "bg-emerald-500/12" },
|
||||
amber: { bg: "from-amber-500/12 to-amber-600/5", text: "text-amber-400", icon: "bg-amber-500/12" },
|
||||
rose: { bg: "from-rose-500/12 to-rose-600/5", text: "text-rose-400", icon: "bg-rose-500/12" },
|
||||
violet: { bg: "from-neutral-500/12 to-neutral-600/5", text: "text-neutral-400", icon: "bg-neutral-500/12" },
|
||||
cyan: { bg: "from-neutral-500/12 to-neutral-600/5", text: "text-neutral-400", icon: "bg-neutral-500/12" },
|
||||
emerald: { bg: "from-neutral-500/12 to-neutral-600/5", text: "text-neutral-400", icon: "bg-neutral-500/12" },
|
||||
amber: { bg: "from-neutral-500/12 to-neutral-600/5", text: "text-neutral-400", icon: "bg-neutral-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] ?? "";
|
||||
}
|
||||
@@ -110,14 +110,14 @@ export default function AdminPage() {
|
||||
Sistem genelinde yönetim ve izleme
|
||||
</p>
|
||||
</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>
|
||||
|
||||
{/* İstatistik Kartları */}
|
||||
<motion.div variants={fadeUp} className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3">
|
||||
{isLoading ? (
|
||||
<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>
|
||||
) : (
|
||||
statCards.map((card) => {
|
||||
@@ -154,15 +154,10 @@ export default function AdminPage() {
|
||||
<Link
|
||||
key={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 ${
|
||||
link.color === "violet" ? "from-violet-500 to-violet-700" :
|
||||
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 className={`w-11 h-11 rounded-xl bg-[var(--color-bg-inverted)] flex items-center justify-center shadow-lg`}>
|
||||
<Icon size={20} className="text-[var(--color-text-inverted)]" />
|
||||
</div>
|
||||
<div>
|
||||
<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">
|
||||
<div className="card-surface p-5">
|
||||
<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>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(projectsByStatus as Record<string, number>).map(([status, count]) => (
|
||||
@@ -192,14 +187,14 @@ export default function AdminPage() {
|
||||
|
||||
<div className="card-surface p-5">
|
||||
<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>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ key: "QUEUED", icon: Clock, color: "text-amber-400", label: "Beklemede" },
|
||||
{ key: "PROCESSING", icon: Loader2, color: "text-cyan-400", label: "İşleniyor" },
|
||||
{ key: "COMPLETED", icon: CheckCircle2, color: "text-emerald-400", label: "Tamamlandı" },
|
||||
{ key: "FAILED", icon: XCircle, color: "text-rose-400", label: "Başarısız" },
|
||||
{ key: "QUEUED", icon: Clock, color: "text-neutral-400", label: "Beklemede" },
|
||||
{ key: "PROCESSING", icon: Loader2, color: "text-neutral-400", label: "İşleniyor" },
|
||||
{ key: "COMPLETED", icon: CheckCircle2, color: "text-neutral-400", label: "Tamamlandı" },
|
||||
{ key: "FAILED", icon: XCircle, color: "text-neutral-400", label: "Başarısız" },
|
||||
].map(({ key, icon: Icon, color, label }) => (
|
||||
<div key={key} className="flex items-center justify-between text-sm">
|
||||
<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">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<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>
|
||||
<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
|
||||
</Link>
|
||||
</div>
|
||||
@@ -236,7 +231,7 @@ export default function AdminPage() {
|
||||
<p className="text-xs text-[var(--color-text-muted)]">{user.email}</p>
|
||||
</div>
|
||||
<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"}
|
||||
</span>
|
||||
<span className="text-xs text-[var(--color-text-ghost)]">
|
||||
|
||||
@@ -66,9 +66,9 @@ export default function AdminPlansPage() {
|
||||
}
|
||||
|
||||
const PLAN_COLORS: Record<string, string> = {
|
||||
free: "from-gray-500/10 to-gray-600/5 border-gray-500/20",
|
||||
pro: "from-violet-500/10 to-violet-600/5 border-violet-500/20",
|
||||
business: "from-amber-500/10 to-amber-600/5 border-amber-500/20",
|
||||
free: "from-neutral-500/10 to-neutral-600/5 border-neutral-500/20",
|
||||
pro: "from-[var(--color-bg-inverted)] to-neutral-800 border-[var(--color-border-faint)] text-[var(--color-text-inverted)]",
|
||||
business: "from-neutral-500/20 to-neutral-600/10 border-neutral-500/30",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -81,7 +81,7 @@ export default function AdminPlansPage() {
|
||||
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
||||
<div>
|
||||
<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>
|
||||
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
||||
Abonelik planlarını düzenle ve fiyatları güncelle
|
||||
@@ -94,7 +94,7 @@ export default function AdminPlansPage() {
|
||||
|
||||
{isLoading ? (
|
||||
<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 className="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||||
@@ -129,8 +129,8 @@ export default function AdminPlansPage() {
|
||||
{field.type === "toggle" ? (
|
||||
<button
|
||||
onClick={() => setEdit(plan.id, field.key, !val)}
|
||||
className={`flex items-center gap-2 text-sm px-3 py-1.5 rounded-xl ${
|
||||
val ? "bg-emerald-500/10 text-emerald-400" : "bg-rose-500/10 text-rose-400"
|
||||
className={`flex items-center gap-2 text-sm px-3 py-1.5 rounded-xl border ${
|
||||
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"}
|
||||
@@ -160,7 +160,7 @@ export default function AdminPlansPage() {
|
||||
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 ${
|
||||
savedIds.has(plan.id)
|
||||
? "bg-emerald-500/10 text-emerald-400"
|
||||
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] opacity-80"
|
||||
: hasEdits
|
||||
? "btn-primary"
|
||||
: "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";
|
||||
|
||||
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
||||
DRAFT: { label: "Taslak", color: "bg-gray-500/10 text-gray-400" },
|
||||
GENERATING_SCRIPT: { label: "Senaryo Üretiliyor", color: "bg-blue-500/10 text-blue-400" },
|
||||
PENDING: { label: "Kuyrukta", color: "bg-amber-500/10 text-amber-400" },
|
||||
GENERATING_MEDIA: { label: "Medya Üretiliyor", color: "bg-cyan-500/10 text-cyan-400" },
|
||||
RENDERING: { label: "Render", color: "bg-violet-500/10 text-violet-400" },
|
||||
COMPLETED: { label: "Tamamlandı", color: "bg-emerald-500/10 text-emerald-400" },
|
||||
FAILED: { label: "Başarısız", color: "bg-rose-500/10 text-rose-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-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||
PENDING: { label: "Kuyrukta", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||
GENERATING_MEDIA: { label: "Medya Üretiliyor", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||
RENDERING: { label: "Render", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||
COMPLETED: { label: "Tamamlandı", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||
FAILED: { label: "Başarısız", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||||
};
|
||||
|
||||
const fadeUp = {
|
||||
@@ -66,7 +66,7 @@ export default function AdminProjectsPage() {
|
||||
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
||||
<div>
|
||||
<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>
|
||||
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
||||
Toplam {meta.total ?? "—"} proje
|
||||
@@ -104,7 +104,7 @@ export default function AdminProjectsPage() {
|
||||
<motion.div variants={fadeUp} className="card-surface overflow-hidden">
|
||||
{isLoading ? (
|
||||
<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 className="overflow-x-auto">
|
||||
@@ -141,7 +141,7 @@ export default function AdminProjectsPage() {
|
||||
</span>
|
||||
{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="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>
|
||||
)}
|
||||
</td>
|
||||
@@ -156,7 +156,7 @@ export default function AdminProjectsPage() {
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
<ExternalLink size={13} />
|
||||
@@ -168,7 +168,7 @@ export default function AdminProjectsPage() {
|
||||
}
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<Trash2 size={13} />
|
||||
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
import { useAdminRenderJobs } from "@/hooks/use-api";
|
||||
|
||||
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 },
|
||||
PROCESSING: { label: "İşleniyor", color: "text-cyan-400 bg-cyan-500/10", icon: Loader2 },
|
||||
COMPLETED: { label: "Tamamlandı", color: "text-emerald-400 bg-emerald-500/10", icon: CheckCircle2 },
|
||||
FAILED: { label: "Başarısız", color: "text-rose-400 bg-rose-500/10", icon: XCircle },
|
||||
CANCELLED: { label: "İptal", color: "text-gray-400 bg-gray-500/10", icon: AlertTriangle },
|
||||
QUEUED: { label: "Beklemede", color: "text-neutral-400 bg-neutral-500/10 border border-neutral-500/20", icon: Clock },
|
||||
PROCESSING: { label: "İşleniyor", color: "text-neutral-400 bg-neutral-500/10 border border-neutral-500/20", icon: Loader2 },
|
||||
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-neutral-400 bg-neutral-500/10 border border-neutral-500/20", icon: XCircle },
|
||||
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) {
|
||||
@@ -62,7 +62,7 @@ export default function AdminRenderJobsPage() {
|
||||
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
||||
<div>
|
||||
<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>
|
||||
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
||||
Toplam {meta.total ?? "—"} render işi (15 saniyede bir güncellenir)
|
||||
@@ -81,8 +81,8 @@ export default function AdminRenderJobsPage() {
|
||||
onClick={() => { setStatusFilter(value); setPage(1); }}
|
||||
className={`text-xs px-3 py-1.5 rounded-full transition-colors border ${
|
||||
statusFilter === value
|
||||
? "border-violet-500 bg-violet-500/10 text-violet-400"
|
||||
: "border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-violet-500/40"
|
||||
? "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-neutral-500/40"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
@@ -93,7 +93,7 @@ export default function AdminRenderJobsPage() {
|
||||
<motion.div variants={fadeUp} className="card-surface overflow-hidden">
|
||||
{isLoading ? (
|
||||
<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 className="overflow-x-auto">
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function AdminUsersPage() {
|
||||
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
||||
<div>
|
||||
<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>
|
||||
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
||||
Toplam {meta.total ?? "—"} kullanıcı
|
||||
@@ -92,7 +92,7 @@ export default function AdminUsersPage() {
|
||||
<motion.div variants={fadeUp} className="card-surface overflow-hidden">
|
||||
{isLoading ? (
|
||||
<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 className="overflow-x-auto">
|
||||
@@ -127,7 +127,7 @@ export default function AdminUsersPage() {
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{roleNames.length > 0 ? (
|
||||
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}
|
||||
</span>
|
||||
))
|
||||
@@ -138,10 +138,10 @@ export default function AdminUsersPage() {
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded-full ${
|
||||
className={`text-xs px-2 py-0.5 rounded-full border ${
|
||||
user.isActive
|
||||
? "bg-emerald-500/10 text-emerald-400"
|
||||
: "bg-rose-500/10 text-rose-400"
|
||||
? "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"}
|
||||
@@ -154,7 +154,7 @@ export default function AdminUsersPage() {
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Coins size={14} />
|
||||
@@ -162,11 +162,7 @@ export default function AdminUsersPage() {
|
||||
<button
|
||||
onClick={() => toggleActive.mutate(user.id)}
|
||||
disabled={toggleActive.isPending}
|
||||
className={`p-1.5 rounded-lg transition-colors ${
|
||||
user.isActive
|
||||
? "hover:bg-rose-500/10 text-rose-400"
|
||||
: "hover:bg-emerald-500/10 text-emerald-400"
|
||||
}`}
|
||||
className={`p-1.5 rounded-lg transition-colors hover:bg-neutral-500/10 text-neutral-400`}
|
||||
title={user.isActive ? "Pasif Yap" : "Aktif Yap"}
|
||||
>
|
||||
{user.isActive ? <UserX size={14} /> : <UserCheck size={14} />}
|
||||
|
||||
@@ -1,53 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
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 {
|
||||
FileText,
|
||||
Upload,
|
||||
Loader2,
|
||||
ArrowRight,
|
||||
Clock,
|
||||
Palette,
|
||||
Monitor,
|
||||
Smartphone,
|
||||
Square,
|
||||
Sparkles,
|
||||
Wand2,
|
||||
X,
|
||||
File,
|
||||
} from "lucide-react";
|
||||
|
||||
const videoStyles = [
|
||||
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬" },
|
||||
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹" },
|
||||
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚" },
|
||||
{ id: "STORYTELLING", label: "Hikâye", emoji: "📖" },
|
||||
{ id: "NEWS", label: "Haber", emoji: "📰" },
|
||||
];
|
||||
|
||||
const aspectRatios = [
|
||||
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
|
||||
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
|
||||
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
|
||||
];
|
||||
|
||||
const languages = [
|
||||
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
|
||||
{ code: "en", label: "English", flag: "🇺🇸" },
|
||||
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
||||
{ code: "es", label: "Español", flag: "🇪🇸" },
|
||||
];
|
||||
import {
|
||||
LanguageSelector,
|
||||
StyleSelector,
|
||||
DurationSelector,
|
||||
AspectRatioSelector,
|
||||
} from "@/components/projects/ProjectConfiguration";
|
||||
|
||||
export default function DocumentToVideoPage() {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const createFromDoc = useCreateFromDocument();
|
||||
|
||||
const extractDocumentTopics = useExtractDocumentTopics();
|
||||
const createFromExtractedText = useCreateFromExtractedText();
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(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 [cinematicReference, setCinematicReference] = useState("");
|
||||
@@ -57,41 +38,34 @@ export default function DocumentToVideoPage() {
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setFile(e.target.files[0]);
|
||||
setExtractedData(null);
|
||||
setSelectedTopic(null);
|
||||
const selectedFile = e.target.files[0];
|
||||
|
||||
// Boyut kontrolü (örn: 10MB)
|
||||
if (selectedFile.size > 10 * 1024 * 1024) {
|
||||
toast("error", "Dosya boyutu 10MB'dan küçük olmalıdır.");
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExtractTopics = async () => {
|
||||
if (!file) {
|
||||
toast("error", "Lütfen bir belge seçin.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await extractDocumentTopics.mutateAsync({ file });
|
||||
setExtractedData(result);
|
||||
if (result.topics.length > 0) {
|
||||
setSelectedTopic(result.topics[0]);
|
||||
}
|
||||
toast("success", "Belge incelendi ve konular çıkarıldı!");
|
||||
} catch (error) {
|
||||
toast("error", "Konu çıkarılırken hata oluştu. Belki belge okunamıyor veya çok büyük.");
|
||||
const clearFile = () => {
|
||||
setFile(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!extractedData || !selectedTopic) {
|
||||
toast("error", "Lütfen bir belge yükleyip konu seçin.");
|
||||
if (!file) {
|
||||
toast("error", "Lütfen bir belge yükleyin.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result: any = await createFromExtractedText.mutateAsync({
|
||||
text: extractedData.text,
|
||||
topic: selectedTopic,
|
||||
originalFilename: extractedData.originalFilename,
|
||||
const result: any = await createFromDoc.mutateAsync({
|
||||
file,
|
||||
language,
|
||||
aspectRatio,
|
||||
videoStyle: style,
|
||||
@@ -99,10 +73,10 @@ export default function DocumentToVideoPage() {
|
||||
targetDuration: duration,
|
||||
});
|
||||
|
||||
toast("success", "Video projesi oluşturuldu!");
|
||||
toast("success", "Belge → Video projesi oluşturuldu!");
|
||||
router.push(`/dashboard/projects/${result.id}`);
|
||||
} catch (error) {
|
||||
toast("error", "Proje oluşturulurken hata oluştu.");
|
||||
} catch {
|
||||
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">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-3 pb-4">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-blue-500/10 text-blue-500 mb-2 ring-1 ring-blue-500/20 shadow-[0_0_30px_rgba(59,130,246,0.15)]">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
|
||||
<FileText size={32} />
|
||||
</div>
|
||||
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
|
||||
Belgeden Video Üret
|
||||
</h1>
|
||||
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
|
||||
PDF, Word, TXT vb. belgenizi yükleyin, yapay zeka içeriği tarayıp senaryolastirsin.
|
||||
PDF, Word veya Text dosyalarınızı yükleyin, yapay zeka sizin için profesyonel bir videoya dönüştürsün
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="card p-6 md:p-8 space-y-4">
|
||||
{/* Main Form */}
|
||||
<div className="card p-6 md:p-8 space-y-6">
|
||||
|
||||
{/* File Upload */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
Belge Yükle (PDF, DOCX, TXT)
|
||||
Belge Yükle (.pdf, .docx, .txt, vb.)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
accept=".pdf,.docx,.txt,.csv,.xlsx,.pptx"
|
||||
className="block w-full text-sm text-[var(--color-text-muted)]
|
||||
file:mr-4 file:py-3 file:px-4
|
||||
file:rounded-xl file:border-0
|
||||
file:text-sm file:font-semibold
|
||||
file:bg-blue-500/10 file:text-blue-500
|
||||
hover:file:bg-blue-500/20 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!file ? (
|
||||
<div
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="w-full h-32 border-2 border-dashed border-[var(--color-border-default)] rounded-xl flex flex-col items-center justify-center gap-3 bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-elevated)] transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-[var(--color-bg-elevated)] flex items-center justify-center text-[var(--color-text-muted)]">
|
||||
<Upload size={20} />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-medium text-[var(--color-text-primary)]">Tıklayın veya sürükleyin</p>
|
||||
<p className="text-xs text-[var(--color-text-ghost)] mt-1">Maksimum 10MB</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full p-4 border border-[var(--color-border-default)] rounded-xl flex items-center justify-between bg-[var(--color-bg-elevated)]">
|
||||
<div className="flex items-center gap-3 overflow-hidden">
|
||||
<div className="w-10 h-10 rounded-lg bg-[var(--color-bg-surface)] flex items-center justify-center text-[var(--color-text-secondary)] shrink-0">
|
||||
<File size={20} />
|
||||
</div>
|
||||
<div className="truncate">
|
||||
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate">{file.name}</p>
|
||||
<p className="text-xs text-[var(--color-text-ghost)]">{(file.size / 1024 / 1024).toFixed(2)} MB</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={clearFile}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-[var(--color-bg-surface)] text-[var(--color-text-muted)] transition-colors shrink-0"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
accept=".pdf,.doc,.docx,.txt,.csv"
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!extractedData && file && (
|
||||
<button
|
||||
onClick={handleExtractTopics}
|
||||
disabled={extractDocumentTopics.isPending}
|
||||
className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl font-medium text-white shadow-lg bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{extractDocumentTopics.isPending ? (
|
||||
<>
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
Belge İnceleniyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText size={18} />
|
||||
Konu Çıkar
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{extractedData && (
|
||||
<div className="mt-6 p-4 rounded-xl bg-blue-500/5 border border-blue-500/20">
|
||||
<h3 className="font-medium text-blue-400 mb-3 text-sm flex items-center gap-2">
|
||||
<Wand2 size={16} />
|
||||
Şu Konulardan Birini Seçin:
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{extractedData.topics.map((topic, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => setSelectedTopic(topic)}
|
||||
className={cn(
|
||||
"p-3 rounded-lg border text-sm cursor-pointer transition-all",
|
||||
selectedTopic === topic
|
||||
? "bg-blue-500/20 border-blue-500 text-blue-400"
|
||||
: "bg-[var(--color-bg-surface)] border-[var(--color-border-faint)] text-[var(--color-text-secondary)] hover:border-blue-500/50"
|
||||
)}
|
||||
>
|
||||
{topic}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 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>
|
||||
|
||||
{/* Video Settings */}
|
||||
<div className="space-y-6">
|
||||
<div className="card p-5 space-y-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
Video Dili
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{languages.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
onClick={() => setLanguage(l.code)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
||||
language === l.code
|
||||
? "bg-blue-500/12 border border-blue-500/30 text-blue-400"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<span>{l.flag}</span>
|
||||
{l.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-5 space-y-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
<Palette size={14} className="inline mr-1.5 text-blue-400" />
|
||||
Video Stili
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{videoStyles.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setStyle(s.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
||||
style === s.id
|
||||
? "bg-blue-500/12 border border-blue-500/30 text-blue-400"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<span>{s.emoji}</span>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{style === "CINEMATIC" && (
|
||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
Özel Sinematik Referans (Opsiyonel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
||||
value={cinematicReference}
|
||||
onChange={(e) => setCinematicReference(e.target.value)}
|
||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-xl py-2 px-3 text-sm focus:border-blue-500/50 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card p-5 space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
||||
Hedef Süre: <span className="text-blue-400">{duration}s</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={15}
|
||||
max={120}
|
||||
step={5}
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(Number(e.target.value))}
|
||||
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none
|
||||
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
|
||||
[&::-webkit-slider-thumb]:rounded-full
|
||||
[&::-webkit-slider-thumb]:bg-blue-500
|
||||
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(59,130,246,0.4)]
|
||||
[&::-webkit-slider-thumb]:cursor-grab"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
En-Boy Oranı
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{aspectRatios.map((ar) => {
|
||||
const Icon = ar.icon;
|
||||
return (
|
||||
<button
|
||||
key={ar.id}
|
||||
onClick={() => setAspectRatio(ar.id)}
|
||||
className={cn(
|
||||
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all",
|
||||
aspectRatio === ar.id
|
||||
? "bg-blue-500/12 border border-blue-500/30 text-blue-400"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<Icon size={20} />
|
||||
<span className="font-semibold">{ar.label}</span>
|
||||
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
||||
{ar.desc}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={createFromExtractedText.isPending || !selectedTopic}
|
||||
disabled={createFromDoc.isPending || !file}
|
||||
className={cn(
|
||||
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
|
||||
createFromExtractedText.isPending
|
||||
? "bg-blue-500/20 text-blue-400 cursor-wait"
|
||||
: "bg-blue-500 hover:bg-blue-600 text-white shadow-lg shadow-blue-500/20",
|
||||
!selectedTopic && "opacity-50 cursor-not-allowed"
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
{createFromExtractedText.isPending ? (
|
||||
{createFromDoc.isPending ? (
|
||||
<>
|
||||
<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} />
|
||||
<span>Konudan Video Oluştur</span>
|
||||
<ArrowRight size={16} />
|
||||
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
|
||||
<span>Belgeden Video Üret</span>
|
||||
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,36 +61,36 @@ function getStatCards(data?: typeof MOCK_STATS, creditBalance?: { balance: numbe
|
||||
value: String(stats.totalProjects),
|
||||
change: `${stats.completedVideos} tamamlandı`,
|
||||
icon: FolderOpen,
|
||||
gradient: "from-violet-500/12 to-violet-600/5",
|
||||
iconBg: "bg-violet-500/12",
|
||||
iconColor: "text-violet-400",
|
||||
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||||
iconBg: "bg-neutral-500/12",
|
||||
iconColor: "text-neutral-400",
|
||||
},
|
||||
{
|
||||
label: "Devam Eden",
|
||||
value: String(stats.activeRenderJobs),
|
||||
change: "İşleniyor",
|
||||
icon: PlayCircle,
|
||||
gradient: "from-cyan-500/12 to-cyan-600/5",
|
||||
iconBg: "bg-cyan-500/12",
|
||||
iconColor: "text-cyan-400",
|
||||
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||||
iconBg: "bg-neutral-500/12",
|
||||
iconColor: "text-neutral-400",
|
||||
},
|
||||
{
|
||||
label: "Tamamlanan",
|
||||
value: String(stats.completedVideos),
|
||||
change: "Bu ay",
|
||||
icon: CheckCircle2,
|
||||
gradient: "from-emerald-500/12 to-emerald-600/5",
|
||||
iconBg: "bg-emerald-500/12",
|
||||
iconColor: "text-emerald-400",
|
||||
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||||
iconBg: "bg-neutral-500/12",
|
||||
iconColor: "text-neutral-400",
|
||||
},
|
||||
{
|
||||
label: "Kalan Kredi",
|
||||
value: String(credits.balance),
|
||||
change: `${credits.monthlyLimit} üzerinden`,
|
||||
icon: Coins,
|
||||
gradient: "from-amber-500/12 to-amber-600/5",
|
||||
iconBg: "bg-amber-500/12",
|
||||
iconColor: "text-amber-400",
|
||||
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||||
iconBg: "bg-neutral-500/12",
|
||||
iconColor: "text-neutral-400",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -116,7 +116,7 @@ export default function DashboardPage() {
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<Loader2 size={32} className="animate-spin text-violet-500" />
|
||||
<Loader2 size={32} className="animate-spin text-neutral-500" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -151,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">
|
||||
{isLoading ? (
|
||||
<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>
|
||||
) : (
|
||||
statCards.map((stat) => {
|
||||
@@ -184,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">
|
||||
<Link
|
||||
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">
|
||||
<Sparkles size={20} className="text-white" />
|
||||
<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-[var(--color-text-inverted)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">AI ile Video Oluştur</h3>
|
||||
@@ -197,10 +197,10 @@ export default function DashboardPage() {
|
||||
|
||||
<Link
|
||||
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">
|
||||
<TrendingUp size={20} className="text-white" />
|
||||
<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-[var(--color-text-inverted)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">Şablon Keşfet</h3>
|
||||
@@ -210,10 +210,10 @@ export default function DashboardPage() {
|
||||
|
||||
<Link
|
||||
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">
|
||||
<Clock size={20} className="text-white" />
|
||||
<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-[var(--color-text-inverted)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">Devam Eden İşler</h3>
|
||||
@@ -223,14 +223,14 @@ export default function DashboardPage() {
|
||||
|
||||
<Link
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
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">
|
||||
<XIcon size={20} className="text-white" />
|
||||
<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-[var(--color-text-inverted)]" />
|
||||
</div>
|
||||
<div>
|
||||
<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 }> = {
|
||||
DRAFT: { label: 'Taslak', color: 'text-slate-400', icon: FileText, bgClass: 'bg-slate-500/10 border-slate-500/20' },
|
||||
GENERATING_SCRIPT: { label: 'Senaryo Üretiliyor', color: 'text-violet-400', icon: Sparkles, bgClass: 'bg-violet-500/10 border-violet-500/20' },
|
||||
PENDING: { label: 'Kuyrukta', color: 'text-amber-400', icon: Clock, bgClass: 'bg-amber-500/10 border-amber-500/20' },
|
||||
GENERATING_MEDIA: { label: 'Medya Üretiliyor', color: 'text-cyan-400', icon: Sparkles, bgClass: 'bg-cyan-500/10 border-cyan-500/20' },
|
||||
RENDERING: { label: 'Video İşleniyor', color: 'text-blue-400', icon: Film, bgClass: 'bg-blue-500/10 border-blue-500/20' },
|
||||
COMPLETED: { label: 'Tamamlandı', color: 'text-emerald-400', icon: CheckCircle2, bgClass: 'bg-emerald-500/10 border-emerald-500/20' },
|
||||
FAILED: { label: 'Başarısız', color: 'text-red-400', icon: AlertCircle, bgClass: 'bg-red-500/10 border-red-500/20' },
|
||||
DRAFT: { label: 'Taslak', color: 'text-neutral-400', icon: FileText, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
|
||||
GENERATING_SCRIPT: { label: 'Senaryo Üretiliyor', color: 'text-neutral-300', icon: Sparkles, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
|
||||
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-neutral-300', icon: Sparkles, bgClass: 'bg-neutral-500/10 border-neutral-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-neutral-100', icon: CheckCircle2, bgClass: 'bg-neutral-500/20 border-neutral-500/30' },
|
||||
FAILED: { label: 'Başarısız', color: 'text-neutral-500', icon: AlertCircle, bgClass: 'bg-neutral-800/50 border-neutral-800' },
|
||||
};
|
||||
|
||||
const videoStyles = [
|
||||
@@ -240,7 +240,7 @@ export default function ProjectDetailPage() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -379,7 +379,7 @@ export default function ProjectDetailPage() {
|
||||
value={project.videoStyle}
|
||||
onChange={(e) => handleStyleChange(e.target.value)}
|
||||
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) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
@@ -393,7 +393,7 @@ export default function ProjectDetailPage() {
|
||||
value={project.cinematicReference || ''}
|
||||
onChange={(e) => handleCinematicReferenceChange(e.target.value)}
|
||||
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>
|
||||
{CINEMATIC_REFERENCES.map(ref => (
|
||||
@@ -438,7 +438,7 @@ export default function ProjectDetailPage() {
|
||||
<button
|
||||
onClick={handleGenerateScript}
|
||||
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 ? (
|
||||
<Loader2 size={15} className="animate-spin" />
|
||||
@@ -470,7 +470,7 @@ export default function ProjectDetailPage() {
|
||||
<button
|
||||
onClick={handleApprove}
|
||||
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 ? (
|
||||
<Loader2 size={15} className="animate-spin" />
|
||||
@@ -554,7 +554,7 @@ export default function ProjectDetailPage() {
|
||||
<motion.div variants={fadeUp}>
|
||||
<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">
|
||||
<Film size={15} className="text-violet-400" />
|
||||
<Film size={15} className="text-neutral-400" />
|
||||
Senaryo — {project.scenes!.length} sahne
|
||||
</h2>
|
||||
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
||||
@@ -585,8 +585,8 @@ export default function ProjectDetailPage() {
|
||||
{/* ── Boş durum (senaryo yok) ── */}
|
||||
{!hasScript && isEditable && (
|
||||
<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">
|
||||
<Sparkles size={28} className="text-violet-400" />
|
||||
<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-neutral-400" />
|
||||
</div>
|
||||
<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">
|
||||
@@ -595,7 +595,7 @@ export default function ProjectDetailPage() {
|
||||
<button
|
||||
onClick={handleGenerateScript}
|
||||
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 ? (
|
||||
<Loader2 size={15} className="animate-spin" />
|
||||
@@ -612,7 +612,7 @@ export default function ProjectDetailPage() {
|
||||
<motion.div variants={fadeUp}>
|
||||
<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">
|
||||
<Clock size={15} className="text-cyan-400" />
|
||||
<Clock size={15} className="text-neutral-400" />
|
||||
Render Geçmişi
|
||||
</h2>
|
||||
{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 gap-2.5">
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
job.status === 'COMPLETED' ? 'bg-emerald-400' :
|
||||
job.status === 'FAILED' ? 'bg-red-400' :
|
||||
job.status === 'CANCELLED' ? 'bg-slate-400' :
|
||||
'bg-amber-400 animate-pulse'
|
||||
job.status === 'COMPLETED' ? 'bg-neutral-100' :
|
||||
job.status === 'FAILED' ? 'bg-neutral-500' :
|
||||
job.status === 'CANCELLED' ? 'bg-neutral-600' :
|
||||
'bg-neutral-300 animate-pulse'
|
||||
}`} />
|
||||
<span className="text-xs text-[var(--color-text-secondary)]">
|
||||
Deneme #{job.attemptNumber}
|
||||
@@ -658,9 +658,9 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
{(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
|
||||
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)}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-white/20 animate-pulse" />
|
||||
|
||||
@@ -1,105 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Sparkles,
|
||||
Languages,
|
||||
Clock,
|
||||
Palette,
|
||||
Monitor,
|
||||
Smartphone,
|
||||
Square,
|
||||
Loader2,
|
||||
Check,
|
||||
Wand2,
|
||||
Search,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCreateProject } from "@/hooks/use-api";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
import { projectsApi } from "@/lib/api/api-service";
|
||||
import {
|
||||
LanguageSelector,
|
||||
StyleSelector,
|
||||
DurationSelector,
|
||||
AspectRatioSelector,
|
||||
languages,
|
||||
videoStyles,
|
||||
aspectRatios,
|
||||
} from "@/components/projects/ProjectConfiguration";
|
||||
|
||||
const steps = ["Konu & Dil", "Stil & Süre", "AI Senaryo"];
|
||||
|
||||
const languages = [
|
||||
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
|
||||
{ code: "en", label: "English", flag: "🇺🇸" },
|
||||
{ code: "es", label: "Español", flag: "🇪🇸" },
|
||||
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
||||
{ code: "fr", label: "Français", flag: "🇫🇷" },
|
||||
{ code: "ar", label: "العربية", flag: "🇸🇦" },
|
||||
{ code: "pt", label: "Português", flag: "🇧🇷" },
|
||||
{ code: "ja", label: "日本語", flag: "🇯🇵" },
|
||||
];
|
||||
|
||||
const videoStyles = [
|
||||
// Film & Sinema
|
||||
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller", category: "Film & Sinema" },
|
||||
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton", category: "Film & Sinema" },
|
||||
{ id: "STORYTELLING", label: "Hikâye Anlatımı", emoji: "📖", desc: "Anlatı odaklı, sürükleyici", category: "Film & Sinema" },
|
||||
{ id: "NEWS", label: "Haber", emoji: "📰", desc: "Güncel ve bilgilendirici", category: "Film & Sinema" },
|
||||
{ id: "ARTISTIC", label: "Sanatsal", emoji: "🎨", desc: "Yaratıcı ve sıra dışı", category: "Film & Sinema" },
|
||||
{ id: "NOIR", label: "Film Noir", emoji: "🖤", desc: "Karanlık, dramatik", category: "Film & Sinema" },
|
||||
{ id: "VLOG", label: "Vlog", emoji: "📱", desc: "Günlük, samimi", category: "Film & Sinema" },
|
||||
// Animasyon
|
||||
{ id: "ANIME", label: "Anime", emoji: "⛩️", desc: "Japon animasyon stili", category: "Animasyon" },
|
||||
{ id: "ANIMATION_3D", label: "3D Animasyon", emoji: "🧊", desc: "Pixar kalitesi", category: "Animasyon" },
|
||||
{ id: "ANIMATION_2D", label: "2D Animasyon", emoji: "✏️", desc: "Klasik el çizimi", category: "Animasyon" },
|
||||
{ id: "STOP_MOTION", label: "Stop Motion", emoji: "🧸", desc: "Kare kare animasyon", category: "Animasyon" },
|
||||
{ id: "MOTION_COMIC", label: "Hareketli Çizgi Roman", emoji: "💥", desc: "Panel bazlı anlatım", category: "Animasyon" },
|
||||
{ id: "CARTOON", label: "Karikatür", emoji: "🎭", desc: "Çizgi film stili", category: "Animasyon" },
|
||||
{ id: "CLAYMATION", label: "Claymation", emoji: "🏺", desc: "Kil animasyon", category: "Animasyon" },
|
||||
{ id: "PIXEL_ART", label: "Pixel Art", emoji: "👾", desc: "8-bit retro oyun", category: "Animasyon" },
|
||||
{ id: "ISOMETRIC", label: "İzometrik", emoji: "🔷", desc: "İzometrik animasyon", category: "Animasyon" },
|
||||
// Eğitim & Bilgi
|
||||
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "🎓", desc: "Öğretici ve açıklayıcı", category: "Eğitim & Bilgi" },
|
||||
{ id: "INFOGRAPHIC", label: "İnfografik", emoji: "📊", desc: "Veri görselleştirme", category: "Eğitim & Bilgi" },
|
||||
{ id: "WHITEBOARD", label: "Whiteboard", emoji: "📝", desc: "Tahta animasyonu", category: "Eğitim & Bilgi" },
|
||||
{ id: "EXPLAINER", label: "Explainer", emoji: "💡", desc: "Ürün/konsept anlatımı", category: "Eğitim & Bilgi" },
|
||||
{ id: "DATA_VIZ", label: "Veri Görselleştirme", emoji: "📈", desc: "Grafikler ve tablolar", category: "Eğitim & Bilgi" },
|
||||
// Retro & Nostaljik
|
||||
{ id: "RETRO_80S", label: "Retro 80s", emoji: "🕹️", desc: "Synthwave estetik", category: "Retro & Nostaljik" },
|
||||
{ id: "VINTAGE_FILM", label: "Vintage Film", emoji: "📽️", desc: "Super 8 filmi", category: "Retro & Nostaljik" },
|
||||
{ id: "VHS", label: "VHS", emoji: "📼", desc: "Kaset estetik", category: "Retro & Nostaljik" },
|
||||
{ id: "POLAROID", label: "Polaroid", emoji: "📸", desc: "Analog fotoğraf", category: "Retro & Nostaljik" },
|
||||
{ id: "RETRO_90S", label: "Retro 90s Y2K", emoji: "💿", desc: "Y2K & internet", category: "Retro & Nostaljik" },
|
||||
// Sanat Akımları
|
||||
{ id: "WATERCOLOR", label: "Suluboya", emoji: "🎨", desc: "Suluboya resim", category: "Sanat Akımları" },
|
||||
{ id: "OIL_PAINTING", label: "Yağlı Boya", emoji: "🖌️", desc: "Klasik tuval", category: "Sanat Akımları" },
|
||||
{ id: "IMPRESSIONIST", label: "Empresyonist", emoji: "🌅", desc: "Monet tarzı", category: "Sanat Akımları" },
|
||||
{ id: "POP_ART", label: "Pop Art", emoji: "🎯", desc: "Warhol stili", category: "Sanat Akımları" },
|
||||
{ id: "UKIYO_E", label: "Ukiyo-e", emoji: "🏯", desc: "Japon gravür", category: "Sanat Akımları" },
|
||||
{ id: "ART_DECO", label: "Art Deco", emoji: "✨", desc: "1920s zarafet", category: "Sanat Akımları" },
|
||||
{ id: "SURREAL", label: "Sürrealist", emoji: "🌀", desc: "Dalí tarzı", category: "Sanat Akımları" },
|
||||
{ id: "COMIC_BOOK", label: "Çizgi Roman", emoji: "💬", desc: "Marvel/DC stili", category: "Sanat Akımları" },
|
||||
{ id: "SKETCH", label: "Karakalem", emoji: "✍️", desc: "Kalem çizim", category: "Sanat Akımları" },
|
||||
// Modern & Minimal
|
||||
{ id: "MINIMALIST", label: "Minimalist", emoji: "⚪", desc: "Apple estetiği", category: "Modern & Minimal" },
|
||||
{ id: "GLASSMORPHISM", label: "Glassmorphism", emoji: "🔮", desc: "Cam efekti", category: "Modern & Minimal" },
|
||||
{ id: "NEON", label: "Neon Glow", emoji: "💜", desc: "Neon ışıkları", category: "Modern & Minimal" },
|
||||
{ id: "CYBERPUNK", label: "Cyberpunk", emoji: "🤖", desc: "Gelecek distopya", category: "Modern & Minimal" },
|
||||
{ id: "STEAMPUNK", label: "Steampunk", emoji: "⚙️", desc: "Viktoryan mekanik", category: "Modern & Minimal" },
|
||||
{ id: "ABSTRACT", label: "Soyut", emoji: "🔵", desc: "Abstract sanat", category: "Modern & Minimal" },
|
||||
// Fotoğrafik
|
||||
{ id: "PRODUCT", label: "Ürün Fotoğrafı", emoji: "📦", desc: "Studio çekim", category: "Fotoğrafik" },
|
||||
{ id: "FASHION", label: "Moda", emoji: "👗", desc: "Editöryal çekim", category: "Fotoğrafik" },
|
||||
{ id: "AERIAL", label: "Havadan", emoji: "🚁", desc: "Drone görüntüsü", category: "Fotoğrafik" },
|
||||
{ id: "MACRO", label: "Makro", emoji: "🔬", desc: "Yakın çekim", category: "Fotoğrafik" },
|
||||
{ id: "PORTRAIT", label: "Portre", emoji: "🧑", desc: "Portre fotoğraf", category: "Fotoğrafik" },
|
||||
];
|
||||
|
||||
const aspectRatios = [
|
||||
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
|
||||
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
|
||||
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
|
||||
];
|
||||
|
||||
export default function NewProjectPage() {
|
||||
const router = useRouter();
|
||||
const createProject = useCreateProject();
|
||||
@@ -114,26 +42,6 @@ export default function NewProjectPage() {
|
||||
const [duration, setDuration] = useState(60);
|
||||
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
|
||||
|
||||
// Search & Category state
|
||||
const [styleSearch, setStyleSearch] = useState("");
|
||||
const [expandedCategory, setExpandedCategory] = useState<string | null>("Film & Sinema");
|
||||
|
||||
const filteredStyles = useMemo(() => {
|
||||
return videoStyles.filter(s =>
|
||||
s.label.toLowerCase().includes(styleSearch.toLowerCase()) ||
|
||||
s.desc.toLowerCase().includes(styleSearch.toLowerCase())
|
||||
);
|
||||
}, [styleSearch]);
|
||||
|
||||
const groupedStyles = useMemo(() => {
|
||||
return filteredStyles.reduce((acc, curr) => {
|
||||
const cat = curr.category || "Diğer";
|
||||
if (!acc[cat]) acc[cat] = [];
|
||||
acc[cat].push(curr);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof videoStyles>);
|
||||
}, [filteredStyles]);
|
||||
|
||||
const canProceed = currentStep === 0 ? topic.trim().length >= 5 : true;
|
||||
const isGenerating = createProject.isPending;
|
||||
|
||||
@@ -142,9 +50,9 @@ export default function NewProjectPage() {
|
||||
// Backend DTO alanları: prompt (zorunlu), videoStyle, title, language, aspectRatio, targetDuration
|
||||
const result = await createProject.mutateAsync({
|
||||
title: topic.slice(0, 80),
|
||||
prompt: topic, // ← topic → prompt (backend alanı)
|
||||
prompt: topic,
|
||||
language,
|
||||
videoStyle: style, // ← style → videoStyle (backend alanı)
|
||||
videoStyle: style,
|
||||
cinematicReference: cinematicReference ? cinematicReference : undefined,
|
||||
targetDuration: duration,
|
||||
aspectRatio,
|
||||
@@ -190,9 +98,9 @@ export default function NewProjectPage() {
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium transition-all",
|
||||
i === currentStep
|
||||
? "bg-violet-500/15 text-violet-400 border border-violet-500/25"
|
||||
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
|
||||
: 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)]"
|
||||
)}
|
||||
>
|
||||
@@ -208,7 +116,7 @@ export default function NewProjectPage() {
|
||||
{i < steps.length - 1 && (
|
||||
<div className={cn(
|
||||
"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>
|
||||
@@ -229,14 +137,14 @@ export default function NewProjectPage() {
|
||||
{/* Konu */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
<Sparkles size={14} className="inline mr-1.5 text-violet-400" />
|
||||
<Sparkles size={14} className="inline mr-1.5 text-[var(--color-text-primary)]" />
|
||||
Videonun Konusu
|
||||
</label>
|
||||
<textarea
|
||||
value={topic}
|
||||
onChange={(e) => setTopic(e.target.value)}
|
||||
placeholder="Örn: Boötes Boşluğu — evrendeki en büyük boşluk ve gizemi..."
|
||||
className="w-full h-32 px-4 py-3 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-primary)] text-sm placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-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">
|
||||
Ne kadar detaylı yazarsan, AI o kadar iyi senaryo üretir ({topic.length} karakter)
|
||||
@@ -244,29 +152,7 @@ export default function NewProjectPage() {
|
||||
</div>
|
||||
|
||||
{/* Dil Seçimi */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
<Languages size={14} className="inline mr-1.5 text-cyan-400" />
|
||||
Video Dili
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => setLanguage(lang.code)}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 py-3 px-2 rounded-xl text-xs transition-all",
|
||||
language === lang.code
|
||||
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-[var(--color-border-default)]"
|
||||
)}
|
||||
>
|
||||
<span className="text-lg">{lang.flag}</span>
|
||||
<span className="font-medium">{lang.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<LanguageSelector value={language} onChange={setLanguage} />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -280,154 +166,18 @@ export default function NewProjectPage() {
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* Video Stili */}
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] block">
|
||||
<Palette size={14} className="inline mr-1.5 text-violet-400" />
|
||||
Video Stili
|
||||
</label>
|
||||
<div className="relative w-full sm:w-56">
|
||||
<div className="absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none">
|
||||
<Search size={14} className="text-[var(--color-text-ghost)]" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Stil ara..."
|
||||
value={styleSearch}
|
||||
onChange={(e) => setStyleSearch(e.target.value)}
|
||||
className="w-full pl-8 pr-3 py-1.5 bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-md text-xs text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-violet-500/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1 custom-scrollbar">
|
||||
{Object.entries(groupedStyles).map(([category, items]) => {
|
||||
const isExpanded = styleSearch ? true : expandedCategory === category;
|
||||
return (
|
||||
<div key={category} className="bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl overflow-hidden">
|
||||
<button
|
||||
onClick={() => !styleSearch && setExpandedCategory(isExpanded ? null : category)}
|
||||
className="w-full flex items-center justify-between p-3 bg-[var(--color-bg-elevated)] hover:bg-[var(--color-bg-surface-hover)] transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium text-[var(--color-text-secondary)]">{category} <span className="text-[11px] text-[var(--color-text-ghost)] ml-1">({items.length})</span></span>
|
||||
{!styleSearch && (
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={cn("text-[var(--color-text-ghost)] transition-transform duration-200", isExpanded && "rotate-180")}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="p-3 pt-0 mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{items.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setStyle(s.id)}
|
||||
className={cn(
|
||||
"flex flex-col items-start gap-1 p-2.5 rounded-xl text-left transition-all",
|
||||
style === s.id
|
||||
? "bg-violet-500/12 border border-violet-500/30 glow-violet"
|
||||
: "bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]"
|
||||
)}
|
||||
>
|
||||
<span className="text-xl mb-0.5">{s.emoji}</span>
|
||||
<span className="text-xs font-semibold leading-tight">{s.label}</span>
|
||||
<span className="text-[10px] text-[var(--color-text-ghost)] leading-tight">{s.desc}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Object.keys(groupedStyles).length === 0 && (
|
||||
<div className="text-center py-8 text-[var(--color-text-ghost)] text-sm">
|
||||
"{styleSearch}" için sonuç bulunamadı.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{style === "CINEMATIC" && (
|
||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
Özel Sinematik Referans (Opsiyonel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
||||
value={cinematicReference}
|
||||
onChange={(e) => setCinematicReference(e.target.value)}
|
||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-md py-1.5 px-3 text-sm focus:border-violet-500/50 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<StyleSelector
|
||||
value={style}
|
||||
onChange={setStyle}
|
||||
cinematicReference={cinematicReference}
|
||||
onCinematicReferenceChange={setCinematicReference}
|
||||
/>
|
||||
|
||||
{/* Süre */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
||||
Hedef Süre: <span className="text-violet-400">{duration}s</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={15}
|
||||
max={180}
|
||||
step={5}
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(Number(e.target.value))}
|
||||
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none
|
||||
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
|
||||
[&::-webkit-slider-thumb]:rounded-full
|
||||
[&::-webkit-slider-thumb]:bg-violet-500
|
||||
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(139,92,246,0.4)]
|
||||
[&::-webkit-slider-thumb]:cursor-grab"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-[var(--color-text-ghost)] mt-1">
|
||||
<span>15s</span>
|
||||
<span>60s</span>
|
||||
<span>120s</span>
|
||||
<span>180s</span>
|
||||
</div>
|
||||
</div>
|
||||
<DurationSelector value={duration} onChange={setDuration} />
|
||||
|
||||
{/* En-Boy Oranı */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
En-Boy Oranı
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{aspectRatios.map((ar) => {
|
||||
const Icon = ar.icon;
|
||||
return (
|
||||
<button
|
||||
key={ar.id}
|
||||
onClick={() => setAspectRatio(ar.id)}
|
||||
className={cn(
|
||||
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all",
|
||||
aspectRatio === ar.id
|
||||
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-[var(--color-border-default)]"
|
||||
)}
|
||||
>
|
||||
<Icon size={20} />
|
||||
<span className="font-semibold">{ar.label}</span>
|
||||
<span className="text-[10px] text-[var(--color-text-ghost)]">{ar.desc}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -476,7 +226,7 @@ export default function NewProjectPage() {
|
||||
className={cn(
|
||||
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
|
||||
isGenerating
|
||||
? "bg-violet-500/20 text-violet-300 cursor-wait"
|
||||
? "bg-[var(--color-bg-inverted)]/20 text-[var(--color-text-primary)] cursor-wait"
|
||||
: "btn-primary text-lg"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -40,20 +40,20 @@ const statusMap: Record<
|
||||
},
|
||||
scripting: {
|
||||
icon: Clock,
|
||||
color: "text-blue-400",
|
||||
bgColor: "bg-blue-500/10",
|
||||
color: "text-neutral-400",
|
||||
bgColor: "bg-neutral-200 dark:bg-neutral-800",
|
||||
label: "Senaryo",
|
||||
},
|
||||
reviewing: {
|
||||
icon: Clock,
|
||||
color: "text-purple-400",
|
||||
bgColor: "bg-purple-500/10",
|
||||
color: "text-neutral-500",
|
||||
bgColor: "bg-neutral-200 dark:bg-neutral-800",
|
||||
label: "İnceleme",
|
||||
},
|
||||
rendering: {
|
||||
icon: Video,
|
||||
color: "text-cyan-400",
|
||||
bgColor: "bg-cyan-500/10",
|
||||
color: "text-[var(--color-text-primary)]",
|
||||
bgColor: "bg-neutral-200 dark:bg-neutral-800",
|
||||
label: "Render",
|
||||
},
|
||||
completed: {
|
||||
@@ -136,7 +136,7 @@ export default function ProjectsPage() {
|
||||
<p className="text-sm text-[var(--color-text-muted)] mt-0.5">
|
||||
Tüm video projelerini yönet
|
||||
{projects.length > 0 && (
|
||||
<span className="ml-1 text-violet-400">
|
||||
<span className="ml-1 text-[var(--color-text-primary)] font-medium">
|
||||
({projects.length} proje)
|
||||
</span>
|
||||
)}
|
||||
@@ -163,7 +163,7 @@ export default function ProjectsPage() {
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
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>
|
||||
@@ -177,7 +177,7 @@ export default function ProjectsPage() {
|
||||
className={cn(
|
||||
"px-3.5 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-all",
|
||||
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)]",
|
||||
)}
|
||||
>
|
||||
@@ -191,7 +191,7 @@ export default function ProjectsPage() {
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<Loader2
|
||||
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)]">
|
||||
Projeler yükleniyor...
|
||||
@@ -221,7 +221,7 @@ export default function ProjectsPage() {
|
||||
{!searchQuery && activeFilter === "all" && (
|
||||
<Link
|
||||
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 →
|
||||
</Link>
|
||||
@@ -234,7 +234,7 @@ export default function ProjectsPage() {
|
||||
return (
|
||||
<div
|
||||
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
|
||||
href={`/dashboard/projects/${project.id}`}
|
||||
@@ -246,7 +246,7 @@ export default function ProjectsPage() {
|
||||
<StIcon size={18} />
|
||||
</div>
|
||||
<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}
|
||||
</p>
|
||||
<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>
|
||||
<ExternalLink
|
||||
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>
|
||||
|
||||
|
||||
@@ -5,39 +5,13 @@ 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 {
|
||||
FileText,
|
||||
Loader2,
|
||||
ArrowRight,
|
||||
Clock,
|
||||
Palette,
|
||||
Monitor,
|
||||
Smartphone,
|
||||
Square,
|
||||
Wand2,
|
||||
Type,
|
||||
} from "lucide-react";
|
||||
|
||||
const videoStyles = [
|
||||
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬" },
|
||||
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹" },
|
||||
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚" },
|
||||
{ id: "STORYTELLING", label: "Hikâye", emoji: "📖" },
|
||||
{ id: "NEWS", label: "Haber", emoji: "📰" },
|
||||
];
|
||||
|
||||
const aspectRatios = [
|
||||
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
|
||||
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
|
||||
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
|
||||
];
|
||||
|
||||
const languages = [
|
||||
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
|
||||
{ code: "en", label: "English", flag: "🇺🇸" },
|
||||
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
||||
{ code: "es", label: "Español", flag: "🇪🇸" },
|
||||
];
|
||||
LanguageSelector,
|
||||
StyleSelector,
|
||||
DurationSelector,
|
||||
AspectRatioSelector,
|
||||
} from "@/components/projects/ProjectConfiguration";
|
||||
|
||||
export default function TextToVideoPage() {
|
||||
const router = useRouter();
|
||||
@@ -60,6 +34,7 @@ export default function TextToVideoPage() {
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result: any = await createFromText.mutateAsync({
|
||||
text: textInput,
|
||||
language,
|
||||
@@ -80,7 +55,7 @@ export default function TextToVideoPage() {
|
||||
<div className="max-w-3xl mx-auto space-y-8 pb-24">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-3 pb-4">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-blue-500/10 text-blue-500 mb-2 ring-1 ring-blue-500/20 shadow-[0_0_30px_rgba(59,130,246,0.15)]">
|
||||
<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)]">
|
||||
@@ -102,123 +77,26 @@ export default function TextToVideoPage() {
|
||||
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-subtle)] border border-[var(--color-border-faint)] rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 resize-none transition-all placeholder:text-[var(--color-text-ghost)]"
|
||||
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="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4 border-t border-[var(--color-border-faint)]">
|
||||
<div className="space-y-8 pt-6 border-t border-[var(--color-border-faint)]">
|
||||
|
||||
{/* Style */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-xs font-semibold uppercase tracking-wider text-[var(--color-text-ghost)] flex items-center gap-2">
|
||||
<Palette size={14} /> Video Stili
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{videoStyles.slice(0, 4).map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setStyle(s.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-3 rounded-xl border text-sm font-medium transition-all",
|
||||
style === s.id
|
||||
? "bg-blue-500/10 border-blue-500/30 text-blue-400"
|
||||
: "bg-[var(--color-bg-subtle)] border-transparent text-[var(--color-text-muted)] hover:bg-[var(--color-bg-elevated)]"
|
||||
)}
|
||||
>
|
||||
<span>{s.emoji}</span>
|
||||
<span className="truncate">{s.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Cinematic Reference */}
|
||||
{style === "CINEMATIC" && (
|
||||
<div className="mt-3">
|
||||
<input
|
||||
type="text"
|
||||
value={cinematicReference}
|
||||
onChange={(e) => setCinematicReference(e.target.value)}
|
||||
placeholder="Yönetmen/Film stili (Opsiyonel)"
|
||||
className="w-full bg-black/20 border border-[var(--color-border-faint)] rounded-lg px-3 py-2 text-xs focus:outline-none focus:border-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<LanguageSelector value={language} onChange={setLanguage} />
|
||||
|
||||
{/* Aspect Ratio */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-xs font-semibold uppercase tracking-wider text-[var(--color-text-ghost)] flex items-center gap-2">
|
||||
<Monitor size={14} /> Format
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{aspectRatios.map((ar) => {
|
||||
const Icon = ar.icon;
|
||||
return (
|
||||
<button
|
||||
key={ar.id}
|
||||
onClick={() => setAspectRatio(ar.id)}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-2 p-3 rounded-xl border transition-all",
|
||||
aspectRatio === ar.id
|
||||
? "bg-blue-500/10 border-blue-500/30 text-blue-400"
|
||||
: "bg-[var(--color-bg-subtle)] border-transparent text-[var(--color-text-muted)] hover:bg-[var(--color-bg-elevated)]"
|
||||
)}
|
||||
>
|
||||
<Icon size={18} />
|
||||
<div className="text-center">
|
||||
<div className="text-xs font-bold">{ar.label}</div>
|
||||
<div className="text-[9px] opacity-70 truncate">{ar.desc}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<StyleSelector
|
||||
value={style}
|
||||
onChange={setStyle}
|
||||
cinematicReference={cinematicReference}
|
||||
onCinematicReferenceChange={setCinematicReference}
|
||||
/>
|
||||
|
||||
{/* Duration */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-xs font-semibold uppercase tracking-wider text-[var(--color-text-ghost)] flex items-center gap-2">
|
||||
<Clock size={14} /> Süre
|
||||
</label>
|
||||
<div className="flex items-center gap-4 bg-[var(--color-bg-subtle)] p-3 rounded-xl">
|
||||
<input
|
||||
type="range"
|
||||
min="15"
|
||||
max="180"
|
||||
step="15"
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(Number(e.target.value))}
|
||||
className="flex-1 accent-blue-500"
|
||||
/>
|
||||
<span className="text-sm font-mono font-medium text-blue-400 w-12 text-right">
|
||||
{duration}s
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<DurationSelector value={duration} onChange={setDuration} />
|
||||
|
||||
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
|
||||
|
||||
{/* Language */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-xs font-semibold uppercase tracking-wider text-[var(--color-text-ghost)] flex items-center gap-2">
|
||||
<span className="text-[14px]">🗣</span> Seslendirme Dili
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => setLanguage(lang.code)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-2.5 rounded-xl border text-sm font-medium transition-all",
|
||||
language === lang.code
|
||||
? "bg-blue-500/10 border-blue-500/30 text-blue-400"
|
||||
: "bg-[var(--color-bg-subtle)] border-transparent text-[var(--color-text-muted)] hover:bg-[var(--color-bg-elevated)]"
|
||||
)}
|
||||
>
|
||||
<span className="text-lg leading-none">{lang.flag}</span>
|
||||
<span>{lang.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -228,8 +106,8 @@ export default function TextToVideoPage() {
|
||||
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-white shadow-[0_0_40px_rgba(59,130,246,0.3)] transition-all",
|
||||
"bg-gradient-to-r from-blue-600 to-violet-600 hover:from-blue-500 hover:to-violet-500 hover:scale-[1.02]",
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -8,11 +8,6 @@ import {
|
||||
Link2,
|
||||
Loader2,
|
||||
ArrowRight,
|
||||
Clock,
|
||||
Palette,
|
||||
Monitor,
|
||||
Smartphone,
|
||||
Square,
|
||||
Sparkles,
|
||||
Wand2,
|
||||
MessageSquare,
|
||||
@@ -24,31 +19,16 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTweetPreview, useCreateFromTweet } from "@/hooks/use-api";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
|
||||
const videoStyles = [
|
||||
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬" },
|
||||
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹" },
|
||||
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚" },
|
||||
{ id: "STORYTELLING", label: "Hikâye", emoji: "📖" },
|
||||
{ id: "NEWS", label: "Haber", emoji: "📰" },
|
||||
];
|
||||
|
||||
const aspectRatios = [
|
||||
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
|
||||
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
|
||||
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
|
||||
];
|
||||
|
||||
const languages = [
|
||||
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
|
||||
{ code: "en", label: "English", flag: "🇺🇸" },
|
||||
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
||||
{ code: "es", label: "Español", flag: "🇪🇸" },
|
||||
];
|
||||
import {
|
||||
LanguageSelector,
|
||||
StyleSelector,
|
||||
DurationSelector,
|
||||
AspectRatioSelector,
|
||||
} from "@/components/projects/ProjectConfiguration";
|
||||
|
||||
export default function XToVideoPage() {
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const { toast } = useToast();
|
||||
const tweetPreview = useTweetPreview();
|
||||
const createFromTweet = useCreateFromTweet();
|
||||
|
||||
@@ -66,15 +46,15 @@ export default function XToVideoPage() {
|
||||
|
||||
const handlePreview = async () => {
|
||||
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;
|
||||
}
|
||||
try {
|
||||
const result = await tweetPreview.mutateAsync(tweetUrl);
|
||||
setPreviewData(result);
|
||||
toast.success("Tweet başarıyla yüklendi!");
|
||||
toast("success", "Tweet başarıyla yüklendi!");
|
||||
} 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,
|
||||
targetDuration: duration,
|
||||
});
|
||||
toast.success("Tweet → Video projesi oluşturuldu!");
|
||||
toast("success", "Tweet → Video projesi oluşturuldu!");
|
||||
const projectId = result?.id;
|
||||
if (projectId) {
|
||||
router.push(`/dashboard/projects/${projectId}`);
|
||||
@@ -97,22 +77,21 @@ export default function XToVideoPage() {
|
||||
router.push("/dashboard/projects");
|
||||
}
|
||||
} catch {
|
||||
toast.error("Proje oluşturulurken bir hata oluştu.");
|
||||
toast("error", "Proje oluşturulurken bir hata oluştu.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
<div className="max-w-3xl mx-auto space-y-6 pb-24">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-violet-500/10 border border-violet-500/20 text-violet-300 text-xs font-medium mb-3">
|
||||
<AtSign size={12} />
|
||||
X → Video
|
||||
<div className="text-center space-y-3 pb-4">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
|
||||
<AtSign size={32} />
|
||||
</div>
|
||||
<h1 className="font-[family-name:var(--font-display)] text-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
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -132,7 +111,7 @@ export default function XToVideoPage() {
|
||||
setPreviewData(null);
|
||||
}}
|
||||
placeholder="https://x.com/username/status/123456..."
|
||||
className="flex-1 px-4 py-2.5 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-violet-500/40 focus:ring-1 focus:ring-violet-500/20 transition-all"
|
||||
className="flex-1 px-4 py-2.5 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-[var(--color-bg-inverted)]/40 focus:ring-1 focus:ring-[var(--color-bg-inverted)]/20 transition-all"
|
||||
/>
|
||||
<button
|
||||
onClick={handlePreview}
|
||||
@@ -235,7 +214,7 @@ export default function XToVideoPage() {
|
||||
{previewData.tweet?.metrics?.views ?? 0}
|
||||
</span>
|
||||
{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} />
|
||||
{previewData.tweet?.threadTweets?.length ?? 0} tweet thread
|
||||
</span>
|
||||
@@ -245,7 +224,7 @@ export default function XToVideoPage() {
|
||||
|
||||
{/* Suggested info */}
|
||||
{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} />
|
||||
Önerilen başlık: {previewData.suggestedTitle} · Tahmini süre: {previewData.estimatedDuration}sn · Viral skoru: {previewData.viralScore}/100
|
||||
</div>
|
||||
@@ -253,7 +232,7 @@ export default function XToVideoPage() {
|
||||
|
||||
{/* Images tag */}
|
||||
{(previewData.tweet?.media?.filter((m: any) => m.type === 'photo')?.length > 0) && (
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-cyan-400">
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-[var(--color-text-secondary)]">
|
||||
<ImageIcon size={12} />
|
||||
{previewData.tweet.media.filter((m: any) => m.type === 'photo').length} görsel referans olarak kullanılacak + AI görsel üretilecek
|
||||
</div>
|
||||
@@ -270,147 +249,46 @@ export default function XToVideoPage() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* Language */}
|
||||
<div className="card p-5 space-y-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
Video Dili
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{languages.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
onClick={() => setLanguage(l.code)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
||||
language === l.code
|
||||
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<span>{l.flag}</span>
|
||||
{l.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Style */}
|
||||
<div className="card p-5 space-y-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
<Palette size={14} className="inline mr-1.5 text-violet-400" />
|
||||
Video Stili
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{videoStyles.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setStyle(s.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
||||
style === s.id
|
||||
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<span>{s.emoji}</span>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{style === "CINEMATIC" && (
|
||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
Özel Sinematik Referans (Opsiyonel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
||||
value={cinematicReference}
|
||||
onChange={(e) => setCinematicReference(e.target.value)}
|
||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-xl py-2 px-3 text-sm focus:border-violet-500/50 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Duration + Aspect Ratio */}
|
||||
<div className="card p-5 space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
||||
Hedef Süre: <span className="text-violet-400">{duration}s</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={15}
|
||||
max={120}
|
||||
step={5}
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(Number(e.target.value))}
|
||||
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none
|
||||
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
|
||||
[&::-webkit-slider-thumb]:rounded-full
|
||||
[&::-webkit-slider-thumb]:bg-violet-500
|
||||
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(139,92,246,0.4)]
|
||||
[&::-webkit-slider-thumb]:cursor-grab"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
En-Boy Oranı
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{aspectRatios.map((ar) => {
|
||||
const Icon = ar.icon;
|
||||
return (
|
||||
<button
|
||||
key={ar.id}
|
||||
onClick={() => setAspectRatio(ar.id)}
|
||||
className={cn(
|
||||
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all",
|
||||
aspectRatio === ar.id
|
||||
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<Icon size={20} />
|
||||
<span className="font-semibold">{ar.label}</span>
|
||||
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
||||
{ar.desc}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card p-6 space-y-8">
|
||||
<LanguageSelector value={language} onChange={setLanguage} />
|
||||
|
||||
<StyleSelector
|
||||
value={style}
|
||||
onChange={setStyle}
|
||||
cinematicReference={cinematicReference}
|
||||
onCinematicReferenceChange={setCinematicReference}
|
||||
/>
|
||||
|
||||
<DurationSelector value={duration} onChange={setDuration} />
|
||||
|
||||
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
|
||||
</div>
|
||||
|
||||
{/* Generate Button */}
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={createFromTweet.isPending}
|
||||
className={cn(
|
||||
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
|
||||
createFromTweet.isPending
|
||||
? "bg-violet-500/20 text-violet-300 cursor-wait"
|
||||
: "btn-primary text-lg",
|
||||
)}
|
||||
>
|
||||
{createFromTweet.isPending ? (
|
||||
<>
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
<span>Video Projesi Oluşturuluyor...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wand2 size={20} />
|
||||
<span>Tweet → Video Oluştur</span>
|
||||
<ArrowRight size={16} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={createFromTweet.isPending}
|
||||
className={cn(
|
||||
"group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
|
||||
"bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
|
||||
"disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
{createFromTweet.isPending ? (
|
||||
<>
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
<span>Video Projesi Oluşturuluyor...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
|
||||
<span>Tweet → Video Oluştur</span>
|
||||
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-[11px] text-[var(--color-text-ghost)]">
|
||||
Bu işlem 1 kredi kullanır • AI senaryo + görsel üretim dahil
|
||||
@@ -421,9 +299,9 @@ export default function XToVideoPage() {
|
||||
|
||||
{/* Info Box */}
|
||||
{!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">
|
||||
<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">
|
||||
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
Nasıl Çalışır?
|
||||
|
||||
@@ -2,64 +2,47 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Link2,
|
||||
Loader2,
|
||||
ArrowRight,
|
||||
Clock,
|
||||
Palette,
|
||||
Monitor,
|
||||
Smartphone,
|
||||
Square,
|
||||
Sparkles,
|
||||
Wand2,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCreateFromYoutube } from "@/hooks/use-api";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
|
||||
const videoStyles = [
|
||||
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬" },
|
||||
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹" },
|
||||
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚" },
|
||||
{ id: "STORYTELLING", label: "Hikâye", emoji: "📖" },
|
||||
{ id: "NEWS", label: "Haber", emoji: "📰" },
|
||||
];
|
||||
|
||||
const aspectRatios = [
|
||||
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
|
||||
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
|
||||
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
|
||||
];
|
||||
|
||||
const languages = [
|
||||
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
|
||||
{ code: "en", label: "English", flag: "🇺🇸" },
|
||||
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
||||
{ code: "es", label: "Español", flag: "🇪🇸" },
|
||||
];
|
||||
import {
|
||||
PlaySquare,
|
||||
Link2,
|
||||
Loader2,
|
||||
ArrowRight,
|
||||
Sparkles,
|
||||
Wand2,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
LanguageSelector,
|
||||
StyleSelector,
|
||||
DurationSelector,
|
||||
AspectRatioSelector,
|
||||
} from "@/components/projects/ProjectConfiguration";
|
||||
|
||||
export default function YoutubeToVideoPage() {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const createFromYoutube = useCreateFromYoutube();
|
||||
|
||||
const [youtubeUrl, setYoutubeUrl] = useState("");
|
||||
|
||||
const [style, setStyle] = useState("CINEMATIC");
|
||||
const [cinematicReference, setCinematicReference] = useState("");
|
||||
const [duration, setDuration] = useState(60);
|
||||
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
|
||||
const [language, setLanguage] = useState("tr");
|
||||
|
||||
const isValidUrl = /https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/.+/.test(youtubeUrl);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!youtubeUrl.includes("youtube.com") && !youtubeUrl.includes("youtu.be")) {
|
||||
toast("error", "Lütfen geçerli bir YouTube tam linki girin.");
|
||||
if (!isValidUrl) {
|
||||
toast("error", "Lütfen geçerli bir YouTube URL'si girin.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result: any = await createFromYoutube.mutateAsync({
|
||||
youtubeUrl,
|
||||
language,
|
||||
@@ -71,8 +54,8 @@ export default function YoutubeToVideoPage() {
|
||||
|
||||
toast("success", "YouTube → Video projesi oluşturuldu!");
|
||||
router.push(`/dashboard/projects/${result.id}`);
|
||||
} catch (error) {
|
||||
toast("error", "Proje oluşturulurken hata oluştu.");
|
||||
} catch {
|
||||
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">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-3 pb-4">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-red-500/10 text-red-500 mb-2 ring-1 ring-red-500/20 shadow-[0_0_30px_rgba(239,68,68,0.15)]">
|
||||
<Link2 size={32} />
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
|
||||
<PlaySquare size={32} />
|
||||
</div>
|
||||
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
|
||||
YouTube'dan Video Üret
|
||||
YouTube'dan Video Oluştur
|
||||
</h1>
|
||||
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
|
||||
YouTube linkini yapıştırın, yapay zeka ana içeriği çıkartıp viral bir Reels/Shorts yaratsın.
|
||||
YouTube videolarını veya Shorts içeriklerini kendi tarzınızda yeniden üretin
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="card p-6 md:p-8 space-y-4">
|
||||
{/* Main Form */}
|
||||
<div className="card p-6 md:p-8 space-y-6">
|
||||
|
||||
{/* Input */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
YouTube URL
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block flex items-center gap-1.5">
|
||||
<Link2 size={14} className="text-red-500" />
|
||||
YouTube Video URL
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://www.youtube.com/watch?v=..."
|
||||
value={youtubeUrl}
|
||||
onChange={(e) => setYoutubeUrl(e.target.value)}
|
||||
className="w-full bg-[var(--color-bg-surface)] border-2 border-[var(--color-border-faint)] rounded-2xl py-4 pl-12 pr-4 text-sm
|
||||
focus:border-red-500/50 focus:ring-4 focus:ring-red-500/10 transition-all outline-none"
|
||||
/>
|
||||
<Link2
|
||||
size={18}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="url"
|
||||
value={youtubeUrl}
|
||||
onChange={(e) => setYoutubeUrl(e.target.value)}
|
||||
placeholder="https://youtube.com/watch?v=... veya https://youtu.be/..."
|
||||
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl px-4 py-3.5 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-bg-inverted)]/50 transition-all placeholder:text-[var(--color-text-ghost)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Configurations */}
|
||||
<div className="space-y-8 pt-6 border-t border-[var(--color-border-faint)]">
|
||||
<LanguageSelector value={language} onChange={setLanguage} />
|
||||
|
||||
<StyleSelector
|
||||
value={style}
|
||||
onChange={setStyle}
|
||||
cinematicReference={cinematicReference}
|
||||
onCinematicReferenceChange={setCinematicReference}
|
||||
/>
|
||||
|
||||
<DurationSelector value={duration} onChange={setDuration} />
|
||||
|
||||
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Video Settings */}
|
||||
<div className="space-y-6">
|
||||
<div className="card p-5 space-y-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
Video Dili
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{languages.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
onClick={() => setLanguage(l.code)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
||||
language === l.code
|
||||
? "bg-red-500/12 border border-red-500/30 text-red-400"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<span>{l.flag}</span>
|
||||
{l.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-5 space-y-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
<Palette size={14} className="inline mr-1.5 text-red-400" />
|
||||
Video Stili
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{videoStyles.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setStyle(s.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
||||
style === s.id
|
||||
? "bg-red-500/12 border border-red-500/30 text-red-400"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<span>{s.emoji}</span>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{style === "CINEMATIC" && (
|
||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
Özel Sinematik Referans (Opsiyonel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
||||
value={cinematicReference}
|
||||
onChange={(e) => setCinematicReference(e.target.value)}
|
||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-xl py-2 px-3 text-sm focus:border-red-500/50 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card p-5 space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
||||
Hedef Süre: <span className="text-red-400">{duration}s</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={15}
|
||||
max={120}
|
||||
step={5}
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(Number(e.target.value))}
|
||||
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none
|
||||
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
|
||||
[&::-webkit-slider-thumb]:rounded-full
|
||||
[&::-webkit-slider-thumb]:bg-red-500
|
||||
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(239,68,68,0.4)]
|
||||
[&::-webkit-slider-thumb]:cursor-grab"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
En-Boy Oranı
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{aspectRatios.map((ar) => {
|
||||
const Icon = ar.icon;
|
||||
return (
|
||||
<button
|
||||
key={ar.id}
|
||||
onClick={() => setAspectRatio(ar.id)}
|
||||
className={cn(
|
||||
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all",
|
||||
aspectRatio === ar.id
|
||||
? "bg-red-500/12 border border-red-500/30 text-red-400"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<Icon size={20} />
|
||||
<span className="font-semibold">{ar.label}</span>
|
||||
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
||||
{ar.desc}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={createFromYoutube.isPending || !youtubeUrl}
|
||||
disabled={createFromYoutube.isPending || !isValidUrl}
|
||||
className={cn(
|
||||
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
|
||||
createFromYoutube.isPending
|
||||
? "bg-red-500/20 text-red-400 cursor-wait"
|
||||
: "bg-red-500 hover:bg-red-600 text-white shadow-lg shadow-red-500/20",
|
||||
!youtubeUrl && "opacity-50 cursor-not-allowed"
|
||||
"group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
|
||||
"bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
|
||||
"disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
{createFromYoutube.isPending ? (
|
||||
<>
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
<span>Video Projesi Oluşturuluyor... (Bu işlem uzun sürebilir)</span>
|
||||
<span>Yapay Zeka Videoyu İşliyor...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wand2 size={20} />
|
||||
<span>YouTube → Video Oluştur</span>
|
||||
<ArrowRight size={16} />
|
||||
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
|
||||
<span>YouTube → Video Üret</span>
|
||||
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user