Polish mobile layout and add collapsible sidebar

This commit is contained in:
2026-03-29 14:24:43 +02:00
parent 4253d33dfd
commit 99fc94bc18
7 changed files with 833 additions and 321 deletions
+81 -32
View File
@@ -7,9 +7,11 @@ import {
Chip,
FormControlLabel,
Paper,
Stack,
TextField,
Typography,
} from "@mui/material";
import useMediaQuery from "@mui/material/useMediaQuery";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { api, getApiErrorMessage } from "../api";
@@ -26,6 +28,7 @@ type UserDto = {
};
export default function AdminUsersPage() {
const isMobile = useMediaQuery("(max-width:767.95px)");
const { toast } = useToast();
const { confirmAction } = useDialogActions();
const { t } = useI18n();
@@ -149,23 +152,24 @@ export default function AdminUsersPage() {
], [remove, sendReset, setAdminRole, t]);
return (
<Paper sx={{ p: 2 }}>
<Paper sx={{ p: { xs: 1.5, sm: 2 } }}>
<Typography variant="h6" sx={{ fontWeight: 950, mb: 0.5 }}>
{t("adminUsersTitle")}
</Typography>
<Typography sx={{ color: "text.secondary", mb: 2 }}>{t("adminUsersSubtitle")}</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Paper sx={{ p: { xs: 1.5, sm: 2 }, mb: 2 }}>
<Typography sx={{ fontWeight: 900, mb: 1 }}>{t("adminUsersCreateUser")}</Typography>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
<TextField label={t("profileEmail")} value={newEmail} onChange={(e) => setNewEmail(e.target.value)} />
<TextField label={t("profileNewPassword")} type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
<TextField label={t("profileEmail")} value={newEmail} onChange={(e) => setNewEmail(e.target.value)} fullWidth />
<TextField label={t("profileNewPassword")} type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} fullWidth />
</Box>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, mt: 1.5, flexWrap: "wrap" }}>
<Box sx={{ display: "flex", alignItems: { xs: "stretch", sm: "center" }, justifyContent: "space-between", gap: 2, mt: 1.5, flexWrap: "wrap" }}>
<FormControlLabel control={<Checkbox checked={newIsAdmin} onChange={(e) => setNewIsAdmin(e.target.checked)} />} label={t("adminUsersAdmin")} />
<Button
variant="contained"
disabled={!canCreate || loading}
sx={{ width: { xs: "100%", sm: "auto" } }}
onClick={async () => {
try {
await api.post("/users", { email: newEmail, password: newPassword, roles: newIsAdmin ? ["Admin"] : [] });
@@ -185,33 +189,78 @@ export default function AdminUsersPage() {
</Box>
</Paper>
<Paper sx={{ borderRadius: 3, border: "1px solid", borderColor: "divider", overflow: "hidden" }}>
<DataGrid
autoHeight
rows={rows}
columns={columns}
disableRowSelectionOnClick
loading={loading}
pageSizeOptions={[5, 10, 25]}
initialState={{
pagination: { paginationModel: { pageSize: 10, page: 0 } },
sorting: { sortModel: [{ field: "email", sort: "asc" }] },
}}
sx={{
border: 0,
'& .MuiDataGrid-columnHeaders': {
backgroundColor: 'action.hover',
fontWeight: 800,
},
'& .MuiDataGrid-cell': {
alignItems: 'center',
},
}}
/>
{!loading && rows.length === 0 ? (
<Typography sx={{ color: "text.secondary", py: 2, textAlign: "center" }}>{t("adminUsersNoUsers")}</Typography>
) : null}
</Paper>
{isMobile ? (
<Stack spacing={1.5}>
{!loading && rows.length === 0 ? (
<Typography sx={{ color: "text.secondary", py: 2, textAlign: "center" }}>{t("adminUsersNoUsers")}</Typography>
) : null}
{rows.map((row) => {
const user = row.raw as UserDto;
const isAdmin = user.roles.includes("Admin");
return (
<Paper key={row.id} sx={{ p: 1.5, borderRadius: 3 }}>
<Stack spacing={1.25}>
<Box>
<Typography sx={{ fontWeight: 900, overflowWrap: "anywhere" }}>
{row.userName || row.email || row.id}
</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", overflowWrap: "anywhere" }}>
{row.email || "—"}
</Typography>
</Box>
<Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap" }}>
{(row.roles as string[]).length ? (row.roles as string[]).map((role) => (
<Chip key={role} size="small" label={role} variant="outlined" />
)) : <Chip size="small" label="—" variant="outlined" />}
<Chip size="small" label={row.emailConfirmed ? t("yes") : t("noWord")} color={row.emailConfirmed ? "success" : "default"} variant={row.emailConfirmed ? "filled" : "outlined"} />
</Box>
<Stack spacing={1}>
<Button variant={isAdmin ? "contained" : "outlined"} onClick={() => void setAdminRole(user, !isAdmin)} fullWidth>
{t("adminUsersAdmin")}
</Button>
<Button variant="outlined" onClick={() => void sendReset(user)} fullWidth>
{t("adminUsersSendReset")}
</Button>
<Button color="error" variant="outlined" onClick={() => void remove(user)} fullWidth>
{t("adminUsersDelete")}
</Button>
</Stack>
</Stack>
</Paper>
);
})}
</Stack>
) : (
<Paper sx={{ borderRadius: 3, border: "1px solid", borderColor: "divider", overflow: "hidden" }}>
<DataGrid
autoHeight
rows={rows}
columns={columns}
disableRowSelectionOnClick
loading={loading}
pageSizeOptions={[5, 10, 25]}
initialState={{
pagination: { paginationModel: { pageSize: 10, page: 0 } },
sorting: { sortModel: [{ field: "email", sort: "asc" }] },
}}
sx={{
border: 0,
'& .MuiDataGrid-columnHeaders': {
backgroundColor: 'action.hover',
fontWeight: 800,
},
'& .MuiDataGrid-cell': {
alignItems: 'center',
},
}}
/>
{!loading && rows.length === 0 ? (
<Typography sx={{ color: "text.secondary", py: 2, textAlign: "center" }}>{t("adminUsersNoUsers")}</Typography>
) : null}
</Paper>
)}
</Paper>
);
}