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

This commit is contained in:
Harun CAN
2026-03-30 00:22:06 +03:00
parent 45a540c530
commit 8bd995ea18
44 changed files with 3721 additions and 11852 deletions
@@ -1,78 +1,490 @@
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
User,
Shield,
CreditCard,
Bell,
Palette,
Globe,
Shield,
LogOut,
ChevronRight,
Save,
Eye,
EyeOff,
Loader2,
CheckCircle,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
useCurrentUser,
useUpdateProfile,
useChangePassword,
useCreditBalance,
useCreditHistory,
useSubscription,
} from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
const sections = [
{ id: "profile", label: "Profil", icon: User, desc: "Ad, e-posta ve avatar" },
{ id: "billing", label: "Abonelik & Fatura", icon: CreditCard, desc: "Plan, kredi ve ödeme bilgileri" },
{ id: "notifications", label: "Bildirimler", icon: Bell, desc: "E-posta ve push bildirimleri" },
{ id: "appearance", label: "Görünüm", icon: Palette, desc: "Tema ve dil tercihleri" },
{ id: "language", label: "Dil", icon: Globe, desc: "Varsayılan video ve arayüz dili" },
{ id: "security", label: "Güvenlik", icon: Shield, desc: "Şifre ve iki faktörlü doğrulama" },
];
const tabs = [
{ id: "profile", label: "Profil", icon: User },
{ id: "security", label: "Güvenlik", icon: Shield },
{ id: "billing", label: "Abonelik", icon: CreditCard },
{ id: "notifications", label: "Bildirimler", icon: Bell },
] as const;
type TabId = (typeof tabs)[number]["id"];
export default function SettingsPage() {
const [activeSection, setActiveSection] = useState("profile");
const [activeTab, setActiveTab] = useState<TabId>("profile");
return (
<div className="max-w-3xl mx-auto space-y-6">
<div className="max-w-4xl mx-auto space-y-6">
{/* Header */}
<div>
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold">Ayarlar</h1>
<p className="text-sm text-[var(--color-text-muted)] mt-0.5">Hesap ve uygulama ayarlarını yönet</p>
<h1 className="text-2xl md:text-3xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)]">
Ayarlar
</h1>
<p className="text-sm text-[var(--color-text-muted)] mt-1">
Hesap ayarlarınızı ve tercihlerinizi yönetin
</p>
</div>
<div className="space-y-2">
{sections.map((section) => {
const Icon = section.icon;
{/* Tab Navigation */}
<div className="flex gap-1 p-1 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] overflow-x-auto">
{tabs.map((tab) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<motion.button
key={section.id}
onClick={() => setActiveSection(section.id)}
whileTap={{ scale: 0.99 }}
className={cn(
"w-full flex items-center gap-4 p-4 rounded-xl text-left transition-all",
activeSection === section.id
? "bg-violet-500/8 border border-violet-500/20"
: "card-surface hover:border-[var(--color-border-default)]"
)}
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`relative flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${
isActive
? "text-white"
: "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
}`}
>
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center shrink-0",
activeSection === section.id
? "bg-violet-500/15 text-violet-400"
: "bg-[var(--color-bg-elevated)] text-[var(--color-text-muted)]"
)}>
<Icon size={18} />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold">{section.label}</h3>
<p className="text-xs text-[var(--color-text-muted)] mt-0.5">{section.desc}</p>
</div>
<ChevronRight size={16} className="text-[var(--color-text-ghost)]" />
</motion.button>
{isActive && (
<motion.div
layoutId="settings-tab"
className="absolute inset-0 rounded-lg bg-violet-500/15 border border-violet-500/20"
transition={{ type: "spring", bounce: 0.2, duration: 0.5 }}
/>
)}
<Icon size={16} className="relative z-10" />
<span className="relative z-10">{tab.label}</span>
</button>
);
})}
</div>
{/* Çıkış */}
<button className="w-full flex items-center gap-4 p-4 rounded-xl text-left bg-rose-500/5 border border-rose-500/15 text-rose-400 hover:bg-rose-500/10 transition-colors">
<div className="w-10 h-10 rounded-xl bg-rose-500/10 flex items-center justify-center">
<LogOut size={18} />
</div>
<span className="text-sm font-semibold">Çıkış Yap</span>
</button>
{/* Tab Content */}
<AnimatePresence mode="wait">
<motion.div
key={activeTab}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2 }}
>
{activeTab === "profile" && <ProfileTab />}
{activeTab === "security" && <SecurityTab />}
{activeTab === "billing" && <BillingTab />}
{activeTab === "notifications" && <NotificationsTab />}
</motion.div>
</AnimatePresence>
</div>
);
}
/* ─── PROFILE TAB ─── */
function ProfileTab() {
const { data: userData, isLoading } = useCurrentUser();
const updateProfile = useUpdateProfile();
const toast = useToast();
const user = userData?.data ?? userData;
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
useEffect(() => {
if (user) {
setFirstName(user.firstName ?? "");
setLastName(user.lastName ?? "");
}
}, [user]);
const handleSave = async () => {
try {
await updateProfile.mutateAsync({ firstName, lastName });
toast.success("Profil başarıyla güncellendi");
} catch {
toast.error("Profil güncellenirken bir hata oluştu");
}
};
if (isLoading) {
return (
<div className="card p-12 flex items-center justify-center">
<Loader2 className="animate-spin text-violet-400" size={24} />
</div>
);
}
return (
<div className="card p-6 space-y-6">
<h2 className="text-lg font-semibold text-[var(--color-text-primary)]">
Profil Bilgileri
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-[var(--color-text-muted)] mb-1.5">
Ad
</label>
<input
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
className="w-full px-4 py-2.5 rounded-xl bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:border-violet-500/50 focus:ring-1 focus:ring-violet-500/25 transition-all"
placeholder="Adınız"
/>
</div>
<div>
<label className="block text-xs font-medium text-[var(--color-text-muted)] mb-1.5">
Soyad
</label>
<input
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
className="w-full px-4 py-2.5 rounded-xl bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:border-violet-500/50 focus:ring-1 focus:ring-violet-500/25 transition-all"
placeholder="Soyadınız"
/>
</div>
</div>
<div>
<label className="block text-xs font-medium text-[var(--color-text-muted)] mb-1.5">
E-posta
</label>
<input
type="email"
value={user?.email ?? ""}
disabled
className="w-full px-4 py-2.5 rounded-xl bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-[var(--color-text-ghost)] text-sm cursor-not-allowed"
/>
<p className="text-[10px] text-[var(--color-text-ghost)] mt-1">
E-posta adresi değiştirilemez
</p>
</div>
<div className="flex justify-end pt-2">
<button
onClick={handleSave}
disabled={updateProfile.isPending}
className="btn-primary flex items-center gap-2 px-6 py-2.5 text-sm"
>
{updateProfile.isPending ? (
<Loader2 size={16} className="animate-spin" />
) : (
<Save size={16} />
)}
Kaydet
</button>
</div>
</div>
);
}
/* ─── SECURITY TAB ─── */
function SecurityTab() {
const changePassword = useChangePassword();
const toast = useToast();
const [current, setCurrent] = useState("");
const [newPw, setNewPw] = useState("");
const [confirm, setConfirm] = useState("");
const [showCurrent, setShowCurrent] = useState(false);
const [showNew, setShowNew] = useState(false);
const handleChange = async () => {
if (newPw !== confirm) {
toast.warning("Yeni şifre ve onay eşleşmiyor");
return;
}
if (newPw.length < 8) {
toast.warning("Yeni şifre en az 8 karakter olmalı");
return;
}
try {
await changePassword.mutateAsync({
currentPassword: current,
newPassword: newPw,
});
toast.success("Şifre başarıyla güncellendi");
setCurrent("");
setNewPw("");
setConfirm("");
} catch {
toast.error("Mevcut şifre hatalı veya bir sorun oluştu");
}
};
return (
<div className="card p-6 space-y-6">
<h2 className="text-lg font-semibold text-[var(--color-text-primary)]">
Şifre Değiştir
</h2>
<div className="space-y-4 max-w-md">
{/* Current */}
<div>
<label className="block text-xs font-medium text-[var(--color-text-muted)] mb-1.5">
Mevcut Şifre
</label>
<div className="relative">
<input
type={showCurrent ? "text" : "password"}
value={current}
onChange={(e) => setCurrent(e.target.value)}
className="w-full px-4 py-2.5 pr-10 rounded-xl bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:border-violet-500/50 focus:ring-1 focus:ring-violet-500/25 transition-all"
/>
<button
onClick={() => setShowCurrent(!showCurrent)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]"
type="button"
>
{showCurrent ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
{/* New */}
<div>
<label className="block text-xs font-medium text-[var(--color-text-muted)] mb-1.5">
Yeni Şifre
</label>
<div className="relative">
<input
type={showNew ? "text" : "password"}
value={newPw}
onChange={(e) => setNewPw(e.target.value)}
className="w-full px-4 py-2.5 pr-10 rounded-xl bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:border-violet-500/50 focus:ring-1 focus:ring-violet-500/25 transition-all"
/>
<button
onClick={() => setShowNew(!showNew)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]"
type="button"
>
{showNew ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
{/* Confirm */}
<div>
<label className="block text-xs font-medium text-[var(--color-text-muted)] mb-1.5">
Yeni Şifre (Tekrar)
</label>
<input
type="password"
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
className="w-full px-4 py-2.5 rounded-xl bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:border-violet-500/50 focus:ring-1 focus:ring-violet-500/25 transition-all"
/>
{newPw && confirm && newPw !== confirm && (
<p className="text-[10px] text-red-400 mt-1">Şifreler eşleşmiyor</p>
)}
</div>
</div>
<div className="flex justify-end pt-2">
<button
onClick={handleChange}
disabled={changePassword.isPending || !current || !newPw || !confirm}
className="btn-primary flex items-center gap-2 px-6 py-2.5 text-sm disabled:opacity-40"
>
{changePassword.isPending ? (
<Loader2 size={16} className="animate-spin" />
) : (
<Shield size={16} />
)}
Şifreyi Güncelle
</button>
</div>
</div>
);
}
/* ─── BILLING TAB ─── */
function BillingTab() {
const { data: creditData } = useCreditBalance();
const { data: subData } = useSubscription();
const { data: historyData } = useCreditHistory();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const credits = (creditData as any)?.data ?? creditData;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const subscription = (subData as any)?.data ?? subData;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const transactions = (historyData as any)?.data?.transactions ?? (historyData as any)?.transactions ?? [];
return (
<div className="space-y-6">
{/* Abonelik Kartı */}
<div className="card p-6">
<h2 className="text-lg font-semibold text-[var(--color-text-primary)] mb-4">
Abonelik Durumu
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 rounded-xl bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)]">
<p className="text-xs text-[var(--color-text-muted)] mb-1">Plan</p>
<p className="text-lg font-bold text-[var(--color-text-primary)]">
{subscription?.plan ?? "Free"}
</p>
</div>
<div className="p-4 rounded-xl bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)]">
<p className="text-xs text-[var(--color-text-muted)] mb-1">Kalan Kredi</p>
<p className="text-lg font-bold text-emerald-400">
{credits?.remaining ?? credits?.balance ?? 0}
</p>
</div>
<div className="p-4 rounded-xl bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)]">
<p className="text-xs text-[var(--color-text-muted)] mb-1">Aylık Limit</p>
<p className="text-lg font-bold text-[var(--color-text-primary)]">
{subscription?.monthlyCredits ?? credits?.total ?? 3}
</p>
</div>
</div>
</div>
{/* Kredi Geçmişi */}
<div className="card p-6">
<h2 className="text-lg font-semibold text-[var(--color-text-primary)] mb-4">
Kredi İşlem Geçmişi
</h2>
{transactions.length === 0 ? (
<p className="text-sm text-[var(--color-text-muted)] text-center py-8">
Henüz işlem geçmişi bulunmuyor
</p>
) : (
<div className="space-y-2">
{transactions.slice(0, 10).map((tx: { id: string; amount: number; type: string; description: string; createdAt: string }) => (
<div
key={tx.id}
className="flex items-center justify-between p-3 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)]"
>
<div>
<p className="text-sm font-medium text-[var(--color-text-primary)]">
{tx.description || tx.type}
</p>
<p className="text-[10px] text-[var(--color-text-ghost)]">
{new Date(tx.createdAt).toLocaleDateString("tr-TR")}
</p>
</div>
<span
className={`text-sm font-bold ${
tx.amount > 0 ? "text-emerald-400" : "text-red-400"
}`}
>
{tx.amount > 0 ? `+${tx.amount}` : tx.amount}
</span>
</div>
))}
</div>
)}
</div>
</div>
);
}
/* ─── NOTIFICATIONS TAB ─── */
function NotificationsTab() {
const toast = useToast();
const [prefs, setPrefs] = useState({
projectComplete: true,
creditLow: true,
weeklyReport: false,
marketingEmails: false,
});
const toggle = (key: keyof typeof prefs) => {
setPrefs((p) => ({ ...p, [key]: !p[key] }));
};
const handleSave = () => {
toast.success("Bildirim tercihleri kaydedildi");
};
const notifItems = [
{
key: "projectComplete" as const,
label: "Proje Tamamlandı",
desc: "Video render işlemi tamamlandığında bildirim al",
},
{
key: "creditLow" as const,
label: "Düşük Kredi Uyarısı",
desc: "Kredileriniz %20'nin altına düştüğünde uyarı al",
},
{
key: "weeklyReport" as const,
label: "Haftalık Rapor",
desc: "Haftalık kullanım raporunu e-posta ile al",
},
{
key: "marketingEmails" as const,
label: "Pazarlama E-postaları",
desc: "Yeni özellikler ve kampanyalar hakkında bilgi al",
},
];
return (
<div className="card p-6 space-y-6">
<h2 className="text-lg font-semibold text-[var(--color-text-primary)]">
Bildirim Tercihleri
</h2>
<div className="space-y-3">
{notifItems.map((item) => (
<div
key={item.key}
className="flex items-center justify-between p-4 rounded-xl bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)]"
>
<div>
<p className="text-sm font-medium text-[var(--color-text-primary)]">
{item.label}
</p>
<p className="text-[11px] text-[var(--color-text-ghost)] mt-0.5">
{item.desc}
</p>
</div>
<button
onClick={() => toggle(item.key)}
className={`relative w-11 h-6 rounded-full transition-colors ${
prefs[item.key] ? "bg-violet-500" : "bg-[var(--color-bg-elevated)]"
}`}
>
<motion.div
className="absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-md"
animate={{ x: prefs[item.key] ? 20 : 0 }}
transition={{ type: "spring", bounce: 0.25, duration: 0.3 }}
/>
</button>
</div>
))}
</div>
<div className="flex justify-end pt-2">
<button
onClick={handleSave}
className="btn-primary flex items-center gap-2 px-6 py-2.5 text-sm"
>
<CheckCircle size={16} />
Tercihleri Kaydet
</button>
</div>
</div>
);
}