310 lines
15 KiB
TypeScript
310 lines
15 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState, useRef } from "react";
|
||
import { useAppDispatch, useAppSelector } from "@/lib/hooks";
|
||
import { fetchProfile, updateProfile, changePassword, changeEmail } from "@/lib/features/auth/authSlice";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||
import { getAvatarUrl } from "@/lib/utils";
|
||
import Swal from "sweetalert2";
|
||
import { Loader2, Camera, Shield, Mail, User as UserIcon } from "lucide-react";
|
||
|
||
export default function ProfilePage() {
|
||
const dispatch = useAppDispatch();
|
||
const { user, isLoading } = useAppSelector((state) => state.auth);
|
||
const [file, setFile] = useState<File | null>(null);
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
|
||
// Form states
|
||
const [username, setUsername] = useState("");
|
||
|
||
// Password change states
|
||
const [currentPassword, setCurrentPassword] = useState("");
|
||
const [newPassword, setNewPassword] = useState("");
|
||
|
||
// Email change states
|
||
const [newEmail, setNewEmail] = useState("");
|
||
const [emailPassword, setEmailPassword] = useState("");
|
||
const [isMounted, setIsMounted] = useState(false);
|
||
|
||
useEffect(() => {
|
||
setIsMounted(true);
|
||
dispatch(fetchProfile());
|
||
}, [dispatch]);
|
||
|
||
useEffect(() => {
|
||
if (user) {
|
||
setUsername(user.username);
|
||
}
|
||
}, [user]);
|
||
|
||
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
if (e.target.files && e.target.files[0]) {
|
||
setFile(e.target.files[0]);
|
||
}
|
||
};
|
||
|
||
const handleProfileUpdate = () => {
|
||
const formData = new FormData();
|
||
formData.append("user_name", username);
|
||
if (file) {
|
||
formData.append("avatar", file);
|
||
}
|
||
|
||
dispatch(updateProfile(formData))
|
||
.unwrap()
|
||
.then(() => {
|
||
Swal.fire("Başarılı", "Profil bilgileriniz güncellendi.", "success");
|
||
setFile(null);
|
||
})
|
||
.catch((err) => {
|
||
Swal.fire("Hata", err || "Profil güncellenemedi.", "error");
|
||
});
|
||
};
|
||
|
||
const handleChangePassword = () => {
|
||
if (!currentPassword || !newPassword) {
|
||
Swal.fire("Uyarı", "Lütfen tüm alanları doldurun.", "warning");
|
||
return;
|
||
}
|
||
|
||
if (newPassword.length < 6) {
|
||
Swal.fire("Uyarı", "Yeni şifre en az 6 karakter olmalıdır.", "warning");
|
||
return;
|
||
}
|
||
|
||
dispatch(changePassword({ current_password: currentPassword, new_password: newPassword }))
|
||
.unwrap()
|
||
.then(() => {
|
||
Swal.fire("Başarılı", "Şifreniz değiştirildi. Lütfen yeni şifrenizle tekrar giriş yapın.", "success");
|
||
setCurrentPassword("");
|
||
setNewPassword("");
|
||
})
|
||
.catch((err) => {
|
||
Swal.fire("Hata", err || "Şifre değiştirilemedi.", "error");
|
||
});
|
||
};
|
||
|
||
const handleChangeEmail = () => {
|
||
if (!newEmail || !emailPassword) {
|
||
Swal.fire("Uyarı", "Lütfen tüm alanları doldurun.", "warning");
|
||
return;
|
||
}
|
||
|
||
dispatch(changeEmail({ new_email: newEmail, password: emailPassword }))
|
||
.unwrap()
|
||
.then((res: any) => {
|
||
Swal.fire({
|
||
title: "Başarılı",
|
||
text: `Email adresiniz güncellendi. Lütfen ${newEmail} adresine gönderilen doğrulama linkine tıklayın.`,
|
||
icon: "success"
|
||
});
|
||
setNewEmail("");
|
||
setEmailPassword("");
|
||
})
|
||
.catch((err) => {
|
||
Swal.fire("Hata", err || "Email değiştirilemedi.", "error");
|
||
});
|
||
};
|
||
|
||
if (!isMounted) {
|
||
return <div className="flex justify-center pt-10"><Loader2 className="h-8 w-8 animate-spin" /></div>;
|
||
}
|
||
|
||
if (!user && isLoading) {
|
||
return <div className="flex justify-center pt-10"><Loader2 className="h-8 w-8 animate-spin" /></div>;
|
||
}
|
||
|
||
if (!user) {
|
||
return <div className="text-center pt-10">Kullanıcı bilgileri yüklenemedi.</div>;
|
||
}
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto space-y-8">
|
||
<h1 className="text-3xl font-bold tracking-tight">Profilim</h1>
|
||
|
||
{/* Profile Info & Edit */}
|
||
<div className="grid gap-6 md:grid-cols-2">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>Profil Bilgileri</CardTitle>
|
||
<CardDescription>Kişisel bilgilerinizi buradan güncelleyebilirsiniz.</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
<div className="flex flex-col items-center space-y-4">
|
||
<div className="relative">
|
||
<Avatar className="h-24 w-24 border-2 border-border">
|
||
<AvatarImage src={file ? URL.createObjectURL(file) : getAvatarUrl(user.avatar_url)} />
|
||
<AvatarFallback className="text-xl font-bold">
|
||
{user.username.slice(0, 2).toUpperCase()}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<Button
|
||
size="icon"
|
||
variant="secondary"
|
||
className="absolute bottom-0 right-0 h-8 w-8 rounded-full shadow-md"
|
||
onClick={() => fileInputRef.current?.click()}
|
||
>
|
||
<Camera className="h-4 w-4" />
|
||
</Button>
|
||
<input
|
||
type="file"
|
||
ref={fileInputRef}
|
||
className="hidden"
|
||
accept="image/*"
|
||
onChange={handleAvatarChange}
|
||
/>
|
||
</div>
|
||
<div className="text-center">
|
||
<p className="font-semibold text-lg">{user.username}</p>
|
||
<p className="text-sm text-muted-foreground">{user.email}</p>
|
||
<div className="mt-2 flex items-center justify-center gap-2">
|
||
{user.roles.map(role => (
|
||
<span key={role.id} className="inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground hover:bg-primary/80">
|
||
{role.name}
|
||
</span>
|
||
))}
|
||
{user.is_oauth_user ? (
|
||
<span className="inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors border-transparent bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300">
|
||
OAuth
|
||
</span>
|
||
) : (
|
||
user.email_verified && <span className="inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors border-transparent bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300">
|
||
Email Onaylı
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="username">Kullanıcı Adı</Label>
|
||
<div className="relative">
|
||
<UserIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||
<Input
|
||
id="username"
|
||
value={username}
|
||
onChange={(e) => setUsername(e.target.value)}
|
||
className="pl-9"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
<CardFooter>
|
||
<Button onClick={handleProfileUpdate} disabled={isLoading}>
|
||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||
Güncelle
|
||
</Button>
|
||
</CardFooter>
|
||
</Card>
|
||
|
||
{/* Security Settings - Only for non-OAuth users */}
|
||
{!user.is_oauth_user && (
|
||
<div className="space-y-6">
|
||
{/* Change Password */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Shield className="h-5 w-5" /> Şifre Değiştir
|
||
</CardTitle>
|
||
<CardDescription>Güvenliğiniz için güçlü bir şifre kullanın.</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="current-password">Mevcut Şifre</Label>
|
||
<Input
|
||
id="current-password"
|
||
type="password"
|
||
value={currentPassword}
|
||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="new-password">Yeni Şifre</Label>
|
||
<Input
|
||
id="new-password"
|
||
type="password"
|
||
value={newPassword}
|
||
onChange={(e) => setNewPassword(e.target.value)}
|
||
/>
|
||
</div>
|
||
</CardContent>
|
||
<CardFooter>
|
||
<Button onClick={handleChangePassword} disabled={isLoading} variant="outline">
|
||
Şifreyi Değiştir
|
||
</Button>
|
||
</CardFooter>
|
||
</Card>
|
||
|
||
{/* Change Email */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Mail className="h-5 w-5" /> Email Değiştir
|
||
</CardTitle>
|
||
<CardDescription>Email adresinizi değiştirirseniz yeniden doğrulama yapmanız gerekir.</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="new-email">Yeni Email Adresi</Label>
|
||
<Input
|
||
id="new-email"
|
||
type="email"
|
||
value={newEmail}
|
||
onChange={(e) => setNewEmail(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="email-password">Şifreniz (Onay için)</Label>
|
||
<Input
|
||
id="email-password"
|
||
type="password"
|
||
value={emailPassword}
|
||
onChange={(e) => setEmailPassword(e.target.value)}
|
||
/>
|
||
</div>
|
||
</CardContent>
|
||
<CardFooter>
|
||
<Button onClick={handleChangeEmail} disabled={isLoading} variant="outline">
|
||
Email'i Güncelle
|
||
</Button>
|
||
</CardFooter>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
|
||
{/* OAuth Info - Only for OAuth users */}
|
||
{user.is_oauth_user && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>Bağlı Hesaplar</CardTitle>
|
||
<CardDescription>Hesabınız aşağıdaki sosyal medya platformlarına bağlıdır.</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="flex gap-4">
|
||
{user.social_accounts?.map((account) => (
|
||
<div key={account.id} className="flex items-center gap-3 p-3 border rounded-lg bg-muted/50 w-full">
|
||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||
{account.provider === 'google' ? 'G' : account.provider.charAt(0).toUpperCase()}
|
||
</div>
|
||
<div>
|
||
<p className="font-medium capitalize">{account.provider}</p>
|
||
<p className="text-xs text-muted-foreground">{account.email}</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
{(!user.social_accounts || user.social_accounts.length === 0) && (
|
||
<p className="text-sm text-muted-foreground">Bağlı hesap bilgisi bulunamadı (OAuth User flag true olmasına rağmen).</p>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|