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

This commit is contained in:
Harun CAN
2026-03-29 12:44:02 +03:00
parent fe9aff3fec
commit 45a540c530
26 changed files with 10706 additions and 86 deletions

View File

@@ -0,0 +1,161 @@
"use client";
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
} from "recharts";
const weeklyData = [
{ name: "Pzt", videos: 1, credits: 4 },
{ name: "Sal", videos: 2, credits: 8 },
{ name: "Çar", videos: 0, credits: 0 },
{ name: "Per", videos: 3, credits: 12 },
{ name: "Cum", videos: 1, credits: 4 },
{ name: "Cmt", videos: 2, credits: 8 },
{ name: "Paz", videos: 1, credits: 4 },
];
const statusData = [
{ name: "Tamamlanan", value: 8, color: "#34d399" },
{ name: "Devam Eden", value: 2, color: "#8b5cf6" },
{ name: "Taslak", value: 2, color: "#4a4a6a" },
];
function CustomTooltip({
active,
payload,
label,
}: {
active?: boolean;
payload?: Array<{ value: number; dataKey: string; color: string }>;
label?: string;
}) {
if (!active || !payload?.length) return null;
return (
<div className="glass rounded-lg px-3 py-2 text-xs shadow-xl">
<p className="text-[var(--color-text-muted)] mb-1">{label}</p>
{payload.map((entry) => (
<p key={entry.dataKey} className="text-[var(--color-text-primary)] font-medium">
{entry.dataKey === "videos" ? "Video" : "Kredi"}: {entry.value}
</p>
))}
</div>
);
}
export function DashboardCharts() {
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* ── Haftalık Video Üretimi (Area Chart) ── */}
<div className="lg:col-span-2 card-surface p-5">
<div className="flex items-center justify-between mb-4">
<h2 className="font-[family-name:var(--font-display)] text-base font-semibold">
Haftalık Üretim
</h2>
<span className="badge badge-violet">Bu Hafta</span>
</div>
<div className="h-48 md:h-56">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={weeklyData} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="gradientViolet" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#8b5cf6" stopOpacity={0.25} />
<stop offset="100%" stopColor="#8b5cf6" stopOpacity={0} />
</linearGradient>
<linearGradient id="gradientCyan" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#06b6d4" stopOpacity={0.2} />
<stop offset="100%" stopColor="#06b6d4" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: "#6a6a8a", fontSize: 11 }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: "#6a6a8a", fontSize: 11 }}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="videos"
stroke="#8b5cf6"
strokeWidth={2}
fill="url(#gradientViolet)"
/>
<Area
type="monotone"
dataKey="credits"
stroke="#06b6d4"
strokeWidth={1.5}
fill="url(#gradientCyan)"
strokeDasharray="4 4"
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="flex items-center gap-4 mt-2 text-xs text-[var(--color-text-muted)]">
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-violet-500" />
Video
</span>
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-cyan-500" />
Kredi Kullanımı
</span>
</div>
</div>
{/* ── Proje Durumu (Donut Chart) ── */}
<div className="card-surface p-5">
<h2 className="font-[family-name:var(--font-display)] text-base font-semibold mb-4">
Proje Durumu
</h2>
<div className="h-40 flex items-center justify-center">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={statusData}
cx="50%"
cy="50%"
innerRadius={45}
outerRadius={65}
paddingAngle={3}
dataKey="value"
stroke="none"
>
{statusData.map((entry, i) => (
<Cell key={i} fill={entry.color} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
<div className="space-y-2 mt-2">
{statusData.map((item) => (
<div key={item.name} className="flex items-center justify-between text-xs">
<span className="flex items-center gap-2 text-[var(--color-text-secondary)]">
<span
className="w-2 h-2 rounded-full"
style={{ backgroundColor: item.color }}
/>
{item.name}
</span>
<span className="font-medium text-[var(--color-text-primary)]">{item.value}</span>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,196 @@
"use client";
import { motion } from "framer-motion";
import {
MoreHorizontal,
Play,
CheckCircle2,
Clock,
AlertCircle,
FileText,
Download,
Trash2,
} from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
type ProjectStatus = "COMPLETED" | "RENDERING" | "GENERATING_MEDIA" | "DRAFT" | "FAILED";
interface Project {
id: string;
title: string;
status: ProjectStatus;
progress: number;
language: string;
duration: number;
createdAt: string;
thumbnailUrl?: string;
}
const mockProjects: Project[] = [
{
id: "1",
title: "Boötes Boşluğu — Evrenin En Büyük Gizemi",
status: "COMPLETED",
progress: 100,
language: "tr",
duration: 45,
createdAt: "2 saat önce",
},
{
id: "2",
title: "Kuantum Dolanıklık Nedir?",
status: "RENDERING",
progress: 72,
language: "tr",
duration: 60,
createdAt: "30 dk önce",
},
{
id: "3",
title: "The Dark History of the Bermuda Triangle",
status: "GENERATING_MEDIA",
progress: 35,
language: "en",
duration: 40,
createdAt: "15 dk önce",
},
{
id: "4",
title: "Yapay Zeka Sanatı Öldürecek mi?",
status: "DRAFT",
progress: 0,
language: "tr",
duration: 55,
createdAt: "1 gün önce",
},
{
id: "5",
title: "5 Misterios sin Resolver de la Historia",
status: "FAILED",
progress: 48,
language: "es",
duration: 50,
createdAt: "3 saat önce",
},
];
const statusConfig: Record<
ProjectStatus,
{ label: string; icon: React.ElementType; badgeClass: string }
> = {
COMPLETED: { label: "Tamamlandı", icon: CheckCircle2, badgeClass: "badge-emerald" },
RENDERING: { label: "Render", icon: Play, badgeClass: "badge-violet" },
GENERATING_MEDIA: { label: "Medya Üretimi", icon: Clock, badgeClass: "badge-cyan" },
DRAFT: { label: "Taslak", icon: FileText, badgeClass: "badge-amber" },
FAILED: { label: "Başarısız", icon: AlertCircle, badgeClass: "badge-rose" },
};
const flagEmoji: Record<string, string> = {
tr: "🇹🇷",
en: "🇺🇸",
es: "🇪🇸",
de: "🇩🇪",
fr: "🇫🇷",
ar: "🇸🇦",
};
export function RecentProjects() {
return (
<div className="card-surface overflow-hidden">
<div className="flex items-center justify-between p-4 md:p-5 border-b border-[var(--color-border-faint)]">
<h2 className="font-[family-name:var(--font-display)] text-base font-semibold">
Son Projeler
</h2>
<Link
href="/dashboard/projects"
className="text-xs text-violet-400 hover:text-violet-300 transition-colors"
>
Tümünü gör
</Link>
</div>
<div className="divide-y divide-[var(--color-border-faint)]">
{mockProjects.map((project, i) => {
const config = statusConfig[project.status];
const StatusIcon = config.icon;
return (
<motion.div
key={project.id}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.06, duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
>
<Link
href={`/dashboard/projects/${project.id}`}
className="group flex items-center gap-3 md:gap-4 px-4 md:px-5 py-3.5 hover:bg-[var(--color-bg-elevated)]/50 transition-colors"
>
{/* Thumbnail placeholder */}
<div className="w-12 h-12 md:w-14 md:h-14 rounded-lg bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] flex items-center justify-center shrink-0 overflow-hidden">
<span className="text-xl">{flagEmoji[project.language] || "🌍"}</span>
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium truncate group-hover:text-violet-300 transition-colors">
{project.title}
</h3>
<div className="flex items-center gap-2 mt-1">
<span className={cn("badge text-[10px]", config.badgeClass)}>
<StatusIcon size={10} className="mr-1" />
{config.label}
</span>
<span className="text-[10px] text-[var(--color-text-ghost)]">
{project.duration}s
</span>
<span className="text-[10px] text-[var(--color-text-ghost)]">
{project.createdAt}
</span>
</div>
{/* Progress bar */}
{project.progress > 0 && project.progress < 100 && (
<div className="progress-bar mt-2 w-full max-w-[200px]">
<div
className="progress-bar-fill"
style={{ width: `${project.progress}%` }}
/>
</div>
)}
</div>
{/* Actions */}
<div className="hidden md:flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{project.status === "COMPLETED" && (
<button
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-emerald-400 hover:bg-emerald-500/10 transition-colors"
aria-label="İndir"
onClick={(e) => e.preventDefault()}
>
<Download size={15} />
</button>
)}
<button
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-rose-400 hover:bg-rose-500/10 transition-colors"
aria-label="Sil"
onClick={(e) => e.preventDefault()}
>
<Trash2 size={15} />
</button>
<button
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-colors"
aria-label="Daha fazla"
onClick={(e) => e.preventDefault()}
>
<MoreHorizontal size={15} />
</button>
</div>
</Link>
</motion.div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,386 @@
"use client";
import { useState, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Twitter,
Loader2,
Heart,
Repeat2,
Eye,
MessageCircle,
Sparkles,
ArrowRight,
CheckCircle2,
XCircle,
Flame,
Image as ImageIcon,
Link2,
Zap,
} from "lucide-react";
import { useTweet } from "@/hooks/use-tweet";
import { cn } from "@/lib/utils";
interface TweetImportCardProps {
onProjectCreated?: (projectId: string) => void;
}
export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
const {
preview,
isLoadingPreview,
isCreatingProject,
error,
createdProject,
isValidTweetUrl,
previewTweet,
createProjectFromTweet,
reset,
} = useTweet();
const [tweetUrl, setTweetUrl] = useState("");
const [customTitle, setCustomTitle] = useState("");
const handlePreview = useCallback(async () => {
if (!tweetUrl.trim()) return;
await previewTweet(tweetUrl.trim());
}, [tweetUrl, previewTweet]);
const handleCreate = useCallback(async () => {
if (!tweetUrl.trim()) return;
const project = await createProjectFromTweet({
tweetUrl: tweetUrl.trim(),
title: customTitle.trim() || undefined,
});
if (project && onProjectCreated) {
onProjectCreated(project.id);
}
}, [tweetUrl, customTitle, createProjectFromTweet, onProjectCreated]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
if (!preview) {
handlePreview();
} else {
handleCreate();
}
}
};
const isUrlValid = tweetUrl.trim().length > 0 && isValidTweetUrl(tweetUrl.trim());
const formatNumber = (num: number): string => {
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
return num.toString();
};
const viralColor = (score: number) => {
if (score >= 70) return "text-emerald-400";
if (score >= 40) return "text-amber-400";
return "text-[var(--color-text-muted)]";
};
const viralBg = (score: number) => {
if (score >= 70) return "bg-emerald-500/10 border-emerald-500/30";
if (score >= 40) return "bg-amber-500/10 border-amber-500/30";
return "bg-[var(--color-bg-elevated)] border-[var(--color-border-faint)]";
};
return (
<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">
<Twitter size={18} className="text-sky-400" />
</div>
<div>
<h3 className="font-[family-name:var(--font-display)] text-sm font-semibold">
X/Twitter Import
</h3>
<p className="text-[11px] text-[var(--color-text-ghost)]">
Tweet Video pipeline
</p>
</div>
</div>
{/* URL Input */}
<div className="p-4 md:p-5 space-y-4">
<div className="relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]">
<Link2 size={16} />
</div>
<input
type="url"
value={tweetUrl}
onChange={(e) => {
setTweetUrl(e.target.value);
if (preview) reset();
}}
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"
/>
<button
onClick={preview ? handleCreate : handlePreview}
disabled={!isUrlValid || isLoadingPreview || isCreatingProject}
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-surface)] text-[var(--color-text-ghost)] cursor-not-allowed"
)}
>
{isLoadingPreview ? (
<>
<Loader2 size={13} className="animate-spin" />
Çekiliyor
</>
) : isCreatingProject ? (
<>
<Loader2 size={13} className="animate-spin" />
Üretiliyor
</>
) : preview ? (
<>
<Zap size={13} />
Oluştur
</>
) : (
<>
<ArrowRight size={13} />
Ön İzle
</>
)}
</button>
</div>
{/* Error */}
<AnimatePresence mode="wait">
{error && (
<motion.div
initial={{ opacity: 0, y: -8, height: 0 }}
animate={{ opacity: 1, y: 0, height: "auto" }}
exit={{ opacity: 0, y: -8, height: 0 }}
className="flex items-center gap-2 text-xs text-rose-400 bg-rose-500/10 border border-rose-500/20 rounded-lg px-3 py-2"
>
<XCircle size={14} />
{error}
</motion.div>
)}
</AnimatePresence>
{/* Preview Card */}
<AnimatePresence mode="wait">
{preview && (
<motion.div
initial={{ opacity: 0, y: 12, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 12, scale: 0.97 }}
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
className="space-y-3"
>
{/* Author + Viral Score */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] overflow-hidden flex items-center justify-center">
{preview.tweet.author.avatarUrl ? (
<img
src={preview.tweet.author.avatarUrl}
alt={preview.tweet.author.name}
className="w-full h-full object-cover"
/>
) : (
<Twitter size={16} className="text-sky-400" />
)}
</div>
<div>
<div className="flex items-center gap-1">
<span className="text-sm font-medium">
{preview.tweet.author.name}
</span>
{preview.tweet.author.verified && (
<CheckCircle2
size={13}
className="text-sky-400 fill-sky-400"
/>
)}
</div>
<span className="text-[11px] text-[var(--color-text-ghost)]">
@{preview.tweet.author.username}
</span>
</div>
</div>
{/* Viral Score Badge */}
<div
className={cn(
"flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border text-xs font-medium",
viralBg(preview.viralScore)
)}
>
<Flame size={13} className={viralColor(preview.viralScore)} />
<span className={viralColor(preview.viralScore)}>
{preview.viralScore}/100
</span>
</div>
</div>
{/* Tweet Text */}
<div className="bg-[var(--color-bg-elevated)] rounded-xl p-3 border border-[var(--color-border-faint)]">
<p className="text-[13px] text-[var(--color-text-secondary)] leading-relaxed line-clamp-4">
{preview.tweet.text}
</p>
</div>
{/* Media preview */}
{preview.tweet.media.length > 0 && (
<div className="flex gap-1.5 overflow-x-auto pb-1">
{preview.tweet.media.slice(0, 4).map((m, i) => (
<div
key={i}
className="w-16 h-16 rounded-lg bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] overflow-hidden flex-shrink-0"
>
{m.type === "photo" ? (
<img
src={m.url}
alt={`Media ${i + 1}`}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-[var(--color-text-ghost)]">
<ImageIcon size={20} />
</div>
)}
</div>
))}
</div>
)}
{/* Metrics */}
<div className="grid grid-cols-4 gap-2">
{[
{
icon: Heart,
value: preview.tweet.metrics.likes,
color: "text-rose-400",
},
{
icon: Repeat2,
value: preview.tweet.metrics.retweets,
color: "text-emerald-400",
},
{
icon: Eye,
value: preview.tweet.metrics.views,
color: "text-sky-400",
},
{
icon: MessageCircle,
value: preview.tweet.metrics.replies,
color: "text-amber-400",
},
].map(({ icon: Icon, value, color }, i) => (
<div
key={i}
className="flex items-center justify-center gap-1.5 py-2 rounded-lg bg-[var(--color-bg-elevated)]/50 border border-[var(--color-border-faint)]"
>
<Icon size={13} className={color} />
<span className="text-[11px] font-medium">
{formatNumber(value)}
</span>
</div>
))}
</div>
{/* Info badges */}
<div className="flex flex-wrap gap-1.5">
<span className="badge badge-cyan text-[10px]">
{preview.contentType === "thread"
? "Thread"
: preview.contentType === "quote_tweet"
? "Alıntı"
: "Tweet"}
</span>
<span className="badge badge-violet text-[10px]">
~{preview.estimatedDuration}s video
</span>
{preview.tweet.media.length > 0 && (
<span className="badge badge-amber text-[10px]">
{preview.tweet.media.length} medya
</span>
)}
</div>
{/* Custom Title */}
<input
type="text"
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"
/>
{/* Create Button */}
<button
onClick={handleCreate}
disabled={isCreatingProject}
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]"
)}
>
{isCreatingProject ? (
<>
<Loader2 size={16} className="animate-spin" />
Senaryo üretiliyor...
</>
) : (
<>
<Sparkles size={16} />
Tweet&apos;ten Video Oluştur
</>
)}
</button>
</motion.div>
)}
</AnimatePresence>
{/* Success */}
<AnimatePresence>
{createdProject && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="flex items-center gap-3 p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/25"
>
<CheckCircle2 size={18} className="text-emerald-400 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-emerald-300">
Proje oluşturuldu!
</p>
<p className="text-[11px] text-emerald-400/70 truncate">
{createdProject.title}
</p>
</div>
<button
onClick={() => {
if (onProjectCreated) onProjectCreated(createdProject.id);
}}
className="text-[11px] text-emerald-400 hover:text-emerald-300 font-medium transition-colors whitespace-nowrap"
>
Görüntüle
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}

View File

@@ -0,0 +1,192 @@
"use client";
import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { Home, FolderOpen, LayoutGrid, Settings, Sparkles } from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
const navItems = [
{ href: "/dashboard", icon: Home, label: "Ana Sayfa" },
{ href: "/dashboard/projects", icon: FolderOpen, label: "Projeler" },
{ href: "/dashboard/templates", icon: LayoutGrid, label: "Şablonlar" },
{ href: "/dashboard/settings", icon: Settings, label: "Ayarlar" },
];
export function MobileNav() {
const pathname = usePathname();
const localePath = pathname.replace(/^\/[a-z]{2}/, "");
return (
<nav className="mobile-nav md:hidden">
<div className="flex items-center justify-around px-2">
{navItems.map((item) => {
const isActive =
localePath === item.href ||
(item.href !== "/dashboard" && localePath.startsWith(item.href));
const Icon = item.icon;
return (
<Link
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",
isActive
? "text-violet-400"
: "text-[var(--color-text-muted)] active:text-[var(--color-text-secondary)]"
)}
>
<div className="relative">
<Icon size={22} strokeWidth={isActive ? 2.2 : 1.8} />
{isActive && (
<motion.div
layoutId="nav-indicator"
className="absolute -inset-2 rounded-xl bg-violet-500/10"
transition={{ type: "spring", bounce: 0.2, duration: 0.5 }}
/>
)}
</div>
<span className="text-[10px] font-medium tracking-wide">
{item.label}
</span>
</Link>
);
})}
</div>
</nav>
);
}
export function DesktopSidebar() {
const pathname = usePathname();
const localePath = pathname.replace(/^\/[a-z]{2}/, "");
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)]">
{/* 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>
<div>
<h1 className="font-[family-name:var(--font-display)] text-lg font-bold tracking-tight text-[var(--color-text-primary)]">
ContentGen
</h1>
<p className="text-[10px] text-[var(--color-text-muted)] uppercase tracking-widest">
AI Studio
</p>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 px-3 py-4 space-y-1">
{navItems.map((item) => {
const isActive =
localePath === item.href ||
(item.href !== "/dashboard" && localePath.startsWith(item.href));
const Icon = item.icon;
return (
<Link
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",
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)]"
)}
>
{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"
transition={{ type: "spring", bounce: 0.2, duration: 0.5 }}
/>
)}
<Icon size={18} strokeWidth={isActive ? 2.2 : 1.6} />
<span>{item.label}</span>
</Link>
);
})}
</nav>
{/* Credits Card */}
<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">Pro</span>
</div>
<div className="text-2xl font-bold font-[family-name:var(--font-display)] text-[var(--color-text-primary)]">
47
</div>
<div className="progress-bar mt-2">
<div className="progress-bar-fill" style={{ width: "94%" }} />
</div>
<p className="text-[10px] text-[var(--color-text-ghost)] mt-1.5">
50 kredilik planınızın 47'si kaldı
</p>
</div>
</aside>
);
}
export function TopBar() {
return (
<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>
<span className="font-[family-name:var(--font-display)] font-bold text-base">
ContentGen
</span>
</div>
{/* Spacer — desktop */}
<div className="hidden md:block" />
{/* Sağ taraf */}
<div className="flex items-center gap-3">
{/* Bildirim */}
<button
className="relative w-9 h-9 rounded-xl flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-elevated)] transition-colors"
aria-label="Bildirimler"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
</svg>
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-violet-500 rounded-full" />
</button>
{/* Avatar */}
<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"
aria-label="Profil"
>
H
</button>
</div>
</div>
</header>
);
}
export function AppShell({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen">
<DesktopSidebar />
<div className="flex-1 flex flex-col min-h-screen">
<TopBar />
<main className="flex-1 px-[var(--spacing-page)] py-5 pb-safe page-enter">
{children}
</main>
<MobileNav />
</div>
</div>
);
}

View File

@@ -1,22 +1,21 @@
"use client";
import { ChakraProvider } from "@chakra-ui/react";
import { SessionProvider } from "next-auth/react";
import { ColorModeProvider, type ColorModeProviderProps } from "./color-mode";
import { system } from "../../theme/theme";
import { Toaster } from "./feedback/toaster";
import TopLoader from "./top-loader";
import { ThemeProvider } from "next-themes";
import ReactQueryProvider from "@/provider/react-query-provider";
export function Provider(props: ColorModeProviderProps) {
export function Provider({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
<ReactQueryProvider>
<ChakraProvider value={system}>
<TopLoader />
<ColorModeProvider {...props} />
<Toaster />
</ChakraProvider>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
disableTransitionOnChange
>
{children}
</ThemeProvider>
</ReactQueryProvider>
</SessionProvider>
);