generated from fahricansecer/boilerplate-fe
This commit is contained in:
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Film,
|
Film,
|
||||||
Trash2,
|
Trash2,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -244,7 +245,7 @@ export default function ProjectDetailPage() {
|
|||||||
const statusInfo = STATUS_MAP[project.status] || STATUS_MAP.DRAFT;
|
const statusInfo = STATUS_MAP[project.status] || STATUS_MAP.DRAFT;
|
||||||
const StatusIcon = statusInfo.icon;
|
const StatusIcon = statusInfo.icon;
|
||||||
const isRendering = ['PENDING', 'GENERATING_MEDIA', 'RENDERING', 'GENERATING_SCRIPT'].includes(project.status);
|
const isRendering = ['PENDING', 'GENERATING_MEDIA', 'RENDERING', 'GENERATING_SCRIPT'].includes(project.status);
|
||||||
const isEditable = !isRendering;
|
const isEditable = !isRendering; // COMPLETED, DRAFT, FAILED → editable
|
||||||
const hasScript = project.scenes && project.scenes.length > 0;
|
const hasScript = project.scenes && project.scenes.length > 0;
|
||||||
const isCompleted = project.status === 'COMPLETED';
|
const isCompleted = project.status === 'COMPLETED';
|
||||||
const tweetData = project.sourceTweetData as Record<string, any> | undefined;
|
const tweetData = project.sourceTweetData as Record<string, any> | undefined;
|
||||||
@@ -350,38 +351,33 @@ export default function ProjectDetailPage() {
|
|||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock size={12} /> {project.targetDuration}s
|
<Clock size={12} /> {project.targetDuration}s
|
||||||
</span>
|
</span>
|
||||||
{isEditable ? (
|
|
||||||
<select
|
|
||||||
value={project.videoStyle}
|
|
||||||
onChange={(e) => handleStyleChange(e.target.value)}
|
|
||||||
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 cursor-pointer"
|
|
||||||
>
|
|
||||||
{videoStyles.map((s) => (
|
|
||||||
<option key={s.id} value={s.id}>
|
|
||||||
{s.emoji} {s.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
) : (
|
|
||||||
<span>{currentStyle ? `${currentStyle.emoji} ${currentStyle.label}` : project.videoStyle}</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{project.videoStyle === 'CINEMATIC' && isEditable ? (
|
<select
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{videoStyles.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.emoji} {s.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{project.videoStyle === 'CINEMATIC' && (
|
||||||
<select
|
<select
|
||||||
value={project.cinematicReference || ''}
|
value={project.cinematicReference || ''}
|
||||||
onChange={(e) => handleCinematicReferenceChange(e.target.value)}
|
onChange={(e) => handleCinematicReferenceChange(e.target.value)}
|
||||||
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 cursor-pointer w-44 truncate"
|
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"
|
||||||
>
|
>
|
||||||
<option value="">🎬 Sinematik Yönetmen/Film...</option>
|
<option value="">🎬 Sinematik Yönetmen/Film...</option>
|
||||||
{CINEMATIC_REFERENCES.map(ref => (
|
{CINEMATIC_REFERENCES.map(ref => (
|
||||||
<option key={ref.value} value={ref.value}>{ref.label}</option>
|
<option key={ref.value} value={ref.value}>{ref.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
) : project.videoStyle === 'CINEMATIC' && project.cinematicReference ? (
|
)}
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
🎬 <span className="truncate max-w-[150px]">{project.cinematicReference}</span>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<span className="uppercase text-[10px] tracking-wider">{project.language}</span>
|
<span className="uppercase text-[10px] tracking-wider">{project.language}</span>
|
||||||
<span className="text-[10px]">
|
<span className="text-[10px]">
|
||||||
@@ -415,11 +411,11 @@ export default function ProjectDetailPage() {
|
|||||||
{/* Aksiyon butonları */}
|
{/* Aksiyon butonları */}
|
||||||
<div className="flex flex-wrap items-center gap-2 mt-4 pt-4 border-t border-[var(--color-border-faint)]">
|
<div className="flex flex-wrap items-center gap-2 mt-4 pt-4 border-t border-[var(--color-border-faint)]">
|
||||||
{/* Senaryo üret (draft, senaryo yok) */}
|
{/* Senaryo üret (draft, senaryo yok) */}
|
||||||
{isEditable && !hasScript && (
|
{!hasScript && (
|
||||||
<button
|
<button
|
||||||
onClick={handleGenerateScript}
|
onClick={handleGenerateScript}
|
||||||
disabled={generateScriptMutation.isPending}
|
disabled={isRendering || generateScriptMutation.isPending}
|
||||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-violet-500 to-violet-600 text-white text-sm font-medium shadow-lg shadow-violet-500/20 hover:shadow-violet-500/30 transition-shadow disabled:opacity-50"
|
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"
|
||||||
>
|
>
|
||||||
{generateScriptMutation.isPending ? (
|
{generateScriptMutation.isPending ? (
|
||||||
<Loader2 size={15} className="animate-spin" />
|
<Loader2 size={15} className="animate-spin" />
|
||||||
@@ -431,27 +427,27 @@ export default function ProjectDetailPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Senaryo yeniden üret (draft/failed, senaryo var) */}
|
{/* Senaryo yeniden üret (draft/failed, senaryo var) */}
|
||||||
{isEditable && hasScript && (
|
{hasScript && (
|
||||||
<button
|
<button
|
||||||
onClick={handleGenerateScript}
|
onClick={handleGenerateScript}
|
||||||
disabled={generateScriptMutation.isPending}
|
disabled={isRendering || generateScriptMutation.isPending}
|
||||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-[var(--color-bg-elevated)] text-[var(--color-text-secondary)] text-sm font-medium hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50"
|
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-[var(--color-bg-elevated)] text-[var(--color-text-secondary)] text-sm font-medium hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{generateScriptMutation.isPending ? (
|
{generateScriptMutation.isPending ? (
|
||||||
<Loader2 size={15} className="animate-spin" />
|
<Loader2 size={15} className="animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<RefreshCw size={15} />
|
<RefreshCw size={15} />
|
||||||
)}
|
)}
|
||||||
Yeniden Üret
|
Senaryoyu Yeniden Üret
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Onayla ve video üretimini başlat */}
|
{/* Onayla ve video üretimini başlat */}
|
||||||
{isEditable && hasScript && (
|
{hasScript && (
|
||||||
<button
|
<button
|
||||||
onClick={handleApprove}
|
onClick={handleApprove}
|
||||||
disabled={approveMutation.isPending}
|
disabled={isRendering || approveMutation.isPending}
|
||||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-emerald-500 to-emerald-600 text-white text-sm font-medium shadow-lg shadow-emerald-500/20 hover:shadow-emerald-500/30 transition-shadow disabled:opacity-50"
|
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"
|
||||||
>
|
>
|
||||||
{approveMutation.isPending ? (
|
{approveMutation.isPending ? (
|
||||||
<Loader2 size={15} className="animate-spin" />
|
<Loader2 size={15} className="animate-spin" />
|
||||||
@@ -463,14 +459,35 @@ export default function ProjectDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hata mesajı */}
|
{/* Hata mesajı — kapatılabilir ve aksiyon butonlu */}
|
||||||
{project.errorMessage && (
|
{project.errorMessage && (
|
||||||
<div className="mt-3 p-3 rounded-lg bg-red-500/10 border border-red-500/20">
|
<div className="mt-3 p-3 rounded-lg bg-red-500/10 border border-red-500/20">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<AlertCircle size={14} className="text-red-400" />
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-medium text-red-400">Hata</span>
|
<AlertCircle size={14} className="text-red-400" />
|
||||||
|
<span className="text-xs font-medium text-red-400">Hata</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// Sayfayı yeniden yükleyerek güncel veriyi al
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
className="text-red-400/60 hover:text-red-400 transition-colors"
|
||||||
|
title="Kapat"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-red-400/80">{project.errorMessage}</p>
|
<p className="text-xs text-red-400/80 mb-2">{project.errorMessage}</p>
|
||||||
|
{hasScript && (
|
||||||
|
<button
|
||||||
|
onClick={handleGenerateScript}
|
||||||
|
disabled={generateScriptMutation.isPending}
|
||||||
|
className="text-xs px-3 py-1.5 rounded-md bg-red-500/20 text-red-300 hover:bg-red-500/30 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{generateScriptMutation.isPending ? 'Üretiliyor...' : '🔄 Senaryoyu Yeniden Üret'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -512,6 +529,7 @@ export default function ProjectDetailPage() {
|
|||||||
key={scene.id}
|
key={scene.id}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
|
isRendering={isRendering}
|
||||||
onUpdate={handleSceneUpdate}
|
onUpdate={handleSceneUpdate}
|
||||||
onRegenerate={handleSceneRegenerate}
|
onRegenerate={handleSceneRegenerate}
|
||||||
onGenerateImage={handleGenerateImage}
|
onGenerateImage={handleGenerateImage}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo, useCallback } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Loader2,
|
Loader2,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -83,17 +84,28 @@ interface ProjectItem {
|
|||||||
export default function ProjectsPage() {
|
export default function ProjectsPage() {
|
||||||
const [activeFilter, setActiveFilter] = useState("all");
|
const [activeFilter, setActiveFilter] = useState("all");
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null);
|
||||||
|
|
||||||
const { data, isLoading } = useProjects({ limit: 100 });
|
const { data, isLoading } = useProjects({ limit: 100 });
|
||||||
const deleteMutation = useDeleteProject();
|
const deleteMutation = useDeleteProject();
|
||||||
|
|
||||||
const handleDelete = (e: React.MouseEvent, id: string) => {
|
// Silme onay modal'ını aç (native confirm yerine)
|
||||||
|
const openDeleteConfirm = useCallback((e: React.MouseEvent, project: ProjectItem) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (confirm("Bu projeyi silmek istediğinize emin misiniz?")) {
|
setDeleteTarget({ id: project.id, title: project.title });
|
||||||
deleteMutation.mutate(id);
|
}, []);
|
||||||
}
|
|
||||||
};
|
// Silme işlemini gerçekleştir
|
||||||
|
const confirmDelete = useCallback(() => {
|
||||||
|
if (!deleteTarget) return;
|
||||||
|
deleteMutation.mutate(deleteTarget.id, {
|
||||||
|
onSettled: () => {
|
||||||
|
setDeleteTarget(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [deleteTarget, deleteMutation]);
|
||||||
|
|
||||||
// useProjects returns PaginatedResponse<Project> which has .data as Project[]
|
// useProjects returns PaginatedResponse<Project> which has .data as Project[]
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const raw = data as any;
|
const raw = data as any;
|
||||||
@@ -220,63 +232,141 @@ export default function ProjectsPage() {
|
|||||||
const st = statusMap[project.status] ?? statusMap.draft;
|
const st = statusMap[project.status] ?? statusMap.draft;
|
||||||
const StIcon = st.icon;
|
const StIcon = st.icon;
|
||||||
return (
|
return (
|
||||||
<Link
|
<div
|
||||||
key={project.id}
|
key={project.id}
|
||||||
href={`/dashboard/projects/${project.id}`}
|
className="flex items-center rounded-xl card hover:border-violet-500/20 transition-all group relative"
|
||||||
className="flex items-center gap-4 p-4 rounded-xl card hover:border-violet-500/20 transition-all group"
|
|
||||||
>
|
>
|
||||||
<div
|
<Link
|
||||||
className={`w-10 h-10 rounded-xl ${st.bgColor} flex items-center justify-center shrink-0 ${st.color}`}
|
href={`/dashboard/projects/${project.id}`}
|
||||||
|
className="flex items-center gap-4 p-4 flex-1 min-w-0"
|
||||||
>
|
>
|
||||||
<StIcon size={18} />
|
<div
|
||||||
</div>
|
className={`w-10 h-10 rounded-xl ${st.bgColor} flex items-center justify-center shrink-0 ${st.color}`}
|
||||||
<div className="flex-1 min-w-0">
|
>
|
||||||
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-violet-300 transition-colors">
|
<StIcon size={18} />
|
||||||
{project.title}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]">
|
|
||||||
<span>
|
|
||||||
{new Date(project.createdAt).toLocaleDateString(
|
|
||||||
"tr-TR",
|
|
||||||
{
|
|
||||||
day: "numeric",
|
|
||||||
month: "short",
|
|
||||||
year: "numeric",
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{project.language && <span>• {project.language}</span>}
|
|
||||||
{typeof project.creditsUsed === "number" &&
|
|
||||||
project.creditsUsed > 0 && (
|
|
||||||
<span>• {project.creditsUsed} kredi</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex-1 min-w-0">
|
||||||
<span
|
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-violet-300 transition-colors">
|
||||||
className={`text-[10px] font-medium px-2.5 py-1 rounded-full border ${st.color} border-current/20 ${st.bgColor} shrink-0 mr-2`}
|
{project.title}
|
||||||
>
|
</p>
|
||||||
{st.label}
|
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]">
|
||||||
</span>
|
<span>
|
||||||
|
{new Date(project.createdAt).toLocaleDateString(
|
||||||
|
"tr-TR",
|
||||||
|
{
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{project.language && <span>• {project.language}</span>}
|
||||||
|
{typeof project.creditsUsed === "number" &&
|
||||||
|
project.creditsUsed > 0 && (
|
||||||
|
<span>• {project.creditsUsed} kredi</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-[10px] font-medium px-2.5 py-1 rounded-full border ${st.color} border-current/20 ${st.bgColor} shrink-0 mr-2`}
|
||||||
|
>
|
||||||
|
{st.label}
|
||||||
|
</span>
|
||||||
|
<ExternalLink
|
||||||
|
size={14}
|
||||||
|
className="text-[var(--color-text-ghost)] group-hover:text-violet-400 transition-colors shrink-0"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={(e) => handleDelete(e, project.id)}
|
onClick={(e) => openDeleteConfirm(e, project)}
|
||||||
className="p-2 rounded-lg text-[var(--color-text-ghost)] hover:text-red-400 hover:bg-red-500/10 transition-colors shrink-0 z-10 mr-1"
|
className="p-2 rounded-lg text-[var(--color-text-ghost)] hover:text-red-400 hover:bg-red-500/10 transition-colors shrink-0 z-10 mr-3"
|
||||||
title="Projeyi Sil"
|
title="Projeyi Sil"
|
||||||
disabled={deleteMutation.isPending}
|
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
<ExternalLink
|
|
||||||
size={14}
|
|
||||||
className="text-[var(--color-text-ghost)] group-hover:text-violet-400 transition-colors shrink-0"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ─── Silme Onay Modal ─── */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{deleteTarget && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||||
|
onClick={() => !deleteMutation.isPending && setDeleteTarget(null)}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||||
|
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
className="relative bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] rounded-2xl p-6 max-w-sm w-full mx-4 shadow-2xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Kapatma butonu */}
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteTarget(null)}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
className="absolute top-3 right-3 p-1 rounded-lg text-[var(--color-text-ghost)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-red-500/10 mb-4">
|
||||||
|
<Trash2 size={22} className="text-red-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* İçerik */}
|
||||||
|
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-1.5">
|
||||||
|
Projeyi Sil
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--color-text-muted)] mb-1">
|
||||||
|
Bu projeyi silmek istediğinize emin misiniz?
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--color-text-ghost)] mb-5 line-clamp-2 italic">
|
||||||
|
“{deleteTarget.title}”
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Butonlar */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteTarget(null)}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
className="flex-1 px-4 py-2.5 rounded-xl border border-[var(--color-border-default)] text-sm font-medium text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
İptal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={confirmDelete}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-red-500 hover:bg-red-600 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{deleteMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 size={14} className="animate-spin" />
|
||||||
|
Siliniyor...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
Evet, Sil
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface SceneCardProps {
|
|||||||
mediaAssets?: Array<{ id: string; type: string; url?: string }>;
|
mediaAssets?: Array<{ id: string; type: string; url?: string }>;
|
||||||
};
|
};
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
|
isRendering?: boolean;
|
||||||
onUpdate?: (sceneId: string, data: { narrationText?: string; visualPrompt?: string; subtitleText?: string }) => void;
|
onUpdate?: (sceneId: string, data: { narrationText?: string; visualPrompt?: string; subtitleText?: string }) => void;
|
||||||
onRegenerate?: (sceneId: string) => void;
|
onRegenerate?: (sceneId: string) => void;
|
||||||
onGenerateImage?: (sceneId: string, customPrompt?: string) => void;
|
onGenerateImage?: (sceneId: string, customPrompt?: string) => void;
|
||||||
@@ -29,6 +30,7 @@ interface SceneCardProps {
|
|||||||
export function SceneCard({
|
export function SceneCard({
|
||||||
scene,
|
scene,
|
||||||
isEditable,
|
isEditable,
|
||||||
|
isRendering = false,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
onGenerateImage,
|
onGenerateImage,
|
||||||
@@ -91,19 +93,20 @@ export function SceneCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Aksiyon butonları */}
|
{/* Aksiyon butonları */}
|
||||||
{isEditable && !isEditing && (
|
{!isEditing && (
|
||||||
<div className="flex items-center gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
|
<div className="flex items-center gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsEditing(true)}
|
onClick={() => setIsEditing(true)}
|
||||||
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-violet-400 hover:bg-violet-500/10 transition-colors"
|
disabled={!isEditable || isRendering}
|
||||||
|
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 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
title="Düzenle"
|
title="Düzenle"
|
||||||
>
|
>
|
||||||
<Pencil size={13} />
|
<Pencil size={13} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onRegenerate?.(scene.id)}
|
onClick={() => onRegenerate?.(scene.id)}
|
||||||
disabled={isRegenerating}
|
disabled={!isEditable || isRendering || isRegenerating}
|
||||||
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-cyan-400 hover:bg-cyan-500/10 transition-colors disabled:opacity-40"
|
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"
|
||||||
title="AI ile yeniden üret"
|
title="AI ile yeniden üret"
|
||||||
>
|
>
|
||||||
<RefreshCw size={13} className={isRegenerating ? 'animate-spin' : ''} />
|
<RefreshCw size={13} className={isRegenerating ? 'animate-spin' : ''} />
|
||||||
@@ -235,28 +238,27 @@ export function SceneCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isEditable && (
|
{/* Görsel üretim butonları — tüm projelerde her zaman göster, render sürecinde disable et */}
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<button
|
||||||
|
onClick={() => onGenerateImage?.(scene.id, scene.visualPrompt)}
|
||||||
|
disabled={isRendering || isGeneratingImage || isUpscalingImage}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-500/15 text-emerald-400 text-xs font-medium hover:bg-emerald-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isGeneratingImage ? <RefreshCw size={13} className="animate-spin" /> : <ImageIcon size={13} />}
|
||||||
|
{thumbnailAsset ? "Görseli Yeniden Üret" : "Görsel Üret"}
|
||||||
|
</button>
|
||||||
|
{thumbnailAsset?.url && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onGenerateImage?.(scene.id, scene.visualPrompt)}
|
onClick={() => onUpscaleImage?.(scene.id)}
|
||||||
disabled={isGeneratingImage || isUpscalingImage}
|
disabled={isRendering || isUpscalingImage || isGeneratingImage}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-500/15 text-emerald-400 text-xs font-medium hover:bg-emerald-500/25 transition-colors disabled:opacity-50"
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-orange-500/15 text-orange-400 text-xs font-medium hover:bg-orange-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{isGeneratingImage ? <RefreshCw size={13} className="animate-spin" /> : <ImageIcon size={13} />}
|
{isUpscalingImage ? <RefreshCw size={13} className="animate-spin" /> : <Wand2 size={13} />}
|
||||||
{thumbnailAsset ? "Görseli Yeniden Üret" : "Görsel Üret"}
|
Upscale (4K)
|
||||||
</button>
|
</button>
|
||||||
{thumbnailAsset?.url && (
|
)}
|
||||||
<button
|
</div>
|
||||||
onClick={() => onUpscaleImage?.(scene.id)}
|
|
||||||
disabled={isUpscalingImage || isGeneratingImage}
|
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-orange-500/15 text-orange-400 text-xs font-medium hover:bg-orange-500/25 transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isUpscalingImage ? <RefreshCw size={13} className="animate-spin" /> : <Wand2 size={13} />}
|
|
||||||
Upscale (4K)
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user