Admin
This commit is contained in:
parent
b1061e67ac
commit
0fbbfdc997
9
exam/dist/assets/index-Cag5GO1b.js
vendored
9
exam/dist/assets/index-Cag5GO1b.js
vendored
File diff suppressed because one or more lines are too long
9
exam/dist/assets/index-SI5snNZz.js
vendored
Normal file
9
exam/dist/assets/index-SI5snNZz.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
exam/dist/index.html
vendored
4
exam/dist/index.html
vendored
@ -23,10 +23,10 @@
|
|||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>GuestBook</title>
|
<title>GuestBook</title>
|
||||||
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-Cag5GO1b.js"></script>
|
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-SI5snNZz.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C9_qfvjK.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C9_qfvjK.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-BnAUJOoN.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-BnAUJOoN.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-BqkrhB-y.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-DpDh5IPY.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/zustand-DKxXQGKw.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/zustand-DKxXQGKw.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-Be01V9yD.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-Be01V9yD.js">
|
||||||
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
|
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
|
||||||
|
|||||||
13
exam/dist/locales/de/translation.json
vendored
13
exam/dist/locales/de/translation.json
vendored
@ -20,6 +20,9 @@
|
|||||||
"NotAllowed_postUpdate": "Keine Berechtigung",
|
"NotAllowed_postUpdate": "Keine Berechtigung",
|
||||||
"NotFound_post:postUpdate": "Post nicht gefunden",
|
"NotFound_post:postUpdate": "Post nicht gefunden",
|
||||||
|
|
||||||
|
"NotAllowed_delete|PermissionsUser": "Keine Berechtigung",
|
||||||
|
"NotFound_user:delete|PermissionsUser": "Benutzer nicht gefunden",
|
||||||
|
|
||||||
"Duplicate_user:register": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert schon",
|
"Duplicate_user:register": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert schon",
|
||||||
|
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
@ -101,5 +104,13 @@
|
|||||||
|
|
||||||
"Password confirm": "Passwort bestätigen",
|
"Password confirm": "Passwort bestätigen",
|
||||||
"Password match": "Passwörter stimmen nicht überein",
|
"Password match": "Passwörter stimmen nicht überein",
|
||||||
"Change password": "Passwort ändern"
|
"Change password": "Passwort ändern",
|
||||||
|
|
||||||
|
"Confirm user delete title": "Diesen User löschen?",
|
||||||
|
"Confirm user delete body": "Möchtest du {{name}} wirklich Löschen?",
|
||||||
|
|
||||||
|
"Manage users": "Benutzer verwalten",
|
||||||
|
"Admin": "Administration",
|
||||||
|
"Make Admin": "Administratorrecht erteilen",
|
||||||
|
"Demote Admin": "Administratorrecht entziehen"
|
||||||
}
|
}
|
||||||
|
|||||||
13
exam/dist/locales/en/translation.json
vendored
13
exam/dist/locales/en/translation.json
vendored
@ -20,6 +20,9 @@
|
|||||||
"NotAllowed_postUpdate": "Not allowed",
|
"NotAllowed_postUpdate": "Not allowed",
|
||||||
"NotFound_post:postUpdate": "Post not found",
|
"NotFound_post:postUpdate": "Post not found",
|
||||||
|
|
||||||
|
"NotAllowed_delete|PermissionsUser": "Not allowed",
|
||||||
|
"NotFound_user:deleteUserdelete|PermissionsUser": "User not found",
|
||||||
|
|
||||||
"Duplicate_user:register": "A user with this username or email already exists",
|
"Duplicate_user:register": "A user with this username or email already exists",
|
||||||
|
|
||||||
"username": "username",
|
"username": "username",
|
||||||
@ -102,5 +105,13 @@
|
|||||||
|
|
||||||
"Password confirm": "Confirm password",
|
"Password confirm": "Confirm password",
|
||||||
"Password match": "Password do not match",
|
"Password match": "Password do not match",
|
||||||
"Change password": "Change password"
|
"Change password": "Change password",
|
||||||
|
|
||||||
|
"Confirm user delete title": "Diesen User löschen?",
|
||||||
|
"Confirm user delete body": "Möchtest du {{name}} wirklich Löschen?",
|
||||||
|
|
||||||
|
"Manage users": "Manage users",
|
||||||
|
"Admin": "Administration",
|
||||||
|
"Make Admin": "Make Admin",
|
||||||
|
"Demote Admin": "Demote Admin"
|
||||||
}
|
}
|
||||||
|
|||||||
2
exam/dist/stats.html
vendored
2
exam/dist/stats.html
vendored
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "react",
|
"name": "react",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.1",
|
"version": "3.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -20,6 +20,9 @@
|
|||||||
"NotAllowed_postUpdate": "Keine Berechtigung",
|
"NotAllowed_postUpdate": "Keine Berechtigung",
|
||||||
"NotFound_post:postUpdate": "Post nicht gefunden",
|
"NotFound_post:postUpdate": "Post nicht gefunden",
|
||||||
|
|
||||||
|
"NotAllowed_delete|PermissionsUser": "Keine Berechtigung",
|
||||||
|
"NotFound_user:delete|PermissionsUser": "Benutzer nicht gefunden",
|
||||||
|
|
||||||
"Duplicate_user:register": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert schon",
|
"Duplicate_user:register": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert schon",
|
||||||
|
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
@ -101,5 +104,13 @@
|
|||||||
|
|
||||||
"Password confirm": "Passwort bestätigen",
|
"Password confirm": "Passwort bestätigen",
|
||||||
"Password match": "Passwörter stimmen nicht überein",
|
"Password match": "Passwörter stimmen nicht überein",
|
||||||
"Change password": "Passwort ändern"
|
"Change password": "Passwort ändern",
|
||||||
|
|
||||||
|
"Confirm user delete title": "Diesen User löschen?",
|
||||||
|
"Confirm user delete body": "Möchtest du {{name}} wirklich Löschen?",
|
||||||
|
|
||||||
|
"Manage users": "Benutzer verwalten",
|
||||||
|
"Admin": "Administration",
|
||||||
|
"Make Admin": "Administratorrecht erteilen",
|
||||||
|
"Demote Admin": "Administratorrecht entziehen"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,9 @@
|
|||||||
"NotAllowed_postUpdate": "Not allowed",
|
"NotAllowed_postUpdate": "Not allowed",
|
||||||
"NotFound_post:postUpdate": "Post not found",
|
"NotFound_post:postUpdate": "Post not found",
|
||||||
|
|
||||||
|
"NotAllowed_delete|PermissionsUser": "Not allowed",
|
||||||
|
"NotFound_user:deleteUserdelete|PermissionsUser": "User not found",
|
||||||
|
|
||||||
"Duplicate_user:register": "A user with this username or email already exists",
|
"Duplicate_user:register": "A user with this username or email already exists",
|
||||||
|
|
||||||
"username": "username",
|
"username": "username",
|
||||||
@ -102,5 +105,13 @@
|
|||||||
|
|
||||||
"Password confirm": "Confirm password",
|
"Password confirm": "Confirm password",
|
||||||
"Password match": "Password do not match",
|
"Password match": "Password do not match",
|
||||||
"Change password": "Change password"
|
"Change password": "Change password",
|
||||||
|
|
||||||
|
"Confirm user delete title": "Diesen User löschen?",
|
||||||
|
"Confirm user delete body": "Möchtest du {{name}} wirklich Löschen?",
|
||||||
|
|
||||||
|
"Manage users": "Manage users",
|
||||||
|
"Admin": "Administration",
|
||||||
|
"Make Admin": "Make Admin",
|
||||||
|
"Demote Admin": "Demote Admin"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,17 @@
|
|||||||
import { createContext, FC, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react';
|
import { createContext, FC, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
import { ERRORS } from '../components/Error/Errors';
|
||||||
import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
|
import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
|
||||||
import useGuestBookStore from '../store/store';
|
import useGuestBookStore from '../store/store';
|
||||||
import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
|
import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
|
||||||
import { User, UserCreate, UserImageUpdate, UserUpdate } from '../types/User';
|
import {
|
||||||
|
User,
|
||||||
|
UserCreate,
|
||||||
|
UserDelete,
|
||||||
|
UserImageUpdate,
|
||||||
|
UserList,
|
||||||
|
UserPermissionsUpdate,
|
||||||
|
UserUpdate,
|
||||||
|
} from '../types/User';
|
||||||
|
|
||||||
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
||||||
|
|
||||||
@ -19,11 +28,14 @@ interface ApiContext {
|
|||||||
updatePost?: (data: PostUpdate, id: number) => Promise<PostAuth>;
|
updatePost?: (data: PostUpdate, id: number) => Promise<PostAuth>;
|
||||||
deletePost?: (id: number) => Promise<PostDelete>;
|
deletePost?: (id: number) => Promise<PostDelete>;
|
||||||
|
|
||||||
|
users?: (page?: number) => Promise<UserList>;
|
||||||
user?: (id?: number) => Promise<User>;
|
user?: (id?: number) => Promise<User>;
|
||||||
createUser?: (data: UserCreate) => Promise<User>;
|
createUser?: (data: UserCreate) => Promise<User>;
|
||||||
confirmUser?: (code: string) => Promise<User>;
|
confirmUser?: (code: string) => Promise<User>;
|
||||||
updateUser?: (data: UserUpdate, id?: number) => Promise<User>;
|
updateUser?: (data: UserUpdate, id?: number) => Promise<User>;
|
||||||
updateUserImage?: (data: UserImageUpdate, id?: number) => Promise<User>;
|
updateUserImage?: (data: UserImageUpdate, id?: number) => Promise<User>;
|
||||||
|
updateUserPermissions?: (data: UserPermissionsUpdate, id: number) => Promise<User>;
|
||||||
|
deleteUser?: (id: number) => Promise<UserDelete>;
|
||||||
|
|
||||||
userPosts?: (id?: number) => Promise<PostListAuth>;
|
userPosts?: (id?: number) => Promise<PostListAuth>;
|
||||||
}
|
}
|
||||||
@ -47,11 +59,14 @@ export const useApi = () => {
|
|||||||
updatePost,
|
updatePost,
|
||||||
deletePost,
|
deletePost,
|
||||||
|
|
||||||
|
users,
|
||||||
user,
|
user,
|
||||||
createUser,
|
createUser,
|
||||||
confirmUser,
|
confirmUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateUserImage,
|
updateUserImage,
|
||||||
|
updateUserPermissions,
|
||||||
|
deleteUser,
|
||||||
|
|
||||||
userPosts,
|
userPosts,
|
||||||
} = useContext(ApiContext);
|
} = useContext(ApiContext);
|
||||||
@ -63,11 +78,14 @@ export const useApi = () => {
|
|||||||
newPost &&
|
newPost &&
|
||||||
updatePost &&
|
updatePost &&
|
||||||
deletePost &&
|
deletePost &&
|
||||||
|
users &&
|
||||||
user &&
|
user &&
|
||||||
createUser &&
|
createUser &&
|
||||||
confirmUser &&
|
confirmUser &&
|
||||||
updateUser &&
|
updateUser &&
|
||||||
updateUserImage &&
|
updateUserImage &&
|
||||||
|
updateUserPermissions &&
|
||||||
|
deleteUser &&
|
||||||
userPosts
|
userPosts
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
@ -82,11 +100,14 @@ export const useApi = () => {
|
|||||||
updatePost,
|
updatePost,
|
||||||
deletePost,
|
deletePost,
|
||||||
|
|
||||||
|
users,
|
||||||
user,
|
user,
|
||||||
createUser,
|
createUser,
|
||||||
confirmUser,
|
confirmUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateUserImage,
|
updateUserImage,
|
||||||
|
updateUserPermissions,
|
||||||
|
deleteUser,
|
||||||
|
|
||||||
userPosts,
|
userPosts,
|
||||||
};
|
};
|
||||||
@ -157,6 +178,12 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
|
|||||||
return await (await reAuth(() => _delete(`posts/${id}?l=${POST_LIMIT}`))).json();
|
return await (await reAuth(() => _delete(`posts/${id}?l=${POST_LIMIT}`))).json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const users = async (page?: number): Promise<UserList> => {
|
||||||
|
const url = `users?p=${page ?? 0}&l=${POST_LIMIT}`;
|
||||||
|
|
||||||
|
return await (await reAuth(() => getAuth(url))).json();
|
||||||
|
};
|
||||||
|
|
||||||
const user = async (id?: number): Promise<User> => {
|
const user = async (id?: number): Promise<User> => {
|
||||||
return await (await reAuth(() => getAuth(`users/${id ?? authenticatedUser?.id}`))).json();
|
return await (await reAuth(() => getAuth(`users/${id ?? authenticatedUser?.id}`))).json();
|
||||||
};
|
};
|
||||||
@ -187,10 +214,21 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
|
|||||||
return _user;
|
return _user;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateUserPermissions = async (data: UserPermissionsUpdate, id: number): Promise<User> => {
|
||||||
|
const _user = await (
|
||||||
|
await reAuth(() => patchAuth(`users/${id}/permissions`, data as Record<string, unknown>))
|
||||||
|
).json();
|
||||||
|
return _user;
|
||||||
|
};
|
||||||
|
|
||||||
const userPosts = async (id?: number): Promise<PostListAuth> => {
|
const userPosts = async (id?: number): Promise<PostListAuth> => {
|
||||||
return await (await reAuth(() => getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`))).json();
|
return await (await reAuth(() => getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`))).json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteUser = async (id: number): Promise<UserDelete> => {
|
||||||
|
return await (await reAuth(() => _delete(`users/${id}?l=${POST_LIMIT}`))).json();
|
||||||
|
};
|
||||||
|
|
||||||
/* IMPL */
|
/* IMPL */
|
||||||
|
|
||||||
const post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
const post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||||
@ -284,9 +322,13 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
|
|||||||
const ret = await callback();
|
const ret = await callback();
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
} catch {
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('[REAUTH] failed once', error);
|
||||||
|
if (error.code !== ERRORS.UNAUTHORIZED) throw error;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[REAUTH] fail, refreshing');
|
console.log('[REAUTH] failed due to authentication, try refreshing session');
|
||||||
// REAUTH
|
// REAUTH
|
||||||
await refresh();
|
await refresh();
|
||||||
// DO AGAIN
|
// DO AGAIN
|
||||||
@ -294,13 +336,13 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
|
|||||||
const ret = await callback();
|
const ret = await callback();
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
console.log('[REAUTH] terminating session', error);
|
console.log('[REAUTH] terminating session', _error);
|
||||||
setAuthenticatedUser(undefined);
|
setAuthenticatedUser(undefined);
|
||||||
setHasAuth(false);
|
setHasAuth(false);
|
||||||
setCurrentSession([undefined, undefined]);
|
setCurrentSession([undefined, undefined]);
|
||||||
token.current = undefined;
|
token.current = undefined;
|
||||||
throw error;
|
throw _error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -332,11 +374,14 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
|
|||||||
updatePost,
|
updatePost,
|
||||||
deletePost,
|
deletePost,
|
||||||
|
|
||||||
|
users,
|
||||||
user,
|
user,
|
||||||
createUser,
|
createUser,
|
||||||
confirmUser,
|
confirmUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateUserImage,
|
updateUserImage,
|
||||||
|
updateUserPermissions,
|
||||||
|
deleteUser,
|
||||||
|
|
||||||
userPosts,
|
userPosts,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -35,9 +35,7 @@ const PostEditDialog: FC<Props> = ({ post, open, onClose }) => {
|
|||||||
const Api = useApi();
|
const Api = useApi();
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ data, id }: { data: PostUpdate; id: number }) => {
|
mutationFn: ({ data, id }: { data: PostUpdate; id: number }) => Api.updatePost(data, id),
|
||||||
return Api.updatePost(data, id);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<PostUpdate>({
|
const form = useForm<PostUpdate>({
|
||||||
|
|||||||
@ -35,9 +35,7 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
|
|||||||
const Api = useApi();
|
const Api = useApi();
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: ({ data }: { data: UserCreate }) => {
|
mutationFn: ({ data }: { data: UserCreate }) => Api.createUser(data),
|
||||||
return Api.createUser(data);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<UserCreate & { passwordConfirm: string }>({
|
const form = useForm<UserCreate & { passwordConfirm: string }>({
|
||||||
|
|||||||
@ -34,9 +34,7 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
|
|||||||
const Api = useApi();
|
const Api = useApi();
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => {
|
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => Api.updateUser(data, id),
|
||||||
return Api.updateUser(data, id);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<UserUpdate>({
|
const form = useForm<UserUpdate>({
|
||||||
|
|||||||
@ -44,9 +44,7 @@ const UserImageDialog: FC<Props> = ({ user, open, onClose }) => {
|
|||||||
const Api = useApi();
|
const Api = useApi();
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ data, id }: { data: UserImageUpdate; id?: number }) => {
|
mutationFn: ({ data, id }: { data: UserImageUpdate; id?: number }) => Api.updateUserImage(data, id),
|
||||||
return Api.updateUserImage(data, id);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<UserImageUpdate>({
|
const form = useForm<UserImageUpdate>({
|
||||||
|
|||||||
@ -34,9 +34,7 @@ const UserPasswordDialog: FC<Props> = ({ user, open, onClose }) => {
|
|||||||
const Api = useApi();
|
const Api = useApi();
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => {
|
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => Api.updateUser(data, id),
|
||||||
return Api.updateUser(data, id);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<UserUpdate & { passwordConfirm: string }>({
|
const form = useForm<UserUpdate & { passwordConfirm: string }>({
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ErrorOutline } from '@mui/icons-material';
|
import { ErrorOutline } from '@mui/icons-material';
|
||||||
import { Grid, Link as MUILink, Typography } from '@mui/material';
|
import { Grid, Link as MUILink, Typography } from '@mui/material';
|
||||||
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
|
import { useQueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
|
||||||
import { ErrorRouteComponent as TSErrorRouteComponent, useNavigate, useRouter } from '@tanstack/react-router';
|
import { ErrorRouteComponent as TSErrorRouteComponent, useNavigate, useRouter } from '@tanstack/react-router';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -13,10 +13,11 @@ const ErrorRouterComponent: TSErrorRouteComponent = ({ error }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryErrorResetBoundary = useQueryErrorResetBoundary();
|
const queryErrorResetBoundary = useQueryErrorResetBoundary();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset the query error boundary
|
// Reset the query error boundary
|
||||||
console.log(queryErrorResetBoundary.isReset());
|
console.log(queryErrorResetBoundary);
|
||||||
queryErrorResetBoundary.reset();
|
queryErrorResetBoundary.reset();
|
||||||
}, [queryErrorResetBoundary]);
|
}, [queryErrorResetBoundary]);
|
||||||
|
|
||||||
@ -34,10 +35,9 @@ const ErrorRouterComponent: TSErrorRouteComponent = ({ error }) => {
|
|||||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<MUILink
|
<MUILink
|
||||||
variant="h6"
|
variant="h6"
|
||||||
underline="hover"
|
|
||||||
sx={{ cursor: 'pointer' }}
|
sx={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('CLICK AS WELL');
|
queryClient.clear();
|
||||||
router.invalidate();
|
router.invalidate();
|
||||||
navigate({ to: ROUTES.INDEX });
|
navigate({ to: ROUTES.INDEX });
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -19,9 +19,7 @@ const PostForm: FC = () => {
|
|||||||
const Api = useApi();
|
const Api = useApi();
|
||||||
|
|
||||||
const newMutation = useMutation({
|
const newMutation = useMutation({
|
||||||
mutationFn: ({ data }: { data: PostCreate }) => {
|
mutationFn: ({ data }: { data: PostCreate }) => Api.newPost(data),
|
||||||
return Api.newPost(data);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<PostCreate>({
|
const form = useForm<PostCreate>({
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Box, Link, Menu, MenuItem, Typography } from '@mui/material';
|
import { Box, Divider, Link, Menu, MenuItem, Typography } from '@mui/material';
|
||||||
import { useMatch, useNavigate, useRouter } from '@tanstack/react-router';
|
import { useMatch, useNavigate, useRouter } from '@tanstack/react-router';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
@ -69,6 +69,14 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
|
|||||||
>
|
>
|
||||||
{t('Log out')}
|
{t('Log out')}
|
||||||
</MenuItem>,
|
</MenuItem>,
|
||||||
|
Api.authenticatedUser.isAdmin && [
|
||||||
|
<Divider>
|
||||||
|
<Typography variant="caption">{t('Admin')}</Typography>
|
||||||
|
</Divider>,
|
||||||
|
<MenuItem key="users" onClick={() => navigate({ to: ROUTES.USERS })}>
|
||||||
|
{t('Manage users')}
|
||||||
|
</MenuItem>,
|
||||||
|
],
|
||||||
]
|
]
|
||||||
) : register ? (
|
) : register ? (
|
||||||
<RegisterDialog open={register} onClose={() => setRegister(false)} />
|
<RegisterDialog open={register} onClose={() => setRegister(false)} />
|
||||||
@ -81,7 +89,6 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
|
|||||||
<Link
|
<Link
|
||||||
sx={{ cursor: 'pointer' }}
|
sx={{ cursor: 'pointer' }}
|
||||||
variant="body1"
|
variant="body1"
|
||||||
underline="hover"
|
|
||||||
color="secondary.main"
|
color="secondary.main"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setRegister(true);
|
setRegister(true);
|
||||||
|
|||||||
@ -44,9 +44,7 @@ const Post: FC<Props> = ({ page = 0, post, disableActions }) => {
|
|||||||
const Api = useApi();
|
const Api = useApi();
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: (id: number) => {
|
mutationFn: (id: number) => Api.deletePost(id),
|
||||||
return Api.deletePost(id);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -41,11 +41,17 @@ const Profile: FC<Props> = ({ user, posts, canEdit }) => {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center' }}>
|
<Grid item sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center' }}>
|
||||||
<IconButton onClick={() => setImageOpen(true)}>
|
{canEdit ? (
|
||||||
|
<IconButton onClick={() => setImageOpen(true)}>
|
||||||
|
<Avatar alt={user.username} src={`${user.image}`} sx={{ width: '100px', height: '100px' }}>
|
||||||
|
<Person sx={{ width: '60px', height: '60px' }} />
|
||||||
|
</Avatar>
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
<Avatar alt={user.username} src={`${user.image}`} sx={{ width: '100px', height: '100px' }}>
|
<Avatar alt={user.username} src={`${user.image}`} sx={{ width: '100px', height: '100px' }}>
|
||||||
<Person sx={{ width: '60px', height: '60px' }} />
|
<Person sx={{ width: '60px', height: '60px' }} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</IconButton>
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item sx={{ display: 'flex', alignItems: 'center' }}>
|
<Grid item sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', columnGap: 1 }}>
|
<Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', columnGap: 1 }}>
|
||||||
|
|||||||
10
exam/react/src/queries/usersQuery.ts
Normal file
10
exam/react/src/queries/usersQuery.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
import { useApi } from '../api/Api';
|
||||||
|
import { ERRORS } from '../components/Error/Errors';
|
||||||
|
|
||||||
|
export const usersQueryOptions = (Api: ReturnType<typeof useApi>, page?: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ['users', { page: page ?? 0 }],
|
||||||
|
queryFn: async () => await Api.users(page),
|
||||||
|
retry: (count, error) => ('code' in error && error.code !== ERRORS.UNAUTHORIZED ? count < 3 : false),
|
||||||
|
});
|
||||||
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
|
import { Route as UsersIndexImport } from './routes/users/index'
|
||||||
import { Route as ProfileIndexImport } from './routes/profile/index'
|
import { Route as ProfileIndexImport } from './routes/profile/index'
|
||||||
import { Route as ConfirmIndexImport } from './routes/confirm/index'
|
import { Route as ConfirmIndexImport } from './routes/confirm/index'
|
||||||
import { Route as ProfileIdImport } from './routes/profile/$id'
|
import { Route as ProfileIdImport } from './routes/profile/$id'
|
||||||
@ -23,6 +24,11 @@ const IndexRoute = IndexImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const UsersIndexRoute = UsersIndexImport.update({
|
||||||
|
path: '/users/',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const ProfileIndexRoute = ProfileIndexImport.update({
|
const ProfileIndexRoute = ProfileIndexImport.update({
|
||||||
path: '/profile/',
|
path: '/profile/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@ -70,6 +76,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ProfileIndexImport
|
preLoaderRoute: typeof ProfileIndexImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/users/': {
|
||||||
|
id: '/users/'
|
||||||
|
path: '/users'
|
||||||
|
fullPath: '/users'
|
||||||
|
preLoaderRoute: typeof UsersIndexImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +93,7 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
ProfileIdRoute,
|
ProfileIdRoute,
|
||||||
ConfirmIndexRoute,
|
ConfirmIndexRoute,
|
||||||
ProfileIndexRoute,
|
ProfileIndexRoute,
|
||||||
|
UsersIndexRoute,
|
||||||
})
|
})
|
||||||
|
|
||||||
/* prettier-ignore-end */
|
/* prettier-ignore-end */
|
||||||
@ -93,7 +107,8 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"/",
|
"/",
|
||||||
"/profile/$id",
|
"/profile/$id",
|
||||||
"/confirm/",
|
"/confirm/",
|
||||||
"/profile/"
|
"/profile/",
|
||||||
|
"/users/"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
@ -107,6 +122,9 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
},
|
},
|
||||||
"/profile/": {
|
"/profile/": {
|
||||||
"filePath": "profile/index.tsx"
|
"filePath": "profile/index.tsx"
|
||||||
|
},
|
||||||
|
"/users/": {
|
||||||
|
"filePath": "users/index.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { createRouter, ErrorRouteComponent, RouterProvider, useRouter } from '@t
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useApi } from './api/Api';
|
import { useApi } from './api/Api';
|
||||||
|
|
||||||
|
import ErrorRouterComponent from './components/Error/ErrorRouterComponent';
|
||||||
|
import NotFoundComponent from './components/NotFound/NotFoundComponent';
|
||||||
import { routeTree } from './routeTree.gen';
|
import { routeTree } from './routeTree.gen';
|
||||||
|
|
||||||
//TODO: REAUTH HERE
|
//TODO: REAUTH HERE
|
||||||
@ -51,6 +53,8 @@ const router = createRouter({
|
|||||||
defaultPreloadStaleTime: 0,
|
defaultPreloadStaleTime: 0,
|
||||||
basepath: process.env.NODE_ENV === 'development' ? 'phpCourse/exam/dist' : '/phpCourse/exam',
|
basepath: process.env.NODE_ENV === 'development' ? 'phpCourse/exam/dist' : '/phpCourse/exam',
|
||||||
//defaultErrorComponent: Error,
|
//defaultErrorComponent: Error,
|
||||||
|
defaultNotFoundComponent: NotFoundComponent,
|
||||||
|
defaultErrorComponent: ErrorRouterComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register the router instance for type safety
|
// Register the router instance for type safety
|
||||||
|
|||||||
@ -3,8 +3,6 @@ import { QueryClient } from '@tanstack/react-query';
|
|||||||
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
||||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||||
import { useApi } from '../api/Api';
|
import { useApi } from '../api/Api';
|
||||||
import ErrorRouterComponent from '../components/Error/ErrorRouterComponent';
|
|
||||||
import NotFoundComponent from '../components/NotFound/NotFoundComponent';
|
|
||||||
|
|
||||||
const Root = () => {
|
const Root = () => {
|
||||||
return (
|
return (
|
||||||
@ -17,6 +15,4 @@ const Root = () => {
|
|||||||
|
|
||||||
export const Route = createRootRouteWithContext<{ queryClient: QueryClient; Api: ReturnType<typeof useApi> }>()({
|
export const Route = createRootRouteWithContext<{ queryClient: QueryClient; Api: ReturnType<typeof useApi> }>()({
|
||||||
component: Root,
|
component: Root,
|
||||||
notFoundComponent: NotFoundComponent,
|
|
||||||
errorComponent: ErrorRouterComponent,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,9 +15,7 @@ const Home = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const confirmMutation = useMutation({
|
const confirmMutation = useMutation({
|
||||||
mutationFn: ({ code: _code }: { code: string }) => {
|
mutationFn: ({ code: _code }: { code: string }) => Api.confirmUser(_code),
|
||||||
return Api.confirmUser(_code);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const Home = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((page ?? 0) >= postsQuery.pages) {
|
if ((page ?? 0) >= postsQuery.pages) {
|
||||||
navigate({ to: '/', search: { page: postsQuery.pages - 1 } });
|
navigate({ to: ROUTES.INDEX, search: { page: postsQuery.pages - 1 } });
|
||||||
}
|
}
|
||||||
}, [page]); //eslint-disable-line react-hooks/exhaustive-deps
|
}, [page]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
|||||||
203
exam/react/src/routes/users/index.tsx
Normal file
203
exam/react/src/routes/users/index.tsx
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardActions,
|
||||||
|
CardContent,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
Link as MUILink,
|
||||||
|
Pagination,
|
||||||
|
PaginationItem,
|
||||||
|
Snackbar,
|
||||||
|
Typography,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
import { createFileRoute, Link, redirect, useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useApi } from '../../api/Api';
|
||||||
|
import ErrorComponent from '../../components/Error/ErrorComponent';
|
||||||
|
import { ERRORS } from '../../components/Error/Errors';
|
||||||
|
import HeaderLayout from '../../components/Layouts/HeaderLayout';
|
||||||
|
import { usersQueryOptions } from '../../queries/usersQuery';
|
||||||
|
import { ROUTES } from '../../types/Routes';
|
||||||
|
import { UserPermissionsUpdate } from '../../types/User';
|
||||||
|
|
||||||
|
const Users = () => {
|
||||||
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const [_error, setError] = useState<any>();
|
||||||
|
|
||||||
|
const Api = useApi();
|
||||||
|
const { page } = Route.useSearch();
|
||||||
|
const { data: usersQuery, isFetching, failureReason, error } = useSuspenseQuery(usersQueryOptions(Api, page));
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const theme = useTheme();
|
||||||
|
const oneSibling = useMediaQuery(theme.breakpoints.not('xs'), { noSsr: true });
|
||||||
|
|
||||||
|
if (failureReason && 'code' in failureReason && failureReason.code === ERRORS.UNAUTHORIZED) {
|
||||||
|
throw failureReason;
|
||||||
|
}
|
||||||
|
if (error && 'code' in error && error.code === ERRORS.UNAUTHORIZED) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: (id: number) => Api.deleteUser(id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const permissionMutation = useMutation({
|
||||||
|
mutationFn: ({ data, id }: { data: UserPermissionsUpdate; id: number }) => Api.updateUserPermissions(data, id),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((page ?? 0) >= usersQuery.pages) {
|
||||||
|
navigate({ to: ROUTES.USERS, search: { page: usersQuery.pages - 1 } });
|
||||||
|
}
|
||||||
|
}, [page]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HeaderLayout>
|
||||||
|
<Snackbar open={isFetching} message={t('Updating')} />
|
||||||
|
<Grid container spacing={2} sx={{ marginTop: 0 }}>
|
||||||
|
{usersQuery.data.map((user) => (
|
||||||
|
<Grid item xs={12} key={user.id}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
{user.id !== Api.authenticatedUser?.id ? (
|
||||||
|
<MUILink component={Link} to="/profile/$id" params={{ id: user.id }}>
|
||||||
|
{user.username}
|
||||||
|
</MUILink>
|
||||||
|
) : (
|
||||||
|
<Typography>{user.username}</Typography>
|
||||||
|
)}
|
||||||
|
<Typography>{user.email}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
{user.id !== Api.authenticatedUser?.id && (
|
||||||
|
<>
|
||||||
|
<CardActions>
|
||||||
|
<Button size="small" color="error" onClick={() => setDeleteOpen(true)}>
|
||||||
|
{t('Delete')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
permissionMutation.mutate(
|
||||||
|
{ data: { isAdmin: !user.isAdmin }, id: user.id },
|
||||||
|
{
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ['users'],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: setError,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(user.isAdmin ? 'Demote Admin' : 'Make Admin')}
|
||||||
|
</Button>
|
||||||
|
<Dialog open={deleteOpen} onClose={() => setDeleteOpen(false)}>
|
||||||
|
<DialogTitle>{t('Confirm user delete title')}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>{t('Confirm user delete body', { name: user.username })}</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDeleteOpen(false)} autoFocus variant="contained">
|
||||||
|
{t('No')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={() => {
|
||||||
|
deleteMutation.mutate(user.id, {
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ['users'],
|
||||||
|
});
|
||||||
|
if ((page ?? 0) >= data.pages)
|
||||||
|
navigate({ to: ROUTES.PROFILE, search: { page: data.pages - 1 } });
|
||||||
|
},
|
||||||
|
onError: setError,
|
||||||
|
});
|
||||||
|
setDeleteOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Yes')}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</CardActions>
|
||||||
|
<Snackbar
|
||||||
|
open={deleteMutation.isError || permissionMutation.isError}
|
||||||
|
autoHideDuration={2000}
|
||||||
|
onClose={() => {
|
||||||
|
deleteMutation.reset();
|
||||||
|
permissionMutation.reset();
|
||||||
|
}}
|
||||||
|
TransitionProps={{
|
||||||
|
onExited: () => setError(undefined),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Alert severity="error" variant="filled" sx={{ width: '100%' }}>
|
||||||
|
{error && <ErrorComponent error={_error} context="delete|PermissionsUser" color="white" />}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
<Snackbar open={deleteMutation.isPending} message={t('Deleting')} />
|
||||||
|
<Snackbar open={permissionMutation.isPending} message={t('Updating')} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Divider variant="middle" />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Pagination
|
||||||
|
page={(page ?? 0) + 1}
|
||||||
|
count={usersQuery.pages}
|
||||||
|
siblingCount={oneSibling ? 1 : 0}
|
||||||
|
color="primary"
|
||||||
|
renderItem={(item) => (
|
||||||
|
<PaginationItem
|
||||||
|
{...item}
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
search={{ page: (item.page ?? 0) > 0 ? (item.page ?? 1) - 1 : undefined }}
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
onClick={(e) => item.onClick(e as any)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</HeaderLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Route = createFileRoute(`${ROUTES.USERS}/`)({
|
||||||
|
loaderDeps: ({ search: { page } }) => ({ page }),
|
||||||
|
loader: ({ context: { queryClient, Api }, deps: { page } }) =>
|
||||||
|
queryClient.ensureQueryData(usersQueryOptions(Api, page)),
|
||||||
|
validateSearch: (search: Record<string, unknown>): { page?: number } => {
|
||||||
|
return {
|
||||||
|
page: search?.page !== undefined ? Number(search?.page ?? 0) : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
beforeLoad: ({ context: { Api } }) => {
|
||||||
|
if ((!Api.hasAuth && !Api.currentSession[0]) || !Api.authenticatedUser?.isAdmin)
|
||||||
|
throw redirect({ to: ROUTES.INDEX });
|
||||||
|
},
|
||||||
|
component: Users,
|
||||||
|
});
|
||||||
@ -2,4 +2,5 @@ export enum ROUTES {
|
|||||||
INDEX = '/',
|
INDEX = '/',
|
||||||
PROFILE = '/profile',
|
PROFILE = '/profile',
|
||||||
CONFIRM = '/confirm',
|
CONFIRM = '/confirm',
|
||||||
|
USERS = '/users',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,10 @@ export interface UserUpdate {
|
|||||||
password?: string;
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserPermissionsUpdate {
|
||||||
|
isAdmin?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserCreate {
|
export interface UserCreate {
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -32,3 +36,13 @@ export interface UserImageUpdate {
|
|||||||
image?: File;
|
image?: File;
|
||||||
predefined?: string;
|
predefined?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserList {
|
||||||
|
pages: number;
|
||||||
|
data: User[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDelete {
|
||||||
|
pages: number;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user