generated from fahricansecer/boilerplate-fe
269 lines
9.8 KiB
TypeScript
269 lines
9.8 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { motion } from "framer-motion";
|
||
import {
|
||
FolderOpen,
|
||
PlayCircle,
|
||
CheckCircle2,
|
||
Coins,
|
||
Plus,
|
||
ArrowUpRight,
|
||
Clock,
|
||
Sparkles,
|
||
TrendingUp,
|
||
Loader2,
|
||
} from "lucide-react";
|
||
|
||
// X (Twitter) logosu — lucide-react'ta mevcut değil
|
||
const XIcon = ({ size = 20, className = "" }: { size?: number; className?: string }) => (
|
||
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||
</svg>
|
||
);
|
||
import Link from "next/link";
|
||
import { DashboardCharts } from "@/components/dashboard/dashboard-charts";
|
||
import { RecentProjects } from "@/components/dashboard/recent-projects";
|
||
import { TweetImportCard } from "@/components/dashboard/tweet-import-card";
|
||
import { YoutubeImportCard } from "@/components/dashboard/youtube-import-card";
|
||
import { useDashboardStats, useCreditBalance } from "@/hooks/use-api";
|
||
|
||
const stagger = {
|
||
hidden: { opacity: 0 },
|
||
show: {
|
||
opacity: 1,
|
||
transition: { staggerChildren: 0.08 },
|
||
},
|
||
};
|
||
|
||
const fadeUp = {
|
||
hidden: { opacity: 0, y: 16 },
|
||
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: [0.16, 1, 0.3, 1] as const } },
|
||
};
|
||
|
||
// Mock-mode: Backend yokken statik veri kullan
|
||
const isMockMode = process.env.NEXT_PUBLIC_ENABLE_MOCK_MODE === "true";
|
||
|
||
const MOCK_STATS = {
|
||
totalProjects: 12,
|
||
completedVideos: 8,
|
||
activeRenderJobs: 2,
|
||
totalCreditsUsed: 27,
|
||
creditsRemaining: 47,
|
||
};
|
||
|
||
function getStatCards(data?: typeof MOCK_STATS, creditBalance?: { balance: number; monthlyLimit: number }) {
|
||
const stats = data ?? MOCK_STATS;
|
||
const credits = creditBalance ?? { balance: stats.creditsRemaining, monthlyLimit: 50 };
|
||
|
||
return [
|
||
{
|
||
label: "Toplam Proje",
|
||
value: String(stats.totalProjects),
|
||
change: `${stats.completedVideos} tamamlandı`,
|
||
icon: FolderOpen,
|
||
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||
iconBg: "bg-neutral-500/12",
|
||
iconColor: "text-neutral-400",
|
||
},
|
||
{
|
||
label: "Devam Eden",
|
||
value: String(stats.activeRenderJobs),
|
||
change: "İşleniyor",
|
||
icon: PlayCircle,
|
||
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||
iconBg: "bg-neutral-500/12",
|
||
iconColor: "text-neutral-400",
|
||
},
|
||
{
|
||
label: "Tamamlanan",
|
||
value: String(stats.completedVideos),
|
||
change: "Bu ay",
|
||
icon: CheckCircle2,
|
||
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||
iconBg: "bg-neutral-500/12",
|
||
iconColor: "text-neutral-400",
|
||
},
|
||
{
|
||
label: "Kalan Kredi",
|
||
value: String(credits.balance),
|
||
change: `${credits.monthlyLimit} üzerinden`,
|
||
icon: Coins,
|
||
gradient: "from-neutral-500/12 to-neutral-600/5",
|
||
iconBg: "bg-neutral-500/12",
|
||
iconColor: "text-neutral-400",
|
||
},
|
||
];
|
||
}
|
||
|
||
export default function DashboardPage() {
|
||
const [mounted, setMounted] = useState(false);
|
||
|
||
useEffect(() => {
|
||
setMounted(true);
|
||
}, []);
|
||
|
||
// Real API hook'ları — mock modunda çağrılmaz
|
||
const statsQuery = useDashboardStats();
|
||
const creditQuery = useCreditBalance();
|
||
|
||
// Mock/Real veri birleştir
|
||
const isLoading = !isMockMode && (statsQuery.isLoading || creditQuery.isLoading);
|
||
const statsData = isMockMode ? MOCK_STATS : (statsQuery.data as typeof MOCK_STATS | undefined);
|
||
const creditData = isMockMode ? undefined : (creditQuery.data as { balance: number; monthlyLimit: number } | undefined);
|
||
|
||
const statCards = getStatCards(statsData, creditData);
|
||
|
||
if (!mounted) {
|
||
return (
|
||
<div className="flex items-center justify-center min-h-[60vh]">
|
||
<Loader2 size={32} className="animate-spin text-neutral-500" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<motion.div
|
||
variants={stagger}
|
||
initial="hidden"
|
||
animate="show"
|
||
className="space-y-6 max-w-7xl mx-auto"
|
||
>
|
||
{/* ── Başlık + CTA ── */}
|
||
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="font-[family-name:var(--font-display)] text-2xl md:text-3xl font-bold tracking-tight">
|
||
Hoş geldin 👋
|
||
</h1>
|
||
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
||
AI ile videolarını oluşturmaya devam et
|
||
</p>
|
||
</div>
|
||
<Link
|
||
href="/dashboard/create-project"
|
||
className="btn-primary flex items-center gap-2 text-sm"
|
||
>
|
||
<Plus size={16} />
|
||
<span className="hidden sm:inline">Yeni Proje</span>
|
||
</Link>
|
||
</motion.div>
|
||
|
||
{/* ── İstatistik Kartları ── */}
|
||
<motion.div variants={fadeUp} className="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-4">
|
||
{isLoading ? (
|
||
<div className="col-span-4 flex items-center justify-center py-12">
|
||
<Loader2 size={28} className="animate-spin text-neutral-400" />
|
||
</div>
|
||
) : (
|
||
statCards.map((stat) => {
|
||
const Icon = stat.icon;
|
||
return (
|
||
<div
|
||
key={stat.label}
|
||
className={`card-surface p-4 md:p-5 bg-gradient-to-br ${stat.gradient}`}
|
||
>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className={`w-9 h-9 rounded-xl ${stat.iconBg} flex items-center justify-center`}>
|
||
<Icon size={18} className={stat.iconColor} />
|
||
</div>
|
||
<ArrowUpRight size={14} className="text-[var(--color-text-ghost)]" />
|
||
</div>
|
||
<div className="font-[family-name:var(--font-display)] text-2xl md:text-3xl font-bold">
|
||
{stat.value}
|
||
</div>
|
||
<div className="flex items-center justify-between mt-1">
|
||
<span className="text-xs text-[var(--color-text-muted)]">{stat.label}</span>
|
||
<span className="text-[10px] text-[var(--color-text-ghost)]">{stat.change}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
})
|
||
)}
|
||
</motion.div>
|
||
|
||
{/* ── Hızlı Eylemler ── */}
|
||
<motion.div variants={fadeUp} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||
<Link
|
||
href="/dashboard/create-project"
|
||
className="group card-surface p-4 flex items-center gap-4 hover:border-neutral-500/30"
|
||
>
|
||
<div className="w-11 h-11 rounded-xl bg-[var(--color-bg-inverted)] flex items-center justify-center shrink-0 shadow-lg group-hover:shadow-neutral-500/20 transition-shadow">
|
||
<Sparkles size={20} className="text-[var(--color-text-inverted)]" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-sm font-semibold">AI ile Video Oluştur</h3>
|
||
<p className="text-xs text-[var(--color-text-muted)] mt-0.5">Bir konu gir, gerisini AI halletsin</p>
|
||
</div>
|
||
</Link>
|
||
|
||
<Link
|
||
href="/dashboard/templates"
|
||
className="group card-surface p-4 flex items-center gap-4 hover:border-neutral-500/30"
|
||
>
|
||
<div className="w-11 h-11 rounded-xl bg-[var(--color-bg-inverted)] flex items-center justify-center shrink-0 shadow-lg group-hover:shadow-neutral-500/20 transition-shadow">
|
||
<TrendingUp size={20} className="text-[var(--color-text-inverted)]" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-sm font-semibold">Şablon Keşfet</h3>
|
||
<p className="text-xs text-[var(--color-text-muted)] mt-0.5">Topluluk şablonlarını kullan</p>
|
||
</div>
|
||
</Link>
|
||
|
||
<Link
|
||
href="/dashboard/projects"
|
||
className="group card-surface p-4 flex items-center gap-4 hover:border-neutral-500/30"
|
||
>
|
||
<div className="w-11 h-11 rounded-xl bg-[var(--color-bg-inverted)] flex items-center justify-center shrink-0 shadow-lg group-hover:shadow-neutral-500/20 transition-shadow">
|
||
<Clock size={20} className="text-[var(--color-text-inverted)]" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-sm font-semibold">Devam Eden İşler</h3>
|
||
<p className="text-xs text-[var(--color-text-muted)] mt-0.5">İşlenen videolarını takip et</p>
|
||
</div>
|
||
</Link>
|
||
|
||
<Link
|
||
href="#tweet-import"
|
||
className="group card-surface p-4 flex items-center gap-4 hover:border-neutral-500/30"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
document.getElementById('tweet-import')?.scrollIntoView({ behavior: 'smooth' });
|
||
}}
|
||
>
|
||
<div className="w-11 h-11 rounded-xl bg-[var(--color-bg-inverted)] flex items-center justify-center shrink-0 shadow-lg group-hover:shadow-neutral-500/20 transition-shadow">
|
||
<XIcon size={20} className="text-[var(--color-text-inverted)]" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-sm font-semibold">Tweet → Video</h3>
|
||
<p className="text-xs text-[var(--color-text-muted)] mt-0.5">Viral tweet'leri videoya dönüştür</p>
|
||
</div>
|
||
</Link>
|
||
</motion.div>
|
||
|
||
{/* ── Tweet Import + Grafikler ── */}
|
||
<motion.div variants={fadeUp} className="grid grid-cols-1 lg:grid-cols-5 gap-4">
|
||
<div id="tweet-import" className="lg:col-span-2 flex flex-col gap-4">
|
||
<TweetImportCard
|
||
onProjectCreated={(id) => {
|
||
window.location.href = `/dashboard/projects/${id}`;
|
||
}}
|
||
/>
|
||
<YoutubeImportCard
|
||
onProjectCreated={(id) => {
|
||
window.location.href = `/dashboard/projects/${id}`;
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="lg:col-span-3">
|
||
<DashboardCharts />
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* ── Son Projeler ── */}
|
||
<motion.div variants={fadeUp}>
|
||
<RecentProjects />
|
||
</motion.div>
|
||
</motion.div>
|
||
);
|
||
}
|