hPiBot openclaw ve Opencode ilk versiyonu[B
This commit is contained in:
632
frontend/src/pages/Profile/ProfilePage.tsx
Normal file
632
frontend/src/pages/Profile/ProfilePage.tsx
Normal file
@@ -0,0 +1,632 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user