generated from fahricansecer/boilerplate-fe
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user