+ ));
+ }
+
+ render() {
+ return
+ {this.forEachNotificationRender(this.notificationStore.errors, NotificationType.ERROR)}
+ {this.forEachNotificationRender(this.notificationStore.successes, NotificationType.SUCCESS)}
+ {this.forEachNotificationRender(this.notificationStore.warnings, NotificationType.WARNING)}
+ {this.forEachNotificationRender(this.notificationStore.infos, NotificationType.INFO)}
+
+ }
+}
+
+@observer
+class NotificationPopup extends ComponentContext<{ notification: Notification, type: NotificationType }> {
+
+ constructor(props: { notification: Notification, type: NotificationType }) {
+ super(props);
+ makeObservable(this);
+ }
+
+ @action.bound
+ close() {
+ this.notificationStore.close(this.props.notification.uuid);
+ }
+
+ get cardClassName() {
+ switch (this.props.type) {
+ case NotificationType.ERROR:
+ return 'text-bg-danger';
+ case NotificationType.WARNING:
+ return 'text-bg-warning';
+ case NotificationType.INFO:
+ return 'text-bg-info';
+ case NotificationType.SUCCESS:
+ return 'text-bg-success';
+ }
+ }
+
+ render() {
+ const hasTitle = !!this.props.notification.title && this.props.notification.title.length > 0;
+ const closeIcon = ;
+
+ return
+ {
+ hasTitle &&
+
+
+
+
+ {this.props.notification.title}
+
+
+ {closeIcon}
+
+
+
+
+ }
+
+
+
+
+ {this.props.notification.message}
+
+ {
+ !hasTitle &&
+
+ {closeIcon}
+
+ }
+
+
+
+
+ }
+}
\ No newline at end of file
diff --git a/web/src/components/Page/DefaultPage.tsx b/web/src/components/Page/DefaultPage.tsx
deleted file mode 100644
index 1188054..0000000
--- a/web/src/components/Page/DefaultPage.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import {Component, ReactNode} from "react";
-import Header from "./Header.tsx";
-import {Container} from "react-bootstrap";
-import Footer from "./Footer.tsx";
-
-export abstract class DefaultPage extends Component {
- abstract get page(): ReactNode;
- // declare context: ContextType
-
- render() {
- return <>
-
-
- {this.page}
-
-
- >
- }
-}
diff --git a/web/src/components/Page/Footer.tsx b/web/src/components/Page/Footer.tsx
deleted file mode 100644
index ff4dfef..0000000
--- a/web/src/components/Page/Footer.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import {Container, Nav, Navbar} from "react-bootstrap";
-import {GitHubLogo} from "../../utils/svg.tsx";
-
-const Footer = () => {
- return (
-
- )
-}
-
-export default Footer;
\ No newline at end of file
diff --git a/web/src/components/Page/Header.tsx b/web/src/components/Page/Header.tsx
deleted file mode 100644
index 8471a56..0000000
--- a/web/src/components/Page/Header.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
-import {FC} from "react";
-import {RouterLink} from "mobx-state-router";
-import {useRootStore} from "../../store/RootStore.tsx";
-import {IAuthenticated} from "../../models/user.ts";
-import {observer} from "mobx-react";
-
-export const Header: FC = observer(() => {
- const store = useRootStore();
- const user = store.userStore.user;
-
- return
-
-
-
- TDMS
-
-
-
-
-
-
-
-});
-
-export default Header;
\ No newline at end of file
diff --git a/web/src/components/Page/Root.tsx b/web/src/components/Page/Root.tsx
deleted file mode 100644
index 600f315..0000000
--- a/web/src/components/Page/Root.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import {DefaultPage} from "./DefaultPage.tsx";
-
-export default class Root extends DefaultPage {
- get page() {
- return Home
- }
-}
\ No newline at end of file
diff --git a/web/src/components/Page/UserProfile.tsx b/web/src/components/Page/UserProfile.tsx
deleted file mode 100644
index f215714..0000000
--- a/web/src/components/Page/UserProfile.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import {DefaultPage} from "./DefaultPage.tsx";
-
-export default class UserProfile extends DefaultPage {
- get page() {
- return User Profile
- }
-}
diff --git a/web/src/components/custom/DataTable.tsx b/web/src/components/custom/DataTable.tsx
new file mode 100644
index 0000000..1dd04ba
--- /dev/null
+++ b/web/src/components/custom/DataTable.tsx
@@ -0,0 +1,147 @@
+import {ComponentContext} from "../../utils/ComponentContext";
+import {TableDescriptor} from "../../utils/tables";
+import {observer} from "mobx-react";
+import {action, makeObservable} from "mobx";
+import {FormSelect, Pagination, Table} from "react-bootstrap";
+
+export interface DataTableProps {
+ tableDescriptor: TableDescriptor;
+}
+
+@observer
+export class DataTable extends ComponentContext> {
+ constructor(props: DataTableProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ header() {
+ return
+ {this.props.tableDescriptor.columns.map(column => | {column.title} | )}
+
+ }
+
+ body() {
+ const firstColumnKey = this.props.tableDescriptor.columns[0].key;
+ return this.props.tableDescriptor.data.map(row => {
+ const rowAny = row as any;
+ return
+ {
+ this.props.tableDescriptor.columns.map(column => {
+ return |
+ {column.format(rowAny[column.key])}
+ |
+ })
+ }
+
+ });
+ }
+
+ isFirstPage() {
+ if (typeof this.props.tableDescriptor.page === 'undefined') {
+ return true;
+ }
+
+ return this.props.tableDescriptor.page === 0;
+ }
+
+ isLastPage() {
+ if (typeof this.props.tableDescriptor.page === 'undefined' || typeof this.props.tableDescriptor.pageSize === 'undefined') {
+ return true;
+ }
+
+ return this.props.tableDescriptor.page === (this.props.tableDescriptor.data.length / this.props.tableDescriptor.pageSize);
+ }
+
+ @action.bound
+ goFirstPage() {
+ if (typeof this.props.tableDescriptor.page === 'undefined') {
+ return;
+ }
+
+ this.props.tableDescriptor.page = 0;
+ }
+
+ @action.bound
+ goLastPage() {
+ if (typeof this.props.tableDescriptor.page === 'undefined' || typeof this.props.tableDescriptor.pageSize === 'undefined') {
+ return;
+ }
+
+ this.props.tableDescriptor.page = this.props.tableDescriptor.data.length / this.props.tableDescriptor.pageSize;
+ }
+
+ @action.bound
+ goNextPage() {
+ if (typeof this.props.tableDescriptor.page === 'undefined' || typeof this.props.tableDescriptor.pageSize === 'undefined') {
+ return;
+ }
+
+ this.props.tableDescriptor.page++;
+ }
+
+ @action.bound
+ goPrevPage() {
+ if (typeof this.props.tableDescriptor.page === 'undefined') {
+ return;
+ }
+
+ this.props.tableDescriptor.page--;
+ }
+
+ @action.bound
+ changePageSize(e: any) {
+ this.props.tableDescriptor.pageSize = parseInt(e.target.value);
+ }
+
+ footer() {
+ const table = this.props.tableDescriptor;
+ if (typeof table.page === 'undefined' || typeof table.pageSize === 'undefined') {
+ return null;
+ }
+
+ return
+
+
+
+
+
+
+
+ {this.props.tableDescriptor.page}
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ }
+
+ render() {
+ const table = this.props.tableDescriptor;
+ return
+
+ {this.header()}
+
+
+ {this.body()}
+
+ {
+ table.pageable &&
+
+ {this.footer()}
+
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/web/src/components/custom/controls/ReactiveControls.css b/web/src/components/custom/controls/ReactiveControls.css
new file mode 100644
index 0000000..9efa40b
--- /dev/null
+++ b/web/src/components/custom/controls/ReactiveControls.css
@@ -0,0 +1,3 @@
+.l-no-bg label::after {
+ background-color: rgba(0, 0, 0, 0) !important;
+}
\ No newline at end of file
diff --git a/web/src/components/custom/controls/ReactiveControls.tsx b/web/src/components/custom/controls/ReactiveControls.tsx
new file mode 100644
index 0000000..14b381d
--- /dev/null
+++ b/web/src/components/custom/controls/ReactiveControls.tsx
@@ -0,0 +1,116 @@
+import React from "react";
+import {ReactiveValue} from "../../../utils/reactive/reactiveValue";
+import {observer} from "mobx-react";
+import {action, makeObservable, observable} from "mobx";
+import {Button, ButtonGroup, FloatingLabel, FormControl, FormText, ToggleButton} from "react-bootstrap";
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
+import './ReactiveControls.css';
+
+export interface ReactiveInputProps {
+ value: ReactiveValue;
+ label?: string;
+ disabled?: boolean;
+ className?: string;
+}
+
+@observer
+export class StringInput extends React.Component> {
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ if (this.props.value.value === undefined) {
+ this.props.value.setAuto('');
+ }
+ this.props.value.setField(this.props.label);
+ }
+
+ @action.bound
+ onChange(event: React.ChangeEvent) {
+ this.props.value.set(event.currentTarget.value);
+ }
+
+ render() {
+ return
+ {/*todo: disable background-color for label*/}
+
+
+
+
+
+ }
+}
+
+@observer
+export class PasswordInput extends React.Component> {
+ @observable showPassword = false;
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ if (this.props.value.value === undefined) {
+ this.props.value.setAuto('');
+ }
+ this.props.value.setField(this.props.label);
+ }
+
+ @action.bound
+ onChange(event: React.ChangeEvent) {
+ this.props.value.set(event.currentTarget.value);
+ }
+
+ @action.bound
+ toggleShowPassword() {
+ this.showPassword = !this.showPassword;
+ }
+
+ render() {
+ return
+ }
+}
+
+@observer
+export class SelectButtonInput extends React.Component> {
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ if (this.props.value.value === undefined) {
+ this.props.value.setAuto('');
+ }
+ this.props.value.setField(this.props.label);
+ }
+
+ @action.bound
+ onChange(event: React.ChangeEvent) {
+ this.props.value.set(event.currentTarget.value);
+ }
+
+ render() {
+ return <>
+
+
+
+
+
+ >
+ }
+}
\ No newline at end of file
diff --git a/web/src/components/layout/DefaultPage.tsx b/web/src/components/layout/DefaultPage.tsx
new file mode 100644
index 0000000..8c377bc
--- /dev/null
+++ b/web/src/components/layout/DefaultPage.tsx
@@ -0,0 +1,40 @@
+import {ReactNode} from "react";
+import {Container} from "react-bootstrap";
+import Header from "./Header";
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
+import {ComponentContext} from "../../utils/ComponentContext";
+import {NotificationContainer} from "../NotificationContainer";
+import {Footer} from "./Footer";
+
+export abstract class DefaultPage extends ComponentContext {
+ get page(): ReactNode {
+ throw new Error('This is not abstract method, ' +
+ 'because mobx cant handle abstract methods. ' +
+ 'Please override this method in child class. ' +
+ 'Do not call it directly.');
+ }
+
+ render() {
+ const thinking = this.thinkStore.isThinking();
+
+ return <>
+
+
+ {
+ thinking &&
+
+
+
+ }
+ {
+ !thinking &&
+ <>
+
+ {this.page}
+ >
+ }
+
+
+ >
+ }
+}
\ No newline at end of file
diff --git a/web/src/components/Page/Error.tsx b/web/src/components/layout/Error.tsx
similarity index 53%
rename from web/src/components/Page/Error.tsx
rename to web/src/components/layout/Error.tsx
index 4471802..25cec15 100644
--- a/web/src/components/Page/Error.tsx
+++ b/web/src/components/layout/Error.tsx
@@ -1,5 +1,7 @@
-import {DefaultPage} from "./DefaultPage.tsx";
+import {observer} from "mobx-react";
+import {DefaultPage} from "./DefaultPage";
+@observer
export default class Error extends DefaultPage {
get page() {
return Error
diff --git a/web/src/components/layout/Footer.tsx b/web/src/components/layout/Footer.tsx
new file mode 100644
index 0000000..19e3535
--- /dev/null
+++ b/web/src/components/layout/Footer.tsx
@@ -0,0 +1,41 @@
+import {ComponentContext} from "../../utils/ComponentContext";
+import {observer} from "mobx-react";
+import {makeObservable} from "mobx";
+import {Container, Nav, Navbar, NavbarText, NavLink} from "react-bootstrap";
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
+import {findIconDefinition} from "@fortawesome/fontawesome-svg-core";
+
+@observer
+export class Footer extends ComponentContext {
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ }
+
+ render() {
+ return
+ }
+}
diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx
new file mode 100644
index 0000000..4a0b55a
--- /dev/null
+++ b/web/src/components/layout/Header.tsx
@@ -0,0 +1,101 @@
+import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
+import {RouterLink} from "mobx-state-router";
+import {IAuthenticated} from "../../models/user";
+import {RootStoreContext, RootStoreContextType} from "../../store/RootStoreContext";
+import {observer} from "mobx-react";
+import {post} from "../../utils/request";
+import {LoginModal} from "../user/LoginModal";
+import {ModalState} from "../../utils/modalState";
+import {action, makeObservable} from "mobx";
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
+import {ComponentContext} from "../../utils/ComponentContext";
+
+@observer
+class Header extends ComponentContext {
+
+ loginModalState = new ModalState();
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ }
+
+ render() {
+ const userStore = this.context.userStore;
+ const user = userStore.user;
+ let thinking = this.thinkStore.isThinking('updateCurrentUser');
+
+ return <>
+
+
+
+
+ TDMS
+
+
+
+
+
+
+
+
+ >
+ }
+}
+
+@observer
+class AuthenticatedItems extends ComponentContext {
+ declare context: RootStoreContextType;
+ static contextType = RootStoreContext;
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ }
+
+ @action.bound
+ logout() {
+ post('user/logout').then(() => this.context.userStore.updateCurrentUser());
+ }
+
+ render() {
+ const userStore = this.context.userStore;
+ const user = userStore.user;
+
+ return <>
+ Пользователь:
+
+ Моя страница
+
+ Выйти
+
+ >
+ }
+}
+
+export default Header;
\ No newline at end of file
diff --git a/web/src/components/layout/Home.tsx b/web/src/components/layout/Home.tsx
new file mode 100644
index 0000000..940af02
--- /dev/null
+++ b/web/src/components/layout/Home.tsx
@@ -0,0 +1,9 @@
+import {DefaultPage} from "./DefaultPage";
+import {observer} from "mobx-react";
+
+@observer
+export default class Home extends DefaultPage {
+ get page() {
+ return Home
+ }
+}
diff --git a/web/src/components/user/LoginModal.tsx b/web/src/components/user/LoginModal.tsx
new file mode 100644
index 0000000..4991915
--- /dev/null
+++ b/web/src/components/user/LoginModal.tsx
@@ -0,0 +1,142 @@
+import {ChangeEvent} from "react";
+import {Button, FormControl, FormGroup, FormLabel, FormText, Modal} from "react-bootstrap";
+import {ModalState} from "../../utils/modalState";
+import {observer} from "mobx-react";
+import {action, computed, makeObservable, observable, reaction} from "mobx";
+import {post} from "../../utils/request";
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
+import {ComponentContext} from "../../utils/ComponentContext";
+
+interface LoginModalProps {
+ modalState: ModalState;
+}
+
+@observer
+export class LoginModal extends ComponentContext {
+ @observable login = '';
+ @observable loginError = '';
+ @observable password = '';
+ @observable passwordError = '';
+
+ constructor(props: LoginModalProps) {
+ super(props);
+ makeObservable(this);
+ reaction(() => this.login, this.validateLogin);
+ reaction(() => this.password, this.validatePassword);
+ }
+
+ @action.bound
+ validateLogin() {
+ if (!this.login) {
+ this.loginError = 'Имя пользователя не может быть пустым';
+ } else if (this.login.length < 5) {
+ this.loginError = 'Имя пользователя должно быть не менее 5 символов';
+ } else if (this.login.length > 50) {
+ this.loginError = 'Имя пользователя должно быть не более 50 символов';
+ } else if (!/^[a-zA-Z0-9_]+$/.test(this.login)) {
+ this.loginError = 'Имя пользователя должно содержать только латинские буквы, цифры и знак подчеркивания';
+ } else {
+ this.loginError = '';
+ }
+ }
+
+ @action.bound
+ validatePassword() {
+ if (!this.password) {
+ this.passwordError = 'Пароль не может быть пустым';
+ } else if (this.password.length < 5) {
+ this.passwordError = 'Пароль должен быть не менее 5 символов';
+ } else if (this.password.length > 32) {
+ this.passwordError = 'Пароль должен быть не более 32 символов';
+ } else if (!/^[a-zA-Z0-9!@#$%^&*()_+]+$/.test(this.password)) {
+ this.passwordError = 'Пароль должен содержать только латинские буквы, цифры и специальные символы';
+ } else {
+ this.passwordError = '';
+ }
+ }
+
+ @computed
+ get loginButtonDisabled() {
+ return !this.login || !this.password || !!this.loginError || !!this.passwordError;
+ }
+
+ @action.bound
+ onLoginInput(event: ChangeEvent) {
+ this.login = event.target.value;
+ }
+
+ @action.bound
+ onPasswordInput(event: ChangeEvent) {
+ this.password = event.target.value;
+ }
+
+ @action.bound
+ tryLogin() {
+ if (this.loginButtonDisabled)
+ return;
+
+ this.thinkStore.think('loginModal');
+ post('user/login', {
+ username: this.login,
+ password: this.password
+ }).then(() => {
+ this.userStore.updateCurrentUser((user) => {
+ if (user.authenticated) {
+ this.routerStore.goTo('profile').then();
+ this.notificationStore.success('Вы успешно вошли в систему, ' + user.fullName, 'Успешный вход');
+ } else {
+ this.routerStore.goTo('root').then();
+ this.notificationStore.error('Произошла ошибка при попытке входа в систему', 'Ошибка входа');
+ }
+ });
+ }).finally(() => {
+ this.props.modalState.close();
+ this.thinkStore.completeAll('loginModal');
+ });
+ }
+
+ render() {
+ const open = this.props.modalState.isOpen;
+ const thinking = this.thinkStore.isThinking('loginModal');
+
+ return
+
+ Вход
+
+ {
+ thinking &&
+
+
+
+ }
+ {
+ !thinking &&
+ <>
+
+
+ Имя пользователя
+
+ {
+ this.loginError &&
+ {this.loginError}
+ }
+
+
+ Пароль
+
+ {
+ this.passwordError &&
+ {this.passwordError}
+ }
+
+
+
+
+
+
+ >
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/web/src/components/user/UserList.tsx b/web/src/components/user/UserList.tsx
new file mode 100644
index 0000000..1fb2eca
--- /dev/null
+++ b/web/src/components/user/UserList.tsx
@@ -0,0 +1,60 @@
+import {observer} from "mobx-react";
+import {action, makeObservable, observable, reaction, runInAction} from "mobx";
+import {DefaultPage} from "../layout/DefaultPage";
+import {IAuthenticated} from "../../models/user";
+import {DataTable} from "../custom/DataTable";
+import {get} from "../../utils/request";
+import {Column, TableDescriptor} from "../../utils/tables";
+
+@observer
+export class UserList extends DefaultPage {
+ constructor(props: {}) {
+ super(props);
+ makeObservable(this);
+ }
+
+ componentDidMount() {
+ this.requestUsers();
+ reaction(() => this.users, () => {
+ if (typeof this.users === 'undefined') {
+ return;
+ }
+
+ this.tableDescriptor = new TableDescriptor(this.userColumns, this.users);
+ }, {fireImmediately: true});
+ }
+
+ @observable users?: IAuthenticated[];
+ @observable tableDescriptor?: TableDescriptor;
+
+ userColumns = [
+ new Column('login', 'Логин'),
+ new Column('fullName', 'Полное имя'),
+ new Column('email', 'Email'),
+ new Column('phone', 'Телефон'),
+ new Column('createdAt', 'Дата создания'),
+ new Column('updatedAt', 'Дата обновления', (value: string) => value ? value : 'Не обновлялось'),
+ ];
+
+
+ @action.bound
+ requestUsers() {
+ this.thinkStore.think('userList');
+ get('user/get-all').then((users) => {
+ runInAction(() => {
+ this.users = users;
+ });
+ }).finally(() => {
+ this.thinkStore.completeAll('userList');
+ });
+ }
+
+ get page() {
+ return <>
+ {
+ this.tableDescriptor &&
+
+ }
+ >
+ }
+}
\ No newline at end of file
diff --git a/web/src/components/user/UserProfilePage.tsx b/web/src/components/user/UserProfilePage.tsx
new file mode 100644
index 0000000..bf3093a
--- /dev/null
+++ b/web/src/components/user/UserProfilePage.tsx
@@ -0,0 +1,177 @@
+import {DefaultPage} from "../layout/DefaultPage";
+import {Col, Form, Row} from "react-bootstrap";
+import {observer} from "mobx-react";
+import {RootStoreContext, type RootStoreContextType} from "../../store/RootStoreContext";
+import {IAuthenticated} from "../../models/user";
+import {Component} from "react";
+import {dateConverter} from "../../utils/converters";
+import {IStudent} from "../../models/student";
+import {makeObservable, observable} from "mobx";
+
+@observer
+class UserInfo extends Component<{user: IAuthenticated}> {
+ @observable
+ user = this.props.user;
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ }
+
+ render() {
+ return (
+
+
+
+ ФИО
+
+
+
+ Имя пользователя
+
+
+
+ Электронная почта
+
+
+
+ Телефон
+ {/* todo: format phone */}
+
+
+
+
+
+ Роли
+ a.name).join(', ')}
+ disabled={true}/>
+
+
+ Дата создания
+
+
+
+ Дата последней модификации
+
+
+
+
+ )
+ }
+}
+
+@observer
+class StudentInfo extends Component<{student: IStudent}> {
+ @observable
+ student = this.props.student;
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ }
+
+ render() {
+ let student = this.student;
+
+ return (
+
+
+
+ Тема дипломной работы
+
+
+
+ Очередь защиты
+
+
+
+ Презентация в электронном формате
+
+
+
+ Оценка за комментарий {/* todo: обсудить с аналитиком */}
+
+
+
+ Оценка за практику {/* todo: обсудить с аналитиком */}
+
+
+
+ Комментарий к предзащите
+
+
+
+ Форма контроля {/* todo: обсудить с аналитиком */}
+
+
+
+ Антиплагиат (процент
+ уникальности) {/* todo: обсудить с аналитиком */}
+
+
+
+ Примечание
+
+
+
+
+
+ Группа
+
+
+
+ Куратор
+
+
+
+ Форма обучения {/* todo: обсудить с аналитиком */}
+
+
+
+ Научный руководитель
+
+
+
+ Зачетная книжка сдана
+
+
+
+ Работа {/* todo: обсудить с аналитиком */}
+
+
+
+ Магистратура {/* todo: обсудить с аналитиком */}
+
+
+
+
+ );
+ }
+}
+
+export default class UserProfilePage extends DefaultPage {
+ declare context: RootStoreContextType;
+ static contextType = RootStoreContext;
+
+ get page() {
+ let user = this.context.userStore.user;
+ if (!user.authenticated) {
+ // todo: implement login page with redirects
+ this.context.routerStore.goTo('login', {redirect: 'profile'});
+ }
+ let student = this.context.userStore.student;
+
+ return
+ }
+}
diff --git a/web/src/components/user/UserRegistration.tsx b/web/src/components/user/UserRegistration.tsx
new file mode 100644
index 0000000..c9299c1
--- /dev/null
+++ b/web/src/components/user/UserRegistration.tsx
@@ -0,0 +1,87 @@
+import {observer} from "mobx-react";
+import {DefaultPage} from "../layout/DefaultPage";
+import {action, computed, makeObservable, observable} from "mobx";
+import {Button, Col, Form, Row} from "react-bootstrap";
+import {UserRegistrationDTO} from "../../models/registration";
+import {post} from "../../utils/request";
+import {ReactiveValue} from "../../utils/reactive/reactiveValue";
+import {PasswordInput, SelectButtonInput, StringInput} from "../custom/controls/ReactiveControls";
+import {
+ email,
+ loginChars,
+ loginLength,
+ nameChars,
+ nameLength,
+ passwordChars,
+ passwordLength,
+ phone,
+ required
+} from "../../utils/reactive/validators";
+
+@observer
+export class UserRegistration extends DefaultPage {
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ }
+
+ @observable login = new ReactiveValue().addValidator(required).addValidator(loginLength).addValidator(loginChars);
+ @observable password = new ReactiveValue().addValidator(required).addValidator(passwordLength).addValidator(passwordChars);
+ @observable fullName = new ReactiveValue().addValidator(required).addValidator(nameLength).addValidator(nameChars);
+ @observable email = new ReactiveValue().addValidator(required).addValidator(email);
+ @observable numberPhone = new ReactiveValue().addValidator(required).addValidator(phone).setAuto('+7');
+
+
+ @observable accountType = new ReactiveValue().addValidator(required).addValidator((value) => {
+ if (!['student', 'admin'].includes(value)) {
+ return 'Тип аккаунта должен быть "СТУДЕНТ" или "АДМИНИСТРАТОР"';
+ }
+ });
+
+ @computed
+ get formInvalid() {
+ return this.login.invalid || !this.login.touched
+ || this.password.invalid || !this.password.touched
+ || this.fullName.invalid || !this.fullName.touched
+ || this.email.invalid || !this.email.touched
+ || this.numberPhone.invalid || !this.numberPhone.touched
+ || this.accountType.invalid || !this.accountType.touched;
+ }
+
+ @action.bound
+ submit() {
+ post('user/register', {
+ login: this.login.value,
+ password: this.password.value,
+ fullName: this.fullName.value,
+ email: this.email.value,
+ numberPhone: this.numberPhone.value,
+ // studentData: { groupId: 1 }
+ } as UserRegistrationDTO).then(() => {
+ this.notificationStore.success('Пользователь успешно зарегистрирован');
+ }).catch(() => {
+ this.notificationStore.error('Ошибка регистрации пользователя');
+ });
+ }
+
+ get page() {
+ return
+ }
+}
\ No newline at end of file
diff --git a/web/src/index.css b/web/src/index.css
index 114dedb..93c7567 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -64,4 +64,11 @@ body {
footer {
margin-top: auto;
-}
\ No newline at end of file
+}
+
+#fullscreen-loader {
+ min-height: calc(100vh - (56px + 64px + 48px + 48px));
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
diff --git a/web/index.html b/web/src/index.html
similarity index 78%
rename from web/index.html
rename to web/src/index.html
index 041e4a7..d7f333e 100644
--- a/web/index.html
+++ b/web/src/index.html
@@ -3,10 +3,10 @@
+
TDMS
-