hPiBot openclaw ve Opencode ilk versiyonu[B

This commit is contained in:
2026-03-04 05:17:51 +03:00
commit d49edbfba3
75 changed files with 42117 additions and 0 deletions

View 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;