generated from fahricansecer/boilerplate-fe
491 lines
17 KiB
TypeScript
491 lines
17 KiB
TypeScript
"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>
|
||
);
|
||
}
|