Files
ContentGen_FE/src/app/[locale]/(dashboard)/dashboard/settings/page.tsx
T
Harun CAN 8bd995ea18
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
main
2026-03-30 00:22:06 +03:00

491 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
User,
Shield,
CreditCard,
Bell,
Save,
Eye,
EyeOff,
Loader2,
CheckCircle,
} from "lucide-react";
import {
useCurrentUser,
useUpdateProfile,
useChangePassword,
useCreditBalance,
useCreditHistory,
useSubscription,
} from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
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 [activeTab, setActiveTab] = useState<TabId>("profile");
return (
<div className="max-w-4xl mx-auto space-y-6">
{/* Header */}
<div>
<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>
{/* 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 (
<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)]"
}`}
>
{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>
{/* 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>
);
}