generated from fahricansecer/boilerplate-fe
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5144ee4d9a | |||
| 1b69eaf219 |
@@ -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];
|
||||
|
||||
const handleExtractTopics = async () => {
|
||||
if (!file) {
|
||||
toast("error", "Lütfen bir belge seçin.");
|
||||
// Boyut kontrolü (örn: 10MB)
|
||||
if (selectedFile.size > 10 * 1024 * 1024) {
|
||||
toast("error", "Dosya boyutu 10MB'dan küçük olmalıdır.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await extractDocumentTopics.mutateAsync({ file });
|
||||
setExtractedData(result);
|
||||
if (result.topics.length > 0) {
|
||||
setSelectedTopic(result.topics[0]);
|
||||
setFile(selectedFile);
|
||||
}
|
||||
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">
|
||||
|
||||
{!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,.docx,.txt,.csv,.xlsx,.pptx"
|
||||
className="block w-full text-sm text-[var(--color-text-muted)]
|
||||
file:mr-4 file:py-3 file:px-4
|
||||
file:rounded-xl file:border-0
|
||||
file:text-sm file:font-semibold
|
||||
file:bg-blue-500/10 file:text-blue-500
|
||||
hover:file:bg-blue-500/20 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!extractedData && file && (
|
||||
<button
|
||||
onClick={handleExtractTopics}
|
||||
disabled={extractDocumentTopics.isPending}
|
||||
className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl font-medium text-white shadow-lg bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{extractDocumentTopics.isPending ? (
|
||||
<>
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
Belge İnceleniyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText size={18} />
|
||||
Konu Çıkar
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{extractedData && (
|
||||
<div className="mt-6 p-4 rounded-xl bg-blue-500/5 border border-blue-500/20">
|
||||
<h3 className="font-medium text-blue-400 mb-3 text-sm flex items-center gap-2">
|
||||
<Wand2 size={16} />
|
||||
Şu Konulardan Birini Seçin:
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{extractedData.topics.map((topic, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => setSelectedTopic(topic)}
|
||||
className={cn(
|
||||
"p-3 rounded-lg border text-sm cursor-pointer transition-all",
|
||||
selectedTopic === topic
|
||||
? "bg-blue-500/20 border-blue-500 text-blue-400"
|
||||
: "bg-[var(--color-bg-surface)] border-[var(--color-border-faint)] text-[var(--color-text-secondary)] hover:border-blue-500/50"
|
||||
)}
|
||||
>
|
||||
{topic}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Video Settings */}
|
||||
<div className="space-y-6">
|
||||
<div className="card p-5 space-y-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
Video Dili
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{languages.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
onClick={() => setLanguage(l.code)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
||||
language === l.code
|
||||
? "bg-blue-500/12 border border-blue-500/30 text-blue-400"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<span>{l.flag}</span>
|
||||
{l.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-5 space-y-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
<Palette size={14} className="inline mr-1.5 text-blue-400" />
|
||||
Video Stili
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{videoStyles.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setStyle(s.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
||||
style === s.id
|
||||
? "bg-blue-500/12 border border-blue-500/30 text-blue-400"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<span>{s.emoji}</span>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{style === "CINEMATIC" && (
|
||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
Özel Sinematik Referans (Opsiyonel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
||||
value={cinematicReference}
|
||||
onChange={(e) => setCinematicReference(e.target.value)}
|
||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-xl py-2 px-3 text-sm focus:border-blue-500/50 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card p-5 space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
||||
Hedef Süre: <span className="text-blue-400">{duration}s</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={15}
|
||||
max={120}
|
||||
step={5}
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(Number(e.target.value))}
|
||||
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none
|
||||
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
|
||||
[&::-webkit-slider-thumb]:rounded-full
|
||||
[&::-webkit-slider-thumb]:bg-blue-500
|
||||
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(59,130,246,0.4)]
|
||||
[&::-webkit-slider-thumb]:cursor-grab"
|
||||
accept=".pdf,.doc,.docx,.txt,.csv"
|
||||
className="hidden"
|
||||
/>
|
||||
</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>
|
||||
{/* Configurations */}
|
||||
<div className="space-y-8 pt-6 border-t border-[var(--color-border-faint)]">
|
||||
<LanguageSelector value={language} onChange={setLanguage} />
|
||||
|
||||
<StyleSelector
|
||||
value={style}
|
||||
onChange={setStyle}
|
||||
cinematicReference={cinematicReference}
|
||||
onCinematicReferenceChange={setCinematicReference}
|
||||
/>
|
||||
|
||||
<DurationSelector value={duration} onChange={setDuration} />
|
||||
|
||||
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
FolderOpen,
|
||||
@@ -60,41 +61,47 @@ 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",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Real API hook'ları — mock modunda çağrılmaz
|
||||
const statsQuery = useDashboardStats();
|
||||
const creditQuery = useCreditBalance();
|
||||
@@ -106,6 +113,14 @@ export default function DashboardPage() {
|
||||
|
||||
const statCards = getStatCards(statsData, creditData);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<Loader2 size={32} className="animate-spin text-neutral-500" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
variants={stagger}
|
||||
@@ -136,7 +151,7 @@ export default function DashboardPage() {
|
||||
<motion.div variants={fadeUp} className="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-4">
|
||||
{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) => {
|
||||
@@ -169,10 +184,10 @@ export default function DashboardPage() {
|
||||
<motion.div variants={fadeUp} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<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>
|
||||
@@ -182,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>
|
||||
@@ -195,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>
|
||||
@@ -208,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" />
|
||||
@@ -534,7 +534,7 @@ export default function ProjectDetailPage() {
|
||||
{/* ── Render Progress (WebSocket) ── */}
|
||||
{isRendering && (
|
||||
<motion.div variants={fadeUp}>
|
||||
<RenderProgress renderState={renderState} />
|
||||
<RenderProgress renderState={renderState} projectStatus={project.status} />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
<StyleSelector
|
||||
value={style}
|
||||
onChange={setStyle}
|
||||
cinematicReference={cinematicReference}
|
||||
onCinematicReferenceChange={setCinematicReference}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1 custom-scrollbar">
|
||||
{Object.entries(groupedStyles).map(([category, items]) => {
|
||||
const isExpanded = styleSearch ? true : expandedCategory === category;
|
||||
return (
|
||||
<div key={category} className="bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl overflow-hidden">
|
||||
<button
|
||||
onClick={() => !styleSearch && setExpandedCategory(isExpanded ? null : category)}
|
||||
className="w-full flex items-center justify-between p-3 bg-[var(--color-bg-elevated)] hover:bg-[var(--color-bg-surface-hover)] transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium text-[var(--color-text-secondary)]">{category} <span className="text-[11px] text-[var(--color-text-ghost)] ml-1">({items.length})</span></span>
|
||||
{!styleSearch && (
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={cn("text-[var(--color-text-ghost)] transition-transform duration-200", isExpanded && "rotate-180")}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="p-3 pt-0 mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{items.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setStyle(s.id)}
|
||||
className={cn(
|
||||
"flex flex-col items-start gap-1 p-2.5 rounded-xl text-left transition-all",
|
||||
style === s.id
|
||||
? "bg-violet-500/12 border border-violet-500/30 glow-violet"
|
||||
: "bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]"
|
||||
)}
|
||||
>
|
||||
<span className="text-xl mb-0.5">{s.emoji}</span>
|
||||
<span className="text-xs font-semibold leading-tight">{s.label}</span>
|
||||
<span className="text-[10px] text-[var(--color-text-ghost)] leading-tight">{s.desc}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Object.keys(groupedStyles).length === 0 && (
|
||||
<div className="text-center py-8 text-[var(--color-text-ghost)] text-sm">
|
||||
"{styleSearch}" için sonuç bulunamadı.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{style === "CINEMATIC" && (
|
||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
Özel Sinematik Referans (Opsiyonel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
||||
value={cinematicReference}
|
||||
onChange={(e) => setCinematicReference(e.target.value)}
|
||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-md py-1.5 px-3 text-sm focus:border-violet-500/50 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Süre */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
||||
Hedef Süre: <span className="text-violet-400">{duration}s</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={15}
|
||||
max={180}
|
||||
step={5}
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(Number(e.target.value))}
|
||||
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none
|
||||
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
|
||||
[&::-webkit-slider-thumb]:rounded-full
|
||||
[&::-webkit-slider-thumb]:bg-violet-500
|
||||
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(139,92,246,0.4)]
|
||||
[&::-webkit-slider-thumb]:cursor-grab"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-[var(--color-text-ghost)] mt-1">
|
||||
<span>15s</span>
|
||||
<span>60s</span>
|
||||
<span>120s</span>
|
||||
<span>180s</span>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('TEXT TO VIDEO ERROR:', error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong!</h2>
|
||||
<pre>{error.message}</pre>
|
||||
<pre>{error.stack}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCreateFromText } from "@/hooks/use-api";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
import { Loader2, ArrowRight, Wand2, Type } from "lucide-react";
|
||||
import {
|
||||
LanguageSelector,
|
||||
StyleSelector,
|
||||
DurationSelector,
|
||||
AspectRatioSelector,
|
||||
} from "@/components/projects/ProjectConfiguration";
|
||||
|
||||
export default function TextToVideoPage() {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const createFromText = useCreateFromText();
|
||||
|
||||
const [textInput, setTextInput] = useState("");
|
||||
|
||||
const [style, setStyle] = useState("CINEMATIC");
|
||||
const [cinematicReference, setCinematicReference] = useState("");
|
||||
const [duration, setDuration] = useState(60);
|
||||
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
|
||||
const [language, setLanguage] = useState("tr");
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!textInput.trim()) {
|
||||
toast("error", "Lütfen bir konu, hikaye veya metin girin.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result: any = await createFromText.mutateAsync({
|
||||
text: textInput,
|
||||
language,
|
||||
aspectRatio,
|
||||
videoStyle: style,
|
||||
cinematicReference: cinematicReference ? cinematicReference : undefined,
|
||||
targetDuration: duration,
|
||||
});
|
||||
|
||||
toast("success", "Video projesi başarıyla oluşturuldu!");
|
||||
router.push(`/dashboard/projects/${result.id}`);
|
||||
} catch (error) {
|
||||
toast("error", "Proje oluşturulurken bir hata oluştu.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto space-y-8 pb-24">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-3 pb-4">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
|
||||
<Type size={32} />
|
||||
</div>
|
||||
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
|
||||
Metinden Video Üret
|
||||
</h1>
|
||||
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
|
||||
İstediğiniz konuyu, bir hikayeyi veya makaleyi kopyalayıp yapıştırın; yapay zeka sizin için detaylı bir video senaryosu üretsin.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="card p-6 md:p-8 space-y-6">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
Fikriniz veya Metniniz
|
||||
</label>
|
||||
<textarea
|
||||
value={textInput}
|
||||
onChange={(e) => setTextInput(e.target.value)}
|
||||
placeholder="Örn: Evrenin başlangıcı ve kara deliklerin sırları hakkında detaylı ve sürükleyici bir anlatım..."
|
||||
rows={6}
|
||||
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-bg-inverted)]/50 resize-none transition-all placeholder:text-[var(--color-text-ghost)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Configurations */}
|
||||
<div className="space-y-8 pt-6 border-t border-[var(--color-border-faint)]">
|
||||
|
||||
<LanguageSelector value={language} onChange={setLanguage} />
|
||||
|
||||
<StyleSelector
|
||||
value={style}
|
||||
onChange={setStyle}
|
||||
cinematicReference={cinematicReference}
|
||||
onCinematicReferenceChange={setCinematicReference}
|
||||
/>
|
||||
|
||||
<DurationSelector value={duration} onChange={setDuration} />
|
||||
|
||||
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={createFromText.isPending || !textInput.trim()}
|
||||
className={cn(
|
||||
"group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
|
||||
"bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
|
||||
"disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
{createFromText.isPending ? (
|
||||
<>
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
<span>Yapay Zeka Senaryoyu Yazıyor...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
|
||||
<span>Video Projesi Oluştur</span>
|
||||
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,11 +8,6 @@ import {
|
||||
Link2,
|
||||
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,132 +249,30 @@ 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>
|
||||
<div className="card p-6 space-y-8">
|
||||
<LanguageSelector value={language} onChange={setLanguage} />
|
||||
|
||||
{/* 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"
|
||||
<StyleSelector
|
||||
value={style}
|
||||
onChange={setStyle}
|
||||
cinematicReference={cinematicReference}
|
||||
onCinematicReferenceChange={setCinematicReference}
|
||||
/>
|
||||
</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>
|
||||
<DurationSelector value={duration} onChange={setDuration} />
|
||||
|
||||
<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>
|
||||
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
|
||||
</div>
|
||||
|
||||
{/* Generate Button */}
|
||||
<div className="flex justify-end">
|
||||
<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",
|
||||
"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 ? (
|
||||
@@ -405,12 +282,13 @@ export default function XToVideoPage() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wand2 size={20} />
|
||||
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
|
||||
<span>Tweet → Video Oluştur</span>
|
||||
<ArrowRight size={16} />
|
||||
<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>
|
||||
|
||||
{/* Main Form */}
|
||||
<div className="card p-6 md:p-8 space-y-6">
|
||||
|
||||
{/* Input */}
|
||||
<div className="card p-6 md:p-8 space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
YouTube URL
|
||||
<label 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=..."
|
||||
type="url"
|
||||
value={youtubeUrl}
|
||||
onChange={(e) => setYoutubeUrl(e.target.value)}
|
||||
className="w-full bg-[var(--color-bg-surface)] border-2 border-[var(--color-border-faint)] rounded-2xl py-4 pl-12 pr-4 text-sm
|
||||
focus:border-red-500/50 focus:ring-4 focus:ring-red-500/10 transition-all outline-none"
|
||||
/>
|
||||
<Link2
|
||||
size={18}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Video Settings */}
|
||||
<div className="space-y-6">
|
||||
<div className="card p-5 space-y-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
Video Dili
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{languages.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
onClick={() => setLanguage(l.code)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
||||
language === l.code
|
||||
? "bg-red-500/12 border border-red-500/30 text-red-400"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<span>{l.flag}</span>
|
||||
{l.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-5 space-y-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
<Palette size={14} className="inline mr-1.5 text-red-400" />
|
||||
Video Stili
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{videoStyles.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setStyle(s.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
|
||||
style === s.id
|
||||
? "bg-red-500/12 border border-red-500/30 text-red-400"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
<span>{s.emoji}</span>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{style === "CINEMATIC" && (
|
||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
Özel Sinematik Referans (Opsiyonel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
||||
value={cinematicReference}
|
||||
onChange={(e) => setCinematicReference(e.target.value)}
|
||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-xl py-2 px-3 text-sm focus:border-red-500/50 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card p-5 space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
||||
Hedef Süre: <span className="text-red-400">{duration}s</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={15}
|
||||
max={120}
|
||||
step={5}
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(Number(e.target.value))}
|
||||
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none
|
||||
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
|
||||
[&::-webkit-slider-thumb]:rounded-full
|
||||
[&::-webkit-slider-thumb]:bg-red-500
|
||||
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(239,68,68,0.4)]
|
||||
[&::-webkit-slider-thumb]:cursor-grab"
|
||||
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>
|
||||
|
||||
<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>
|
||||
{/* Configurations */}
|
||||
<div className="space-y-8 pt-6 border-t border-[var(--color-border-faint)]">
|
||||
<LanguageSelector value={language} onChange={setLanguage} />
|
||||
|
||||
<StyleSelector
|
||||
value={style}
|
||||
onChange={setStyle}
|
||||
cinematicReference={cinematicReference}
|
||||
onCinematicReferenceChange={setCinematicReference}
|
||||
/>
|
||||
|
||||
<DurationSelector value={duration} onChange={setDuration} />
|
||||
|
||||
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={createFromYoutube.isPending || !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>
|
||||
);
|
||||
}
|
||||
|
||||
+68
-114
@@ -2,41 +2,36 @@
|
||||
|
||||
/* ================================================
|
||||
ContentGen AI — Design System
|
||||
Aesthetic Direction: Cinematic Dark + Violet Neon
|
||||
Frontend Design Skill: Bold, intentional, unforgettable
|
||||
Aesthetic Direction: Premium Monochrome & High Contrast
|
||||
Frontend Design Skill: Clean, spacious, highly legible
|
||||
================================================ */
|
||||
|
||||
@theme {
|
||||
/* ── Color Palette ── */
|
||||
/* ── Color Palette (Monochrome Focus) ── */
|
||||
--color-bg-void: #000000;
|
||||
--color-bg-deep: #050509;
|
||||
--color-bg-base: #0a0a12;
|
||||
--color-bg-surface: #111120;
|
||||
--color-bg-elevated: #1a1a2e;
|
||||
--color-bg-subtle: #232340;
|
||||
--color-bg-deep: #0a0a0a;
|
||||
--color-bg-base: #111111;
|
||||
--color-bg-surface: #1a1a1a;
|
||||
--color-bg-elevated: #222222;
|
||||
--color-bg-subtle: #2a2a2a;
|
||||
|
||||
--color-border-faint: #1e1e3a;
|
||||
--color-border-default: #2a2a4a;
|
||||
--color-border-strong: #3a3a5a;
|
||||
--color-border-faint: rgba(255, 255, 255, 0.05);
|
||||
--color-border-default: rgba(255, 255, 255, 0.1);
|
||||
--color-border-strong: rgba(255, 255, 255, 0.2);
|
||||
|
||||
--color-text-primary: #f0f0ff;
|
||||
--color-text-secondary: #a0a0c0;
|
||||
--color-text-muted: #6a6a8a;
|
||||
--color-text-ghost: #4a4a6a;
|
||||
--color-text-primary: #ffffff;
|
||||
--color-text-secondary: #a3a3a3;
|
||||
--color-text-muted: #737373;
|
||||
--color-text-ghost: #525252;
|
||||
|
||||
/* Brand */
|
||||
--color-violet-400: #a78bfa;
|
||||
--color-violet-500: #8b5cf6;
|
||||
--color-violet-600: #7c3aed;
|
||||
--color-violet-700: #6d28d9;
|
||||
--color-violet-glow: rgba(139, 92, 246, 0.15);
|
||||
/* Accent: Clean White/Gray (Replaces Neon) */
|
||||
--color-accent-400: #e5e5e5;
|
||||
--color-accent-500: #ffffff;
|
||||
--color-accent-glow: rgba(255, 255, 255, 0.08);
|
||||
|
||||
--color-cyan-400: #22d3ee;
|
||||
--color-cyan-500: #06b6d4;
|
||||
--color-cyan-glow: rgba(6, 182, 212, 0.12);
|
||||
|
||||
--color-emerald-400: #34d399;
|
||||
--color-emerald-500: #10b981;
|
||||
/* Status Colors (Subtle versions) */
|
||||
--color-emerald-400: #4ade80;
|
||||
--color-emerald-500: #22c55e;
|
||||
|
||||
--color-amber-400: #fbbf24;
|
||||
--color-amber-500: #f59e0b;
|
||||
@@ -44,23 +39,26 @@
|
||||
--color-rose-400: #fb7185;
|
||||
--color-rose-500: #f43f5e;
|
||||
|
||||
/* ── Spacing ── */
|
||||
--spacing-page: clamp(1rem, 4vw, 2.5rem);
|
||||
--color-cyan-400: #38bdf8; /* Kept for processing, but subdued */
|
||||
--color-cyan-500: #0ea5e9;
|
||||
|
||||
/* ── Radius ── */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.625rem;
|
||||
--radius-lg: 0.875rem;
|
||||
--radius-xl: 1.25rem;
|
||||
/* ── Spacing (Increased for breathable UI) ── */
|
||||
--spacing-page: clamp(1.5rem, 5vw, 3rem);
|
||||
|
||||
/* ── Radius (Sharper, more professional) ── */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* ── Shadows ── */
|
||||
--shadow-glow-sm: 0 0 12px rgba(139, 92, 246, 0.08);
|
||||
--shadow-glow-md: 0 0 24px rgba(139, 92, 246, 0.12);
|
||||
--shadow-glow-lg: 0 0 48px rgba(139, 92, 246, 0.18);
|
||||
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
--shadow-elevated: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
/* ── Shadows (Soft, realistic depth instead of glows) ── */
|
||||
--shadow-glow-sm: 0 4px 14px rgba(0, 0, 0, 0.5);
|
||||
--shadow-glow-md: 0 10px 30px rgba(0, 0, 0, 0.6);
|
||||
--shadow-glow-lg: 0 20px 40px rgba(0, 0, 0, 0.8);
|
||||
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.8), 0 8px 16px rgba(0, 0, 0, 0.4);
|
||||
--shadow-elevated: 0 12px 32px rgba(0, 0, 0, 0.6);
|
||||
|
||||
/* ── Animations ── */
|
||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
@@ -82,11 +80,14 @@ body {
|
||||
color: var(--color-text-primary);
|
||||
overflow-x: hidden;
|
||||
min-height: 100dvh;
|
||||
/* Slightly larger base text */
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* ── Selection ── */
|
||||
::selection {
|
||||
background-color: rgba(139, 92, 246, 0.3);
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -106,55 +107,18 @@ body {
|
||||
background: var(--color-border-strong);
|
||||
}
|
||||
|
||||
/* ── Glass Effect ── */
|
||||
/* ── Glass Effect (Desaturated, High Blur) ── */
|
||||
.glass {
|
||||
background: rgba(17, 17, 32, 0.6);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
background: rgba(10, 10, 10, 0.7);
|
||||
backdrop-filter: blur(24px) saturate(100%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(100%);
|
||||
border: 1px solid var(--color-border-faint);
|
||||
}
|
||||
|
||||
.glass-subtle {
|
||||
background: rgba(17, 17, 32, 0.35);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
/* ── Glow Effects ── */
|
||||
.glow-violet {
|
||||
box-shadow: 0 0 20px rgba(139, 92, 246, 0.15),
|
||||
0 0 40px rgba(139, 92, 246, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.glow-cyan {
|
||||
box-shadow: 0 0 20px rgba(6, 182, 212, 0.15),
|
||||
0 0 40px rgba(6, 182, 212, 0.08);
|
||||
}
|
||||
|
||||
/* ── Gradient Borders ── */
|
||||
.gradient-border {
|
||||
position: relative;
|
||||
}
|
||||
.gradient-border::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg, var(--color-violet-500), var(--color-cyan-400));
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
-webkit-mask-composite: xor;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Animated Gradient Background ── */
|
||||
.gradient-mesh {
|
||||
background-image:
|
||||
radial-gradient(ellipse at 20% 50%, var(--color-violet-glow) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 20%, var(--color-cyan-glow) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 50% 80%, rgba(16, 185, 129, 0.06) 0%, transparent 50%);
|
||||
background: rgba(10, 10, 10, 0.4);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
/* ── Card Styles ── */
|
||||
@@ -163,58 +127,48 @@ body {
|
||||
border: 1px solid var(--color-border-faint);
|
||||
border-radius: var(--radius-xl);
|
||||
transition: all var(--duration-normal) var(--ease-out-expo);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
.card-surface:hover {
|
||||
border-color: var(--color-border-default);
|
||||
box-shadow: var(--shadow-glow-sm);
|
||||
transform: translateY(-1px);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ── Badge Styles ── */
|
||||
/* ── Badge Styles (Minimalist) ── */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.125rem 0.625rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius-full);
|
||||
letter-spacing: 0.02em;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.badge-violet {
|
||||
background: rgba(139, 92, 246, 0.12);
|
||||
color: var(--color-violet-400);
|
||||
border: 1px solid rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
.badge-cyan {
|
||||
background: rgba(6, 182, 212, 0.12);
|
||||
color: var(--color-cyan-400);
|
||||
border: 1px solid rgba(6, 182, 212, 0.2);
|
||||
.badge-violet, .badge-cyan {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.badge-emerald {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
background: rgba(74, 222, 128, 0.1);
|
||||
color: var(--color-emerald-400);
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
border: 1px solid rgba(74, 222, 128, 0.2);
|
||||
}
|
||||
.badge-amber {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
color: var(--color-amber-400);
|
||||
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||
border: 1px solid rgba(251, 191, 36, 0.2);
|
||||
}
|
||||
.badge-rose {
|
||||
background: rgba(244, 63, 94, 0.12);
|
||||
background: rgba(251, 113, 133, 0.1);
|
||||
color: var(--color-rose-400);
|
||||
border: 1px solid rgba(244, 63, 94, 0.2);
|
||||
border: 1px solid rgba(251, 113, 133, 0.2);
|
||||
}
|
||||
|
||||
/* ── Button Styles ── */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--color-violet-600), var(--color-violet-500));
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 0.625rem 1.25rem;
|
||||
transition: all var(--duration-normal) var(--ease-out-expo);
|
||||
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.25);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.4);
|
||||
@@ -246,7 +200,7 @@ body {
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
padding: 0.5rem 0 calc(0.5rem + env(safe-area-inset-bottom));
|
||||
background: rgba(5, 5, 9, 0.85);
|
||||
background: rgba(10, 10, 10, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-top: 1px solid var(--color-border-faint);
|
||||
@@ -262,7 +216,7 @@ body {
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(90deg, var(--color-violet-500), var(--color-cyan-400));
|
||||
background: var(--color-text-primary);
|
||||
transition: width var(--duration-slow) var(--ease-out-expo);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
} from "recharts";
|
||||
import { useDashboardStats } from "@/hooks/use-api";
|
||||
|
||||
const COLORS = ["#8b5cf6", "#06b6d4", "#f59e0b", "#ef4444", "#10b981"];
|
||||
const COLORS = ["#ffffff", "#a3a3a3", "#525252", "#262626"];
|
||||
|
||||
function formatWeekData(stats: Record<string, unknown> | undefined) {
|
||||
if (!stats) {
|
||||
@@ -59,19 +60,25 @@ function formatPieData(stats: Record<string, unknown> | undefined) {
|
||||
}
|
||||
|
||||
export function DashboardCharts() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { data, isLoading } = useDashboardStats();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const stats = (data as any)?.data ?? data;
|
||||
const weekData = formatWeekData(stats);
|
||||
const pieData = formatPieData(stats);
|
||||
|
||||
if (isLoading) {
|
||||
if (!mounted || isLoading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
||||
{[1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="card p-5 h-[280px] animate-pulse bg-[var(--color-bg-surface)]"
|
||||
className="card p-6 md:p-8 h-[300px] animate-pulse bg-[var(--color-bg-surface)] rounded-2xl border border-[var(--color-border-faint)]"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -81,42 +88,42 @@ export function DashboardCharts() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
||||
{/* Haftalik Aktivite */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-semibold text-[var(--color-text-secondary)] mb-4">
|
||||
<div className="card-surface p-6 md:p-8">
|
||||
<h3 className="text-base font-semibold text-[var(--color-text-secondary)] mb-6">
|
||||
Haftalık Aktivite
|
||||
</h3>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<AreaChart data={weekData}>
|
||||
<defs>
|
||||
<linearGradient id="colorProjects" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#8b5cf6" stopOpacity={0} />
|
||||
<stop offset="5%" stopColor="#ffffff" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#ffffff" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="colorVideos" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#06b6d4" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#06b6d4" stopOpacity={0} />
|
||||
<stop offset="5%" stopColor="#737373" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#737373" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 11, fill: "var(--color-text-ghost)" }}
|
||||
tick={{ fontSize: 12, fill: "var(--color-text-ghost)" }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis hide />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "rgba(15,15,30,0.9)",
|
||||
border: "1px solid rgba(139,92,246,0.2)",
|
||||
backgroundColor: "rgba(10,10,10,0.95)",
|
||||
border: "1px solid rgba(255,255,255,0.1)",
|
||||
borderRadius: 12,
|
||||
fontSize: 12,
|
||||
fontSize: 13,
|
||||
color: "#fff",
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="projects"
|
||||
stroke="#8b5cf6"
|
||||
stroke="#ffffff"
|
||||
strokeWidth={2}
|
||||
fill="url(#colorProjects)"
|
||||
name="Projeler"
|
||||
@@ -124,7 +131,7 @@ export function DashboardCharts() {
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="videos"
|
||||
stroke="#06b6d4"
|
||||
stroke="#737373"
|
||||
strokeWidth={2}
|
||||
fill="url(#colorVideos)"
|
||||
name="Videolar"
|
||||
@@ -134,26 +141,27 @@ export function DashboardCharts() {
|
||||
</div>
|
||||
|
||||
{/* Proje Durumu */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-semibold text-[var(--color-text-secondary)] mb-4">
|
||||
<div className="card-surface p-6 md:p-8">
|
||||
<h3 className="text-base font-semibold text-[var(--color-text-secondary)] mb-6">
|
||||
Proje Durumu
|
||||
</h3>
|
||||
{pieData.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-[200px] text-sm text-[var(--color-text-ghost)]">
|
||||
<div className="flex items-center justify-center h-[220px] text-sm text-[var(--color-text-ghost)]">
|
||||
Henüz proje verisi yok
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-4">
|
||||
<ResponsiveContainer width="50%" height={200}>
|
||||
<div className="flex items-center gap-6">
|
||||
<ResponsiveContainer width="50%" height={220}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={70}
|
||||
innerRadius={40}
|
||||
outerRadius={80}
|
||||
innerRadius={50}
|
||||
dataKey="value"
|
||||
stroke="none"
|
||||
stroke="var(--color-bg-surface)"
|
||||
strokeWidth={2}
|
||||
>
|
||||
{pieData.map((_: unknown, index: number) => (
|
||||
<Cell key={index} fill={COLORS[index % COLORS.length]} />
|
||||
@@ -161,10 +169,10 @@ export function DashboardCharts() {
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "rgba(15,15,30,0.9)",
|
||||
border: "1px solid rgba(139,92,246,0.2)",
|
||||
backgroundColor: "rgba(10,10,10,0.95)",
|
||||
border: "1px solid rgba(255,255,255,0.1)",
|
||||
borderRadius: 12,
|
||||
fontSize: 12,
|
||||
fontSize: 13,
|
||||
color: "#fff",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -44,7 +44,7 @@ export function RecentProjects() {
|
||||
</h3>
|
||||
<Link
|
||||
href="/dashboard/projects"
|
||||
className="text-xs text-violet-400 hover:text-violet-300 flex items-center gap-1 transition-colors"
|
||||
className="text-xs text-neutral-400 hover:text-neutral-300 flex items-center gap-1 transition-colors"
|
||||
>
|
||||
Tümü <ExternalLink size={12} />
|
||||
</Link>
|
||||
@@ -61,7 +61,7 @@ export function RecentProjects() {
|
||||
</p>
|
||||
<Link
|
||||
href="/dashboard/projects/new"
|
||||
className="mt-3 text-xs text-violet-400 hover:text-violet-300"
|
||||
className="mt-3 text-xs text-neutral-400 hover:text-neutral-300"
|
||||
>
|
||||
İlk projenizi oluşturun →
|
||||
</Link>
|
||||
@@ -89,7 +89,7 @@ export function RecentProjects() {
|
||||
<StIcon size={14} />
|
||||
</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-neutral-300 transition-colors">
|
||||
{project.title}
|
||||
</p>
|
||||
<p className="text-[10px] text-[var(--color-text-ghost)]">
|
||||
|
||||
@@ -100,8 +100,8 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
||||
<div className="card-surface overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 p-4 md:p-5 border-b border-[var(--color-border-faint)]">
|
||||
<div className="w-9 h-9 rounded-xl bg-sky-500/15 flex items-center justify-center">
|
||||
<XIcon size={18} className="text-sky-400" />
|
||||
<div className="w-9 h-9 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] flex items-center justify-center">
|
||||
<XIcon size={18} className="text-neutral-300" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-[family-name:var(--font-display)] text-sm font-semibold">
|
||||
@@ -128,7 +128,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="https://x.com/user/status/123..."
|
||||
className="w-full h-11 pl-10 pr-24 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:border-sky-500/50 focus:ring-1 focus:ring-sky-500/25 outline-none transition-all"
|
||||
className="w-full h-11 pl-10 pr-24 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:border-neutral-500/50 focus:ring-1 focus:ring-neutral-500/25 outline-none transition-all"
|
||||
/>
|
||||
<button
|
||||
onClick={preview ? handleCreate : handlePreview}
|
||||
@@ -136,7 +136,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
||||
className={cn(
|
||||
"absolute right-1.5 top-1/2 -translate-y-1/2 h-8 px-3 rounded-lg text-xs font-medium transition-all flex items-center gap-1.5",
|
||||
isUrlValid && !isLoadingPreview && !isCreatingProject
|
||||
? "bg-sky-500 text-white hover:bg-sky-400 shadow-sm"
|
||||
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] hover:bg-neutral-800 shadow-sm"
|
||||
: "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
@@ -200,7 +200,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<XIcon size={16} className="text-sky-400" />
|
||||
<XIcon size={16} className="text-neutral-400" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
@@ -211,7 +211,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
||||
{preview.tweet.author.verified && (
|
||||
<CheckCircle2
|
||||
size={13}
|
||||
className="text-sky-400 fill-sky-400"
|
||||
className="text-neutral-400 fill-neutral-400"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -304,18 +304,18 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
||||
|
||||
{/* Info badges */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<span className="badge badge-cyan text-[10px]">
|
||||
<span className="bg-neutral-500/10 text-neutral-400 border border-neutral-500/20 px-2 py-0.5 rounded-md text-[10px]">
|
||||
{preview.contentType === "thread"
|
||||
? "Thread"
|
||||
: preview.contentType === "quote_tweet"
|
||||
? "Alıntı"
|
||||
: "Tweet"}
|
||||
</span>
|
||||
<span className="badge badge-violet text-[10px]">
|
||||
<span className="bg-neutral-500/10 text-neutral-400 border border-neutral-500/20 px-2 py-0.5 rounded-md text-[10px]">
|
||||
~{preview.estimatedDuration}s video
|
||||
</span>
|
||||
{preview.tweet.media.length > 0 && (
|
||||
<span className="badge badge-amber text-[10px]">
|
||||
<span className="bg-neutral-500/10 text-neutral-400 border border-neutral-500/20 px-2 py-0.5 rounded-md text-[10px]">
|
||||
{preview.tweet.media.length} medya
|
||||
</span>
|
||||
)}
|
||||
@@ -327,7 +327,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
||||
value={customTitle}
|
||||
onChange={(e) => setCustomTitle(e.target.value)}
|
||||
placeholder={preview.suggestedTitle}
|
||||
className="w-full h-10 px-3 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)]/50 focus:border-violet-500/50 focus:ring-1 focus:ring-violet-500/25 outline-none transition-all"
|
||||
className="w-full h-10 px-3 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)]/50 focus:border-neutral-500/50 focus:ring-1 focus:ring-neutral-500/25 outline-none transition-all"
|
||||
/>
|
||||
|
||||
{/* Create Button */}
|
||||
@@ -337,8 +337,8 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
|
||||
className={cn(
|
||||
"w-full h-11 rounded-xl text-sm font-semibold shadow-sm transition-all flex items-center justify-center gap-2",
|
||||
isCreatingProject
|
||||
? "bg-violet-600/50 text-violet-200 cursor-not-allowed"
|
||||
: "bg-gradient-to-r from-violet-600 to-purple-600 text-white hover:from-violet-500 hover:to-purple-500 hover:shadow-lg hover:shadow-violet-500/25 active:scale-[0.98]"
|
||||
? "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] cursor-not-allowed"
|
||||
: "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] hover:bg-neutral-800 active:scale-[0.98]"
|
||||
)}
|
||||
>
|
||||
{isCreatingProject ? (
|
||||
|
||||
@@ -13,6 +13,7 @@ const navItems = [
|
||||
{ href: "/dashboard", icon: Home, label: "Ana Sayfa" },
|
||||
{ href: "/dashboard/projects", icon: FolderOpen, label: "Projeler" },
|
||||
{ href: "/dashboard/render-queue", icon: Activity, label: "Render Monitör" },
|
||||
{ href: "/dashboard/text-to-video", icon: FileText, label: "Metin → Video" },
|
||||
{ href: "/dashboard/x-to-video", icon: AtSign, label: "X → Video" },
|
||||
{ href: "/dashboard/youtube-to-video", icon: Link2, label: "YT → Video" },
|
||||
{ href: "/dashboard/document-to-video", icon: FileText, label: "Belge → Video" },
|
||||
@@ -40,23 +41,23 @@ export function MobileNav() {
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"relative flex flex-col items-center gap-0.5 py-1.5 px-3 rounded-xl min-w-[4rem] transition-colors",
|
||||
"relative flex flex-col items-center gap-1 py-2 px-3 rounded-xl min-w-[4.5rem] transition-colors",
|
||||
isActive
|
||||
? "text-violet-400"
|
||||
: "text-[var(--color-text-muted)] active:text-[var(--color-text-secondary)]"
|
||||
? "text-white"
|
||||
: "text-[var(--color-text-muted)] active:text-[var(--color-text-primary)]"
|
||||
)}
|
||||
>
|
||||
<div className="relative">
|
||||
<Icon size={22} strokeWidth={isActive ? 2.2 : 1.8} />
|
||||
<Icon size={24} strokeWidth={isActive ? 2.5 : 1.8} />
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="nav-indicator"
|
||||
className="absolute -inset-2 rounded-xl bg-violet-500/10"
|
||||
className="absolute -inset-2 rounded-xl bg-white/10"
|
||||
transition={{ type: "spring", bounce: 0.2, duration: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] font-medium tracking-wide">
|
||||
<span className="text-xs font-semibold tracking-wide mt-1">
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
@@ -79,12 +80,12 @@ function CreditCard() {
|
||||
const pct = isAdmin ? 100 : (total > 0 ? Math.round((remaining / total) * 100) : 0);
|
||||
|
||||
return (
|
||||
<div className="mx-3 mb-4 p-4 rounded-xl bg-gradient-to-br from-violet-500/8 to-cyan-400/5 border border-[var(--color-border-faint)]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs text-[var(--color-text-muted)]">Kalan Kredi</span>
|
||||
<span className="badge badge-violet">{planName}</span>
|
||||
<div className="mx-4 mb-6 p-5 rounded-2xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium text-[var(--color-text-muted)]">Kalan Kredi</span>
|
||||
<span className="badge bg-white/10 text-white border border-white/20">{planName}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold font-[family-name:var(--font-display)] text-[var(--color-text-primary)]">
|
||||
<div className="text-3xl font-bold font-[family-name:var(--font-display)] text-[var(--color-text-primary)]">
|
||||
{isLoading ? "..." : isAdmin ? "∞" : remaining}
|
||||
</div>
|
||||
<div className="progress-bar mt-2">
|
||||
@@ -93,7 +94,7 @@ function CreditCard() {
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] text-[var(--color-text-ghost)] mt-1.5">
|
||||
<p className="text-xs text-[var(--color-text-ghost)] mt-2">
|
||||
{isAdmin ? "Sınırsız admin erişimi" : `${total} kredilik planınızın ${remaining}'${remaining === 1 ? "i" : "si"} kaldı`}
|
||||
</p>
|
||||
</div>
|
||||
@@ -107,7 +108,7 @@ export function DesktopSidebar() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const user = (data as any)?.data ?? data;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const roles: string[] = (user?.roles ?? []).map((r: any) => r?.role?.name ?? r?.name ?? "");
|
||||
const roles: string[] = (user?.roles ?? []).map((r: any) => typeof r === "string" ? r : (r?.role?.name ?? r?.name ?? ""));
|
||||
const isAdmin = roles.includes("admin") || roles.includes("superadmin");
|
||||
|
||||
const handleLogout = () => {
|
||||
@@ -115,24 +116,24 @@ export function DesktopSidebar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="hidden md:flex md:w-64 lg:w-72 flex-col h-screen sticky top-0 border-r border-[var(--color-border-faint)] bg-[var(--color-bg-deep)]">
|
||||
<aside className="hidden md:flex md:w-72 lg:w-80 flex-col h-screen sticky top-0 border-r border-[var(--color-border-faint)] bg-[var(--color-bg-deep)]">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 px-6 py-5 border-b border-[var(--color-border-faint)]">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-cyan-400 flex items-center justify-center shadow-lg">
|
||||
<Sparkles size={18} className="text-white" />
|
||||
<div className="flex items-center gap-4 px-8 py-6 border-b border-[var(--color-border-faint)]">
|
||||
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center shadow-md">
|
||||
<Sparkles size={20} className="text-black" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-[family-name:var(--font-display)] text-lg font-bold tracking-tight text-[var(--color-text-primary)]">
|
||||
<h1 className="font-[family-name:var(--font-display)] text-xl font-bold tracking-tight text-[var(--color-text-primary)]">
|
||||
ContentGen
|
||||
</h1>
|
||||
<p className="text-[10px] text-[var(--color-text-muted)] uppercase tracking-widest">
|
||||
<p className="text-xs font-semibold text-[var(--color-text-muted)] uppercase tracking-[0.2em]">
|
||||
AI Studio
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 py-4 space-y-1">
|
||||
<nav className="flex-1 px-4 py-6 space-y-1.5">
|
||||
{navItems.map((item) => {
|
||||
const isActive =
|
||||
localePath === item.href ||
|
||||
@@ -144,20 +145,20 @@ export function DesktopSidebar() {
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"relative flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-200",
|
||||
"relative flex items-center gap-4 px-4 py-3 rounded-xl text-base font-semibold transition-all duration-200",
|
||||
isActive
|
||||
? "text-white bg-violet-500/12 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]"
|
||||
: "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)]"
|
||||
? "text-white bg-white/5"
|
||||
: "text-[var(--color-text-muted)] hover:text-white hover:bg-[var(--color-bg-surface)]"
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="sidebar-active"
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-6 rounded-r-full bg-gradient-to-b from-violet-400 to-violet-600"
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 rounded-r-full bg-white"
|
||||
transition={{ type: "spring", bounce: 0.2, duration: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
<Icon size={18} strokeWidth={isActive ? 2.2 : 1.6} />
|
||||
<Icon size={20} strokeWidth={isActive ? 2.5 : 2} />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
@@ -169,29 +170,29 @@ export function DesktopSidebar() {
|
||||
|
||||
{/* Admin Panel Linki (sadece admin) */}
|
||||
{isAdmin && (
|
||||
<div className="px-3 pb-3">
|
||||
<div className="px-4 pb-4">
|
||||
<Link
|
||||
href={adminNavItem.href}
|
||||
className={cn(
|
||||
"relative flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-200",
|
||||
"relative flex items-center gap-4 px-4 py-3 rounded-xl text-base font-semibold transition-all duration-200",
|
||||
localePath.startsWith("/dashboard/admin")
|
||||
? "text-rose-300 bg-rose-500/10"
|
||||
: "text-[var(--color-text-muted)] hover:text-rose-300 hover:bg-rose-500/8"
|
||||
? "text-white bg-white/10"
|
||||
: "text-[var(--color-text-ghost)] hover:text-white hover:bg-white/5"
|
||||
)}
|
||||
>
|
||||
<ShieldCheck size={18} strokeWidth={1.8} />
|
||||
<ShieldCheck size={20} strokeWidth={2} />
|
||||
<span>{adminNavItem.label}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Çıkış Butonu */}
|
||||
<div className="px-3 pb-4">
|
||||
<div className="px-4 pb-6">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-[var(--color-text-muted)] hover:text-red-400 hover:bg-red-500/8 transition-all duration-200"
|
||||
className="w-full flex items-center gap-4 px-4 py-3 rounded-xl text-base font-semibold text-[var(--color-text-ghost)] hover:text-white hover:bg-white/5 transition-all duration-200"
|
||||
>
|
||||
<LogOut size={18} strokeWidth={1.6} />
|
||||
<LogOut size={20} strokeWidth={2} />
|
||||
<span>Çıkış Yap</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -207,7 +208,7 @@ function UserAvatar() {
|
||||
|
||||
return (
|
||||
<button
|
||||
className="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-600 to-cyan-500 flex items-center justify-center text-white text-sm font-semibold shadow-md"
|
||||
className="w-10 h-10 rounded-full bg-white flex items-center justify-center text-black text-sm font-bold shadow-sm border border-white/20"
|
||||
aria-label="Profil"
|
||||
>
|
||||
{initial}
|
||||
@@ -220,11 +221,11 @@ export function TopBar() {
|
||||
<header className="sticky top-0 z-40 glass">
|
||||
<div className="flex items-center justify-between px-4 md:px-6 h-14 md:h-16">
|
||||
{/* Mobil logo */}
|
||||
<div className="flex items-center gap-2.5 md:hidden">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-cyan-400 flex items-center justify-center">
|
||||
<Sparkles size={16} className="text-white" />
|
||||
<div className="flex items-center gap-3 md:hidden">
|
||||
<div className="w-8 h-8 rounded-lg bg-white flex items-center justify-center">
|
||||
<Sparkles size={16} className="text-black" />
|
||||
</div>
|
||||
<span className="font-[family-name:var(--font-display)] font-bold text-base">
|
||||
<span className="font-[family-name:var(--font-display)] font-bold text-lg">
|
||||
ContentGen
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -7,21 +7,50 @@ import type { RenderProgressState } from '@/hooks/use-render-progress';
|
||||
const STAGE_ORDER = ['tts', 'image_generation', 'music_generation', 'compositing', 'encoding'];
|
||||
|
||||
const STAGE_DETAILS: Record<string, { label: string; icon: string; color: string }> = {
|
||||
tts: { label: 'Seslendirme', icon: '🔊', color: 'from-violet-500 to-violet-600' },
|
||||
image_generation: { label: 'Görsel Üretim', icon: '🎨', color: 'from-cyan-500 to-cyan-600' },
|
||||
music_generation: { label: 'Müzik Üretim', icon: '🎵', color: 'from-amber-500 to-amber-600' },
|
||||
compositing: { label: 'Birleştirme', icon: '🎬', color: 'from-emerald-500 to-emerald-600' },
|
||||
encoding: { label: 'Kodlama', icon: '📦', color: 'from-rose-500 to-rose-600' },
|
||||
tts: { label: 'Seslendirme', icon: '🔊', color: 'from-neutral-500 to-neutral-600' },
|
||||
image_generation: { label: 'Görsel Üretim', icon: '🎨', color: 'from-neutral-400 to-neutral-500' },
|
||||
music_generation: { label: 'Müzik Üretim', icon: '🎵', color: 'from-neutral-600 to-neutral-700' },
|
||||
compositing: { label: 'Birleştirme', icon: '🎬', color: 'from-neutral-500 to-neutral-600' },
|
||||
encoding: { label: 'Kodlama', icon: '📦', color: 'from-neutral-400 to-neutral-500' },
|
||||
};
|
||||
|
||||
interface RenderProgressProps {
|
||||
renderState: RenderProgressState;
|
||||
projectStatus?: string;
|
||||
}
|
||||
|
||||
export function RenderProgress({ renderState }: RenderProgressProps) {
|
||||
export function RenderProgress({ renderState, projectStatus }: RenderProgressProps) {
|
||||
const { progress, stage, stageLabel, currentScene, totalScenes, eta, status, isConnected } = renderState;
|
||||
|
||||
if (status === 'idle') return null;
|
||||
if (status === 'idle') {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="card-surface p-5 md:p-6"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Loader2 size={18} className="animate-spin text-amber-400" />
|
||||
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
{projectStatus === 'GENERATING_SCRIPT' ? 'AI Senaryo Üretiyor...' : 'İşlem kuyrukta veya başlatılıyor...'}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{isConnected ? (
|
||||
<Wifi size={13} className="text-emerald-400" />
|
||||
) : (
|
||||
<WifiOff size={13} className="text-red-400" />
|
||||
)}
|
||||
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
||||
{isConnected ? 'Canlı' : 'Bağlantı koptu'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -33,7 +62,7 @@ export function RenderProgress({ renderState }: RenderProgressProps) {
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{status === 'rendering' && (
|
||||
<Loader2 size={18} className="animate-spin text-violet-400" />
|
||||
<Loader2 size={18} className="animate-spin text-neutral-400" />
|
||||
)}
|
||||
{status === 'completed' && (
|
||||
<CheckCircle2 size={18} className="text-emerald-400" />
|
||||
@@ -71,7 +100,7 @@ export function RenderProgress({ renderState }: RenderProgressProps) {
|
||||
</div>
|
||||
<div className="h-2.5 w-full rounded-full bg-[var(--color-bg-deep)] overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full rounded-full bg-gradient-to-r from-violet-500 via-cyan-400 to-emerald-400"
|
||||
className="h-full rounded-full bg-gradient-to-r from-neutral-500 via-neutral-400 to-neutral-300"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progress}%` }}
|
||||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||
@@ -94,9 +123,9 @@ export function RenderProgress({ renderState }: RenderProgressProps) {
|
||||
key={s}
|
||||
className={`flex flex-col items-center gap-1 py-2 px-1 rounded-lg transition-all ${
|
||||
isCurrent
|
||||
? 'bg-violet-500/10 border border-violet-500/20'
|
||||
? 'bg-[var(--color-bg-elevated)] border border-neutral-500/20'
|
||||
: isDone
|
||||
? 'bg-emerald-500/5'
|
||||
? 'bg-neutral-500/10'
|
||||
: 'opacity-40'
|
||||
}`}
|
||||
>
|
||||
@@ -104,8 +133,8 @@ export function RenderProgress({ renderState }: RenderProgressProps) {
|
||||
<span className="text-[9px] text-center text-[var(--color-text-ghost)] leading-tight">
|
||||
{detail.label}
|
||||
</span>
|
||||
{isDone && <CheckCircle2 size={10} className="text-emerald-400" />}
|
||||
{isCurrent && <Loader2 size={10} className="animate-spin text-violet-400" />}
|
||||
{isDone && <CheckCircle2 size={10} className="text-neutral-300" />}
|
||||
{isCurrent && <Loader2 size={10} className="animate-spin text-neutral-400" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -74,12 +74,12 @@ export function SceneCard({
|
||||
transition={{ delay: scene.order * 0.05, duration: 0.4 }}
|
||||
className="relative group"
|
||||
>
|
||||
<div className="card-surface p-4 md:p-5 hover:border-violet-500/20 transition-all duration-300">
|
||||
<div className="card-surface p-4 md:p-5 hover:border-neutral-500/20 transition-all duration-300">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500/20 to-violet-600/10 flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-violet-400">{scene.order}</span>
|
||||
<div className="w-8 h-8 rounded-lg bg-[var(--color-bg-elevated)] flex items-center justify-center border border-[var(--color-border-faint)]">
|
||||
<span className="text-xs font-bold text-neutral-400">{scene.order}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
@@ -101,7 +101,7 @@ export function SceneCard({
|
||||
<div className="flex items-center gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-violet-400 hover:bg-violet-500/10 transition-colors"
|
||||
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800 transition-colors"
|
||||
title="Düzenle"
|
||||
>
|
||||
<Pencil size={13} />
|
||||
@@ -109,7 +109,7 @@ export function SceneCard({
|
||||
<button
|
||||
onClick={() => onRegenerate?.(scene.id)}
|
||||
disabled={!isEditable || isRendering || isRegenerating}
|
||||
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-cyan-400 hover:bg-cyan-500/10 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
title="AI ile yeniden üret"
|
||||
>
|
||||
<RefreshCw size={13} className={isRegenerating ? 'animate-spin' : ''} />
|
||||
@@ -136,7 +136,7 @@ export function SceneCard({
|
||||
value={editNarration}
|
||||
onChange={(e) => setEditNarration(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] resize-none focus:outline-none focus:ring-1 focus:ring-violet-500/40 transition-all"
|
||||
className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] resize-none focus:outline-none focus:ring-1 focus:ring-neutral-500/40 transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -149,7 +149,7 @@ export function SceneCard({
|
||||
value={editVisual}
|
||||
onChange={(e) => setEditVisual(e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-secondary)] resize-none focus:outline-none focus:ring-1 focus:ring-cyan-500/40 transition-all"
|
||||
className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-secondary)] resize-none focus:outline-none focus:ring-1 focus:ring-neutral-500/40 transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -157,7 +157,7 @@ export function SceneCard({
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-violet-500/15 text-violet-400 text-xs font-medium hover:bg-violet-500/25 transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] text-xs font-medium hover:bg-neutral-800 transition-colors"
|
||||
>
|
||||
<Check size={13} /> Kaydet
|
||||
</button>
|
||||
@@ -173,8 +173,8 @@ export function SceneCard({
|
||||
<motion.div key="viewing" className="space-y-2.5">
|
||||
{/* Narrasyon */}
|
||||
<div className="flex gap-2">
|
||||
<div className="w-5 h-5 rounded-md bg-violet-500/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<Mic size={11} className="text-violet-400" />
|
||||
<div className="w-5 h-5 rounded-md bg-[var(--color-bg-elevated)] flex items-center justify-center shrink-0 mt-0.5 border border-[var(--color-border-faint)]">
|
||||
<Mic size={11} className="text-neutral-400" />
|
||||
</div>
|
||||
<p className="text-sm text-[var(--color-text-secondary)] leading-relaxed">
|
||||
{scene.narrationText}
|
||||
@@ -183,8 +183,8 @@ export function SceneCard({
|
||||
|
||||
{/* Görsel Prompt */}
|
||||
<div className="flex gap-2">
|
||||
<div className="w-5 h-5 rounded-md bg-cyan-500/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<ImageIcon size={11} className="text-cyan-400" />
|
||||
<div className="w-5 h-5 rounded-md bg-[var(--color-bg-elevated)] flex items-center justify-center shrink-0 mt-0.5 border border-[var(--color-border-faint)]">
|
||||
<ImageIcon size={11} className="text-neutral-400" />
|
||||
</div>
|
||||
<div className="flex-1 group/prompt relative">
|
||||
<p className="text-xs text-[var(--color-text-ghost)] leading-relaxed italic pr-6">
|
||||
@@ -194,7 +194,7 @@ export function SceneCard({
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(scene.visualPrompt);
|
||||
}}
|
||||
className="absolute top-0 right-0 p-1 opacity-0 group-hover/prompt:opacity-100 transition-opacity bg-[var(--color-bg-elevated)] rounded-md text-[var(--color-text-muted)] hover:text-cyan-400 hover:bg-cyan-500/10"
|
||||
className="absolute top-0 right-0 p-1 opacity-0 group-hover/prompt:opacity-100 transition-opacity bg-[var(--color-bg-elevated)] rounded-md text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800"
|
||||
title="Prompt'u Kopyala"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Monitor,
|
||||
Smartphone,
|
||||
Square,
|
||||
Search,
|
||||
ChevronDown,
|
||||
Clock,
|
||||
Palette,
|
||||
Languages,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// --- CONSTANTS ---
|
||||
|
||||
export const languages = [
|
||||
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
|
||||
{ code: "en", label: "English", flag: "🇺🇸" },
|
||||
{ code: "es", label: "Español", flag: "🇪🇸" },
|
||||
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
||||
{ code: "fr", label: "Français", flag: "🇫🇷" },
|
||||
{ code: "ar", label: "العربية", flag: "🇸🇦" },
|
||||
{ code: "pt", label: "Português", flag: "🇧🇷" },
|
||||
{ code: "ja", label: "日本語", flag: "🇯🇵" },
|
||||
];
|
||||
|
||||
export const videoStyles = [
|
||||
// Film & Sinema
|
||||
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller", category: "Film & Sinema" },
|
||||
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton", category: "Film & Sinema" },
|
||||
{ id: "STORYTELLING", label: "Hikâye Anlatımı", emoji: "📖", desc: "Anlatı odaklı, sürükleyici", category: "Film & Sinema" },
|
||||
{ id: "NEWS", label: "Haber", emoji: "📰", desc: "Güncel ve bilgilendirici", category: "Film & Sinema" },
|
||||
{ id: "ARTISTIC", label: "Sanatsal", emoji: "🎨", desc: "Yaratıcı ve sıra dışı", category: "Film & Sinema" },
|
||||
{ id: "NOIR", label: "Film Noir", emoji: "🖤", desc: "Karanlık, dramatik", category: "Film & Sinema" },
|
||||
{ id: "VLOG", label: "Vlog", emoji: "📱", desc: "Günlük, samimi", category: "Film & Sinema" },
|
||||
// Animasyon
|
||||
{ id: "ANIME", label: "Anime", emoji: "⛩️", desc: "Japon animasyon stili", category: "Animasyon" },
|
||||
{ id: "ANIMATION_3D", label: "3D Animasyon", emoji: "🧊", desc: "Pixar kalitesi", category: "Animasyon" },
|
||||
{ id: "ANIMATION_2D", label: "2D Animasyon", emoji: "✏️", desc: "Klasik el çizimi", category: "Animasyon" },
|
||||
{ id: "STOP_MOTION", label: "Stop Motion", emoji: "🧸", desc: "Kare kare animasyon", category: "Animasyon" },
|
||||
{ id: "MOTION_COMIC", label: "Hareketli Çizgi Roman", emoji: "💥", desc: "Panel bazlı anlatım", category: "Animasyon" },
|
||||
{ id: "CARTOON", label: "Karikatür", emoji: "🎭", desc: "Çizgi film stili", category: "Animasyon" },
|
||||
{ id: "CLAYMATION", label: "Claymation", emoji: "🏺", desc: "Kil animasyon", category: "Animasyon" },
|
||||
{ id: "PIXEL_ART", label: "Pixel Art", emoji: "👾", desc: "8-bit retro oyun", category: "Animasyon" },
|
||||
{ id: "ISOMETRIC", label: "İzometrik", emoji: "🔷", desc: "İzometrik animasyon", category: "Animasyon" },
|
||||
// Eğitim & Bilgi
|
||||
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "🎓", desc: "Öğretici ve açıklayıcı", category: "Eğitim & Bilgi" },
|
||||
{ id: "INFOGRAPHIC", label: "İnfografik", emoji: "📊", desc: "Veri görselleştirme", category: "Eğitim & Bilgi" },
|
||||
{ id: "WHITEBOARD", label: "Whiteboard", emoji: "📝", desc: "Tahta animasyonu", category: "Eğitim & Bilgi" },
|
||||
{ id: "EXPLAINER", label: "Explainer", emoji: "💡", desc: "Ürün/konsept anlatımı", category: "Eğitim & Bilgi" },
|
||||
{ id: "DATA_VIZ", label: "Veri Görselleştirme", emoji: "📈", desc: "Grafikler ve tablolar", category: "Eğitim & Bilgi" },
|
||||
// Retro & Nostaljik
|
||||
{ id: "RETRO_80S", label: "Retro 80s", emoji: "🕹️", desc: "Synthwave estetik", category: "Retro & Nostaljik" },
|
||||
{ id: "VINTAGE_FILM", label: "Vintage Film", emoji: "📽️", desc: "Super 8 filmi", category: "Retro & Nostaljik" },
|
||||
{ id: "VHS", label: "VHS", emoji: "📼", desc: "Kaset estetik", category: "Retro & Nostaljik" },
|
||||
{ id: "POLAROID", label: "Polaroid", emoji: "📸", desc: "Analog fotoğraf", category: "Retro & Nostaljik" },
|
||||
{ id: "RETRO_90S", label: "Retro 90s Y2K", emoji: "💿", desc: "Y2K & internet", category: "Retro & Nostaljik" },
|
||||
// Sanat Akımları
|
||||
{ id: "WATERCOLOR", label: "Suluboya", emoji: "🎨", desc: "Suluboya resim", category: "Sanat Akımları" },
|
||||
{ id: "OIL_PAINTING", label: "Yağlı Boya", emoji: "🖌️", desc: "Klasik tuval", category: "Sanat Akımları" },
|
||||
{ id: "IMPRESSIONIST", label: "Empresyonist", emoji: "🌅", desc: "Monet tarzı", category: "Sanat Akımları" },
|
||||
{ id: "POP_ART", label: "Pop Art", emoji: "🎯", desc: "Warhol stili", category: "Sanat Akımları" },
|
||||
{ id: "UKIYO_E", label: "Ukiyo-e", emoji: "🏯", desc: "Japon gravür", category: "Sanat Akımları" },
|
||||
{ id: "ART_DECO", label: "Art Deco", emoji: "✨", desc: "1920s zarafet", category: "Sanat Akımları" },
|
||||
{ id: "SURREAL", label: "Sürrealist", emoji: "🌀", desc: "Dalí tarzı", category: "Sanat Akımları" },
|
||||
{ id: "COMIC_BOOK", label: "Çizgi Roman", emoji: "💬", desc: "Marvel/DC stili", category: "Sanat Akımları" },
|
||||
{ id: "SKETCH", label: "Karakalem", emoji: "✍️", desc: "Kalem çizim", category: "Sanat Akımları" },
|
||||
// Modern & Minimal
|
||||
{ id: "MINIMALIST", label: "Minimalist", emoji: "⚪", desc: "Apple estetiği", category: "Modern & Minimal" },
|
||||
{ id: "GLASSMORPHISM", label: "Glassmorphism", emoji: "🔮", desc: "Cam efekti", category: "Modern & Minimal" },
|
||||
{ id: "NEON", label: "Neon Glow", emoji: "💜", desc: "Neon ışıkları", category: "Modern & Minimal" },
|
||||
{ id: "CYBERPUNK", label: "Cyberpunk", emoji: "🤖", desc: "Gelecek distopya", category: "Modern & Minimal" },
|
||||
{ id: "STEAMPUNK", label: "Steampunk", emoji: "⚙️", desc: "Viktoryan mekanik", category: "Modern & Minimal" },
|
||||
{ id: "ABSTRACT", label: "Soyut", emoji: "🔵", desc: "Abstract sanat", category: "Modern & Minimal" },
|
||||
// Fotoğrafik
|
||||
{ id: "PRODUCT", label: "Ürün Fotoğrafı", emoji: "📦", desc: "Studio çekim", category: "Fotoğrafik" },
|
||||
{ id: "FASHION", label: "Moda", emoji: "👗", desc: "Editöryal çekim", category: "Fotoğrafik" },
|
||||
{ id: "AERIAL", label: "Havadan", emoji: "🚁", desc: "Drone görüntüsü", category: "Fotoğrafik" },
|
||||
{ id: "MACRO", label: "Makro", emoji: "🔬", desc: "Yakın çekim", category: "Fotoğrafik" },
|
||||
{ id: "PORTRAIT", label: "Portre", emoji: "🧑", desc: "Portre fotoğraf", category: "Fotoğrafik" },
|
||||
];
|
||||
|
||||
export const aspectRatios = [
|
||||
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
|
||||
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
|
||||
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
|
||||
];
|
||||
|
||||
// --- COMPONENTS ---
|
||||
|
||||
export function LanguageSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
<Languages size={14} className="inline mr-1.5 text-cyan-400" />
|
||||
Video Dili
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => onChange(lang.code)}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 py-3 px-2 rounded-xl text-xs transition-all",
|
||||
value === lang.code
|
||||
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-[var(--color-border-default)]"
|
||||
)}
|
||||
>
|
||||
<span className="text-lg">{lang.flag}</span>
|
||||
<span className="font-medium">{lang.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StyleSelector({
|
||||
value,
|
||||
onChange,
|
||||
cinematicReference,
|
||||
onCinematicReferenceChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
cinematicReference: string;
|
||||
onCinematicReferenceChange: (val: string) => void;
|
||||
}) {
|
||||
const [styleSearch, setStyleSearch] = useState("");
|
||||
const [expandedCategory, setExpandedCategory] = useState<string | null>("Film & Sinema");
|
||||
|
||||
const filteredStyles = useMemo(() => {
|
||||
return videoStyles.filter(
|
||||
(s) =>
|
||||
s.label.toLowerCase().includes(styleSearch.toLowerCase()) ||
|
||||
s.desc.toLowerCase().includes(styleSearch.toLowerCase())
|
||||
);
|
||||
}, [styleSearch]);
|
||||
|
||||
const groupedStyles = useMemo(() => {
|
||||
return filteredStyles.reduce((acc, curr) => {
|
||||
const cat = curr.category || "Diğer";
|
||||
if (!acc[cat]) acc[cat] = [];
|
||||
acc[cat].push(curr);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof videoStyles>);
|
||||
}, [filteredStyles]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3">
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] block">
|
||||
<Palette size={14} className="inline mr-1.5 text-violet-400" />
|
||||
Video Stili
|
||||
</label>
|
||||
<div className="relative w-full sm:w-56">
|
||||
<div className="absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none">
|
||||
<Search size={14} className="text-[var(--color-text-ghost)]" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Stil ara..."
|
||||
value={styleSearch}
|
||||
onChange={(e) => setStyleSearch(e.target.value)}
|
||||
className="w-full pl-8 pr-3 py-1.5 bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-md text-xs text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-[var(--color-border-default)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1 custom-scrollbar">
|
||||
{Object.entries(groupedStyles).map(([category, items]) => {
|
||||
const isExpanded = styleSearch ? true : expandedCategory === category;
|
||||
return (
|
||||
<div
|
||||
key={category}
|
||||
className="bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onClick={() =>
|
||||
!styleSearch && setExpandedCategory(isExpanded ? null : category)
|
||||
}
|
||||
className="w-full flex items-center justify-between p-3 bg-[var(--color-bg-elevated)] hover:bg-[var(--color-bg-surface-hover)] transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
{category}{" "}
|
||||
<span className="text-[11px] text-[var(--color-text-ghost)] ml-1">
|
||||
({items.length})
|
||||
</span>
|
||||
</span>
|
||||
{!styleSearch && (
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={cn(
|
||||
"text-[var(--color-text-ghost)] transition-transform duration-200",
|
||||
isExpanded && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="p-3 pt-0 mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{items.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => onChange(s.id)}
|
||||
className={cn(
|
||||
"flex flex-col items-start gap-1 p-2.5 rounded-xl text-left transition-all",
|
||||
value === s.id
|
||||
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
|
||||
: "bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]"
|
||||
)}
|
||||
>
|
||||
<span className="text-xl mb-0.5">{s.emoji}</span>
|
||||
<span className="text-xs font-semibold leading-tight">
|
||||
{s.label}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-[10px] leading-tight",
|
||||
value === s.id
|
||||
? "text-[var(--color-text-inverted)]/70"
|
||||
: "text-[var(--color-text-ghost)]"
|
||||
)}
|
||||
>
|
||||
{s.desc}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Object.keys(groupedStyles).length === 0 && (
|
||||
<div className="text-center py-8 text-[var(--color-text-ghost)] text-sm">
|
||||
"{styleSearch}" için sonuç bulunamadı.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{value === "CINEMATIC" && (
|
||||
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
|
||||
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||
Özel Sinematik Referans (Opsiyonel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
|
||||
value={cinematicReference}
|
||||
onChange={(e) => onCinematicReferenceChange(e.target.value)}
|
||||
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-md py-1.5 px-3 text-sm focus:border-[var(--color-border-default)] outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DurationSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (val: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
|
||||
Hedef Süre:{" "}
|
||||
<span className="text-[var(--color-text-primary)] font-bold">{value}s</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={15}
|
||||
max={180}
|
||||
step={5}
|
||||
value={value}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
className={cn(
|
||||
"w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer",
|
||||
"[&::-webkit-slider-thumb]:appearance-none",
|
||||
"[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5",
|
||||
"[&::-webkit-slider-thumb]:rounded-full",
|
||||
"[&::-webkit-slider-thumb]:bg-[var(--color-bg-inverted)]",
|
||||
"[&::-webkit-slider-thumb]:shadow-md",
|
||||
"[&::-webkit-slider-thumb]:cursor-grab"
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-[var(--color-text-ghost)] mt-1">
|
||||
<span>15s</span>
|
||||
<span>60s</span>
|
||||
<span>120s</span>
|
||||
<span>180s</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AspectRatioSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
||||
En-Boy Oranı
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{aspectRatios.map((ar) => {
|
||||
const Icon = ar.icon;
|
||||
return (
|
||||
<button
|
||||
key={ar.id}
|
||||
onClick={() => onChange(ar.id)}
|
||||
className={cn(
|
||||
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all",
|
||||
value === ar.id
|
||||
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
|
||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-[var(--color-border-default)]"
|
||||
)}
|
||||
>
|
||||
<Icon size={20} />
|
||||
<span className="font-semibold">{ar.label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-[10px]",
|
||||
value === ar.id
|
||||
? "text-[var(--color-text-inverted)]/70"
|
||||
: "text-[var(--color-text-ghost)]"
|
||||
)}
|
||||
>
|
||||
{ar.desc}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ChakraProvider } from "@chakra-ui/react";
|
||||
import { system } from "@/theme/theme";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import ReactQueryProvider from "@/provider/react-query-provider";
|
||||
@@ -7,6 +9,7 @@ import { ToastProvider } from "@/components/ui/toast";
|
||||
|
||||
export function Provider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ChakraProvider value={system}>
|
||||
<SessionProvider>
|
||||
<ReactQueryProvider>
|
||||
<ThemeProvider
|
||||
@@ -19,5 +22,6 @@ export function Provider({ children }: { children: React.ReactNode }) {
|
||||
</ThemeProvider>
|
||||
</ReactQueryProvider>
|
||||
</SessionProvider>
|
||||
</ChakraProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type CreateFromDocumentPayload,
|
||||
type ExtractDocumentTopicsPayload,
|
||||
type CreateFromExtractedTextPayload,
|
||||
type CreateFromTextPayload,
|
||||
type ExtractDocumentTopicsResponse,
|
||||
type Template,
|
||||
type PaginatedResponse,
|
||||
@@ -434,6 +435,18 @@ export function useCreateFromExtractedText() {
|
||||
});
|
||||
}
|
||||
|
||||
/** Serbest metin veya fikir üzerinden proje üret */
|
||||
export function useCreateFromText() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateFromTextPayload) => projectsApi.createFromText(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: queryKeys.projects.all });
|
||||
qc.invalidateQueries({ queryKey: queryKeys.dashboard.stats });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// NOTIFICATIONS — Bildirim hook'ları
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
@@ -271,6 +271,16 @@ export interface CreateFromDocumentPayload {
|
||||
targetDuration?: number;
|
||||
}
|
||||
|
||||
export interface CreateFromTextPayload {
|
||||
text: string;
|
||||
title?: string;
|
||||
language?: string;
|
||||
aspectRatio?: string;
|
||||
videoStyle?: string;
|
||||
cinematicReference?: string;
|
||||
targetDuration?: number;
|
||||
}
|
||||
|
||||
export interface ExtractDocumentTopicsPayload {
|
||||
file: File;
|
||||
}
|
||||
@@ -378,6 +388,9 @@ export const projectsApi = {
|
||||
}).then((r) => r.data);
|
||||
},
|
||||
|
||||
createFromText: (data: CreateFromTextPayload) =>
|
||||
apiClient.post<Project>('/projects/from-text', data).then((r) => r.data),
|
||||
|
||||
extractDocumentTopics: (data: ExtractDocumentTopicsPayload) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', data.file);
|
||||
|
||||
+17
-16
@@ -1,6 +1,7 @@
|
||||
import { createSystem, defaultConfig, SystemConfig } from '@chakra-ui/react';
|
||||
|
||||
const customConfig: SystemConfig = {
|
||||
preflight: false,
|
||||
theme: {
|
||||
breakpoints: {
|
||||
tablet: '768px',
|
||||
@@ -15,17 +16,17 @@ const customConfig: SystemConfig = {
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
50: { value: '#E6FFFA' },
|
||||
100: { value: '#B2F5EA' },
|
||||
200: { value: '#81E6D9' },
|
||||
300: { value: '#4FD1C5' },
|
||||
400: { value: '#38B2AC' },
|
||||
500: { value: '#319795' },
|
||||
600: { value: '#2C7A7B' },
|
||||
700: { value: '#285E61' },
|
||||
800: { value: '#234E52' },
|
||||
900: { value: '#1D4044' },
|
||||
950: { value: '#132E30' },
|
||||
50: { value: '#fafafa' },
|
||||
100: { value: '#f5f5f5' },
|
||||
200: { value: '#e5e5e5' },
|
||||
300: { value: '#d4d4d4' },
|
||||
400: { value: '#a3a3a3' },
|
||||
500: { value: '#737373' },
|
||||
600: { value: '#525252' },
|
||||
700: { value: '#404040' },
|
||||
800: { value: '#262626' },
|
||||
900: { value: '#171717' },
|
||||
950: { value: '#0a0a0a' },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -34,20 +35,20 @@ const customConfig: SystemConfig = {
|
||||
primary: {
|
||||
solid: {
|
||||
value: {
|
||||
_light: '{colors.primary.600}',
|
||||
_dark: '{colors.primary.600}',
|
||||
_light: '{colors.primary.900}',
|
||||
_dark: '{colors.primary.100}',
|
||||
},
|
||||
},
|
||||
contrast: {
|
||||
value: {
|
||||
_light: '{colors.white}',
|
||||
_dark: '{colors.white}',
|
||||
_dark: '{colors.black}',
|
||||
},
|
||||
},
|
||||
fg: {
|
||||
value: {
|
||||
_light: '{colors.primary.700}',
|
||||
_dark: '{colors.primary.300}',
|
||||
_light: '{colors.primary.900}',
|
||||
_dark: '{colors.primary.100}',
|
||||
},
|
||||
},
|
||||
muted: {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
|
||||
page.on('pageerror', error => console.log('PAGE ERROR:', error.message));
|
||||
page.on('requestfailed', request => console.log('REQUEST FAILED:', request.url(), request.failure().errorText));
|
||||
|
||||
await page.goto('http://localhost:3001/tr/dashboard/text-to-video');
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
await browser.close();
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user