Files
hMarket/frontend/src/pages/Profile/ProfilePage.tsx

632 lines
21 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.
import React, { useState } from 'react';
import {
Container,
Grid,
Card,
CardContent,
Typography,
Button,
Box,
TextField,
Avatar,
Divider,
Switch,
FormControlLabel,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Tab,
Tabs,
Paper,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
IconButton,
Chip,
} from '@mui/material';
import {
Edit,
Save,
Cancel,
Person,
Security,
Notifications,
Delete,
Visibility,
VisibilityOff,
} from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { useAuth } from '../../contexts/AuthContext';
import { usersAPI, notificationsAPI } from '../../services/api';
import { User, UserSettings } from '../../types';
import toast from 'react-hot-toast';
import { formatDistanceToNow } from 'date-fns';
const profileSchema = yup.object({
firstName: yup.string().required('Ad gereklidir').min(2, 'Ad en az 2 karakter olmalıdır'),
lastName: yup.string().required('Soyad gereklidir').min(2, 'Soyad en az 2 karakter olmalıdır'),
email: yup.string().required('E-posta gereklidir').email('Geçersiz e-posta formatı'),
});
const passwordSchema = yup.object({
currentPassword: yup.string().required('Mevcut şifre gereklidir'),
newPassword: yup.string().required('Yeni şifre gereklidir').min(6, 'Şifre en az 6 karakter olmalıdır'),
confirmPassword: yup.string()
.required('Şifre onayı gereklidir')
.oneOf([yup.ref('newPassword')], 'Şifreler eşleşmelidir'),
});
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`profile-tabpanel-${index}`}
aria-labelledby={`profile-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
const ProfilePage: React.FC = () => {
const { user, updateProfile, changePassword } = useAuth();
const queryClient = useQueryClient();
const [tabValue, setTabValue] = useState(0);
const [editingProfile, setEditingProfile] = useState(false);
const [changePasswordDialogOpen, setChangePasswordDialogOpen] = useState(false);
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
// Fetch user settings
const { data: settingsData } = useQuery({
queryKey: ['userSettings'],
queryFn: () => usersAPI.getUserSettings(),
});
// Fetch user activities - temporarily disabled
// const { data: activitiesData } = useQuery({
// queryKey: ['userActivities'],
// queryFn: () => usersAPI.getUserActivities({ limit: 10 }),
// });
const activitiesData = { data: [] };
// Update profile mutation
const updateProfileMutation = useMutation({
mutationFn: (data: Partial<User>) => updateProfile(data),
onSuccess: () => {
toast.success('Profil başarıyla güncellendi!');
setEditingProfile(false);
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Profil güncellenemedi');
},
});
// Change password mutation
const changePasswordMutation = useMutation({
mutationFn: (data: { currentPassword: string; newPassword: string }) =>
changePassword(data.currentPassword, data.newPassword),
onSuccess: () => {
toast.success('Şifre başarıyla değiştirildi!');
setChangePasswordDialogOpen(false);
passwordReset();
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Şifre değiştirilemedi');
},
});
// Update settings mutation with optimistic cache sync to avoid flicker
const updateSettingsMutation = useMutation({
mutationFn: (data: Partial<UserSettings>) => usersAPI.updateUserSettings(data),
onSuccess: (res) => {
const serverSettings = (res?.data?.data?.settings ?? {}) as Partial<UserSettings>;
// Update local state to the authoritative server value
setLocalSettings(serverSettings);
// Update the react-query cache so refetch won't revert UI
queryClient.setQueryData(['userSettings'], (prev: any) => {
if (!prev) return res;
try {
const next = {
...prev,
data: {
...prev.data,
data: {
...(prev?.data?.data || {}),
settings: serverSettings,
},
},
};
return next;
} catch {
return res;
}
});
toast.success('Ayarlar başarıyla güncellendi!');
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Ayarlar güncellenemedi');
},
});
const {
control: profileControl,
handleSubmit: handleProfileSubmit,
reset: profileReset,
formState: { errors: profileErrors },
} = useForm({
resolver: yupResolver(profileSchema),
defaultValues: {
firstName: user?.firstName || '',
lastName: user?.lastName || '',
email: user?.email || '',
},
});
const {
control: passwordControl,
handleSubmit: handlePasswordSubmit,
reset: passwordReset,
formState: { errors: passwordErrors },
} = useForm({
resolver: yupResolver(passwordSchema),
defaultValues: {
currentPassword: '',
newPassword: '',
confirmPassword: '',
},
});
// Backend returns { success, data: { settings } }
const settings = (settingsData?.data?.data?.settings ?? {}) as Partial<UserSettings>;
const [localSettings, setLocalSettings] = useState<Partial<UserSettings>>({});
const activities = activitiesData?.data || [];
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
const handleUpdateProfile = (data: any) => {
updateProfileMutation.mutate(data);
};
const handleChangePassword = (data: any) => {
changePasswordMutation.mutate({
currentPassword: data.currentPassword,
newPassword: data.newPassword,
});
};
const handleSettingChange = async (key: keyof UserSettings, value: boolean) => {
const previous = localSettings[key];
setLocalSettings((prev) => ({ ...prev, [key]: value }));
try {
await updateSettingsMutation.mutateAsync({ [key]: value } as Partial<UserSettings>);
} catch (error) {
// Revert optimistic update on error
setLocalSettings((prev) => ({ ...prev, [key]: previous as boolean }));
}
};
React.useEffect(() => {
// Avoid overriding optimistic state while a mutation is pending
if (!updateSettingsMutation.isPending) {
setLocalSettings(settings);
}
}, [settings, updateSettingsMutation.isPending]);
React.useEffect(() => {
if (user) {
profileReset({
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
});
}
}, [user, profileReset]);
if (!user) {
return (
<Container maxWidth="lg" sx={{ mt: 4 }}>
<Typography>Yükleniyor...</Typography>
</Container>
);
}
return (
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4 }}>
<Avatar
sx={{
width: 80,
height: 80,
mr: 3,
bgcolor: 'primary.main',
fontSize: '2rem',
}}
>
{user.firstName?.[0] || ''}{user.lastName?.[0] || ''}
</Avatar>
<Box>
<Typography variant="h4" gutterBottom>
{user.firstName || ''} {user.lastName || ''}
</Typography>
<Typography variant="body1" color="text.secondary">
{user.email}
</Typography>
<Typography variant="body2" color="text.secondary">
Üye olma tarihi: {new Date(user.createdAt).toLocaleDateString()}
</Typography>
</Box>
</Box>
{/* Tabs */}
<Paper sx={{ mb: 3 }}>
<Tabs
value={tabValue}
onChange={handleTabChange}
aria-label="profile tabs"
variant="fullWidth"
>
<Tab icon={<Person />} label="Profil" />
<Tab icon={<Security />} label="Güvenlik" />
<Tab icon={<Notifications />} label="Bildirimler" />
</Tabs>
</Paper>
{/* Profile Tab */}
<TabPanel value={tabValue} index={0}>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '2fr 1fr' }, gap: 3 }}>
<Box>
<Card>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h6">Kişisel Bilgiler</Typography>
{!editingProfile ? (
<Button
startIcon={<Edit />}
onClick={() => setEditingProfile(true)}
>
Düzenle
</Button>
) : (
<Box>
<Button
startIcon={<Cancel />}
onClick={() => {
setEditingProfile(false);
profileReset();
}}
sx={{ mr: 1 }}
>
İptal
</Button>
<Button
variant="contained"
startIcon={<Save />}
onClick={handleProfileSubmit(handleUpdateProfile)}
disabled={updateProfileMutation.isPending}
>
Kaydet
</Button>
</Box>
)}
</Box>
<form onSubmit={handleProfileSubmit(handleUpdateProfile)}>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' }, gap: 2 }}>
<Box>
<Controller
name="firstName"
control={profileControl}
render={({ field }) => (
<TextField
{...field}
label="Ad"
fullWidth
disabled={!editingProfile}
error={!!profileErrors.firstName}
helperText={profileErrors.firstName?.message}
/>
)}
/>
</Box>
<Box>
<Controller
name="lastName"
control={profileControl}
render={({ field }) => (
<TextField
{...field}
label="Soyad"
fullWidth
disabled={!editingProfile}
error={!!profileErrors.lastName}
helperText={profileErrors.lastName?.message}
/>
)}
/>
</Box>
<Box sx={{ gridColumn: '1 / -1' }}>
<Controller
name="email"
control={profileControl}
render={({ field }) => (
<TextField
{...field}
label="E-posta"
fullWidth
disabled={!editingProfile}
error={!!profileErrors.email}
helperText={profileErrors.email?.message}
/>
)}
/>
</Box>
</Box>
</form>
</CardContent>
</Card>
</Box>
<Box>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Son Aktiviteler
</Typography>
<List>
{activities.length === 0 ? (
<Typography variant="body2" color="text.secondary">
Son aktivite bulunmuyor
</Typography>
) : (
activities.map((activity: any, index: number) => (
<ListItem key={index} divider={index < activities.length - 1}>
<ListItemText
primary={activity.description}
secondary={formatDistanceToNow(new Date(activity.createdAt), { addSuffix: true })}
/>
</ListItem>
))
)}
</List>
</CardContent>
</Card>
</Box>
</Box>
</TabPanel>
{/* Security Tab */}
<TabPanel value={tabValue} index={1}>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3 }}>
<Box>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Şifre
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Güçlü bir şifre kullanarak hesabınızı güvende tutun
</Typography>
<Button
variant="outlined"
onClick={() => setChangePasswordDialogOpen(true)}
>
Şifre Değiştir
</Button>
</CardContent>
</Card>
</Box>
<Box>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Hesap Durumu
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body2" sx={{ mr: 2 }}>
Hesap Durumu:
</Typography>
<Chip
label={user.isActive ? 'Aktif' : 'Pasif'}
color={user.isActive ? 'success' : 'error'}
size="small"
/>
</Box>
</CardContent>
</Card>
</Box>
</Box>
</TabPanel>
{/* Notifications Tab */}
<TabPanel value={tabValue} index={2}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Bildirim Tercihleri
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Hangi bildirimleri almak istediğinizi seçin
</Typography>
<List>
<ListItem>
<ListItemText
primary="E-posta Bildirimleri"
secondary="E-posta yoluyla bildirim alın"
/>
<ListItemSecondaryAction>
<Switch
checked={Boolean(localSettings.emailNotifications)}
onChange={(e) => handleSettingChange('emailNotifications', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<Divider />
<ListItem>
<ListItemText
primary="Anlık Bildirimler"
secondary="Cihazınızda anlık bildirimler alın"
/>
<ListItemSecondaryAction>
<Switch
checked={Boolean(localSettings.pushNotifications)}
onChange={(e) => handleSettingChange('pushNotifications', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<Divider />
<ListItem>
<ListItemText
primary="Liste Güncellemeleri"
secondary="Paylaşılan listeler güncellendiğinde bildirim alın"
/>
<ListItemSecondaryAction>
<Switch
checked={Boolean(localSettings.itemUpdateNotifications)}
onChange={(e) => handleSettingChange('itemUpdateNotifications', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<Divider />
{/* Pazarlama E-postaları kaldırıldı */}
</List>
</CardContent>
</Card>
</TabPanel>
{/* Change Password Dialog */}
<Dialog
open={changePasswordDialogOpen}
onClose={() => setChangePasswordDialogOpen(false)}
maxWidth="sm"
fullWidth
>
<DialogTitle>Şifre Değiştir</DialogTitle>
<form onSubmit={handlePasswordSubmit(handleChangePassword)}>
<DialogContent>
<Controller
name="currentPassword"
control={passwordControl}
render={({ field }) => (
<TextField
{...field}
margin="dense"
label="Mevcut Şifre"
type={showCurrentPassword ? 'text' : 'password'}
fullWidth
variant="outlined"
error={!!passwordErrors.currentPassword}
helperText={passwordErrors.currentPassword?.message}
InputProps={{
endAdornment: (
<IconButton
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
edge="end"
>
{showCurrentPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
)}
/>
<Controller
name="newPassword"
control={passwordControl}
render={({ field }) => (
<TextField
{...field}
margin="dense"
label="Yeni Şifre"
type={showNewPassword ? 'text' : 'password'}
fullWidth
variant="outlined"
error={!!passwordErrors.newPassword}
helperText={passwordErrors.newPassword?.message}
InputProps={{
endAdornment: (
<IconButton
onClick={() => setShowNewPassword(!showNewPassword)}
edge="end"
>
{showNewPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
)}
/>
<Controller
name="confirmPassword"
control={passwordControl}
render={({ field }) => (
<TextField
{...field}
margin="dense"
label="Yeni Şifre Tekrar"
type={showConfirmPassword ? 'text' : 'password'}
fullWidth
variant="outlined"
error={!!passwordErrors.confirmPassword}
helperText={passwordErrors.confirmPassword?.message}
InputProps={{
endAdornment: (
<IconButton
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
edge="end"
>
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
)}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setChangePasswordDialogOpen(false)}>
İptal
</Button>
<Button
type="submit"
variant="contained"
disabled={changePasswordMutation.isPending}
>
{changePasswordMutation.isPending ? 'Değiştiriliyor...' : 'Şifre Değiştir'}
</Button>
</DialogActions>
</form>
</Dialog>
</Container>
);
};
export default ProfilePage;