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

This commit is contained in:
Harun CAN
2026-04-30 13:25:43 +02:00
parent 1b69eaf219
commit 5144ee4d9a
22 changed files with 989 additions and 1411 deletions
+27 -26
View File
@@ -14,7 +14,7 @@ import {
} from "recharts";
import { useDashboardStats } from "@/hooks/use-api";
const COLORS = ["#8b5cf6", "#06b6d4", "#f59e0b", "#ef4444", "#10b981"];
const COLORS = ["#ffffff", "#a3a3a3", "#525252", "#262626"];
function formatWeekData(stats: Record<string, unknown> | undefined) {
if (!stats) {
@@ -78,7 +78,7 @@ export function DashboardCharts() {
{[1, 2].map((i) => (
<div
key={i}
className="card p-5 h-[280px] animate-pulse bg-[var(--color-bg-surface)]"
className="card p-6 md:p-8 h-[300px] animate-pulse bg-[var(--color-bg-surface)] rounded-2xl border border-[var(--color-border-faint)]"
/>
))}
</div>
@@ -88,42 +88,42 @@ export function DashboardCharts() {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
{/* Haftalik Aktivite */}
<div className="card p-5">
<h3 className="text-sm font-semibold text-[var(--color-text-secondary)] mb-4">
<div className="card-surface p-6 md:p-8">
<h3 className="text-base font-semibold text-[var(--color-text-secondary)] mb-6">
Haftalık Aktivite
</h3>
<ResponsiveContainer width="100%" height={200}>
<ResponsiveContainer width="100%" height={220}>
<AreaChart data={weekData}>
<defs>
<linearGradient id="colorProjects" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#8b5cf6" stopOpacity={0} />
<stop offset="5%" stopColor="#ffffff" stopOpacity={0.2} />
<stop offset="95%" stopColor="#ffffff" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorVideos" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#06b6d4" stopOpacity={0.3} />
<stop offset="95%" stopColor="#06b6d4" stopOpacity={0} />
<stop offset="5%" stopColor="#737373" stopOpacity={0.2} />
<stop offset="95%" stopColor="#737373" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="name"
tick={{ fontSize: 11, fill: "var(--color-text-ghost)" }}
tick={{ fontSize: 12, fill: "var(--color-text-ghost)" }}
axisLine={false}
tickLine={false}
/>
<YAxis hide />
<Tooltip
contentStyle={{
backgroundColor: "rgba(15,15,30,0.9)",
border: "1px solid rgba(139,92,246,0.2)",
backgroundColor: "rgba(10,10,10,0.95)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 12,
fontSize: 12,
fontSize: 13,
color: "#fff",
}}
/>
<Area
type="monotone"
dataKey="projects"
stroke="#8b5cf6"
stroke="#ffffff"
strokeWidth={2}
fill="url(#colorProjects)"
name="Projeler"
@@ -131,7 +131,7 @@ export function DashboardCharts() {
<Area
type="monotone"
dataKey="videos"
stroke="#06b6d4"
stroke="#737373"
strokeWidth={2}
fill="url(#colorVideos)"
name="Videolar"
@@ -141,26 +141,27 @@ export function DashboardCharts() {
</div>
{/* Proje Durumu */}
<div className="card p-5">
<h3 className="text-sm font-semibold text-[var(--color-text-secondary)] mb-4">
<div className="card-surface p-6 md:p-8">
<h3 className="text-base font-semibold text-[var(--color-text-secondary)] mb-6">
Proje Durumu
</h3>
{pieData.length === 0 ? (
<div className="flex items-center justify-center h-[200px] text-sm text-[var(--color-text-ghost)]">
<div className="flex items-center justify-center h-[220px] text-sm text-[var(--color-text-ghost)]">
Henüz proje verisi yok
</div>
) : (
<div className="flex items-center gap-4">
<ResponsiveContainer width="50%" height={200}>
<div className="flex items-center gap-6">
<ResponsiveContainer width="50%" height={220}>
<PieChart>
<Pie
data={pieData}
cx="50%"
cy="50%"
outerRadius={70}
innerRadius={40}
outerRadius={80}
innerRadius={50}
dataKey="value"
stroke="none"
stroke="var(--color-bg-surface)"
strokeWidth={2}
>
{pieData.map((_: unknown, index: number) => (
<Cell key={index} fill={COLORS[index % COLORS.length]} />
@@ -168,10 +169,10 @@ export function DashboardCharts() {
</Pie>
<Tooltip
contentStyle={{
backgroundColor: "rgba(15,15,30,0.9)",
border: "1px solid rgba(139,92,246,0.2)",
backgroundColor: "rgba(10,10,10,0.95)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 12,
fontSize: 12,
fontSize: 13,
color: "#fff",
}}
/>
+3 -3
View File
@@ -44,7 +44,7 @@ export function RecentProjects() {
</h3>
<Link
href="/dashboard/projects"
className="text-xs text-violet-400 hover:text-violet-300 flex items-center gap-1 transition-colors"
className="text-xs text-neutral-400 hover:text-neutral-300 flex items-center gap-1 transition-colors"
>
Tümü <ExternalLink size={12} />
</Link>
@@ -61,7 +61,7 @@ export function RecentProjects() {
</p>
<Link
href="/dashboard/projects/new"
className="mt-3 text-xs text-violet-400 hover:text-violet-300"
className="mt-3 text-xs text-neutral-400 hover:text-neutral-300"
>
İlk projenizi oluşturun
</Link>
@@ -89,7 +89,7 @@ export function RecentProjects() {
<StIcon size={14} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-violet-300 transition-colors">
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-neutral-300 transition-colors">
{project.title}
</p>
<p className="text-[10px] text-[var(--color-text-ghost)]">
+12 -12
View File
@@ -100,8 +100,8 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
<div className="card-surface overflow-hidden">
{/* Header */}
<div className="flex items-center gap-3 p-4 md:p-5 border-b border-[var(--color-border-faint)]">
<div className="w-9 h-9 rounded-xl bg-sky-500/15 flex items-center justify-center">
<XIcon size={18} className="text-sky-400" />
<div className="w-9 h-9 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] flex items-center justify-center">
<XIcon size={18} className="text-neutral-300" />
</div>
<div>
<h3 className="font-[family-name:var(--font-display)] text-sm font-semibold">
@@ -128,7 +128,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
}}
onKeyDown={handleKeyDown}
placeholder="https://x.com/user/status/123..."
className="w-full h-11 pl-10 pr-24 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:border-sky-500/50 focus:ring-1 focus:ring-sky-500/25 outline-none transition-all"
className="w-full h-11 pl-10 pr-24 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:border-neutral-500/50 focus:ring-1 focus:ring-neutral-500/25 outline-none transition-all"
/>
<button
onClick={preview ? handleCreate : handlePreview}
@@ -136,7 +136,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
className={cn(
"absolute right-1.5 top-1/2 -translate-y-1/2 h-8 px-3 rounded-lg text-xs font-medium transition-all flex items-center gap-1.5",
isUrlValid && !isLoadingPreview && !isCreatingProject
? "bg-sky-500 text-white hover:bg-sky-400 shadow-sm"
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] hover:bg-neutral-800 shadow-sm"
: "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] cursor-not-allowed"
)}
>
@@ -200,7 +200,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
className="w-full h-full object-cover"
/>
) : (
<XIcon size={16} className="text-sky-400" />
<XIcon size={16} className="text-neutral-400" />
)}
</div>
<div>
@@ -211,7 +211,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
{preview.tweet.author.verified && (
<CheckCircle2
size={13}
className="text-sky-400 fill-sky-400"
className="text-neutral-400 fill-neutral-400"
/>
)}
</div>
@@ -304,18 +304,18 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
{/* Info badges */}
<div className="flex flex-wrap gap-1.5">
<span className="badge badge-cyan text-[10px]">
<span className="bg-neutral-500/10 text-neutral-400 border border-neutral-500/20 px-2 py-0.5 rounded-md text-[10px]">
{preview.contentType === "thread"
? "Thread"
: preview.contentType === "quote_tweet"
? "Alıntı"
: "Tweet"}
</span>
<span className="badge badge-violet text-[10px]">
<span className="bg-neutral-500/10 text-neutral-400 border border-neutral-500/20 px-2 py-0.5 rounded-md text-[10px]">
~{preview.estimatedDuration}s video
</span>
{preview.tweet.media.length > 0 && (
<span className="badge badge-amber text-[10px]">
<span className="bg-neutral-500/10 text-neutral-400 border border-neutral-500/20 px-2 py-0.5 rounded-md text-[10px]">
{preview.tweet.media.length} medya
</span>
)}
@@ -327,7 +327,7 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
value={customTitle}
onChange={(e) => setCustomTitle(e.target.value)}
placeholder={preview.suggestedTitle}
className="w-full h-10 px-3 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)]/50 focus:border-violet-500/50 focus:ring-1 focus:ring-violet-500/25 outline-none transition-all"
className="w-full h-10 px-3 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)]/50 focus:border-neutral-500/50 focus:ring-1 focus:ring-neutral-500/25 outline-none transition-all"
/>
{/* Create Button */}
@@ -337,8 +337,8 @@ export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
className={cn(
"w-full h-11 rounded-xl text-sm font-semibold shadow-sm transition-all flex items-center justify-center gap-2",
isCreatingProject
? "bg-violet-600/50 text-violet-200 cursor-not-allowed"
: "bg-gradient-to-r from-violet-600 to-purple-600 text-white hover:from-violet-500 hover:to-purple-500 hover:shadow-lg hover:shadow-violet-500/25 active:scale-[0.98]"
? "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] cursor-not-allowed"
: "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] hover:bg-neutral-800 active:scale-[0.98]"
)}
>
{isCreatingProject ? (
+38 -38
View File
@@ -41,23 +41,23 @@ export function MobileNav() {
key={item.href}
href={item.href}
className={cn(
"relative flex flex-col items-center gap-0.5 py-1.5 px-3 rounded-xl min-w-[4rem] transition-colors",
"relative flex flex-col items-center gap-1 py-2 px-3 rounded-xl min-w-[4.5rem] transition-colors",
isActive
? "text-violet-400"
: "text-[var(--color-text-muted)] active:text-[var(--color-text-secondary)]"
? "text-white"
: "text-[var(--color-text-muted)] active:text-[var(--color-text-primary)]"
)}
>
<div className="relative">
<Icon size={22} strokeWidth={isActive ? 2.2 : 1.8} />
<Icon size={24} strokeWidth={isActive ? 2.5 : 1.8} />
{isActive && (
<motion.div
layoutId="nav-indicator"
className="absolute -inset-2 rounded-xl bg-violet-500/10"
className="absolute -inset-2 rounded-xl bg-white/10"
transition={{ type: "spring", bounce: 0.2, duration: 0.5 }}
/>
)}
</div>
<span className="text-[10px] font-medium tracking-wide">
<span className="text-xs font-semibold tracking-wide mt-1">
{item.label}
</span>
</Link>
@@ -80,12 +80,12 @@ function CreditCard() {
const pct = isAdmin ? 100 : (total > 0 ? Math.round((remaining / total) * 100) : 0);
return (
<div className="mx-3 mb-4 p-4 rounded-xl bg-gradient-to-br from-violet-500/8 to-cyan-400/5 border border-[var(--color-border-faint)]">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-[var(--color-text-muted)]">Kalan Kredi</span>
<span className="badge badge-violet">{planName}</span>
<div className="mx-4 mb-6 p-5 rounded-2xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] shadow-sm">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-[var(--color-text-muted)]">Kalan Kredi</span>
<span className="badge bg-white/10 text-white border border-white/20">{planName}</span>
</div>
<div className="text-2xl font-bold font-[family-name:var(--font-display)] text-[var(--color-text-primary)]">
<div className="text-3xl font-bold font-[family-name:var(--font-display)] text-[var(--color-text-primary)]">
{isLoading ? "..." : isAdmin ? "∞" : remaining}
</div>
<div className="progress-bar mt-2">
@@ -94,7 +94,7 @@ function CreditCard() {
style={{ width: `${pct}%` }}
/>
</div>
<p className="text-[10px] text-[var(--color-text-ghost)] mt-1.5">
<p className="text-xs text-[var(--color-text-ghost)] mt-2">
{isAdmin ? "Sınırsız admin erişimi" : `${total} kredilik planınızın ${remaining}'${remaining === 1 ? "i" : "si"} kaldı`}
</p>
</div>
@@ -108,7 +108,7 @@ export function DesktopSidebar() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const user = (data as any)?.data ?? data;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const roles: string[] = (user?.roles ?? []).map((r: any) => r?.role?.name ?? r?.name ?? "");
const roles: string[] = (user?.roles ?? []).map((r: any) => typeof r === "string" ? r : (r?.role?.name ?? r?.name ?? ""));
const isAdmin = roles.includes("admin") || roles.includes("superadmin");
const handleLogout = () => {
@@ -116,24 +116,24 @@ export function DesktopSidebar() {
};
return (
<aside className="hidden md:flex md:w-64 lg:w-72 flex-col h-screen sticky top-0 border-r border-[var(--color-border-faint)] bg-[var(--color-bg-deep)]">
<aside className="hidden md:flex md:w-72 lg:w-80 flex-col h-screen sticky top-0 border-r border-[var(--color-border-faint)] bg-[var(--color-bg-deep)]">
{/* Logo */}
<div className="flex items-center gap-3 px-6 py-5 border-b border-[var(--color-border-faint)]">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-cyan-400 flex items-center justify-center shadow-lg">
<Sparkles size={18} className="text-white" />
<div className="flex items-center gap-4 px-8 py-6 border-b border-[var(--color-border-faint)]">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center shadow-md">
<Sparkles size={20} className="text-black" />
</div>
<div>
<h1 className="font-[family-name:var(--font-display)] text-lg font-bold tracking-tight text-[var(--color-text-primary)]">
<h1 className="font-[family-name:var(--font-display)] text-xl font-bold tracking-tight text-[var(--color-text-primary)]">
ContentGen
</h1>
<p className="text-[10px] text-[var(--color-text-muted)] uppercase tracking-widest">
<p className="text-xs font-semibold text-[var(--color-text-muted)] uppercase tracking-[0.2em]">
AI Studio
</p>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 px-3 py-4 space-y-1">
<nav className="flex-1 px-4 py-6 space-y-1.5">
{navItems.map((item) => {
const isActive =
localePath === item.href ||
@@ -145,20 +145,20 @@ export function DesktopSidebar() {
key={item.href}
href={item.href}
className={cn(
"relative flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-200",
"relative flex items-center gap-4 px-4 py-3 rounded-xl text-base font-semibold transition-all duration-200",
isActive
? "text-white bg-violet-500/12 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]"
: "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)]"
? "text-white bg-white/5"
: "text-[var(--color-text-muted)] hover:text-white hover:bg-[var(--color-bg-surface)]"
)}
>
{isActive && (
<motion.div
layoutId="sidebar-active"
className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-6 rounded-r-full bg-gradient-to-b from-violet-400 to-violet-600"
className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 rounded-r-full bg-white"
transition={{ type: "spring", bounce: 0.2, duration: 0.5 }}
/>
)}
<Icon size={18} strokeWidth={isActive ? 2.2 : 1.6} />
<Icon size={20} strokeWidth={isActive ? 2.5 : 2} />
<span>{item.label}</span>
</Link>
);
@@ -170,29 +170,29 @@ export function DesktopSidebar() {
{/* Admin Panel Linki (sadece admin) */}
{isAdmin && (
<div className="px-3 pb-3">
<div className="px-4 pb-4">
<Link
href={adminNavItem.href}
className={cn(
"relative flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-200",
"relative flex items-center gap-4 px-4 py-3 rounded-xl text-base font-semibold transition-all duration-200",
localePath.startsWith("/dashboard/admin")
? "text-rose-300 bg-rose-500/10"
: "text-[var(--color-text-muted)] hover:text-rose-300 hover:bg-rose-500/8"
? "text-white bg-white/10"
: "text-[var(--color-text-ghost)] hover:text-white hover:bg-white/5"
)}
>
<ShieldCheck size={18} strokeWidth={1.8} />
<ShieldCheck size={20} strokeWidth={2} />
<span>{adminNavItem.label}</span>
</Link>
</div>
)}
{/* Çıkış Butonu */}
<div className="px-3 pb-4">
<div className="px-4 pb-6">
<button
onClick={handleLogout}
className="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-[var(--color-text-muted)] hover:text-red-400 hover:bg-red-500/8 transition-all duration-200"
className="w-full flex items-center gap-4 px-4 py-3 rounded-xl text-base font-semibold text-[var(--color-text-ghost)] hover:text-white hover:bg-white/5 transition-all duration-200"
>
<LogOut size={18} strokeWidth={1.6} />
<LogOut size={20} strokeWidth={2} />
<span>Çıkış Yap</span>
</button>
</div>
@@ -208,7 +208,7 @@ function UserAvatar() {
return (
<button
className="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-600 to-cyan-500 flex items-center justify-center text-white text-sm font-semibold shadow-md"
className="w-10 h-10 rounded-full bg-white flex items-center justify-center text-black text-sm font-bold shadow-sm border border-white/20"
aria-label="Profil"
>
{initial}
@@ -221,11 +221,11 @@ export function TopBar() {
<header className="sticky top-0 z-40 glass">
<div className="flex items-center justify-between px-4 md:px-6 h-14 md:h-16">
{/* Mobil logo */}
<div className="flex items-center gap-2.5 md:hidden">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-cyan-400 flex items-center justify-center">
<Sparkles size={16} className="text-white" />
<div className="flex items-center gap-3 md:hidden">
<div className="w-8 h-8 rounded-lg bg-white flex items-center justify-center">
<Sparkles size={16} className="text-black" />
</div>
<span className="font-[family-name:var(--font-display)] font-bold text-base">
<span className="font-[family-name:var(--font-display)] font-bold text-lg">
ContentGen
</span>
</div>
+11 -11
View File
@@ -7,11 +7,11 @@ import type { RenderProgressState } from '@/hooks/use-render-progress';
const STAGE_ORDER = ['tts', 'image_generation', 'music_generation', 'compositing', 'encoding'];
const STAGE_DETAILS: Record<string, { label: string; icon: string; color: string }> = {
tts: { label: 'Seslendirme', icon: '🔊', color: 'from-violet-500 to-violet-600' },
image_generation: { label: 'Görsel Üretim', icon: '🎨', color: 'from-cyan-500 to-cyan-600' },
music_generation: { label: 'Müzik Üretim', icon: '🎵', color: 'from-amber-500 to-amber-600' },
compositing: { label: 'Birleştirme', icon: '🎬', color: 'from-emerald-500 to-emerald-600' },
encoding: { label: 'Kodlama', icon: '📦', color: 'from-rose-500 to-rose-600' },
tts: { label: 'Seslendirme', icon: '🔊', color: 'from-neutral-500 to-neutral-600' },
image_generation: { label: 'Görsel Üretim', icon: '🎨', color: 'from-neutral-400 to-neutral-500' },
music_generation: { label: 'Müzik Üretim', icon: '🎵', color: 'from-neutral-600 to-neutral-700' },
compositing: { label: 'Birleştirme', icon: '🎬', color: 'from-neutral-500 to-neutral-600' },
encoding: { label: 'Kodlama', icon: '📦', color: 'from-neutral-400 to-neutral-500' },
};
interface RenderProgressProps {
@@ -62,7 +62,7 @@ export function RenderProgress({ renderState, projectStatus }: RenderProgressPro
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2.5">
{status === 'rendering' && (
<Loader2 size={18} className="animate-spin text-violet-400" />
<Loader2 size={18} className="animate-spin text-neutral-400" />
)}
{status === 'completed' && (
<CheckCircle2 size={18} className="text-emerald-400" />
@@ -100,7 +100,7 @@ export function RenderProgress({ renderState, projectStatus }: RenderProgressPro
</div>
<div className="h-2.5 w-full rounded-full bg-[var(--color-bg-deep)] overflow-hidden">
<motion.div
className="h-full rounded-full bg-gradient-to-r from-violet-500 via-cyan-400 to-emerald-400"
className="h-full rounded-full bg-gradient-to-r from-neutral-500 via-neutral-400 to-neutral-300"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.5, ease: 'easeOut' }}
@@ -123,9 +123,9 @@ export function RenderProgress({ renderState, projectStatus }: RenderProgressPro
key={s}
className={`flex flex-col items-center gap-1 py-2 px-1 rounded-lg transition-all ${
isCurrent
? 'bg-violet-500/10 border border-violet-500/20'
? 'bg-[var(--color-bg-elevated)] border border-neutral-500/20'
: isDone
? 'bg-emerald-500/5'
? 'bg-neutral-500/10'
: 'opacity-40'
}`}
>
@@ -133,8 +133,8 @@ export function RenderProgress({ renderState, projectStatus }: RenderProgressPro
<span className="text-[9px] text-center text-[var(--color-text-ghost)] leading-tight">
{detail.label}
</span>
{isDone && <CheckCircle2 size={10} className="text-emerald-400" />}
{isCurrent && <Loader2 size={10} className="animate-spin text-violet-400" />}
{isDone && <CheckCircle2 size={10} className="text-neutral-300" />}
{isCurrent && <Loader2 size={10} className="animate-spin text-neutral-400" />}
</div>
);
})}
+13 -13
View File
@@ -74,12 +74,12 @@ export function SceneCard({
transition={{ delay: scene.order * 0.05, duration: 0.4 }}
className="relative group"
>
<div className="card-surface p-4 md:p-5 hover:border-violet-500/20 transition-all duration-300">
<div className="card-surface p-4 md:p-5 hover:border-neutral-500/20 transition-all duration-300">
{/* Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500/20 to-violet-600/10 flex items-center justify-center">
<span className="text-xs font-bold text-violet-400">{scene.order}</span>
<div className="w-8 h-8 rounded-lg bg-[var(--color-bg-elevated)] flex items-center justify-center border border-[var(--color-border-faint)]">
<span className="text-xs font-bold text-neutral-400">{scene.order}</span>
</div>
<div>
<h4 className="text-sm font-semibold text-[var(--color-text-primary)]">
@@ -101,7 +101,7 @@ export function SceneCard({
<div className="flex items-center gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
<button
onClick={() => setIsEditing(true)}
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-violet-400 hover:bg-violet-500/10 transition-colors"
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800 transition-colors"
title="Düzenle"
>
<Pencil size={13} />
@@ -109,7 +109,7 @@ export function SceneCard({
<button
onClick={() => onRegenerate?.(scene.id)}
disabled={!isEditable || isRendering || isRegenerating}
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-cyan-400 hover:bg-cyan-500/10 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
title="AI ile yeniden üret"
>
<RefreshCw size={13} className={isRegenerating ? 'animate-spin' : ''} />
@@ -136,7 +136,7 @@ export function SceneCard({
value={editNarration}
onChange={(e) => setEditNarration(e.target.value)}
rows={3}
className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] resize-none focus:outline-none focus:ring-1 focus:ring-violet-500/40 transition-all"
className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] resize-none focus:outline-none focus:ring-1 focus:ring-neutral-500/40 transition-all"
/>
</div>
@@ -149,7 +149,7 @@ export function SceneCard({
value={editVisual}
onChange={(e) => setEditVisual(e.target.value)}
rows={2}
className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-secondary)] resize-none focus:outline-none focus:ring-1 focus:ring-cyan-500/40 transition-all"
className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-secondary)] resize-none focus:outline-none focus:ring-1 focus:ring-neutral-500/40 transition-all"
/>
</div>
@@ -157,7 +157,7 @@ export function SceneCard({
<div className="flex items-center gap-2 pt-1">
<button
onClick={handleSave}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-violet-500/15 text-violet-400 text-xs font-medium hover:bg-violet-500/25 transition-colors"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] text-xs font-medium hover:bg-neutral-800 transition-colors"
>
<Check size={13} /> Kaydet
</button>
@@ -173,8 +173,8 @@ export function SceneCard({
<motion.div key="viewing" className="space-y-2.5">
{/* Narrasyon */}
<div className="flex gap-2">
<div className="w-5 h-5 rounded-md bg-violet-500/10 flex items-center justify-center shrink-0 mt-0.5">
<Mic size={11} className="text-violet-400" />
<div className="w-5 h-5 rounded-md bg-[var(--color-bg-elevated)] flex items-center justify-center shrink-0 mt-0.5 border border-[var(--color-border-faint)]">
<Mic size={11} className="text-neutral-400" />
</div>
<p className="text-sm text-[var(--color-text-secondary)] leading-relaxed">
{scene.narrationText}
@@ -183,8 +183,8 @@ export function SceneCard({
{/* Görsel Prompt */}
<div className="flex gap-2">
<div className="w-5 h-5 rounded-md bg-cyan-500/10 flex items-center justify-center shrink-0 mt-0.5">
<ImageIcon size={11} className="text-cyan-400" />
<div className="w-5 h-5 rounded-md bg-[var(--color-bg-elevated)] flex items-center justify-center shrink-0 mt-0.5 border border-[var(--color-border-faint)]">
<ImageIcon size={11} className="text-neutral-400" />
</div>
<div className="flex-1 group/prompt relative">
<p className="text-xs text-[var(--color-text-ghost)] leading-relaxed italic pr-6">
@@ -194,7 +194,7 @@ export function SceneCard({
onClick={() => {
navigator.clipboard.writeText(scene.visualPrompt);
}}
className="absolute top-0 right-0 p-1 opacity-0 group-hover/prompt:opacity-100 transition-opacity bg-[var(--color-bg-elevated)] rounded-md text-[var(--color-text-muted)] hover:text-cyan-400 hover:bg-cyan-500/10"
className="absolute top-0 right-0 p-1 opacity-0 group-hover/prompt:opacity-100 transition-opacity bg-[var(--color-bg-elevated)] rounded-md text-[var(--color-text-muted)] hover:text-neutral-300 hover:bg-neutral-800"
title="Prompt'u Kopyala"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
@@ -0,0 +1,363 @@
"use client";
import { useState, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Monitor,
Smartphone,
Square,
Search,
ChevronDown,
Clock,
Palette,
Languages,
} from "lucide-react";
import { cn } from "@/lib/utils";
// --- CONSTANTS ---
export const languages = [
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
{ code: "en", label: "English", flag: "🇺🇸" },
{ code: "es", label: "Español", flag: "🇪🇸" },
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
{ code: "fr", label: "Français", flag: "🇫🇷" },
{ code: "ar", label: "العربية", flag: "🇸🇦" },
{ code: "pt", label: "Português", flag: "🇧🇷" },
{ code: "ja", label: "日本語", flag: "🇯🇵" },
];
export const videoStyles = [
// Film & Sinema
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller", category: "Film & Sinema" },
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton", category: "Film & Sinema" },
{ id: "STORYTELLING", label: "Hikâye Anlatımı", emoji: "📖", desc: "Anlatı odaklı, sürükleyici", category: "Film & Sinema" },
{ id: "NEWS", label: "Haber", emoji: "📰", desc: "Güncel ve bilgilendirici", category: "Film & Sinema" },
{ id: "ARTISTIC", label: "Sanatsal", emoji: "🎨", desc: "Yaratıcı ve sıra dışı", category: "Film & Sinema" },
{ id: "NOIR", label: "Film Noir", emoji: "🖤", desc: "Karanlık, dramatik", category: "Film & Sinema" },
{ id: "VLOG", label: "Vlog", emoji: "📱", desc: "Günlük, samimi", category: "Film & Sinema" },
// Animasyon
{ id: "ANIME", label: "Anime", emoji: "⛩️", desc: "Japon animasyon stili", category: "Animasyon" },
{ id: "ANIMATION_3D", label: "3D Animasyon", emoji: "🧊", desc: "Pixar kalitesi", category: "Animasyon" },
{ id: "ANIMATION_2D", label: "2D Animasyon", emoji: "✏️", desc: "Klasik el çizimi", category: "Animasyon" },
{ id: "STOP_MOTION", label: "Stop Motion", emoji: "🧸", desc: "Kare kare animasyon", category: "Animasyon" },
{ id: "MOTION_COMIC", label: "Hareketli Çizgi Roman", emoji: "💥", desc: "Panel bazlı anlatım", category: "Animasyon" },
{ id: "CARTOON", label: "Karikatür", emoji: "🎭", desc: "Çizgi film stili", category: "Animasyon" },
{ id: "CLAYMATION", label: "Claymation", emoji: "🏺", desc: "Kil animasyon", category: "Animasyon" },
{ id: "PIXEL_ART", label: "Pixel Art", emoji: "👾", desc: "8-bit retro oyun", category: "Animasyon" },
{ id: "ISOMETRIC", label: "İzometrik", emoji: "🔷", desc: "İzometrik animasyon", category: "Animasyon" },
// Eğitim & Bilgi
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "🎓", desc: "Öğretici ve açıklayıcı", category: "Eğitim & Bilgi" },
{ id: "INFOGRAPHIC", label: "İnfografik", emoji: "📊", desc: "Veri görselleştirme", category: "Eğitim & Bilgi" },
{ id: "WHITEBOARD", label: "Whiteboard", emoji: "📝", desc: "Tahta animasyonu", category: "Eğitim & Bilgi" },
{ id: "EXPLAINER", label: "Explainer", emoji: "💡", desc: "Ürün/konsept anlatımı", category: "Eğitim & Bilgi" },
{ id: "DATA_VIZ", label: "Veri Görselleştirme", emoji: "📈", desc: "Grafikler ve tablolar", category: "Eğitim & Bilgi" },
// Retro & Nostaljik
{ id: "RETRO_80S", label: "Retro 80s", emoji: "🕹️", desc: "Synthwave estetik", category: "Retro & Nostaljik" },
{ id: "VINTAGE_FILM", label: "Vintage Film", emoji: "📽️", desc: "Super 8 filmi", category: "Retro & Nostaljik" },
{ id: "VHS", label: "VHS", emoji: "📼", desc: "Kaset estetik", category: "Retro & Nostaljik" },
{ id: "POLAROID", label: "Polaroid", emoji: "📸", desc: "Analog fotoğraf", category: "Retro & Nostaljik" },
{ id: "RETRO_90S", label: "Retro 90s Y2K", emoji: "💿", desc: "Y2K & internet", category: "Retro & Nostaljik" },
// Sanat Akımları
{ id: "WATERCOLOR", label: "Suluboya", emoji: "🎨", desc: "Suluboya resim", category: "Sanat Akımları" },
{ id: "OIL_PAINTING", label: "Yağlı Boya", emoji: "🖌️", desc: "Klasik tuval", category: "Sanat Akımları" },
{ id: "IMPRESSIONIST", label: "Empresyonist", emoji: "🌅", desc: "Monet tarzı", category: "Sanat Akımları" },
{ id: "POP_ART", label: "Pop Art", emoji: "🎯", desc: "Warhol stili", category: "Sanat Akımları" },
{ id: "UKIYO_E", label: "Ukiyo-e", emoji: "🏯", desc: "Japon gravür", category: "Sanat Akımları" },
{ id: "ART_DECO", label: "Art Deco", emoji: "✨", desc: "1920s zarafet", category: "Sanat Akımları" },
{ id: "SURREAL", label: "Sürrealist", emoji: "🌀", desc: "Dalí tarzı", category: "Sanat Akımları" },
{ id: "COMIC_BOOK", label: "Çizgi Roman", emoji: "💬", desc: "Marvel/DC stili", category: "Sanat Akımları" },
{ id: "SKETCH", label: "Karakalem", emoji: "✍️", desc: "Kalem çizim", category: "Sanat Akımları" },
// Modern & Minimal
{ id: "MINIMALIST", label: "Minimalist", emoji: "⚪", desc: "Apple estetiği", category: "Modern & Minimal" },
{ id: "GLASSMORPHISM", label: "Glassmorphism", emoji: "🔮", desc: "Cam efekti", category: "Modern & Minimal" },
{ id: "NEON", label: "Neon Glow", emoji: "💜", desc: "Neon ışıkları", category: "Modern & Minimal" },
{ id: "CYBERPUNK", label: "Cyberpunk", emoji: "🤖", desc: "Gelecek distopya", category: "Modern & Minimal" },
{ id: "STEAMPUNK", label: "Steampunk", emoji: "⚙️", desc: "Viktoryan mekanik", category: "Modern & Minimal" },
{ id: "ABSTRACT", label: "Soyut", emoji: "🔵", desc: "Abstract sanat", category: "Modern & Minimal" },
// Fotoğrafik
{ id: "PRODUCT", label: "Ürün Fotoğrafı", emoji: "📦", desc: "Studio çekim", category: "Fotoğrafik" },
{ id: "FASHION", label: "Moda", emoji: "👗", desc: "Editöryal çekim", category: "Fotoğrafik" },
{ id: "AERIAL", label: "Havadan", emoji: "🚁", desc: "Drone görüntüsü", category: "Fotoğrafik" },
{ id: "MACRO", label: "Makro", emoji: "🔬", desc: "Yakın çekim", category: "Fotoğrafik" },
{ id: "PORTRAIT", label: "Portre", emoji: "🧑", desc: "Portre fotoğraf", category: "Fotoğrafik" },
];
export const aspectRatios = [
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
];
// --- COMPONENTS ---
export function LanguageSelector({
value,
onChange,
}: {
value: string;
onChange: (val: string) => void;
}) {
return (
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
<Languages size={14} className="inline mr-1.5 text-cyan-400" />
Video Dili
</label>
<div className="grid grid-cols-4 gap-2">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => onChange(lang.code)}
className={cn(
"flex flex-col items-center gap-1 py-3 px-2 rounded-xl text-xs transition-all",
value === lang.code
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-[var(--color-border-default)]"
)}
>
<span className="text-lg">{lang.flag}</span>
<span className="font-medium">{lang.label}</span>
</button>
))}
</div>
</div>
);
}
export function StyleSelector({
value,
onChange,
cinematicReference,
onCinematicReferenceChange,
}: {
value: string;
onChange: (val: string) => void;
cinematicReference: string;
onCinematicReferenceChange: (val: string) => void;
}) {
const [styleSearch, setStyleSearch] = useState("");
const [expandedCategory, setExpandedCategory] = useState<string | null>("Film & Sinema");
const filteredStyles = useMemo(() => {
return videoStyles.filter(
(s) =>
s.label.toLowerCase().includes(styleSearch.toLowerCase()) ||
s.desc.toLowerCase().includes(styleSearch.toLowerCase())
);
}, [styleSearch]);
const groupedStyles = useMemo(() => {
return filteredStyles.reduce((acc, curr) => {
const cat = curr.category || "Diğer";
if (!acc[cat]) acc[cat] = [];
acc[cat].push(curr);
return acc;
}, {} as Record<string, typeof videoStyles>);
}, [filteredStyles]);
return (
<div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3">
<label className="text-sm font-medium text-[var(--color-text-secondary)] block">
<Palette size={14} className="inline mr-1.5 text-violet-400" />
Video Stili
</label>
<div className="relative w-full sm:w-56">
<div className="absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none">
<Search size={14} className="text-[var(--color-text-ghost)]" />
</div>
<input
type="text"
placeholder="Stil ara..."
value={styleSearch}
onChange={(e) => setStyleSearch(e.target.value)}
className="w-full pl-8 pr-3 py-1.5 bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-md text-xs text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-[var(--color-border-default)]"
/>
</div>
</div>
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1 custom-scrollbar">
{Object.entries(groupedStyles).map(([category, items]) => {
const isExpanded = styleSearch ? true : expandedCategory === category;
return (
<div
key={category}
className="bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl overflow-hidden"
>
<button
onClick={() =>
!styleSearch && setExpandedCategory(isExpanded ? null : category)
}
className="w-full flex items-center justify-between p-3 bg-[var(--color-bg-elevated)] hover:bg-[var(--color-bg-surface-hover)] transition-colors"
>
<span className="text-sm font-medium text-[var(--color-text-secondary)]">
{category}{" "}
<span className="text-[11px] text-[var(--color-text-ghost)] ml-1">
({items.length})
</span>
</span>
{!styleSearch && (
<ChevronDown
size={16}
className={cn(
"text-[var(--color-text-ghost)] transition-transform duration-200",
isExpanded && "rotate-180"
)}
/>
)}
</button>
<AnimatePresence initial={false}>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<div className="p-3 pt-0 mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2">
{items.map((s) => (
<button
key={s.id}
onClick={() => onChange(s.id)}
className={cn(
"flex flex-col items-start gap-1 p-2.5 rounded-xl text-left transition-all",
value === s.id
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
: "bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]"
)}
>
<span className="text-xl mb-0.5">{s.emoji}</span>
<span className="text-xs font-semibold leading-tight">
{s.label}
</span>
<span
className={cn(
"text-[10px] leading-tight",
value === s.id
? "text-[var(--color-text-inverted)]/70"
: "text-[var(--color-text-ghost)]"
)}
>
{s.desc}
</span>
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
{Object.keys(groupedStyles).length === 0 && (
<div className="text-center py-8 text-[var(--color-text-ghost)] text-sm">
"{styleSearch}" için sonuç bulunamadı.
</div>
)}
</div>
{value === "CINEMATIC" && (
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
Özel Sinematik Referans (Opsiyonel)
</label>
<input
type="text"
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
value={cinematicReference}
onChange={(e) => onCinematicReferenceChange(e.target.value)}
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-md py-1.5 px-3 text-sm focus:border-[var(--color-border-default)] outline-none transition-colors"
/>
</div>
)}
</div>
);
}
export function DurationSelector({
value,
onChange,
}: {
value: number;
onChange: (val: number) => void;
}) {
return (
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
Hedef Süre:{" "}
<span className="text-[var(--color-text-primary)] font-bold">{value}s</span>
</label>
<input
type="range"
min={15}
max={180}
step={5}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className={cn(
"w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer",
"[&::-webkit-slider-thumb]:appearance-none",
"[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5",
"[&::-webkit-slider-thumb]:rounded-full",
"[&::-webkit-slider-thumb]:bg-[var(--color-bg-inverted)]",
"[&::-webkit-slider-thumb]:shadow-md",
"[&::-webkit-slider-thumb]:cursor-grab"
)}
/>
<div className="flex justify-between text-[10px] text-[var(--color-text-ghost)] mt-1">
<span>15s</span>
<span>60s</span>
<span>120s</span>
<span>180s</span>
</div>
</div>
);
}
export function AspectRatioSelector({
value,
onChange,
}: {
value: string;
onChange: (val: string) => void;
}) {
return (
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
En-Boy Oranı
</label>
<div className="flex gap-2">
{aspectRatios.map((ar) => {
const Icon = ar.icon;
return (
<button
key={ar.id}
onClick={() => onChange(ar.id)}
className={cn(
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all",
value === ar.id
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)]"
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)] hover:border-[var(--color-border-default)]"
)}
>
<Icon size={20} />
<span className="font-semibold">{ar.label}</span>
<span
className={cn(
"text-[10px]",
value === ar.id
? "text-[var(--color-text-inverted)]/70"
: "text-[var(--color-text-ghost)]"
)}
>
{ar.desc}
</span>
</button>
);
})}
</div>
</div>
);
}