From 1b980f637b399c4f5e9cc634c1346cb3ae415b2a Mon Sep 17 00:00:00 2001 From: Harun CAN Date: Thu, 30 Apr 2026 13:46:23 +0200 Subject: [PATCH] main --- .../dashboard/projects/[id]/page.tsx | 121 ++++- .../(dashboard)/dashboard/projects/page.tsx | 434 +++++++++++++----- .../projects/ProjectConfiguration.tsx | 12 + 3 files changed, 439 insertions(+), 128 deletions(-) diff --git a/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx b/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx index 476966e..0d56289 100644 --- a/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useParams, useRouter } from 'next/navigation'; -import { motion } from 'framer-motion'; +import { motion, AnimatePresence } from 'framer-motion'; import { ArrowLeft, Play, @@ -16,6 +16,7 @@ import { Trash2, MoreVertical, X, + Languages, } from 'lucide-react'; import Link from 'next/link'; import { useState, useEffect } from 'react'; @@ -34,8 +35,10 @@ import { useRenderProgress } from '@/hooks/use-render-progress'; import { SceneCard } from '@/components/project/scene-card'; import { RenderProgress } from '@/components/project/render-progress'; import { VideoPlayer } from '@/components/project/video-player'; -import { projectsApi } from '@/lib/api/api-service'; +import { projectsApi, apiClient } from '@/lib/api/api-service'; import { CINEMATIC_REFERENCES } from '@/constants/cinematic-references'; +import { languages } from '@/components/projects/ProjectConfiguration'; +import { toast } from 'sonner'; // X (Twitter) ikonunu burada da tanımlıyoruz const XIcon = ({ size = 16 }: { size?: number }) => ( @@ -126,6 +129,27 @@ export default function ProjectDetailPage() { const [showMenu, setShowMenu] = useState(false); const [regeneratingSceneId, setRegeneratingSceneId] = useState(null); const [mounted, setMounted] = useState(false); + + const [showTranslateModal, setShowTranslateModal] = useState(false); + const [targetLanguage, setTargetLanguage] = useState(""); + const [isTranslating, setIsTranslating] = useState(false); + + const confirmTranslate = async () => { + if (!id || !targetLanguage) return; + try { + setIsTranslating(true); + const res = await apiClient.post(`/projects/${id}/translate`, { targetLanguage }); + toast.success("Proje başarıyla çevrildi!"); + setShowTranslateModal(false); + setTargetLanguage(""); + // refetch() to maybe update some states, or router.push to the new project + router.push(`/dashboard/projects/${res.data.id}`); + } catch (err: any) { + toast.error(err?.response?.data?.message || "Çeviri sırasında bir hata oluştu."); + } finally { + setIsTranslating(false); + } + }; useEffect(() => { setMounted(true); @@ -328,6 +352,12 @@ export default function ProjectDetailPage() { > Yenile + + + {/* Icon */} +
+ +
+ + {/* İçerik */} +

+ Projeyi Çevir +

+

+ "{project?.title}" projesini başka bir dile çevirin. (1 kredi) +

+ + + + {/* Butonlar */} +
+ + +
+ + + )} + ); } diff --git a/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx b/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx index 49953c7..14f9019 100644 --- a/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx @@ -14,10 +14,16 @@ import { Loader2, Trash2, X, + Languages, + ChevronDown, } from "lucide-react"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import { cn } from "@/lib/utils"; import { useProjects, useDeleteProject } from "@/hooks/use-api"; +import { languages } from "@/components/projects/ProjectConfiguration"; +import { toast } from "sonner"; +import { apiClient } from "@/lib/api/api-service"; const statusFilters = [ { id: "all", label: "Tümü" }, @@ -79,16 +85,46 @@ interface ProjectItem { language?: string; progress?: number; creditsUsed?: number; + parentId?: string | null; } export default function ProjectsPage() { + const router = useRouter(); const [activeFilter, setActiveFilter] = useState("all"); const [searchQuery, setSearchQuery] = useState(""); const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null); + const [expandedProjects, setExpandedProjects] = useState>({}); - const { data, isLoading } = useProjects({ limit: 100 }); + const [translateTarget, setTranslateTarget] = useState(null); + const [targetLanguage, setTargetLanguage] = useState(""); + const [isTranslating, setIsTranslating] = useState(false); + + const { data, isLoading, refetch } = useProjects({ limit: 100 }); const deleteMutation = useDeleteProject(); + const confirmTranslate = async () => { + if (!translateTarget || !targetLanguage) return; + try { + setIsTranslating(true); + const res = await apiClient.post(`/projects/${translateTarget.id}/translate`, { targetLanguage }); + toast.success("Proje başarıyla çevrildi!"); + setTranslateTarget(null); + setTargetLanguage(""); + refetch(); + // Yönlendirmek istersen: router.push(`/dashboard/projects/${res.data.id}`); + } catch (err: any) { + toast.error(err?.response?.data?.message || "Çeviri sırasında bir hata oluştu."); + } finally { + setIsTranslating(false); + } + }; + + const toggleExpand = useCallback((e: React.MouseEvent, id: string) => { + e.preventDefault(); + e.stopPropagation(); + setExpandedProjects((prev) => ({ ...prev, [id]: !prev[id] })); + }, []); + // Silme onay modal'ını aç (native confirm yerine) const openDeleteConfirm = useCallback((e: React.MouseEvent, project: ProjectItem) => { e.preventDefault(); @@ -125,6 +161,21 @@ export default function ProjectsPage() { }); }, [projects, activeFilter, searchQuery]); + const rootProjects = useMemo(() => { + return filtered.filter(p => !p.parentId); + }, [filtered]); + + const childrenMap = useMemo(() => { + const map: Record = {}; + filtered.forEach(p => { + if (p.parentId) { + if (!map[p.parentId]) map[p.parentId] = []; + map[p.parentId].push(p); + } + }); + return map; + }, [filtered]); + return (
{/* Başlık */} @@ -228,145 +279,276 @@ export default function ProjectsPage() { )}
) : ( - filtered.map((project) => { - const st = statusMap[project.status] ?? statusMap.draft; - const StIcon = st.icon; - return ( -
- { + const children = childrenMap[project.id] || []; + const hasChildren = children.length > 0; + const isExpanded = expandedProjects[project.id]; + + const renderCard = (p: ProjectItem, isChild = false) => { + const st = statusMap[p.status] ?? statusMap.draft; + const StIcon = st.icon; + return ( +
-
- -
-
-

- {project.title} -

-
- - {new Date(project.createdAt).toLocaleDateString( - "tr-TR", - { - day: "numeric", - month: "short", - year: "numeric", - }, - )} - - {project.language && • {project.language}} - {typeof project.creditsUsed === "number" && - project.creditsUsed > 0 && ( - • {project.creditsUsed} kredi - )} +
+
-
- +

+ {p.title} +

+
+ + {new Date(p.createdAt).toLocaleDateString( + "tr-TR", + { + day: "numeric", + month: "short", + year: "numeric", + }, + )} + + {p.language && • {p.language}} + {typeof p.creditsUsed === "number" && + p.creditsUsed > 0 && ( + • {p.creditsUsed} kredi + )} +
+
+ + {st.label} + + + + +
+ + + + + + + {!isChild && hasChildren && ( + + )} +
); - }) - )} - - )} + }; - {/* ─── Silme Onay Modal ─── */} - - {deleteTarget && ( + return ( +
+ {renderCard(project)} + {isExpanded && hasChildren && ( +
+ {children.map(child => renderCard(child, true))} +
+ )} +
+ ); + }) + )} + + )} + + {/* ─── Silme Onay Modal ─── */} + + {deleteTarget && ( + !deleteMutation.isPending && setDeleteTarget(null)} + > !deleteMutation.isPending && setDeleteTarget(null)} + 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()} > - e.stopPropagation()} + {/* Kapatma butonu */} + + + {/* Icon */} +
+ +
+ + {/* İçerik */} +

