generated from fahricansecer/boilerplate-fe
This commit is contained in:
@@ -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",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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)]">
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user