generated from fahricansecer/boilerplate-fe
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { motion } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Play,
|
Play,
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
X,
|
X,
|
||||||
|
Languages,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
@@ -34,8 +35,10 @@ import { useRenderProgress } from '@/hooks/use-render-progress';
|
|||||||
import { SceneCard } from '@/components/project/scene-card';
|
import { SceneCard } from '@/components/project/scene-card';
|
||||||
import { RenderProgress } from '@/components/project/render-progress';
|
import { RenderProgress } from '@/components/project/render-progress';
|
||||||
import { VideoPlayer } from '@/components/project/video-player';
|
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 { CINEMATIC_REFERENCES } from '@/constants/cinematic-references';
|
||||||
|
import { languages } from '@/components/projects/ProjectConfiguration';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
// X (Twitter) ikonunu burada da tanımlıyoruz
|
// X (Twitter) ikonunu burada da tanımlıyoruz
|
||||||
const XIcon = ({ size = 16 }: { size?: number }) => (
|
const XIcon = ({ size = 16 }: { size?: number }) => (
|
||||||
@@ -127,6 +130,27 @@ export default function ProjectDetailPage() {
|
|||||||
const [regeneratingSceneId, setRegeneratingSceneId] = useState<string | null>(null);
|
const [regeneratingSceneId, setRegeneratingSceneId] = useState<string | null>(null);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
const [showTranslateModal, setShowTranslateModal] = useState(false);
|
||||||
|
const [targetLanguage, setTargetLanguage] = useState<string>("");
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -328,6 +352,12 @@ export default function ProjectDetailPage() {
|
|||||||
>
|
>
|
||||||
<RefreshCw size={14} /> Yenile
|
<RefreshCw size={14} /> Yenile
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowMenu(false); setShowTranslateModal(true); }}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-elevated)] rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Languages size={14} /> Çevir
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { handleDelete(); setShowMenu(false); }}
|
onClick={() => { handleDelete(); setShowMenu(false); }}
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
|
||||||
@@ -672,6 +702,93 @@ export default function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ─── Çeviri Modal ─── */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showTranslateModal && (
|
||||||
|
<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={() => !isTranslating && setShowTranslateModal(false)}
|
||||||
|
>
|
||||||
|
<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={() => setShowTranslateModal(false)}
|
||||||
|
disabled={isTranslating}
|
||||||
|
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-blue-500/10 mb-4">
|
||||||
|
<Languages size={22} className="text-blue-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* İçerik */}
|
||||||
|
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-1.5">
|
||||||
|
Projeyi Çevir
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--color-text-muted)] mb-3">
|
||||||
|
"{project?.title}" projesini başka bir dile çevirin. (1 kredi)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={targetLanguage}
|
||||||
|
onChange={(e) => setTargetLanguage(e.target.value)}
|
||||||
|
disabled={isTranslating}
|
||||||
|
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-2.5 text-sm text-[var(--color-text-primary)] mb-5 outline-none focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Dil Seçin...</option>
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<option key={lang.code} value={lang.code}>
|
||||||
|
{lang.flag} {lang.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Butonlar */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowTranslateModal(false)}
|
||||||
|
disabled={isTranslating}
|
||||||
|
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={confirmTranslate}
|
||||||
|
disabled={isTranslating || !targetLanguage}
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isTranslating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 size={14} className="animate-spin" />
|
||||||
|
Çevriliyor...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Languages size={14} />
|
||||||
|
Çevir (1 🪙)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,16 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
|
Languages,
|
||||||
|
ChevronDown,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useProjects, useDeleteProject } from "@/hooks/use-api";
|
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 = [
|
const statusFilters = [
|
||||||
{ id: "all", label: "Tümü" },
|
{ id: "all", label: "Tümü" },
|
||||||
@@ -79,16 +85,46 @@ interface ProjectItem {
|
|||||||
language?: string;
|
language?: string;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
creditsUsed?: number;
|
creditsUsed?: number;
|
||||||
|
parentId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectsPage() {
|
export default function ProjectsPage() {
|
||||||
|
const router = useRouter();
|
||||||
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 [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null);
|
||||||
|
const [expandedProjects, setExpandedProjects] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const { data, isLoading } = useProjects({ limit: 100 });
|
const [translateTarget, setTranslateTarget] = useState<ProjectItem | null>(null);
|
||||||
|
const [targetLanguage, setTargetLanguage] = useState<string>("");
|
||||||
|
const [isTranslating, setIsTranslating] = useState(false);
|
||||||
|
|
||||||
|
const { data, isLoading, refetch } = useProjects({ limit: 100 });
|
||||||
const deleteMutation = useDeleteProject();
|
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)
|
// Silme onay modal'ını aç (native confirm yerine)
|
||||||
const openDeleteConfirm = useCallback((e: React.MouseEvent, project: ProjectItem) => {
|
const openDeleteConfirm = useCallback((e: React.MouseEvent, project: ProjectItem) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -125,6 +161,21 @@ export default function ProjectsPage() {
|
|||||||
});
|
});
|
||||||
}, [projects, activeFilter, searchQuery]);
|
}, [projects, activeFilter, searchQuery]);
|
||||||
|
|
||||||
|
const rootProjects = useMemo(() => {
|
||||||
|
return filtered.filter(p => !p.parentId);
|
||||||
|
}, [filtered]);
|
||||||
|
|
||||||
|
const childrenMap = useMemo(() => {
|
||||||
|
const map: Record<string, ProjectItem[]> = {};
|
||||||
|
filtered.forEach(p => {
|
||||||
|
if (p.parentId) {
|
||||||
|
if (!map[p.parentId]) map[p.parentId] = [];
|
||||||
|
map[p.parentId].push(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [filtered]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl mx-auto space-y-6">
|
<div className="max-w-5xl mx-auto space-y-6">
|
||||||
{/* Başlık */}
|
{/* Başlık */}
|
||||||
@@ -228,16 +279,24 @@ export default function ProjectsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filtered.map((project) => {
|
rootProjects.map((project) => {
|
||||||
const st = statusMap[project.status] ?? statusMap.draft;
|
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;
|
const StIcon = st.icon;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={project.id}
|
key={p.id}
|
||||||
className="flex items-center rounded-xl card hover:border-neutral-400 dark:hover:border-neutral-600 transition-all group relative"
|
className={cn(
|
||||||
|
"flex items-center rounded-xl card hover:border-neutral-400 dark:hover:border-neutral-600 transition-all group relative",
|
||||||
|
isChild && "bg-[var(--color-bg-surface)]"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`/dashboard/projects/${project.id}`}
|
href={`/dashboard/projects/${p.id}`}
|
||||||
className="flex items-center gap-4 p-4 flex-1 min-w-0"
|
className="flex items-center gap-4 p-4 flex-1 min-w-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -247,11 +306,11 @@ export default function ProjectsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-[var(--color-text-secondary)] 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.title}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]">
|
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]">
|
||||||
<span>
|
<span>
|
||||||
{new Date(project.createdAt).toLocaleDateString(
|
{new Date(p.createdAt).toLocaleDateString(
|
||||||
"tr-TR",
|
"tr-TR",
|
||||||
{
|
{
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
@@ -260,10 +319,10 @@ export default function ProjectsPage() {
|
|||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{project.language && <span>• {project.language}</span>}
|
{p.language && <span>• {p.language}</span>}
|
||||||
{typeof project.creditsUsed === "number" &&
|
{typeof p.creditsUsed === "number" &&
|
||||||
project.creditsUsed > 0 && (
|
p.creditsUsed > 0 && (
|
||||||
<span>• {project.creditsUsed} kredi</span>
|
<span>• {p.creditsUsed} kredi</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,13 +337,49 @@ export default function ProjectsPage() {
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => openDeleteConfirm(e, project)}
|
onClick={(e) => {
|
||||||
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"
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setTranslateTarget(p);
|
||||||
|
}}
|
||||||
|
className="p-2 rounded-lg text-[var(--color-text-ghost)] hover:text-blue-400 hover:bg-blue-500/10 transition-colors shrink-0 z-10"
|
||||||
|
title="Projeyi Çevir"
|
||||||
|
>
|
||||||
|
<Languages size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={(e) => openDeleteConfirm(e, p)}
|
||||||
|
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 mx-1"
|
||||||
title="Projeyi Sil"
|
title="Projeyi Sil"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{!isChild && hasChildren && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => toggleExpand(e, p.id)}
|
||||||
|
className="p-2 rounded-lg text-[var(--color-text-ghost)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-surface)] transition-colors shrink-0 z-10 mr-1"
|
||||||
|
title="Çevirileri Göster"
|
||||||
|
>
|
||||||
|
<ChevronDown size={16} className={cn("transition-transform", isExpanded && "rotate-180")} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={project.id} className="space-y-2">
|
||||||
|
{renderCard(project)}
|
||||||
|
{isExpanded && hasChildren && (
|
||||||
|
<div className="pl-6 md:pl-10 space-y-2 relative before:absolute before:left-[1.25rem] md:before:left-[2.25rem] before:top-0 before:bottom-0 before:w-px before:bg-neutral-200 dark:before:bg-neutral-800">
|
||||||
|
{children.map(child => renderCard(child, true))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -367,6 +462,93 @@ export default function ProjectsPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* ─── Çeviri Modal ─── */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{translateTarget && (
|
||||||
|
<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={() => !isTranslating && setTranslateTarget(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={() => setTranslateTarget(null)}
|
||||||
|
disabled={isTranslating}
|
||||||
|
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-blue-500/10 mb-4">
|
||||||
|
<Languages size={22} className="text-blue-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* İçerik */}
|
||||||
|
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-1.5">
|
||||||
|
Projeyi Çevir
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--color-text-muted)] mb-3">
|
||||||
|
"{translateTarget.title}" projesini başka bir dile çevirin. (1 kredi)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={targetLanguage}
|
||||||
|
onChange={(e) => setTargetLanguage(e.target.value)}
|
||||||
|
disabled={isTranslating}
|
||||||
|
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-2.5 text-sm text-[var(--color-text-primary)] mb-5"
|
||||||
|
>
|
||||||
|
<option value="">Dil Seçin...</option>
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<option key={lang.code} value={lang.code}>
|
||||||
|
{lang.flag} {lang.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Butonlar */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setTranslateTarget(null)}
|
||||||
|
disabled={isTranslating}
|
||||||
|
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={confirmTranslate}
|
||||||
|
disabled={isTranslating || !targetLanguage}
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isTranslating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 size={14} className="animate-spin" />
|
||||||
|
Çevriliyor...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Languages size={14} />
|
||||||
|
Çevir (1 🪙)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ export const languages = [
|
|||||||
{ code: "ar", label: "العربية", flag: "🇸🇦" },
|
{ code: "ar", label: "العربية", flag: "🇸🇦" },
|
||||||
{ code: "pt", label: "Português", flag: "🇧🇷" },
|
{ code: "pt", label: "Português", flag: "🇧🇷" },
|
||||||
{ code: "ja", label: "日本語", 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 = [
|
export const videoStyles = [
|
||||||
|
|||||||
Reference in New Issue
Block a user