+ Projeyi Sil +

+

+ Bu projeyi silmek istediğinize emin misiniz? +

+

+ “{deleteTarget.title}” +

+ + {/* Butonlar */} +
- - {/* Icon */} -
- -
- - {/* İçerik */} -

- Projeyi Sil -

-

- Bu projeyi silmek istediğinize emin misiniz? -

-

- “{deleteTarget.title}” -

- - {/* Butonlar */} -
- - -
- + +
- )} -
-
- ); + + )} + + + {/* ─── Çeviri Modal ─── */} + + {translateTarget && ( + !isTranslating && setTranslateTarget(null)} + > + e.stopPropagation()} + > + {/* Kapatma butonu */} + + + {/* Icon */} +
+ +
+ + {/* İçerik */} +

+ Projeyi Çevir +

+

+ "{translateTarget.title}" projesini başka bir dile çevirin. (1 kredi) +

+ + + + {/* Butonlar */} +
+ + +
+
+
+ )} +
+ +); } diff --git a/src/components/projects/ProjectConfiguration.tsx b/src/components/projects/ProjectConfiguration.tsx index e1e96e4..4e00211 100644 --- a/src/components/projects/ProjectConfiguration.tsx +++ b/src/components/projects/ProjectConfiguration.tsx @@ -25,6 +25,18 @@ export const languages = [ { code: "ar", label: "العربية", flag: "🇸🇦" }, { code: "pt", label: "Português", flag: "🇧🇷" }, { code: "ja", label: "日本語", flag: "🇯🇵" }, + { code: "hi", label: "हिन्दी", flag: "🇮🇳" }, + { code: "ru", label: "Русский", flag: "🇷🇺" }, + { code: "ko", label: "한국어", flag: "🇰🇷" }, + { code: "it", label: "Italiano", flag: "🇮🇹" }, + { code: "id", label: "Bahasa Indonesia", flag: "🇮🇩" }, + { code: "vi", label: "Tiếng Việt", flag: "🇻🇳" }, + { code: "zh", label: "简体中文", flag: "🇨🇳" }, + { code: "pl", label: "Polski", flag: "🇵🇱" }, + { code: "th", label: "ภาษาไทย", flag: "🇹🇭" }, + { code: "nl", label: "Nederlands", flag: "🇳🇱" }, + { code: "tl", label: "Tagalog", flag: "🇵🇭" }, + { code: "uk", label: "Українська", flag: "🇺🇦" }, ]; export const videoStyles = [