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