sidebar fix

This commit is contained in:
2025-08-31 23:15:50 -04:00
parent 1430c97ae7
commit 23dfc171b8
10 changed files with 1836 additions and 171 deletions

3
user/web/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
dist
node_modules

View File

@ -24,7 +24,7 @@ import {
} from '@tabler/icons-react';
import { notifications } from '@mantine/notifications';
import { modals } from '@mantine/modals';
import UserForm from './UserForm';
import UserSidebar from './UserSidebar';
import { userService } from '../services/userService';
import { User, UserStatus, UserRole, ListUsersRequest } from '../types/user';
@ -36,7 +36,7 @@ const UserManagement: React.FC = () => {
const [roleFilter, setRoleFilter] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalUsers, setTotalUsers] = useState(0);
const [userFormOpened, setUserFormOpened] = useState(false);
const [userSidebarOpened, setUserSidebarOpened] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const pageSize = 10;
@ -76,12 +76,12 @@ const UserManagement: React.FC = () => {
const handleCreateUser = () => {
setEditingUser(null);
setUserFormOpened(true);
setUserSidebarOpened(true);
};
const handleEditUser = (user: User) => {
setEditingUser(user);
setUserFormOpened(true);
setUserSidebarOpened(true);
};
const handleDeleteUser = (user: User) => {
@ -115,12 +115,12 @@ const UserManagement: React.FC = () => {
});
};
const handleUserFormSuccess = () => {
const handleUserSidebarSuccess = () => {
loadUsers();
};
const handleUserFormClose = () => {
setUserFormOpened(false);
const handleUserSidebarClose = () => {
setUserSidebarOpened(false);
setEditingUser(null);
};
@ -149,7 +149,13 @@ const UserManagement: React.FC = () => {
return (
<>
{/* Main user management interface */}
<Stack gap="md">
<Stack
gap="md"
style={{
transition: 'margin-right 0.3s ease',
marginRight: userSidebarOpened ? '400px' : '0',
}}
>
<Group justify="space-between">
<Button leftSection={<IconPlus size={16} />} onClick={handleCreateUser}>
Add User
@ -296,11 +302,11 @@ const UserManagement: React.FC = () => {
</Paper>
</Stack>
{/* User form modal */}
<UserForm
opened={userFormOpened}
onClose={handleUserFormClose}
onSuccess={handleUserFormSuccess}
{/* User sidebar */}
<UserSidebar
opened={userSidebarOpened}
onClose={handleUserSidebarClose}
onSuccess={handleUserSidebarSuccess}
editUser={editingUser}
/>
</>

View File

@ -0,0 +1,255 @@
import React, { useEffect } from 'react';
import {
Paper,
TextInput,
Select,
Button,
Group,
Stack,
Title,
ActionIcon,
ScrollArea,
Box,
} from '@mantine/core';
import { IconX } from '@tabler/icons-react';
import { useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
import { userService } from '../services/userService';
import { User, CreateUserRequest, UpdateUserRequest, UserRole, UserStatus } from '../types/user';
interface UserSidebarProps {
opened: boolean;
onClose: () => void;
onSuccess: () => void;
editUser?: User | null;
}
const UserSidebar: React.FC<UserSidebarProps> = ({
opened,
onClose,
onSuccess,
editUser,
}) => {
const isEditing = !!editUser;
const form = useForm({
initialValues: {
email: '',
first_name: '',
last_name: '',
display_name: '',
avatar: '',
role: 'user' as UserRole,
status: 'pending' as UserStatus,
},
validate: {
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
first_name: (value) => (value.trim().length < 1 ? 'First name is required' : null),
last_name: (value) => (value.trim().length < 1 ? 'Last name is required' : null),
},
});
// Update form values when editUser changes
useEffect(() => {
if (editUser) {
form.setValues({
email: editUser.email || '',
first_name: editUser.first_name || '',
last_name: editUser.last_name || '',
display_name: editUser.display_name || '',
avatar: editUser.avatar || '',
role: editUser.role || 'user' as UserRole,
status: editUser.status || 'pending' as UserStatus,
});
} else {
// Reset to default values when not editing
form.setValues({
email: '',
first_name: '',
last_name: '',
display_name: '',
avatar: '',
role: 'user' as UserRole,
status: 'pending' as UserStatus,
});
}
}, [editUser, opened]);
const handleSubmit = async (values: typeof form.values) => {
try {
if (isEditing && editUser) {
const updateRequest: UpdateUserRequest = {
email: values.email !== editUser.email ? values.email : undefined,
first_name: values.first_name !== editUser.first_name ? values.first_name : undefined,
last_name: values.last_name !== editUser.last_name ? values.last_name : undefined,
display_name: values.display_name !== editUser.display_name ? values.display_name : undefined,
avatar: values.avatar !== editUser.avatar ? values.avatar : undefined,
role: values.role !== editUser.role ? values.role : undefined,
status: values.status !== editUser.status ? values.status : undefined,
};
// Only send fields that have changed
const hasChanges = Object.values(updateRequest).some(value => value !== undefined);
if (!hasChanges) {
notifications.show({
title: 'No Changes',
message: 'No changes detected',
color: 'blue',
});
onClose();
return;
}
await userService.updateUser(editUser.id, updateRequest);
notifications.show({
title: 'Success',
message: 'User updated successfully',
color: 'green',
});
} else {
const createRequest: CreateUserRequest = {
email: values.email,
first_name: values.first_name,
last_name: values.last_name,
display_name: values.display_name || undefined,
avatar: values.avatar || undefined,
role: values.role,
status: values.status,
};
await userService.createUser(createRequest);
notifications.show({
title: 'Success',
message: 'User created successfully',
color: 'green',
});
}
onSuccess();
onClose();
form.reset();
} catch (error: any) {
console.error('Failed to save user:', error);
notifications.show({
title: 'Error',
message: error.message || 'Failed to save user',
color: 'red',
});
}
};
return (
<Paper
style={{
position: 'fixed',
top: 60, // Below header
right: opened ? 0 : '-400px',
bottom: 0,
width: '400px',
zIndex: 1000,
borderRadius: 0,
display: 'flex',
flexDirection: 'column',
borderLeft: '1px solid var(--mantine-color-gray-3)',
backgroundColor: 'var(--mantine-color-body)',
transition: 'right 0.3s ease',
}}
>
{/* Header */}
<Group justify="space-between" p="md" style={{ borderBottom: '1px solid var(--mantine-color-gray-3)' }}>
<Title order={4}>
{isEditing ? 'Edit User' : 'Create User'}
</Title>
<ActionIcon
variant="subtle"
color="gray"
onClick={onClose}
>
<IconX size={18} />
</ActionIcon>
</Group>
{/* Content */}
<ScrollArea style={{ flex: 1 }}>
<Box p="md">
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Group grow>
<TextInput
label="First Name"
placeholder="Enter first name"
required
{...form.getInputProps('first_name')}
/>
<TextInput
label="Last Name"
placeholder="Enter last name"
required
{...form.getInputProps('last_name')}
/>
</Group>
<TextInput
label="Display Name"
placeholder="Enter display name (optional)"
{...form.getInputProps('display_name')}
/>
<TextInput
label="Email"
placeholder="Enter email address"
required
type="email"
{...form.getInputProps('email')}
/>
<TextInput
label="Avatar URL"
placeholder="Enter avatar URL (optional)"
{...form.getInputProps('avatar')}
/>
<Group grow>
<Select
label="Role"
placeholder="Select role"
required
data={[
{ value: 'admin', label: 'Admin' },
{ value: 'moderator', label: 'Moderator' },
{ value: 'user', label: 'User' },
{ value: 'viewer', label: 'Viewer' },
]}
{...form.getInputProps('role')}
/>
<Select
label="Status"
placeholder="Select status"
required
data={[
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
{ value: 'suspended', label: 'Suspended' },
{ value: 'pending', label: 'Pending' },
]}
{...form.getInputProps('status')}
/>
</Group>
<Group justify="flex-end" mt="xl">
<Button variant="light" onClick={onClose}>
Cancel
</Button>
<Button type="submit">
{isEditing ? 'Update' : 'Create'} User
</Button>
</Group>
</Stack>
</form>
</Box>
</ScrollArea>
</Paper>
);
};
export default UserSidebar;