first commit
This commit is contained in:
13
app/(dashboard)/layout.tsx
Normal file
13
app/(dashboard)/layout.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<main className="flex-1 p-6 md:p-8 pt-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
309
app/(dashboard)/profile/page.tsx
Normal file
309
app/(dashboard)/profile/page.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user