mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
refactor: refactored course & lesson query hooks
This commit is contained in:
parent
2fa555b8be
commit
dd86228cab
6
package-lock.json
generated
6
package-lock.json
generated
@ -2552,9 +2552,9 @@
|
|||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.4.4",
|
"version": "4.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
|
||||||
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
|
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"use-callback-ref": {
|
"use-callback-ref": {
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
"@types/validator": "^13.6.3",
|
"@types/validator": "^13.6.3",
|
||||||
"@vitejs/plugin-react": "^1.0.0",
|
"@vitejs/plugin-react": "^1.0.0",
|
||||||
"rollup-plugin-visualizer": "^5.5.2",
|
"rollup-plugin-visualizer": "^5.5.2",
|
||||||
"typescript": "^4.3.2",
|
"typescript": "^4.5.2",
|
||||||
"vite": "^2.6.4"
|
"vite": "^2.6.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,17 +17,9 @@ import CourseForm from './CourseForm';
|
|||||||
/**
|
/**
|
||||||
* Hooks
|
* Hooks
|
||||||
*/
|
*/
|
||||||
import { useState } from 'react';
|
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useToast } from '@chakra-ui/toast';
|
import { useCreateCourse } from '@educt/hooks/queries';
|
||||||
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
|
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Services
|
|
||||||
*/
|
|
||||||
import { CourseServiceInstance } from '@educt/services';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema
|
* Schema
|
||||||
@ -37,21 +29,16 @@ import CourseFormSchema from './CourseForm.validator';
|
|||||||
type CreateFormCoursePropsType = {};
|
type CreateFormCoursePropsType = {};
|
||||||
|
|
||||||
const CreateFormCourse: React.FC<CreateFormCoursePropsType> = () => {
|
const CreateFormCourse: React.FC<CreateFormCoursePropsType> = () => {
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
const form = useForm<InputFields>({ resolver: yupResolver(CourseFormSchema) });
|
const form = useForm<InputFields>({ resolver: yupResolver(CourseFormSchema) });
|
||||||
const isMountedRef = useIsMountedRef();
|
const { createCourse, isLoading } = useCreateCourse();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const toast = useToast();
|
|
||||||
const handleError = useErrorHandler();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit handler
|
* Submit handler
|
||||||
*/
|
*/
|
||||||
// TODO: move request to useCreateCourse hook
|
|
||||||
const onSubmit: SubmitHandler<InputFields> = async data => {
|
const onSubmit: SubmitHandler<InputFields> = async data => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
const course = await createCourse({
|
||||||
const course = await CourseServiceInstance.create({
|
|
||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
teacher_id: data.teacher_id,
|
teacher_id: data.teacher_id,
|
||||||
@ -59,22 +46,10 @@ const CreateFormCourse: React.FC<CreateFormCoursePropsType> = () => {
|
|||||||
image: data.image,
|
image: data.image,
|
||||||
status: CourseStatusEnum.DRAFT,
|
status: CourseStatusEnum.DRAFT,
|
||||||
});
|
});
|
||||||
toast({ title: `Course successfully created.`, status: 'success' });
|
|
||||||
history.push(`/courses/edit/${course.data.id}`);
|
if (course) history.push(`/courses/edit/${course.id}`);
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
if (error.response) {
|
console.error(error);
|
||||||
if (error.response.status === 422) {
|
|
||||||
toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
|
|
||||||
} else {
|
|
||||||
toast({ title: `${error.message}`, status: 'error' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handleError(error);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (isMountedRef.current) {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -16,21 +16,14 @@ import CourseForm from './CourseForm';
|
|||||||
/**
|
/**
|
||||||
* Hooks
|
* Hooks
|
||||||
*/
|
*/
|
||||||
import { useState } from 'react';
|
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
|
||||||
import { useToast } from '@chakra-ui/toast';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Services
|
|
||||||
*/
|
|
||||||
import { CourseServiceInstance } from '@educt/services';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema
|
* Schema
|
||||||
*/
|
*/
|
||||||
import CourseFormSchema from './CourseForm.validator';
|
import CourseFormSchema from './CourseForm.validator';
|
||||||
|
import { useUpdateCourse } from '@educt/hooks/queries';
|
||||||
|
|
||||||
type EditFormCoursePropsType = {
|
type EditFormCoursePropsType = {
|
||||||
defaultValues?:
|
defaultValues?:
|
||||||
@ -45,7 +38,6 @@ type EditFormCoursePropsType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EditFormCourse: React.FC<EditFormCoursePropsType> = ({ defaultValues }) => {
|
const EditFormCourse: React.FC<EditFormCoursePropsType> = ({ defaultValues }) => {
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
const form = useForm<InputFields>({
|
const form = useForm<InputFields>({
|
||||||
resolver: yupResolver(CourseFormSchema),
|
resolver: yupResolver(CourseFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -56,44 +48,30 @@ const EditFormCourse: React.FC<EditFormCoursePropsType> = ({ defaultValues }) =>
|
|||||||
image: undefined,
|
image: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const toast = useToast();
|
const { updateCourse, isLoading } = useUpdateCourse();
|
||||||
const handleError = useErrorHandler();
|
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit handler
|
* Submit handler
|
||||||
*/
|
*/
|
||||||
// TODO move request to useUpdateCourse hook
|
|
||||||
const onSubmit: SubmitHandler<InputFields> = async data => {
|
const onSubmit: SubmitHandler<InputFields> = async data => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
const updated = await updateCourse(id, data);
|
||||||
const course = await CourseServiceInstance.update(id, {
|
|
||||||
title: data.title,
|
|
||||||
description: data.description,
|
|
||||||
teacher_id: data.teacher_id,
|
|
||||||
category_id: data.category_id,
|
|
||||||
image: data.image,
|
|
||||||
});
|
|
||||||
toast({ title: `Course updated.`, status: 'info' });
|
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
/**
|
/**
|
||||||
* Update fields with new values
|
* Update fields with new values
|
||||||
*/
|
*/
|
||||||
form.reset({
|
form.reset({
|
||||||
title: course.data.title,
|
title: updated.title,
|
||||||
description: course.data.description,
|
description: updated.description,
|
||||||
teacher_id: course.data.teacher.id,
|
teacher_id: updated.teacher.id,
|
||||||
category_id: course.data.category.id,
|
category_id: updated.category.id,
|
||||||
image: undefined,
|
image: undefined,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
|
||||||
if (error.response) {
|
|
||||||
toast({ title: `${error.message}`, status: 'error' });
|
|
||||||
} else {
|
|
||||||
handleError(error);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} catch (error) {
|
||||||
setIsLoading(false);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
49
src/hooks/queries/course/useCreateCourse.ts
Normal file
49
src/hooks/queries/course/useCreateCourse.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { useToast } from '@chakra-ui/toast';
|
||||||
|
import { CourseStatusEnum } from '@educt/enums';
|
||||||
|
import { ICourse } from '@educt/interfaces';
|
||||||
|
import { CourseServiceInstance } from '@educt/services';
|
||||||
|
import { CreateCourseParamsType } from '@educt/types';
|
||||||
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
|
import useAsync from '../../useAsync';
|
||||||
|
|
||||||
|
type CreatedCourseDataType =
|
||||||
|
| Pick<ICourse, 'id' | 'title' | 'description' | 'status' | 'created_at' | 'updated_at'>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const useCreateCourse = () => {
|
||||||
|
const toast = useToast();
|
||||||
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
|
const createCourse = async (data: CreateCourseParamsType) => {
|
||||||
|
try {
|
||||||
|
const result = await CourseServiceInstance.create({
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
teacher_id: data.teacher_id,
|
||||||
|
category_id: data.category_id,
|
||||||
|
image: data.image,
|
||||||
|
status: CourseStatusEnum.DRAFT,
|
||||||
|
});
|
||||||
|
toast({ title: `Course successfully created.`, status: 'success' });
|
||||||
|
return result.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
if (error.response.status === 422) {
|
||||||
|
toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
|
||||||
|
} else {
|
||||||
|
toast({ title: `${error.message}`, status: 'error' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { execute, ...state } = useAsync<CreatedCourseDataType, Parameters<typeof createCourse>>(createCourse);
|
||||||
|
|
||||||
|
return { createCourse: execute, ...state };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useCreateCourse };
|
||||||
35
src/hooks/queries/course/useDeleteCourse.ts
Normal file
35
src/hooks/queries/course/useDeleteCourse.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useToast } from '@chakra-ui/toast';
|
||||||
|
import { ICourse } from '@educt/interfaces';
|
||||||
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
|
import useAsync from '@educt/hooks/useAsync';
|
||||||
|
|
||||||
|
type DeletedCourseDataType = Omit<ICourse, 'teacher' | 'students' | 'lessons'> | undefined;
|
||||||
|
|
||||||
|
const useDeleteCourse = () => {
|
||||||
|
const { courseStore } = useRootStore();
|
||||||
|
const toast = useToast();
|
||||||
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
|
const deleteCourse = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const result = await courseStore.deleteCourse(id);
|
||||||
|
toast({ title: 'Course deleted.', status: 'info' });
|
||||||
|
return result.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
toast({ title: error.message, status: 'error' });
|
||||||
|
} else {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { execute, ...state } = useAsync<DeletedCourseDataType, Parameters<typeof deleteCourse>>(deleteCourse);
|
||||||
|
|
||||||
|
return { deleteCourse: execute, ...state };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useDeleteCourse };
|
||||||
41
src/hooks/queries/course/useUpdateCourse.ts
Normal file
41
src/hooks/queries/course/useUpdateCourse.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { useToast } from '@chakra-ui/toast';
|
||||||
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
|
import { CourseServiceInstance } from '@educt/services';
|
||||||
|
import { UpdateCourseParamsType } from '@educt/types';
|
||||||
|
import useAsync from '../../useAsync';
|
||||||
|
import { ICourse } from '@educt/interfaces';
|
||||||
|
|
||||||
|
type UpdatedCourseDataType = Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'> | undefined;
|
||||||
|
|
||||||
|
const useUpdateCourse = () => {
|
||||||
|
const toast = useToast();
|
||||||
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
|
const updateCourse = async (id: string, data: UpdateCourseParamsType) => {
|
||||||
|
try {
|
||||||
|
const result = await CourseServiceInstance.update(id, {
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
teacher_id: data.teacher_id,
|
||||||
|
category_id: data.category_id,
|
||||||
|
image: data.image,
|
||||||
|
});
|
||||||
|
toast({ title: `Course updated.`, status: 'info' });
|
||||||
|
return result.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
toast({ title: `${error.message}`, status: 'error' });
|
||||||
|
} else {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { execute, ...state } = useAsync<UpdatedCourseDataType, Parameters<typeof updateCourse>>(updateCourse);
|
||||||
|
|
||||||
|
return { updateCourse: execute, ...state };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useUpdateCourse };
|
||||||
@ -1,3 +1,6 @@
|
|||||||
import { useDeleteLesson } from './useDeleteLesson';
|
import { useDeleteLesson } from './lesson/useDeleteLesson';
|
||||||
|
|
||||||
export { useDeleteLesson };
|
import { useCreateCourse } from './course/useCreateCourse';
|
||||||
|
import { useUpdateCourse } from './course/useUpdateCourse';
|
||||||
|
|
||||||
|
export { useCreateCourse, useUpdateCourse, useDeleteLesson };
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { useToast } from '@chakra-ui/toast';
|
import { useToast } from '@chakra-ui/toast';
|
||||||
import { ILesson } from '@educt/interfaces';
|
import { ILesson } from '@educt/interfaces';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
import useAsync from '../useAsync';
|
import useAsync from '../../useAsync';
|
||||||
import { useRootStore } from '../useRootStore';
|
import { useRootStore } from '../../useRootStore';
|
||||||
|
|
||||||
|
type CreatedLessonDataType = ILesson | undefined;
|
||||||
|
|
||||||
const useDeleteLesson = () => {
|
const useDeleteLesson = () => {
|
||||||
const {
|
const {
|
||||||
@ -22,10 +24,12 @@ const useDeleteLesson = () => {
|
|||||||
} else {
|
} else {
|
||||||
handleError(error);
|
handleError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { execute, ...state } = useAsync<ILesson | undefined, Parameters<typeof deleteLesson>>(deleteLesson);
|
const { execute, ...state } = useAsync<CreatedLessonDataType, Parameters<typeof deleteLesson>>(deleteLesson);
|
||||||
|
|
||||||
return { deleteLesson: execute, ...state };
|
return { deleteLesson: execute, ...state };
|
||||||
};
|
};
|
||||||
@ -1,14 +1,9 @@
|
|||||||
import React, { useContext, useRef, useState } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import {
|
|
||||||
AlertDialog,
|
/**
|
||||||
AlertDialogBody,
|
* Types
|
||||||
AlertDialogFooter,
|
*/
|
||||||
AlertDialogHeader,
|
import { ICourse } from '@educt/interfaces';
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
Button,
|
|
||||||
useToast,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Components
|
* Components
|
||||||
@ -23,9 +18,7 @@ import { CoursesPageContext } from '@educt/contexts';
|
|||||||
/**
|
/**
|
||||||
* Hooks
|
* Hooks
|
||||||
*/
|
*/
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
import { useDeleteCourse } from '@educt/hooks/queries/course/useDeleteCourse';
|
||||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
|
||||||
import { ICourse } from '@educt/interfaces';
|
|
||||||
|
|
||||||
type DeleteCourseDialogPropsType = {
|
type DeleteCourseDialogPropsType = {
|
||||||
course: Pick<ICourse, 'id' | 'title'>;
|
course: Pick<ICourse, 'id' | 'title'>;
|
||||||
@ -34,34 +27,14 @@ type DeleteCourseDialogPropsType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DeleteCourseDialog: React.FC<DeleteCourseDialogPropsType> = ({ onClose, isOpen, course }) => {
|
const DeleteCourseDialog: React.FC<DeleteCourseDialogPropsType> = ({ onClose, isOpen, course }) => {
|
||||||
const { courseStore } = useRootStore();
|
|
||||||
const { setDeletingCourse } = useContext(CoursesPageContext);
|
const { setDeletingCourse } = useContext(CoursesPageContext);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const { deleteCourse, isLoading } = useDeleteCourse();
|
||||||
const handleError = useErrorHandler();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
/**
|
const handleDelete = async (isConfirmed: boolean, id: string) => {
|
||||||
* Delete course handler
|
|
||||||
*
|
|
||||||
* @param isConfirmed Is dialog confirmed
|
|
||||||
* @param id Course id
|
|
||||||
*/
|
|
||||||
const deleteCourseHandler = async (isConfirmed: boolean, id: string) => {
|
|
||||||
if (isConfirmed) {
|
if (isConfirmed) {
|
||||||
try {
|
await deleteCourse(id)
|
||||||
setIsLoading(true);
|
.catch(error => console.error(error))
|
||||||
await courseStore.deleteCourse(id);
|
.finally(() => setDeletingCourse(undefined));
|
||||||
toast({ title: 'Course deleted.', status: 'info' });
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.response) {
|
|
||||||
toast({ title: error.message, status: 'error' });
|
|
||||||
} else {
|
|
||||||
handleError(error);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
setDeletingCourse(undefined);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,7 +47,7 @@ const DeleteCourseDialog: React.FC<DeleteCourseDialogPropsType> = ({ onClose, is
|
|||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onClose={isConfirmed => deleteCourseHandler(isConfirmed, course.id)}
|
onClose={isConfirmed => handleDelete(isConfirmed, course.id)}
|
||||||
title='Confirm deleting'
|
title='Confirm deleting'
|
||||||
message={`The course with title "${course.title}" will be removed from the system. After deleting a course, the lessons in it will also be deleted.`}
|
message={`The course with title "${course.title}" will be removed from the system. After deleting a course, the lessons in it will also be deleted.`}
|
||||||
confirmMessage='Delete'
|
confirmMessage='Delete'
|
||||||
|
|||||||
@ -38,7 +38,7 @@ type UpdateUserContactsFormPropsType = {
|
|||||||
const UpdateUserContactsForm: React.FC<UpdateUserContactsFormPropsType> = ({ contacts }) => {
|
const UpdateUserContactsForm: React.FC<UpdateUserContactsFormPropsType> = ({ contacts }) => {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, dirtyFields, isDirty },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
} = useForm<UpdateUserContactsInputType>({
|
} = useForm<UpdateUserContactsInputType>({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user