main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-04-09 12:05:20 +03:00
parent 5b03bec882
commit 804f5b395e
4 changed files with 221 additions and 111 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <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
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -15,6 +15,7 @@ import {
Film,
Trash2,
MoreVertical,
X,
} from 'lucide-react';
import Link from 'next/link';
import { useState } from 'react';
@@ -244,7 +245,7 @@ export default function ProjectDetailPage() {
const statusInfo = STATUS_MAP[project.status] || STATUS_MAP.DRAFT;
const StatusIcon = statusInfo.icon;
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 isCompleted = project.status === 'COMPLETED';
const tweetData = project.sourceTweetData as Record<string, any> | undefined;
@@ -350,11 +351,12 @@ export default function ProjectDetailPage() {
<span className="flex items-center gap-1">
<Clock size={12} /> {project.targetDuration}s
</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"
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}>
@@ -362,26 +364,20 @@ export default function ProjectDetailPage() {
</option>
))}
</select>
) : (
<span>{currentStyle ? `${currentStyle.emoji} ${currentStyle.label}` : project.videoStyle}</span>
)}
{project.videoStyle === 'CINEMATIC' && isEditable ? (
{project.videoStyle === 'CINEMATIC' && (
<select
value={project.cinematicReference || ''}
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>
{CINEMATIC_REFERENCES.map(ref => (
<option key={ref.value} value={ref.value}>{ref.label}</option>
))}
</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="text-[10px]">
@@ -415,11 +411,11 @@ export default function ProjectDetailPage() {
{/* Aksiyon butonları */}
<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) */}
{isEditable && !hasScript && (
{!hasScript && (
<button
onClick={handleGenerateScript}
disabled={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={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"
>
{generateScriptMutation.isPending ? (
<Loader2 size={15} className="animate-spin" />
@@ -431,27 +427,27 @@ export default function ProjectDetailPage() {
)}
{/* Senaryo yeniden üret (draft/failed, senaryo var) */}
{isEditable && hasScript && (
{hasScript && (
<button
onClick={handleGenerateScript}
disabled={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"
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 disabled:cursor-not-allowed"
>
{generateScriptMutation.isPending ? (
<Loader2 size={15} className="animate-spin" />
) : (
<RefreshCw size={15} />
)}
Yeniden Üret
Senaryoyu Yeniden Üret
</button>
)}
{/* Onayla ve video üretimini başlat */}
{isEditable && hasScript && (
{hasScript && (
<button
onClick={handleApprove}
disabled={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={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"
>
{approveMutation.isPending ? (
<Loader2 size={15} className="animate-spin" />
@@ -463,14 +459,35 @@ export default function ProjectDetailPage() {
)}
</div>
{/* Hata mesajı */}
{/* Hata mesajı — kapatılabilir ve aksiyon butonlu */}
{project.errorMessage && (
<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">
<div className="flex items-center gap-2">
<AlertCircle size={14} className="text-red-400" />
<span className="text-xs font-medium text-red-400">Hata</span>
</div>
<p className="text-xs text-red-400/80">{project.errorMessage}</p>
<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>
<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>
)}
</motion.div>
@@ -512,6 +529,7 @@ export default function ProjectDetailPage() {
key={scene.id}
scene={scene}
isEditable={isEditable}
isRendering={isRendering}
onUpdate={handleSceneUpdate}
onRegenerate={handleSceneRegenerate}
onGenerateImage={handleGenerateImage}
@@ -1,7 +1,7 @@
"use client";
import { useState, useMemo } from "react";
import { motion } from "framer-motion";
import { useState, useMemo, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Plus,
Search,
@@ -13,6 +13,7 @@ import {
ExternalLink,
Loader2,
Trash2,
X,
} from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
@@ -83,17 +84,28 @@ interface ProjectItem {
export default function ProjectsPage() {
const [activeFilter, setActiveFilter] = useState("all");
const [searchQuery, setSearchQuery] = useState("");
const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null);
const { data, isLoading } = useProjects({ limit: 100 });
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.stopPropagation();
if (confirm("Bu projeyi silmek istediğinize emin misiniz?")) {
deleteMutation.mutate(id);
}
};
setDeleteTarget({ id: project.id, title: project.title });
}, []);
// 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[]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const raw = data as any;
@@ -220,10 +232,13 @@ export default function ProjectsPage() {
const st = statusMap[project.status] ?? statusMap.draft;
const StIcon = st.icon;
return (
<Link
<div
key={project.id}
className="flex items-center rounded-xl card hover:border-violet-500/20 transition-all group relative"
>
<Link
href={`/dashboard/projects/${project.id}`}
className="flex items-center gap-4 p-4 rounded-xl card hover:border-violet-500/20 transition-all group"
className="flex items-center gap-4 p-4 flex-1 min-w-0"
>
<div
className={`w-10 h-10 rounded-xl ${st.bgColor} flex items-center justify-center shrink-0 ${st.color}`}
@@ -257,26 +272,101 @@ export default function ProjectsPage() {
>
{st.label}
</span>
<button
onClick={(e) => handleDelete(e, project.id)}
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"
title="Projeyi Sil"
disabled={deleteMutation.isPending}
>
<Trash2 size={16} />
</button>
<ExternalLink
size={14}
className="text-[var(--color-text-ghost)] group-hover:text-violet-400 transition-colors shrink-0"
/>
</Link>
<button
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-3"
title="Projeyi Sil"
>
<Trash2 size={16} />
</button>
</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">
&ldquo;{deleteTarget.title}&rdquo;
</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>
);
}
+12 -10
View File
@@ -17,6 +17,7 @@ interface SceneCardProps {
mediaAssets?: Array<{ id: string; type: string; url?: string }>;
};
isEditable: boolean;
isRendering?: boolean;
onUpdate?: (sceneId: string, data: { narrationText?: string; visualPrompt?: string; subtitleText?: string }) => void;
onRegenerate?: (sceneId: string) => void;
onGenerateImage?: (sceneId: string, customPrompt?: string) => void;
@@ -29,6 +30,7 @@ interface SceneCardProps {
export function SceneCard({
scene,
isEditable,
isRendering = false,
onUpdate,
onRegenerate,
onGenerateImage,
@@ -91,19 +93,20 @@ export function SceneCard({
</div>
{/* Aksiyon butonları */}
{isEditable && !isEditing && (
{!isEditing && (
<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"
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"
>
<Pencil size={13} />
</button>
<button
onClick={() => onRegenerate?.(scene.id)}
disabled={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={!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"
title="AI ile yeniden üret"
>
<RefreshCw size={13} className={isRegenerating ? 'animate-spin' : ''} />
@@ -235,12 +238,12 @@ export function SceneCard({
</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">
<button
onClick={() => onGenerateImage?.(scene.id, scene.visualPrompt)}
disabled={isGeneratingImage || isUpscalingImage}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-500/15 text-emerald-400 text-xs font-medium hover:bg-emerald-500/25 transition-colors disabled:opacity-50"
disabled={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"}
@@ -248,15 +251,14 @@ export function SceneCard({
{thumbnailAsset?.url && (
<button
onClick={() => onUpscaleImage?.(scene.id)}
disabled={isUpscalingImage || isGeneratingImage}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-orange-500/15 text-orange-400 text-xs font-medium hover:bg-orange-500/25 transition-colors disabled:opacity-50"
disabled={isRendering || isUpscalingImage || isGeneratingImage}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-orange-500/15 text-orange-400 text-xs font-medium hover:bg-orange-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isUpscalingImage ? <RefreshCw size={13} className="animate-spin" /> : <Wand2 size={13} />}
Upscale (4K)
</button>
)}
</div>
)}
</div>
</motion.div>
)}