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

This commit is contained in:
Harun CAN
2026-03-30 00:22:06 +03:00
parent 45a540c530
commit 8bd995ea18
44 changed files with 3721 additions and 11852 deletions
@@ -10,99 +10,23 @@ import {
TrendingUp,
Clock,
Sparkles,
Filter,
Loader2,
FolderOpen,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { useTemplates, useCloneTemplate } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
interface Template {
id: string;
title: string;
description: string;
category: string;
language: string;
usageCount: number;
rating: number;
duration: number;
style: string;
featured: boolean;
}
const mockTemplates: Template[] = [
{
id: "t1",
title: "Evrenin Gizemli Boşlukları",
description: "Uzaydaki devasa boşlukları ve karanlık maddeyi keşfet",
category: "Bilim",
language: "tr",
usageCount: 342,
rating: 4.8,
duration: 45,
style: "CINEMATIC",
featured: true,
},
{
id: "t2",
title: "5 Mind-Blowing Physics Facts",
description: "Quantum mechanics to relativity in 60 seconds",
category: "Education",
language: "en",
usageCount: 1205,
rating: 4.9,
duration: 60,
style: "EDUCATIONAL",
featured: true,
},
{
id: "t3",
title: "Mitolojik Yaratıklar",
description: "Antik medeniyetlerin efsanevi canlıları",
category: "Tarih",
language: "tr",
usageCount: 189,
rating: 4.6,
duration: 50,
style: "STORYTELLING",
featured: false,
},
{
id: "t4",
title: "Secretos del Océano Profundo",
description: "Criaturas bioluminiscentes y volcanes submarinos",
category: "Ciencia",
language: "es",
usageCount: 567,
rating: 4.7,
duration: 55,
style: "DOCUMENTARY",
featured: false,
},
{
id: "t5",
title: "AI Tüm Meslekleri Yok Edecek mi?",
description: "Yapay zekanın iş dünyasına etkisi ve gelecek senaryoları",
category: "Teknoloji",
language: "tr",
usageCount: 891,
rating: 4.5,
duration: 60,
style: "NEWS",
featured: true,
},
{
id: "t6",
title: "Die Geheimnisse der Pyramiden",
description: "Ägyptische Pyramiden und ihre versteckten Kammern",
category: "Geschichte",
language: "de",
usageCount: 234,
rating: 4.4,
duration: 40,
style: "DOCUMENTARY",
featured: false,
},
const categories = [
"Tümü",
"Bilim",
"Teknoloji",
"Eğitim",
"Haber",
"Tarih",
"Sanat",
];
const categories = ["Tümü", "Bilim", "Education", "Tarih", "Teknoloji", "Ciencia", "Geschichte"];
const sortOptions = [
{ id: "popular", label: "En Popüler", icon: TrendingUp },
{ id: "newest", label: "En Yeni", icon: Clock },
@@ -115,6 +39,9 @@ const flagEmoji: Record<string, string> = {
es: "🇪🇸",
de: "🇩🇪",
fr: "🇫🇷",
ar: "🇸🇦",
pt: "🇧🇷",
ja: "🇯🇵",
};
const stagger = {
@@ -124,20 +51,80 @@ const stagger = {
const fadeUp = {
hidden: { opacity: 0, y: 16, scale: 0.97 },
show: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.5, ease: [0.16, 1, 0.3, 1] as const } },
show: {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 0.5, ease: [0.16, 1, 0.3, 1] as const },
},
};
interface TemplateItem {
id: string;
title: string;
description?: string;
category: string;
language: string;
usageCount: number;
rating: number;
duration?: number;
targetDuration?: number;
style?: string;
isFeatured?: boolean;
featured?: boolean;
thumbnailUrl?: string;
tags?: string[];
}
export default function TemplatesPage() {
const router = useRouter();
const toast = useToast();
const [search, setSearch] = useState("");
const [activeCategory, setActiveCategory] = useState("Tümü");
const [activeSort, setActiveSort] = useState("popular");
const filtered = mockTemplates.filter((t) => {
const { data, isLoading } = useTemplates({ limit: 50 });
const cloneTemplate = useCloneTemplate();
// API'den gelen veriyi çıkar
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawTemplates: TemplateItem[] =
(data as any)?.data?.items ?? (data as any)?.data ?? (data as any)?.items ?? data ?? [];
const templates = Array.isArray(rawTemplates) ? rawTemplates : [];
// Filtreleme
const filtered = templates.filter((t) => {
if (activeCategory !== "Tümü" && t.category !== activeCategory) return false;
if (search && !t.title.toLowerCase().includes(search.toLowerCase())) return false;
if (
search &&
!t.title.toLowerCase().includes(search.toLowerCase()) &&
!(t.description ?? "").toLowerCase().includes(search.toLowerCase())
)
return false;
return true;
});
// Sıralama
const sorted = [...filtered].sort((a, b) => {
if (activeSort === "popular") return (b.usageCount ?? 0) - (a.usageCount ?? 0);
if (activeSort === "rating") return (b.rating ?? 0) - (a.rating ?? 0);
return 0;
});
const handleClone = async (templateId: string) => {
try {
const result = await cloneTemplate.mutateAsync(templateId);
toast.success("Şablon başarıyla klonlandı!");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const projectId = (result as any)?.id;
if (projectId) {
router.push(`/dashboard/projects/${projectId}`);
}
} catch {
toast.error("Klonlama sırasında bir hata oluştu.");
}
};
return (
<div className="max-w-6xl mx-auto space-y-6">
{/* ── Başlık ── */}
@@ -154,7 +141,10 @@ export default function TemplatesPage() {
{/* ── Arama + Filtreler ── */}
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]" />
<Search
size={16}
className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]"
/>
<input
type="text"
value={search}
@@ -174,7 +164,7 @@ export default function TemplatesPage() {
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-medium transition-all",
activeSort === opt.id
? "bg-violet-500/12 text-violet-400 border border-violet-500/25"
: "text-[var(--color-text-muted)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]"
: "text-[var(--color-text-muted)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]",
)}
>
<Icon size={13} />
@@ -195,7 +185,7 @@ export default function TemplatesPage() {
"px-3.5 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-all",
activeCategory === cat
? "bg-violet-500/15 text-violet-400 border border-violet-500/25"
: "text-[var(--color-text-muted)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]"
: "text-[var(--color-text-muted)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]",
)}
>
{cat}
@@ -203,68 +193,123 @@ export default function TemplatesPage() {
))}
</div>
{/* ── Grid ── */}
<motion.div
variants={stagger}
initial="hidden"
animate="show"
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
>
{filtered.map((template) => (
<motion.div key={template.id} variants={fadeUp}>
<div className="group card-surface overflow-hidden hover:border-violet-500/20">
{/* Cover */}
<div className="relative h-36 bg-gradient-to-br from-[var(--color-bg-elevated)] to-[var(--color-bg-surface)] flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/5 to-cyan-500/5" />
<span className="text-4xl opacity-70">{flagEmoji[template.language] || "🌍"}</span>
{template.featured && (
<span className="absolute top-3 left-3 badge badge-violet text-[9px]">
Öne Çıkan
</span>
)}
<div className="absolute top-3 right-3 flex items-center gap-1 badge badge-amber text-[10px]">
<Star size={10} className="fill-amber-400" />
{template.rating}
</div>
{/* Hover overlay */}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3">
<button className="w-10 h-10 rounded-xl bg-white/10 backdrop-blur flex items-center justify-center text-white hover:bg-white/20 transition-colors">
<Eye size={18} />
</button>
<button className="w-10 h-10 rounded-xl bg-violet-500/80 backdrop-blur flex items-center justify-center text-white hover:bg-violet-500 transition-colors">
<Copy size={18} />
</button>
</div>
</div>
{/* Info */}
<div className="p-4">
<h3 className="text-sm font-semibold line-clamp-1 group-hover:text-violet-300 transition-colors">
{template.title}
</h3>
<p className="text-xs text-[var(--color-text-muted)] mt-1 line-clamp-2">
{template.description}
</p>
<div className="flex items-center justify-between mt-3">
<div className="flex items-center gap-2 text-[10px] text-[var(--color-text-ghost)]">
<span>{template.duration}s</span>
<span></span>
<span>{template.usageCount} kullanım</span>
</div>
<button className="flex items-center gap-1 text-[11px] font-medium text-violet-400 hover:text-violet-300 transition-colors">
<Copy size={12} />
Klonla
</button>
</div>
{/* ── Loading ── */}
{isLoading && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div
key={i}
className="card-surface overflow-hidden animate-pulse"
>
<div className="h-36 bg-[var(--color-bg-elevated)]" />
<div className="p-4 space-y-2">
<div className="h-4 bg-[var(--color-bg-elevated)] rounded w-3/4" />
<div className="h-3 bg-[var(--color-bg-elevated)] rounded w-1/2" />
</div>
</div>
</motion.div>
))}
</motion.div>
))}
</div>
)}
{filtered.length === 0 && (
{/* ── Grid ── */}
{!isLoading && (
<motion.div
variants={stagger}
initial="hidden"
animate="show"
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
>
{sorted.map((template) => (
<motion.div key={template.id} variants={fadeUp}>
<div className="group card-surface overflow-hidden hover:border-violet-500/20">
{/* Cover */}
<div className="relative h-36 bg-gradient-to-br from-[var(--color-bg-elevated)] to-[var(--color-bg-surface)] flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/5 to-cyan-500/5" />
{template.thumbnailUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={template.thumbnailUrl}
alt={template.title}
className="absolute inset-0 w-full h-full object-cover"
/>
) : (
<span className="text-4xl opacity-70">
{flagEmoji[template.language] || "🌍"}
</span>
)}
{(template.isFeatured || template.featured) && (
<span className="absolute top-3 left-3 badge badge-violet text-[9px]">
Öne Çıkan
</span>
)}
{template.rating > 0 && (
<div className="absolute top-3 right-3 flex items-center gap-1 badge badge-amber text-[10px]">
<Star size={10} className="fill-amber-400" />
{template.rating.toFixed(1)}
</div>
)}
{/* Hover overlay */}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3">
<button className="w-10 h-10 rounded-xl bg-white/10 backdrop-blur flex items-center justify-center text-white hover:bg-white/20 transition-colors">
<Eye size={18} />
</button>
<button
onClick={() => handleClone(template.id)}
disabled={cloneTemplate.isPending}
className="w-10 h-10 rounded-xl bg-violet-500/80 backdrop-blur flex items-center justify-center text-white hover:bg-violet-500 transition-colors"
>
{cloneTemplate.isPending ? (
<Loader2 size={18} className="animate-spin" />
) : (
<Copy size={18} />
)}
</button>
</div>
</div>
{/* Info */}
<div className="p-4">
<h3 className="text-sm font-semibold line-clamp-1 group-hover:text-violet-300 transition-colors">
{template.title}
</h3>
<p className="text-xs text-[var(--color-text-muted)] mt-1 line-clamp-2">
{template.description || "—"}
</p>
<div className="flex items-center justify-between mt-3">
<div className="flex items-center gap-2 text-[10px] text-[var(--color-text-ghost)]">
<span>
{template.duration ?? template.targetDuration ?? "—"}s
</span>
<span></span>
<span>{template.usageCount} kullanım</span>
</div>
<button
onClick={() => handleClone(template.id)}
disabled={cloneTemplate.isPending}
className="flex items-center gap-1 text-[11px] font-medium text-violet-400 hover:text-violet-300 transition-colors"
>
<Copy size={12} />
Klonla
</button>
</div>
</div>
</div>
</motion.div>
))}
</motion.div>
)}
{!isLoading && sorted.length === 0 && (
<div className="text-center py-16">
<p className="text-[var(--color-text-muted)]">Aramanızla eşleşen şablon bulunamadı</p>
<FolderOpen
size={40}
className="mx-auto text-[var(--color-text-ghost)] mb-3"
/>
<p className="text-[var(--color-text-muted)]">
{search || activeCategory !== "Tümü"
? "Aramanızla eşleşen şablon bulunamadı"
: "Henüz şablon eklenmemiş"}
</p>
</div>
)}
</div>