From 97f9373ce9c0ad6651b9cd6a65a2aa98fd978ec0 Mon Sep 17 00:00:00 2001 From: Toby Date: Mon, 12 Aug 2024 21:52:31 +0400 Subject: [PATCH 01/10] Add Router --- src/core/Render.ts | 9 ++++++ src/core/Route.ts | 44 +++++++++++++++++++++++++ src/core/Router.ts | 79 +++++++++++++++++++++++++++++++++++++++++++++ src/utils/router.ts | 52 +++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 src/core/Render.ts create mode 100644 src/core/Route.ts create mode 100644 src/core/Router.ts create mode 100644 src/utils/router.ts diff --git a/src/core/Render.ts b/src/core/Render.ts new file mode 100644 index 0000000..50406f7 --- /dev/null +++ b/src/core/Render.ts @@ -0,0 +1,9 @@ +import Block from './Block'; + +export function render(rootQuery: string, block: Block) { + const root = document.querySelector(rootQuery); + if (root) { + root.innerHTML = ''; + root.append(block.getContent()); + } +} diff --git a/src/core/Route.ts b/src/core/Route.ts new file mode 100644 index 0000000..984427f --- /dev/null +++ b/src/core/Route.ts @@ -0,0 +1,44 @@ +import { render } from './Render'; +import Block, { Props, BlockType } from "./Block"; + +class Route { + protected _pathname: string; + protected _blockClass: BlockType; + protected _block: Block | null = null; + protected _props: Props; + + constructor(pathname: string, view: BlockType, props: Props) { + this._pathname = pathname; + this._blockClass = view; + this._block = null; + this._props = props; + } + + navigate(pathname: string) { + if (this.match(pathname)) { + this._pathname = pathname; + this.render(); + } + } + + leave() { + this._block = null; + } + + match(pathname: string) { + return pathname === this._pathname; + } + + render() { + if (!this._block) { + this._block = new this._blockClass({}); + render(this._props.rootQuery as string, this._block); + + return; + } + + this._block.show(); + } +} + +export default Route; diff --git a/src/core/Router.ts b/src/core/Router.ts new file mode 100644 index 0000000..5d162f3 --- /dev/null +++ b/src/core/Router.ts @@ -0,0 +1,79 @@ +import Route from './Route'; +import { BlockType } from './Block'; + +class Router { + static __instance: Router | undefined; + + protected _routes: Route[] = []; + protected _history: History = window.history; + protected _currentRoute: Route | null = null; + protected _rootQuery: string = ''; + + constructor(rootQuery: string) { + if (Router.__instance) { + return Router.__instance; + } + + this._rootQuery = rootQuery; + + Router.__instance = this; + + window.addEventListener('popstate', this._onPopState.bind(this)); + } + + use(pathname: string, block: BlockType) { + const route: Route = new Route(pathname, block, {rootQuery: this._rootQuery}); + this._routes.push(route); + + return this; + } + + start() { + window.onpopstate = (event) => { + const window = event.currentTarget as Window; + if (window) { + this._onRoute(window.location.pathname); + } + }; + + this._onRoute(window.location.pathname); + } + + private _onPopState = () => { + this._onRoute(window.location.pathname); + } + + private _onRoute(pathname: string) { + const route = this.getRoute(pathname); + + if (!route) { + return; + } + + if (this._currentRoute && this._currentRoute !== route) { + this._currentRoute.leave(); + } + + this._currentRoute = route; + route.render(); + } + + go(pathname: string) { + this._history.pushState({}, '', pathname); + this._onRoute(pathname); + } + + back() { + this._history.back(); + } + + forward() { + this._history.forward(); + } + + getRoute(pathname: string) { + return this._routes.find(route => route.match(pathname)); + } +} + +export default Router; diff --git a/src/utils/router.ts b/src/utils/router.ts new file mode 100644 index 0000000..d76f3c5 --- /dev/null +++ b/src/utils/router.ts @@ -0,0 +1,52 @@ +import Router from "../core/Router"; + +const router = new Router('#app'); + +const start = () => { + router.start(); +} + +const goToLogin = () => { + router.go('/login'); +} + +const goToRegister = () => { + router.go('/signup'); +} + +const goToMessenger = () => { + router.go('/messenger'); +} + +const goToSettings = () => { + router.go('/settings'); +} + +const goToSettingsEdit = () => { + router.go('/settings/edit'); +} + +const goToPasswordEdit = () => { + router.go('/settings/edit-password'); +} + +const goToError404 = () => { + router.go('/404'); +} + +const goToError500 = () => { + router.go('/500'); +} + +export { + router, + start, + goToLogin, + goToRegister, + goToMessenger, + goToSettings, + goToSettingsEdit, + goToPasswordEdit, + goToError404, + goToError500 +}; From da084d89d08b9fe4c1868ca2199c5c78804e76d4 Mon Sep 17 00:00:00 2001 From: Toby Date: Mon, 12 Aug 2024 21:53:32 +0400 Subject: [PATCH 02/10] Add Router --- src/core/Block.ts | 17 ++++++ src/main.ts | 53 +++++-------------- src/pages/chat/chat.hbs | 2 +- src/pages/chat/chat.ts | 8 ++- src/pages/login-form/login-form.hbs | 2 +- src/pages/login-form/login-form.ts | 12 ++++- .../profile-edit-password.hbs | 2 +- .../profile-edit-password.ts | 12 ++++- src/pages/profile-edit/profile-edit.hbs | 2 +- src/pages/profile-edit/profile-edit.ts | 12 ++++- src/pages/profile/profile.hbs | 6 +-- src/pages/profile/profile.ts | 14 ++++- .../registration-form/registration-form.ts | 3 +- 13 files changed, 93 insertions(+), 52 deletions(-) diff --git a/src/core/Block.ts b/src/core/Block.ts index 35bfa49..7ff9f0c 100644 --- a/src/core/Block.ts +++ b/src/core/Block.ts @@ -5,6 +5,9 @@ import { v4 as makeUUID } from 'uuid'; export type Events = Record void>; export type Props = Record; export type Children = Record; +export type BlockType = { + new(propsAndParent: Props): Block +}; class Block { static EVENTS: Record = { @@ -205,6 +208,20 @@ class Block { private _createDocumentElement(tagName: string): HTMLElement { return document.createElement(tagName); } + + show() { + const content = this.getContent(); + if (content) { + content.style.display = ''; + } + } + + hide() { + const content = this.getContent(); + if (content) { + content.style.display = 'none'; + } + } } export default Block; diff --git a/src/main.ts b/src/main.ts index 75c0de5..6761afa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,7 @@ import Handlebars from 'handlebars'; import { registerComponent } from './core/RegistrationComponent'; +import { router, start } from "./utils/router"; +import { BlockType } from "./core/Block"; import './style.less'; type ImportValue = Record; @@ -35,42 +37,15 @@ const registerImports = (imports: ImportValue) => { registerImports(components); registerImports(pages); -const navigator = (pageName: string) => { - const Page: any = pages[pageName]; - if (Page) { - const app = document.getElementById('app'); - if (app) { - if (typeof Page === 'function') { - const page = new Page({}); - const content = page.getContent(); - if (content) { - app.innerHTML = ''; - app.appendChild(content); - } - } - } - } -}; - -document.addEventListener('DOMContentLoaded', () => { - navigator('MainPage'); -}); - -document.addEventListener('click', (event) => { - const target: HTMLElement = event.target as HTMLElement; - const page = target.getAttribute('page'); - if (page) { - navigator(page); - - event.preventDefault(); - event.stopImmediatePropagation(); - } -}); - -document.addEventListener('navigate', (event: Event) => { - const customEvent = event as CustomEvent; - const page = customEvent.detail.page; - if (page) { - navigator(page); - } -}); +router + .use('/', pages.LoginPage as unknown as BlockType) + .use('/login', pages.LoginPage as unknown as BlockType) + .use('/signup', pages.RegistrationPage as unknown as BlockType) + .use('/messenger', pages.ChatPage as unknown as BlockType) + .use('/settings', pages.ProfilePage as unknown as BlockType) + .use('/settings/edit', pages.ProfileEditPage as unknown as BlockType) + .use('/settings/edit-password', pages.ProfileEditPasswordPage as unknown as BlockType) + .use('/404', pages.Error404Page as unknown as BlockType) + .use('/500', pages.Error500Page as unknown as BlockType); + +start(); diff --git a/src/pages/chat/chat.hbs b/src/pages/chat/chat.hbs index 9745834..f276c18 100644 --- a/src/pages/chat/chat.hbs +++ b/src/pages/chat/chat.hbs @@ -3,7 +3,7 @@
- {{{ Button class="profile-button" type="button" label='Profile' page="ProfilePage" }}} + {{{ Button class="profile-button" type="button" label='Profile' onClick=goToSettings }}}
diff --git a/src/pages/login-form/login-form.ts b/src/pages/login-form/login-form.ts index e7f104b..0db8a0a 100644 --- a/src/pages/login-form/login-form.ts +++ b/src/pages/login-form/login-form.ts @@ -1,8 +1,18 @@ import './login-form.less'; import LoginFormTmpl from './login-form.hbs?raw'; import { BaseForm } from '../../core/BaseForm'; +import { goToRegister, goToMessenger } from '../../utils/router'; export class LoginPage extends BaseForm { + constructor(props: Record = {}) { + super({ + ...props, + goToRegistration: () => { + goToRegister(); + } + }); + } + render(): string { return LoginFormTmpl; } @@ -10,6 +20,6 @@ export class LoginPage extends BaseForm { protected onValid(formData: Record) { console.log('Login successful', formData); - this.navigate('ChatPage'); + goToMessenger(); } } diff --git a/src/pages/profile-edit-password/profile-edit-password.hbs b/src/pages/profile-edit-password/profile-edit-password.hbs index 60e97a7..277a2c9 100644 --- a/src/pages/profile-edit-password/profile-edit-password.hbs +++ b/src/pages/profile-edit-password/profile-edit-password.hbs @@ -1,6 +1,6 @@
diff --git a/src/pages/profile-edit-password/profile-edit-password.ts b/src/pages/profile-edit-password/profile-edit-password.ts index 7b498cc..6d35ca9 100644 --- a/src/pages/profile-edit-password/profile-edit-password.ts +++ b/src/pages/profile-edit-password/profile-edit-password.ts @@ -1,8 +1,18 @@ import '../profile/profile.less'; import ProfileEditPasswordPageTmpl from './profile-edit-password.hbs?raw'; import { BaseForm } from '../../core/BaseForm'; +import { Props } from '../../core/Block'; +import { router, goToSettings } from '../../utils/router'; export class ProfileEditPasswordPage extends BaseForm { + constructor(props: Props) { + super({ + ...props, + goBack() { + router.back(); + } + }); + } render(): string { return ProfileEditPasswordPageTmpl; } @@ -10,6 +20,6 @@ export class ProfileEditPasswordPage extends BaseForm { protected onValid(formData: Record) { console.log('Profile password edit successful', formData); - this.navigate('ProfilePage'); + goToSettings(); } } diff --git a/src/pages/profile-edit/profile-edit.hbs b/src/pages/profile-edit/profile-edit.hbs index a132e47..0766801 100644 --- a/src/pages/profile-edit/profile-edit.hbs +++ b/src/pages/profile-edit/profile-edit.hbs @@ -1,6 +1,6 @@
diff --git a/src/pages/profile-edit/profile-edit.ts b/src/pages/profile-edit/profile-edit.ts index 5662ff0..fde9b31 100644 --- a/src/pages/profile-edit/profile-edit.ts +++ b/src/pages/profile-edit/profile-edit.ts @@ -1,8 +1,18 @@ import '../profile/profile.less'; import ProfileEditPageTmpl from './profile-edit.hbs?raw'; import { BaseForm } from '../../core/BaseForm'; +import { Props } from '../../core/Block'; +import { router, goToSettings } from '../../utils/router'; export class ProfileEditPage extends BaseForm { + constructor(props: Props) { + super({ + ...props, + goBack() { + router.back(); + } + }); + } render(): string { return ProfileEditPageTmpl; } @@ -10,6 +20,6 @@ export class ProfileEditPage extends BaseForm { protected onValid(formData: Record) { console.log('Profile edit successful', formData); - this.navigate('ProfilePage'); + goToSettings(); } } diff --git a/src/pages/profile/profile.hbs b/src/pages/profile/profile.hbs index 3fbfda2..0a0aa16 100644 --- a/src/pages/profile/profile.hbs +++ b/src/pages/profile/profile.hbs @@ -1,6 +1,6 @@
@@ -23,10 +23,10 @@
- {{{ Button class="profile button-edit-profile" label="Change Profile" page="ProfileEditPage" }}} + {{{ Button class="profile button-edit-profile" label="Change Profile" onClick=goToSettingsEdit }}}
- {{{ Button class="profile button-edit-password" label="Change Password" page="ProfileEditPasswordPage" }}} + {{{ Button class="profile button-edit-password" label="Change Password" onClick=goToPasswordEdit }}}
diff --git a/src/pages/profile/profile.ts b/src/pages/profile/profile.ts index f4882bb..d145534 100644 --- a/src/pages/profile/profile.ts +++ b/src/pages/profile/profile.ts @@ -1,10 +1,22 @@ import './profile.less'; import Block, {Props} from "../../core/Block"; import ProfilePageTmpl from './profile.hbs?raw'; +import { router, goToSettingsEdit, goToPasswordEdit } from '../../utils/router'; export class ProfilePage extends Block { constructor(props: Props) { - super(props); + super({ + ...props, + goToSettingsEdit() { + goToSettingsEdit(); + }, + goToPasswordEdit() { + goToPasswordEdit(); + }, + goBack() { + router.back(); + } + }); } render(): string { diff --git a/src/pages/registration-form/registration-form.ts b/src/pages/registration-form/registration-form.ts index b2d12fe..658f9dd 100644 --- a/src/pages/registration-form/registration-form.ts +++ b/src/pages/registration-form/registration-form.ts @@ -1,6 +1,7 @@ import './registration-form.less'; import RegistrationFormTmpl from './registration-form.hbs?raw'; import { BaseForm } from '../../core/BaseForm'; +import { goToMessenger } from '../../utils/router'; export class RegistrationPage extends BaseForm { render(): string { @@ -10,6 +11,6 @@ export class RegistrationPage extends BaseForm { protected onValid(formData: Record) { console.log('Account registration successful', formData); - this.navigate('ChatPage'); + goToMessenger(); } } From c0cb14cef470c7e801b5db93fe32c3daf1ff5cbb Mon Sep 17 00:00:00 2001 From: Toby Date: Sun, 18 Aug 2024 15:50:54 +0400 Subject: [PATCH 03/10] User config, user data change, auth & user api, auth & user controllers --- package-lock.json | 88 ++++++++- package.json | 2 +- src/api/AuthApi.ts | 33 ++++ src/api/UserApi.ts | 29 +++ src/components/avatar/avatar.hbs | 7 + src/components/avatar/avatar.ts | 18 ++ src/components/input/input.hbs | 3 + src/components/input/input.ts | 16 ++ src/controllers/AuthController.ts | 80 ++++++++ src/controllers/UserController.ts | 66 +++++++ src/core/Block.ts | 34 +++- src/core/HTTPTransport.ts | 60 +++--- src/core/Router.ts | 5 + src/core/Store.ts | 53 ++++++ src/main.ts | 52 +++-- src/pages/login-form/login-form.hbs | 2 +- src/pages/login-form/login-form.ts | 49 ++++- src/pages/main-page/main-page.hbs | 12 -- src/pages/main-page/main-page.ts | 11 -- .../profile-edit-password.hbs | 2 +- .../profile-edit-password.ts | 61 +++++- src/pages/profile-edit/profile-edit.hbs | 28 --- src/pages/profile-edit/profile-edit.ts | 25 --- src/pages/profile/profile.hbs | 45 +++-- src/pages/profile/profile.less | 97 +++++++++- src/pages/profile/profile.ts | 178 +++++++++++++++++- .../registration-form/registration-form.hbs | 2 +- .../registration-form/registration-form.ts | 56 +++++- src/utils/connect.ts | 33 ++++ src/utils/hosts.ts | 7 + src/utils/isEqual.ts | 55 ++++++ src/utils/queryStringify.ts | 41 ++++ src/utils/router.ts | 5 - src/utils/utils.ts | 36 ++++ src/variables.less | 1 + 35 files changed, 1108 insertions(+), 184 deletions(-) create mode 100644 src/api/AuthApi.ts create mode 100644 src/api/UserApi.ts create mode 100644 src/components/avatar/avatar.hbs create mode 100644 src/components/avatar/avatar.ts create mode 100644 src/controllers/AuthController.ts create mode 100644 src/controllers/UserController.ts create mode 100644 src/core/Store.ts delete mode 100644 src/pages/main-page/main-page.hbs delete mode 100644 src/pages/main-page/main-page.ts delete mode 100644 src/pages/profile-edit/profile-edit.hbs delete mode 100644 src/pages/profile-edit/profile-edit.ts create mode 100644 src/utils/connect.ts create mode 100644 src/utils/hosts.ts create mode 100644 src/utils/isEqual.ts create mode 100644 src/utils/queryStringify.ts create mode 100644 src/utils/utils.ts diff --git a/package-lock.json b/package-lock.json index 4b069a4..1a2d468 100644 --- a/package-lock.json +++ b/package-lock.json @@ -571,16 +571,90 @@ } }, "@typescript-eslint/parser": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz", - "integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + } + }, + "@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + } } }, "@typescript-eslint/scope-manager": { diff --git a/package.json b/package.json index 46874d8..89687a9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.16.1", - "@typescript-eslint/parser": "^7.16.1", + "@typescript-eslint/parser": "^7.18.0", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.29.1", diff --git a/src/api/AuthApi.ts b/src/api/AuthApi.ts new file mode 100644 index 0000000..4bce18e --- /dev/null +++ b/src/api/AuthApi.ts @@ -0,0 +1,33 @@ +import HTTPTransport, { TOptionsData } from "../core/HTTPTransport"; + +const auth = new HTTPTransport('/auth'); + +class AuthApi { + public createUser(data: TOptionsData): Promise { + return auth.post('/signup', { + data, + headers: { + 'Content-type': 'application/json; charset=UTF-8', + } + }); + } + + public login(data: TOptionsData): Promise { + return auth.post('/signin', { + data, + headers: { + 'Content-type': 'application/json; charset=UTF-8', + } + }); + } + + public getUser(): Promise { + return auth.get('/user'); + } + + public logout(): Promise { + return auth.post('/logout'); + } +} + +export default new AuthApi(); diff --git a/src/api/UserApi.ts b/src/api/UserApi.ts new file mode 100644 index 0000000..e3aa77f --- /dev/null +++ b/src/api/UserApi.ts @@ -0,0 +1,29 @@ +import HTTPTransport, {TOptionsData} from "../core/HTTPTransport"; + +const user = new HTTPTransport('/user'); + +class UserApi { + public changeData(data: TOptionsData): Promise { + return user.put('/profile', { + data, + headers: { + 'Content-type': 'application/json; charset=UTF-8', + } + }); + } + + public changePassword(data: TOptionsData): Promise { + return user.put('/password', { + data, + headers: { + 'Content-type': 'application/json; charset=UTF-8', + } + }); + } + + changeAvatar(data: FormData): Promise { + return user.put('/profile/avatar', { data }); + } +} + +export default new UserApi(); diff --git a/src/components/avatar/avatar.hbs b/src/components/avatar/avatar.hbs new file mode 100644 index 0000000..df606d1 --- /dev/null +++ b/src/components/avatar/avatar.hbs @@ -0,0 +1,7 @@ +
+ avatar {{name}} +
diff --git a/src/components/avatar/avatar.ts b/src/components/avatar/avatar.ts new file mode 100644 index 0000000..94f1694 --- /dev/null +++ b/src/components/avatar/avatar.ts @@ -0,0 +1,18 @@ +// import './avatar.less'; +import AvatarTmpl from './avatar.hbs?raw'; +import Block, {Props} from '../../core/Block'; + +export class Avatar extends Block { + constructor(props: Props) { + super({ + ...props, + events: { + ...(props.onClick ? { click: props.onClick } : {}), + }, + }); + } + + render(): string { + return AvatarTmpl; + } +} diff --git a/src/components/input/input.hbs b/src/components/input/input.hbs index 86538d7..13924c1 100644 --- a/src/components/input/input.hbs +++ b/src/components/input/input.hbs @@ -4,9 +4,12 @@ type="{{ type }}" name="{{ name }}" class="input {{ class }}" + id="{{ id }}" + style="{{ style }}" placeholder="{{ placeholder }}" value="{{ value }}" {{#if autocomplete}}autocomplete="{{ autocomplete }}"{{/if}} + {{#if readonly}}readonly{{/if}} />
{{ error }}
diff --git a/src/components/input/input.ts b/src/components/input/input.ts index 40e05bf..555deab 100644 --- a/src/components/input/input.ts +++ b/src/components/input/input.ts @@ -19,6 +19,18 @@ export class Input extends Block { input: (e: Event) => { const input = e.target as HTMLInputElement; this.props.value = input.value; + }, + change: (e: Event) => { + const input = e.target as HTMLInputElement; + this.setValue(input.value); + + if (!this.props.skipValidation) { + this.validate(); + } + + if (typeof this.props.onChange === 'function') { + this.props.onChange(e); + } } } }); @@ -33,6 +45,10 @@ export class Input extends Block { } public validate(): boolean { + if (this.props.skipValidation) { + return true; + } + const errorMessage: string | null = validateField(this.props.name, this.props.value); const hasError: boolean = !!errorMessage; this.setProps({ diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts new file mode 100644 index 0000000..e3357f2 --- /dev/null +++ b/src/controllers/AuthController.ts @@ -0,0 +1,80 @@ +import AuthApi from "../api/AuthApi"; +import {TOptionsData} from "../core/HTTPTransport"; +import { goToLogin, goToMessenger, goToError500 } from "../utils/router"; +import Store from "../core/Store"; + +class AuthController { + public async createUser(data: TOptionsData): Promise { + try { + const { status, response } = await AuthApi.createUser(data); + + if (status === 200 && response) { + this.getUser(); + return true; + } else if (status === 500) { + goToError500(); + return false; + } else { + alert(JSON.parse(response).reason ?? 'Bad request'); + return false; + } + } catch (e) { + console.error(e); + return false; + } + } + + public async login(data: TOptionsData): Promise { + try { + const { status, response } = await AuthApi.login(data); + if (status === 200) { + Store.set('auth', true); + goToMessenger(); + this.getUser(); + } else if (status === 500) { + goToError500(); + } else { + alert(JSON.parse(response).reason ?? 'Bad request'); + } + } catch (e) { + console.error(e); + } + } + + public async getUser(): Promise { + try { + const response = await AuthApi.getUser(); + + if (response.status === 200 && response) { + Store.set('user', response.response); + Store.set('auth', true); + return true; + } else { + Store.set('auth', false); + return false; + } + } catch (e) { + console.error('Error in getUser:', e); + Store.set('auth', false); + return false; + } + } + + public async logout(): Promise { + try { + const { status, response } = await AuthApi.logout(); + if (status === 200) { + Store.setResetState(); + goToLogin(); + } else if (status === 500) { + goToError500(); + } else { + alert(JSON.parse(response).reason ?? 'Error response'); + } + } catch (e) { + console.error(e); + } + } +} + +export default new AuthController(); diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts new file mode 100644 index 0000000..869254e --- /dev/null +++ b/src/controllers/UserController.ts @@ -0,0 +1,66 @@ +import UserApi from "../api/UserApi"; +import {goToError500} from "../utils/router"; +import Store from "../core/Store"; + +class UserController { + public async changeUserData(data: any): Promise { + try { + const response = await UserApi.changeData(data); + + if (response && typeof response === 'object') { + if (response instanceof XMLHttpRequest) { + const userData = response.response; + Store.set('user', userData); + return userData; + } else { + Store.set('user', response); + return response; + } + } else { + goToError500(); + } + } catch (e) { + console.error(e); + } + } + + public async changePassword(data: any): Promise { + try { + const response = await UserApi.changePassword(data); + + if (response && typeof response === 'object') { + if (response instanceof XMLHttpRequest) { + return response.response; + } else { + return response; + } + } else { + goToError500(); + } + } catch (e) { + console.error(e); + } + } + + public async changeAvatar(data: FormData) { + try { + const response = await UserApi.changeAvatar(data); + if (response && typeof response === 'object') { + if (response instanceof XMLHttpRequest) { + const userData = response.response; + Store.set('user', userData); + return userData; + } else { + Store.set('user', response); + return response; + } + } else { + goToError500(); + } + } catch (e) { + console.error(e); + } + } +} + +export default new UserController(); diff --git a/src/core/Block.ts b/src/core/Block.ts index 7ff9f0c..ace7c84 100644 --- a/src/core/Block.ts +++ b/src/core/Block.ts @@ -1,6 +1,8 @@ import { EventBus } from './EventBus'; import Handlebars from 'handlebars'; import { v4 as makeUUID } from 'uuid'; +import { Indexed } from "../utils/utils"; +import isEqual from "../utils/isEqual"; export type Events = Record void>; export type Props = Record; @@ -14,6 +16,7 @@ class Block { INIT: "init", FLOW_CDM: "flow:component-did-mount", FLOW_CDU: "flow:component-did-update", + FLOW_CWU: 'flow:component-will-unmount', FLOW_RENDER: "flow:render" } as const; @@ -72,6 +75,7 @@ class Block { eventBus.on(Block.EVENTS.INIT, this._init.bind(this)); eventBus.on(Block.EVENTS.FLOW_CDM, this._componentDidMount.bind(this)); eventBus.on(Block.EVENTS.FLOW_CDU, this._componentDidUpdate.bind(this)); + eventBus.on(Block.EVENTS.FLOW_CWU, this._componentWillUnmount.bind(this)); eventBus.on(Block.EVENTS.FLOW_RENDER, this._render.bind(this)); } @@ -82,7 +86,6 @@ class Block { } public init() { - return this; } private _componentDidMount(): void { @@ -115,10 +118,16 @@ class Block { } protected componentDidUpdate(oldProps: Props, newProps: Props) { - return oldProps !== newProps; + return isEqual(oldProps, newProps); } - public setProps = (nextProps: Props): void => { + protected _componentWillUnmount() { + this.componentWillUnmount(); + } + + public componentWillUnmount() {} + + public setProps = (nextProps: Props) => { if (!nextProps) { return; } @@ -126,7 +135,7 @@ class Block { const oldProps = { ...this.props }; Object.assign(this.props, nextProps); this.eventBus().emit(Block.EVENTS.FLOW_CDU, oldProps, this.props); - }; + } private _render() { const fragment = this._compile(); @@ -141,6 +150,12 @@ class Block { this._element = newElement; this._addEvents(); + + Object.values(this.children).forEach(child => { + if (child instanceof Block) { + child.forceUpdate(); + } + }); } private _compile(): DocumentFragment { @@ -194,9 +209,10 @@ class Block { return typeof value === "function" ? value.bind(target) : value; }, set(target: Props, prop: string, value: unknown){ + const oldTarget = { ...target }; target[prop] = value; - self.eventBus().emit(Block.EVENTS.FLOW_CDU, {...target}, target); + self.eventBus().emit(Block.EVENTS.FLOW_CDU, oldTarget, target); return true; }, deleteProperty() { @@ -222,6 +238,14 @@ class Block { content.style.display = 'none'; } } + + static getStateToProps(_state: Indexed): Props { + return {}; + } + + public forceUpdate() { + this._render(); + } } export default Block; diff --git a/src/core/HTTPTransport.ts b/src/core/HTTPTransport.ts index 657c1ce..c7b97f7 100644 --- a/src/core/HTTPTransport.ts +++ b/src/core/HTTPTransport.ts @@ -1,34 +1,43 @@ +import queryStringify from '../utils/queryStringify'; +import {HOST} from '../utils/hosts'; + enum METHODS { GET = 'GET', POST = 'POST', PUT = 'PUT', DELETE = 'DELETE', } - +export type TOptionsData = Record>; export type Options = { headers?: Record, method?: string, - data?: any, + data?: Record | FormData | TOptionsData, timeout?: number, } export type HTTP = (url: string, options?: Options) => Promise; -export default class HTTPTransport { +class HTTPTransport { + protected apiUrl: string = ''; + + constructor(apiPath: string) { + this.apiUrl = `${HOST}${apiPath}`; + } + get: HTTP = (url = '', options = {}) => { - return this.request(url, {...options, method: METHODS.GET}, options.timeout); + return this.request(`${this.apiUrl}${url}`, {...options, method: METHODS.GET}, options.timeout); }; post: HTTP = (url = '', options = {}) => { - return this.request(url, {...options, method: METHODS.POST}, options.timeout); + return this.request(`${this.apiUrl}${url}`, {...options, method: METHODS.POST}, options.timeout); }; put: HTTP = (url = '', options = {}) => { - return this.request(url, {...options, method: METHODS.PUT}, options.timeout); + return this.request(`${this.apiUrl}${url}`, {...options, method: METHODS.PUT}, options.timeout); }; delete: HTTP = (url = '', options = {}) => { - return this.request(url, {...options, method: METHODS.DELETE}, options.timeout); + return this.request(`${this.apiUrl}${url}`, {...options, method: METHODS.DELETE}, options.timeout); }; request = (url: string, options: Options = {}, timeout = 5000) => { @@ -43,43 +52,44 @@ export default class HTTPTransport { const xhr = new XMLHttpRequest(); const isGet = method === METHODS.GET; + if (method === METHODS.GET && data) { + url += queryStringify(data as Record); + } + xhr.open( method, isGet && !!data ? `${url}${queryStringify(data)}` : url, ); - - Object.keys(headers).forEach(key => { - xhr.setRequestHeader(key, headers[key]); - }); - xhr.onload = function() { - resolve(xhr); + if (xhr.status >= 200 && xhr.status < 300) { + resolve(xhr); + } else { + reject(xhr); + } }; xhr.onabort = reject; xhr.onerror = reject; - xhr.timeout = timeout; xhr.ontimeout = reject; + xhr.responseType = 'json'; + xhr.withCredentials = true; + + // xhr.setRequestHeader('Content-Type', 'application/json'); + + Object.keys(headers).forEach(key => { + xhr.setRequestHeader(key, headers[key]); + }); if (isGet || !data) { xhr.send(); } else { - xhr.send(data); + xhr.send(data instanceof FormData ? data : JSON.stringify(data)); } }); }; } -function queryStringify(data: any): string { - if (typeof data !== 'object') { - throw new Error('Data must be object'); - } - - const keys: string[] = Object.keys(data); - return keys.reduce((result: string, key: string, index: number) => { - return `${result}${key}=${data[key]}${index < keys.length - 1 ? '&' : ''}`; - }, '?'); -} +export default HTTPTransport; diff --git a/src/core/Router.ts b/src/core/Router.ts index 5d162f3..db083dd 100644 --- a/src/core/Router.ts +++ b/src/core/Router.ts @@ -1,5 +1,6 @@ import Route from './Route'; import { BlockType } from './Block'; +// import Store from './Store'; class Router { static __instance: Router | undefined; @@ -74,6 +75,10 @@ class Router { getRoute(pathname: string) { return this._routes.find(route => route.match(pathname)); } + + getCurrentRoute() { + return this._currentRoute; + } } export default Router; diff --git a/src/core/Store.ts b/src/core/Store.ts new file mode 100644 index 0000000..6a69eda --- /dev/null +++ b/src/core/Store.ts @@ -0,0 +1,53 @@ +import { EventBus } from './EventBus'; +import { Indexed, set } from '../utils/utils' + +export enum StoreEvents { + Updated = 'updated', +} + +class Store extends EventBus { + private state: Indexed = {}; + + constructor() { + super(); + } + + public getState() { + return this.state; + } + + public set(path: string, value: unknown) { + set(this.state, path, value); + this.emit(StoreEvents.Updated); + } + + public setNotificationMessage(message: string | null) { + this.set('notificationMessage', message); + } + + public emit(event: string, ...args: unknown[]) { + if (!this.listeners[event]) { + console.warn(`Попытка вызвать несуществующее событие: ${event}`); + return; + } + + this.listeners[event].forEach(listener => { + listener(...args); + }); + } + + public setResetState(): void { + try { + this.state = { + auth: false, + user: null, + getPage: '/', + }; + this.emit(StoreEvents.Updated); + } catch (e) { + console.error(e); + } + } +} + +export default new Store(); \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 6761afa..93962f3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,11 @@ import Handlebars from 'handlebars'; import { registerComponent } from './core/RegistrationComponent'; -import { router, start } from "./utils/router"; +import { router } from "./utils/router"; import { BlockType } from "./core/Block"; import './style.less'; +import Store, {StoreEvents} from './core/Store'; +import AuthController from './controllers/AuthController'; +import {goToSettings, goToLogin} from "./utils/router"; type ImportValue = Record; type ImportGlob = Record; @@ -37,15 +40,38 @@ const registerImports = (imports: ImportValue) => { registerImports(components); registerImports(pages); -router - .use('/', pages.LoginPage as unknown as BlockType) - .use('/login', pages.LoginPage as unknown as BlockType) - .use('/signup', pages.RegistrationPage as unknown as BlockType) - .use('/messenger', pages.ChatPage as unknown as BlockType) - .use('/settings', pages.ProfilePage as unknown as BlockType) - .use('/settings/edit', pages.ProfileEditPage as unknown as BlockType) - .use('/settings/edit-password', pages.ProfileEditPasswordPage as unknown as BlockType) - .use('/404', pages.Error404Page as unknown as BlockType) - .use('/500', pages.Error500Page as unknown as BlockType); - -start(); +async function initApp() { + router + .use('/', pages.LoginPage as unknown as BlockType) + .use('/login', pages.LoginPage as unknown as BlockType) + .use('/signup', pages.RegistrationPage as unknown as BlockType) + .use('/messenger', pages.ChatPage as unknown as BlockType) + .use('/settings', pages.ProfilePage as unknown as BlockType) + .use('/settings/edit-password', pages.ProfileEditPasswordPage as unknown as BlockType) + .use('/404', pages.Error404Page as unknown as BlockType) + .use('/500', pages.Error500Page as unknown as BlockType) + .start(); + + Store.on(StoreEvents.Updated, () => {}); + + try { + const isLoggedIn = await AuthController.getUser(); + if (isLoggedIn) { + const currentRoute = router.getCurrentRoute(); + if (currentRoute && ( + currentRoute.match('/')) + || currentRoute?.match('/login') + || currentRoute?.match('/signup') + ) { + goToSettings(); + } + } else { + goToLogin(); + } + } catch (e) { + console.error('Error during app initialization:', e); + goToLogin(); + } +} + +initApp(); diff --git a/src/pages/login-form/login-form.hbs b/src/pages/login-form/login-form.hbs index 5d7e4ec..78c1527 100644 --- a/src/pages/login-form/login-form.hbs +++ b/src/pages/login-form/login-form.hbs @@ -8,7 +8,7 @@ {{{ Input type='password' name='password' class="password" id="password" label='Password' ref='password' }}}
- {{{ Button class="profile button-save" type="submit" label="Save" onClick=onValid }}} + {{{ Button class="profile button-save" type="submit" label="Save" onClick=onPasswordSave }}}
diff --git a/src/pages/profile-edit-password/profile-edit-password.ts b/src/pages/profile-edit-password/profile-edit-password.ts index 6d35ca9..300ad25 100644 --- a/src/pages/profile-edit-password/profile-edit-password.ts +++ b/src/pages/profile-edit-password/profile-edit-password.ts @@ -2,7 +2,10 @@ import '../profile/profile.less'; import ProfileEditPasswordPageTmpl from './profile-edit-password.hbs?raw'; import { BaseForm } from '../../core/BaseForm'; import { Props } from '../../core/Block'; +import {Input} from "../../components/input/input"; import { router, goToSettings } from '../../utils/router'; +import UserController from "../../controllers/UserController"; +import Store from '../../core/Store'; export class ProfileEditPasswordPage extends BaseForm { constructor(props: Props) { @@ -10,16 +13,62 @@ export class ProfileEditPasswordPage extends BaseForm { ...props, goBack() { router.back(); + }, + onPasswordSave: (event: Event) => { + if (!event) return; + event.preventDefault(); + + const formData: Record = {}; + let isValid: boolean = true; + let hasValues: boolean = false; + + Object.entries(this.children).forEach(([_key, child]) => { + if (child instanceof Input) { + const name = child.props.name as string; + const value = child.getValue() as string; + formData[name] = value; + + if (value.trim() !== '') { + hasValues = true; + const validationResult = child.validate(); + + if (!validationResult) { + isValid = false; + } + } + } + }); + + if (hasValues && isValid) { + this.savePassword(formData); + } else if (!hasValues) { + return; + } else { + console.error('Form validation failed, not saving changes'); + } } }); } - render(): string { - return ProfileEditPasswordPageTmpl; - } - protected onValid(formData: Record) { - console.log('Profile password edit successful', formData); + private async savePassword(formData: Record) { + try { + const passwordData = { + oldPassword: formData.oldPassword, + newPassword: formData.newPassword + }; + + await UserController.changePassword(passwordData); + Store.setNotificationMessage('Password has been successfully changed.'); + goToSettings(); + } catch (error) { + console.error('Ошибка при смене пароля:', error); + this.setProps({ + error: error instanceof Error ? error.message : 'Произошла неизвестная ошибка при смене пароля' + }); + } + } - goToSettings(); + render(): string { + return ProfileEditPasswordPageTmpl; } } diff --git a/src/pages/profile-edit/profile-edit.hbs b/src/pages/profile-edit/profile-edit.hbs deleted file mode 100644 index 0766801..0000000 --- a/src/pages/profile-edit/profile-edit.hbs +++ /dev/null @@ -1,28 +0,0 @@ -
- -
-
-
-
- {{{ Input type='email' name='email' class="email" id="email" label='Email' value="ivan@gmail.com" }}} -
-
- {{{ Input type='input' name='login' class="login" id="login" label='Login' value="ivanivanov" }}} -
-
- {{{ Input type='input' name='first_name' class="first_name" id="first_name" label='First Name' value="Ivan" }}} -
-
- {{{ Input type='input' name='second_name' class="second_name" id="second_name" label='Second Name' value="Ivanov" }}} -
-
- {{{ Input type='tel' name='phone' class="phone" id="phone" label='Phone Number' value="+78005553535" }}} -
-
- {{{ Button class="profile button-save" type="submit" label="Save" onClick=onValid }}} -
-
-
-
diff --git a/src/pages/profile-edit/profile-edit.ts b/src/pages/profile-edit/profile-edit.ts deleted file mode 100644 index fde9b31..0000000 --- a/src/pages/profile-edit/profile-edit.ts +++ /dev/null @@ -1,25 +0,0 @@ -import '../profile/profile.less'; -import ProfileEditPageTmpl from './profile-edit.hbs?raw'; -import { BaseForm } from '../../core/BaseForm'; -import { Props } from '../../core/Block'; -import { router, goToSettings } from '../../utils/router'; - -export class ProfileEditPage extends BaseForm { - constructor(props: Props) { - super({ - ...props, - goBack() { - router.back(); - } - }); - } - render(): string { - return ProfileEditPageTmpl; - } - - protected onValid(formData: Record) { - console.log('Profile edit successful', formData); - - goToSettings(); - } -} diff --git a/src/pages/profile/profile.hbs b/src/pages/profile/profile.hbs index 0a0aa16..1f6039a 100644 --- a/src/pages/profile/profile.hbs +++ b/src/pages/profile/profile.hbs @@ -3,31 +3,48 @@ {{{ Button class="back-button" type="button" id="button" onClick=goBack }}}
-
-
-
- Email +
+ {{{ Avatar avatar=user.avatar name=user.login onClick=onClickAvatar }}} + {{{ Input class="avatar-uploader" style="display: none" id="avatar-uploader" type="file" accept="image/*" skipValidation=true onChange=changeUserAvatar }}} +
+
+ {{user.first_name}} +
+
+
+ {{{ Input type='email' name='email' class="email" id="email" label='Email' value=user.email readonly=readonly }}}
-
- Login +
+ {{{ Input type='input' name='login' class="login" id="login" label='Login' value=user.login readonly=readonly }}}
-
- First NameIvan +
+ {{{ Input type='input' name='first_name' class="first_name" id="first_name" label='First Name' value=user.first_name readonly=readonly }}}
-
- Second NameIvanov +
+ {{{ Input type='input' name='second_name' class="second_name" id="second_name" label='Second Name' value=user.second_name readonly=readonly }}}
-
- Phone +
+ {{{ Input type='tel' name='phone' class="phone" id="phone" label='Phone Number' value=user.phone readonly=readonly }}}
-
+ {{#if editable }} +
+ {{{ Button class="profile button-save" type="submit" label="Save" onClick=saveUserChanges }}} +
+ {{/if}} + + {{#if notificationMessage}} +
{{ notificationMessage }}
+ {{/if}}
- {{{ Button class="profile button-edit-profile" label="Change Profile" onClick=goToSettingsEdit }}} + {{{ Button class="profile button-edit-profile" label="Change Profile" onClick=toggleEdit }}}
{{{ Button class="profile button-edit-password" label="Change Password" onClick=goToPasswordEdit }}}
+
+ {{{ Button class="profile button-logout" label="Log Out" onClick=goToLogout }}} +
diff --git a/src/pages/profile/profile.less b/src/pages/profile/profile.less index 7014cb5..411efdf 100644 --- a/src/pages/profile/profile.less +++ b/src/pages/profile/profile.less @@ -38,20 +38,79 @@ padding: 3rem; .avatar { - margin: 5rem 0; - border: 70px solid @main__background-color; - border-radius: 100%; + .image { + margin: 5rem 0; + border: 70px solid @main__background-color; + border-radius: 100%; + overflow: hidden; + + &:hover { + cursor: pointer; + border-color: rgba(0, 0, 0, 0.5); + + &:before { + content: 'Change avatar'; + font-size: 15px; + } + } - &:before { - font-family: @icon-font; - content: @icon__image; - color: @main__font-color; - font-size: 40px; - position: absolute; - transform: translate(-50%, -50%); + &:before { + font-family: @icon-font; + content: @icon__image; + color: @main__font-color; + font-size: 40px; + position: absolute; + transform: translate(-50%, -50%); + z-index: 1; + } + + &.uploaded { + border: none; + width: 180px; + height: 180px; + position: relative; + + img { + border-radius: 100%; + width: 180px; + height: 180px; + } + + &:hover { + cursor: pointer; + background: rgba(0, 0, 0, 0.5); + + &:after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + } + + &:before { + content: 'Change avatar' !important; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 15px; + } + } + + &:before { + content: ''; + } + } } } + .avatar-uploader { + visibility: hidden; + } + .info { flex-direction: column; width: 60%; @@ -125,6 +184,15 @@ } } + .success-message { + color: green; + background-color: #e6ffe6; + border: 1px solid #b3ffb3; + padding: 10px; + margin-top: 10px; + border-radius: 4px; + } + .edit-buttons { flex-direction: column; width: 60%; @@ -142,6 +210,15 @@ margin: 0; border-bottom: 2px solid @profile__info-underline-color; } + + .button-logout { + .button(@margin: 0.5rem 0, @background: none, @padding: 0, @color: @secondary__button-background-color); + + &:hover { + .custom-button-hover(@color: @secondary__button-background-color); + opacity: 70%; + } + } } } } diff --git a/src/pages/profile/profile.ts b/src/pages/profile/profile.ts index d145534..3efa316 100644 --- a/src/pages/profile/profile.ts +++ b/src/pages/profile/profile.ts @@ -1,25 +1,189 @@ import './profile.less'; -import Block, {Props} from "../../core/Block"; +import Block, {Props} from '../../core/Block'; import ProfilePageTmpl from './profile.hbs?raw'; -import { router, goToSettingsEdit, goToPasswordEdit } from '../../utils/router'; +import {goToPasswordEdit, goToMessenger} from '../../utils/router'; +import {connect} from '../../utils/connect'; +import {Indexed} from '../../utils/utils'; +import AuthController from '../../controllers/AuthController'; +import {Input} from "../../components/input/input"; +import UserController from "../../controllers/UserController"; +import Store from '../../core/Store'; + +export class ProfilePageBase extends Block { + private successMessageTimeout: number | null = null; -export class ProfilePage extends Block { constructor(props: Props) { super({ ...props, - goToSettingsEdit() { - goToSettingsEdit(); - }, + readonly: true, + editable: false, goToPasswordEdit() { goToPasswordEdit(); }, goBack() { - router.back(); + goToMessenger(); + }, + goToLogout() { + AuthController.logout(); + }, + toggleEdit: () => this.toggleEditMode(), + saveUserChanges: (event: Event) => { + if (!event) return; + event.preventDefault(); + + const formData: Record = {}; + let isValid: boolean = true; + let hasValues: boolean = false; + + Object.entries(this.children).forEach(([_key, child]) => { + if (child instanceof Input) { + const name = child.props.name as string; + const value = child.getValue() as string; + formData[name] = value; + + if (value.trim() !== '') { + hasValues = true; + const validationResult = child.validate(); + + if (!validationResult) { + isValid = false; + } + } + } + }); + + if (hasValues && isValid) { + this.saveChanges(formData); + } else if (!hasValues) { + return; + } else { + console.error('Form validation failed, not saving changes'); + } + }, + changeUserAvatar: (event: Event) => { + if (!event) return; + event.preventDefault(); + + const input = event?.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + const file = input.files[0]; + this.uploadAvatar(file); + } + }, + onClickAvatar: (event: Event) => { + if (!event) return; + event.preventDefault(); + this.onClickAvatar(); } }); } + componentDidMount() { + super.componentDidMount(); + this.checkAndStartTimer(); + } + + toggleEditMode() { + this.setProps({ + isEditable: !this.props.isEditable, + editable: !this.props.editable, + }); + this.updateInputsReadonlyState(); + } + + updateInputsReadonlyState() { + const inputs = this._element?.querySelectorAll('input') as NodeListOf; + inputs.forEach(input => { + input.readOnly = !this.props.isEditable; + }); + } + + private async saveChanges(formData: Record) { + try { + const userData = { + first_name: formData.first_name, + second_name: formData.second_name, + login: formData.login, + email: formData.email, + phone: formData.phone, + }; + + const updatedUser = await UserController.changeUserData(userData); + + if (updatedUser) { + this.setProps({ + user: updatedUser, + isEditable: false, + editable: false + }); + Store.setNotificationMessage('Profile has been successfully updated.'); + this.updateInputsReadonlyState(); + this.checkAndStartTimer(); + } + } catch (error) { + console.error('Error updating user data:', error); + } + } + + async uploadAvatar(file: File) { + try { + const data = new FormData(); + data.append('avatar', file, file.name); + const update = await UserController.changeAvatar(data); + + if (update) { + this.setProps({user: update}); + } + } catch (error) { + console.error('AvatarUploader: Error uploading avatar:', error); + } + } + + static getStateToProps(state: Indexed): Indexed { + return { + user: state.user || null, + }; + } + + private checkAndStartTimer() { + if (this.props.notificationMessage) { + this.startSuccessMessageTimer(); + } + } + + private startSuccessMessageTimer() { + if (this.successMessageTimeout) { + clearTimeout(this.successMessageTimeout); + } + + this.successMessageTimeout = window.setTimeout(() => { + Store.setNotificationMessage(''); + }, 2500); + } + + onClickAvatar() { + const avatarUploader: HTMLInputElement | undefined = this._element?.querySelector('#avatar-uploader') as HTMLInputElement | undefined; + if (avatarUploader) { + avatarUploader.click(); + } + } + render(): string { return ProfilePageTmpl; } + + protected componentDidUpdate(oldProps: any, newProps: any): boolean { + if (oldProps.notificationMessage !== newProps.notificationMessage) { + this.checkAndStartTimer(); + } + + return true; + } } + +export const ProfilePage = connect((state) => { + return { + user: state.user, + notificationMessage: state.notificationMessage + }; +})(ProfilePageBase); diff --git a/src/pages/registration-form/registration-form.hbs b/src/pages/registration-form/registration-form.hbs index 7c3e9e1..4384d53 100644 --- a/src/pages/registration-form/registration-form.hbs +++ b/src/pages/registration-form/registration-form.hbs @@ -20,7 +20,7 @@ {{{ Input type='tel' name='phone' class="phone" id="phone" label='Phone Number' }}}
- {{{ Button class="registration__button-registration" type="submit" label='Create account' onClick=onValid }}} + {{{ Button class="registration__button-registration" type="submit" label='Create account' onClick=onRegistration }}}
diff --git a/src/pages/registration-form/registration-form.ts b/src/pages/registration-form/registration-form.ts index 658f9dd..c4a683f 100644 --- a/src/pages/registration-form/registration-form.ts +++ b/src/pages/registration-form/registration-form.ts @@ -1,16 +1,62 @@ import './registration-form.less'; import RegistrationFormTmpl from './registration-form.hbs?raw'; import { BaseForm } from '../../core/BaseForm'; +import Block, { Props } from '../../core/Block'; import { goToMessenger } from '../../utils/router'; +import { Input } from "../../components/input/input"; +import AuthController from "../../controllers/AuthController"; export class RegistrationPage extends BaseForm { - render(): string { - return RegistrationFormTmpl; + constructor(props: Props) { + super({ + props, + onRegistration: (event: Event | undefined) => { + if (!event) return; + event.preventDefault(); + + const formData: Record = {}; + let isValid: boolean = true; + + Object.values(this.children).forEach((child: Block | Element) => { + if (child instanceof Input) { + const name = child.props.name as string; + formData[name] = child.getValue(); + if (!child.validate()) { + isValid = false; + } + } + }); + + if (isValid) { + this.submitForm(formData); + } else { + console.error('Form validation failed'); + } + } + }); } - protected onValid(formData: Record) { - console.log('Account registration successful', formData); + private async submitForm(formData: Record) { + try { + const userData = { + first_name: formData.first_name, + second_name: formData.second_name, + login: formData.login, + email: formData.email, + password: formData.password, + phone: formData.phone, + }; - goToMessenger(); + const success = await AuthController.createUser(userData); + if (success) { + goToMessenger(); + } + } catch (error) { + console.error('Registration error:', error); + } + } + + render(): string { + return RegistrationFormTmpl; } } diff --git a/src/utils/connect.ts b/src/utils/connect.ts new file mode 100644 index 0000000..934a2c5 --- /dev/null +++ b/src/utils/connect.ts @@ -0,0 +1,33 @@ +import Store, { StoreEvents } from '../core/Store'; +import Block from '../core/Block'; +import isEqual from '../utils/isEqual'; + +export function connect(mapStateToProps: (state: any) => any) { + return function(Component: typeof Block) { + return class extends Component { + private onChangeStoreCallback: () => void; + + constructor(props: any) { + let state = mapStateToProps(Store.getState()); + super({ ...props, ...mapStateToProps(state) }); + + this.onChangeStoreCallback = () => { + const newState = mapStateToProps(Store.getState()); + + if (!isEqual(state, newState)) { + this.setProps({ ...newState }); + } + + state = newState; + } + + Store.on(StoreEvents.Updated, this.onChangeStoreCallback); + } + + componentWillUnmount() { + super.componentWillUnmount(); + Store.off(StoreEvents.Updated, this.onChangeStoreCallback); + } + } + } +} diff --git a/src/utils/hosts.ts b/src/utils/hosts.ts new file mode 100644 index 0000000..7b084f0 --- /dev/null +++ b/src/utils/hosts.ts @@ -0,0 +1,7 @@ +const HOST = 'https://ya-praktikum.tech/api/v2'; +const HOST_RESOURCES = 'https://ya-praktikum.tech/api/v2/resources'; + +export { + HOST, + HOST_RESOURCES, +} diff --git a/src/utils/isEqual.ts b/src/utils/isEqual.ts new file mode 100644 index 0000000..462e133 --- /dev/null +++ b/src/utils/isEqual.ts @@ -0,0 +1,55 @@ +type PlainObject = { + [k in string]: T; +}; + +function isEqual(a: PlainObject, b: PlainObject): boolean { + if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) { + return a === b; + } + + const aIsArray = Array.isArray(a); + const bIsArray = Array.isArray(b); + + if (aIsArray !== bIsArray) { + return false; + } + + if (aIsArray && bIsArray) { + if ((a as any[]).length !== (b as any[]).length) { + return false; + } + } else { + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + + if (aKeys.length !== bKeys.length) { + return false; + } + } + + for (const key in a) { + if (Object.prototype.hasOwnProperty.call(a, key)) { + if (!Object.prototype.hasOwnProperty.call(b, key)) { + return false; + } + + const aValue = (a as Record)[key]; + const bValue = (b as Record)[key]; + + if (typeof aValue === 'object' && aValue !== null) { + if (typeof bValue !== 'object' || bValue === null) { + return false; + } + if (!isEqual(aValue as object, bValue as object)) { + return false; + } + } else if (aValue !== bValue) { + return false; + } + } + } + + return true; +} + +export default isEqual; diff --git a/src/utils/queryStringify.ts b/src/utils/queryStringify.ts new file mode 100644 index 0000000..7f512b9 --- /dev/null +++ b/src/utils/queryStringify.ts @@ -0,0 +1,41 @@ +type StringIndexed = Record; + +function queryStringify(data: StringIndexed): string | never { + if (typeof data !== 'object') { + throw new Error('Data must be object'); + } + + const keys = Object.keys(data); + return keys.reduce((res, key, index) => { + const value = data[key]; + const endLine = index < keys.length - 1 ? '&' : ''; + + if (Array.isArray(value)) { + const arrayValue = value.reduce( + (result, arrData, i) => ({ + ...result, + [`${key}[${i}]`]: arrData, + }), + {}, + ); + + return `${res}${queryStringify(arrayValue)}${endLine}`; + } + + if (typeof value === 'object') { + const objValue = Object.keys(value || {}).reduce( + (result, objKey) => ({ + ...result, + [`${key}[${objKey}]`]: value[objKey], + }), + {}, + ); + + return `${res}${queryStringify(objValue)}${endLine}`; + } + + return `${res}${key}=${value}${endLine}`; + }, ''); +} + +export default queryStringify; diff --git a/src/utils/router.ts b/src/utils/router.ts index d76f3c5..fd3d704 100644 --- a/src/utils/router.ts +++ b/src/utils/router.ts @@ -22,10 +22,6 @@ const goToSettings = () => { router.go('/settings'); } -const goToSettingsEdit = () => { - router.go('/settings/edit'); -} - const goToPasswordEdit = () => { router.go('/settings/edit-password'); } @@ -45,7 +41,6 @@ export { goToRegister, goToMessenger, goToSettings, - goToSettingsEdit, goToPasswordEdit, goToError404, goToError500 diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..254eeb6 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,36 @@ +export type Indexed = { + [key in string]: T; +}; + +export function set(object: Indexed | unknown, path: string, value: unknown): Indexed | unknown { + if (typeof object !== 'object' || object === null) { + return object; + } + + if (typeof path !== 'string') { + throw new Error('path must be string'); + } + + const result = path.split('.').reduceRight((acc, key) => ({ + [key]: acc, + }), value as any); + return merge(object as Indexed, result); +} + +export function merge(lhs: Indexed, rhs: Indexed): Indexed { + for (const p in rhs) { + if (!rhs.hasOwnProperty(p)) { + continue; + } + try { + if (rhs[p].constructor === Object) { + rhs[p] = merge(lhs[p] as Indexed, rhs[p] as Indexed); + } else { + lhs[p] = rhs[p]; + } + } catch (e) { + lhs[p] = rhs[p]; + } + } + return lhs; +} diff --git a/src/variables.less b/src/variables.less index a57cae0..572f81d 100644 --- a/src/variables.less +++ b/src/variables.less @@ -2,6 +2,7 @@ @main__font-color: #E2E2E4; @secondary__font-color: #9898B0; @main__button-background-color: #3369F3; +@secondary__button-background-color: #F44D4D; @main__button-background-hover: #3369f39e; @main__input-background-color: #252838; From d6343d66d904a6cd737a471f49c49404fdb16084 Mon Sep 17 00:00:00 2001 From: Toby Date: Sun, 18 Aug 2024 18:12:54 +0400 Subject: [PATCH 04/10] Add users search --- src/api/UserApi.ts | 11 +- src/components/search/search.hbs | 11 ++ src/components/search/search.ts | 60 ++++++++ src/controllers/UserController.ts | 21 +++ src/pages/chat/chat.hbs | 9 +- src/pages/chat/chat.less | 235 +++++++++++++++++------------- src/pages/profile/profile.less | 2 +- src/utils/types.ts | 10 ++ 8 files changed, 253 insertions(+), 106 deletions(-) create mode 100644 src/components/search/search.hbs create mode 100644 src/components/search/search.ts create mode 100644 src/utils/types.ts diff --git a/src/api/UserApi.ts b/src/api/UserApi.ts index e3aa77f..c341447 100644 --- a/src/api/UserApi.ts +++ b/src/api/UserApi.ts @@ -21,9 +21,18 @@ class UserApi { }); } - changeAvatar(data: FormData): Promise { + public changeAvatar(data: FormData): Promise { return user.put('/profile/avatar', { data }); } + + public searchUsers(login: string): Promise { + return user.post('/search', { + data: { login }, + headers: { + 'Content-type': 'application/json; charset=UTF-8', + } + }); + } } export default new UserApi(); diff --git a/src/components/search/search.hbs b/src/components/search/search.hbs new file mode 100644 index 0000000..4ced6f4 --- /dev/null +++ b/src/components/search/search.hbs @@ -0,0 +1,11 @@ + diff --git a/src/components/search/search.ts b/src/components/search/search.ts new file mode 100644 index 0000000..06f0494 --- /dev/null +++ b/src/components/search/search.ts @@ -0,0 +1,60 @@ +import Block from '../../core/Block'; +import SearchTemplate from './search.hbs?raw'; +import { User } from '../../utils/types'; +import UserController from '../../controllers/UserController'; + +interface SearchUsersProps { + onUserSelect: (user: User) => void; +} + +export class SearchUsers extends Block { + private searchTimeout: number | null = null; + + constructor(props: SearchUsersProps) { + super({ + ...props, + users: [], + searchQuery: '', + events: { + input: (e: Event) => { + const input = e.target as HTMLInputElement; + this.setProps({ searchQuery: input.value }); + this.debouncedSearch(); + }, + search: (e: Event) => { + e.preventDefault(); + this.searchUsers(); + } + } + }); + } + + private debouncedSearch() { + if (this.searchTimeout) { + clearTimeout(this.searchTimeout); + } + this.searchTimeout = setTimeout(() => { + this.searchUsers(); + }, 300) as unknown as number; + } + + async searchUsers() { + const query: string = this.props.searchQuery as string; + if (query.length < 3) { + this.setProps({ users: [] }); + return; + } + + try { + const users = await UserController.searchUsers(this.props.searchQuery as string); + this.setProps({ users, error: '' }); + } catch (error) { + console.error('Error searching users:', error); + this.setProps({ users: [], error: 'Error searching users' }); + } + } + + render() { + return SearchTemplate; + } +} diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index 869254e..89f23d9 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -61,6 +61,27 @@ class UserController { console.error(e); } } + + public async searchUsers(query: string): Promise { + try { + const response = await UserApi.searchUsers(query); + if (response instanceof XMLHttpRequest) { + const data = response.response; + if (Array.isArray(data)) { + return data; + } else { + return []; + } + } else if (Array.isArray(response)) { + return response; + } else { + return []; + } + } catch (error) { + console.error('Error searching users:', error); + throw error; + } + } } export default new UserController(); diff --git a/src/pages/chat/chat.hbs b/src/pages/chat/chat.hbs index f276c18..db92330 100644 --- a/src/pages/chat/chat.hbs +++ b/src/pages/chat/chat.hbs @@ -1,13 +1,14 @@ -
+
-
\ No newline at end of file +
diff --git a/src/components/chat-create/chat-create.ts b/src/components/chat-create/chat-create.ts index a331217..d4f498e 100644 --- a/src/components/chat-create/chat-create.ts +++ b/src/components/chat-create/chat-create.ts @@ -74,4 +74,4 @@ export const ChatCreate = connect((state) => { error: state.createChatError, success: state.createChatSuccess }; -})(ChatCreateBase); \ No newline at end of file +})(ChatCreateBase); diff --git a/src/components/chat-item/chat-item.ts b/src/components/chat-item/chat-item.ts index 0f04dc3..877ccdb 100644 --- a/src/components/chat-item/chat-item.ts +++ b/src/components/chat-item/chat-item.ts @@ -28,4 +28,4 @@ export class ChatItem extends Block { render(): string { return ChatItemTmpl; } -} \ No newline at end of file +} diff --git a/src/components/chat-list/chat-list.ts b/src/components/chat-list/chat-list.ts index 12f564a..e94db48 100644 --- a/src/components/chat-list/chat-list.ts +++ b/src/components/chat-list/chat-list.ts @@ -39,4 +39,4 @@ export const ChatList = connect((state) => { chats: state.chats || [], currentChat: state.currentChat }; -})(ChatListBase); \ No newline at end of file +})(ChatListBase); diff --git a/src/components/chat-menu/chat-menu.less b/src/components/chat-menu/chat-menu.less index f9fdb34..b9f3148 100644 --- a/src/components/chat-menu/chat-menu.less +++ b/src/components/chat-menu/chat-menu.less @@ -28,4 +28,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/components/chat-menu/chat-menu.ts b/src/components/chat-menu/chat-menu.ts index eaf1a15..8f85f2d 100644 --- a/src/components/chat-menu/chat-menu.ts +++ b/src/components/chat-menu/chat-menu.ts @@ -51,4 +51,4 @@ export const ChatMenu = connect((state) => { isUserSearchEnabled: state.isUserSearchEnabled || false, usersList: state.usersList || false }; -})(ChatMenuBase); \ No newline at end of file +})(ChatMenuBase); diff --git a/src/components/menu-button/menu-button.less b/src/components/menu-button/menu-button.less index df43492..38f5fff 100644 --- a/src/components/menu-button/menu-button.less +++ b/src/components/menu-button/menu-button.less @@ -72,4 +72,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/components/search/search.hbs b/src/components/search/search.hbs index 8e5678f..5a956b4 100644 --- a/src/components/search/search.hbs +++ b/src/components/search/search.hbs @@ -23,4 +23,4 @@ {{/if}}
-
\ No newline at end of file + diff --git a/src/controllers/ChatController.ts b/src/controllers/ChatController.ts index 406f288..aa55cd7 100644 --- a/src/controllers/ChatController.ts +++ b/src/controllers/ChatController.ts @@ -127,4 +127,4 @@ class ChatController { } } -export default new ChatController(); \ No newline at end of file +export default new ChatController(); diff --git a/src/controllers/MessageController.ts b/src/controllers/MessageController.ts index 2703e8d..fc95b01 100644 --- a/src/controllers/MessageController.ts +++ b/src/controllers/MessageController.ts @@ -77,4 +77,4 @@ export class MessageController { } Store.set('currentChatMessages', newChatMessages); } -} \ No newline at end of file +} diff --git a/src/core/Socket.ts b/src/core/Socket.ts index cdac673..81cea87 100644 --- a/src/core/Socket.ts +++ b/src/core/Socket.ts @@ -92,4 +92,4 @@ class Socket { } } -export default Socket; \ No newline at end of file +export default Socket; diff --git a/src/core/Store.ts b/src/core/Store.ts index fecc917..e018166 100644 --- a/src/core/Store.ts +++ b/src/core/Store.ts @@ -62,4 +62,4 @@ class Store extends EventBus { } } -export default new Store(); \ No newline at end of file +export default new Store(); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index e5c6469..e632b81 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -24,4 +24,4 @@ Handlebars.registerHelper('formatDate', function(dateString: string): string { Handlebars.registerHelper('firstLetter', function(str: string) { return str ? str.charAt(0).toUpperCase() : ''; -}); \ No newline at end of file +}); From 5f512578df8cd3ee96cefdf9c2dc6672e8595221 Mon Sep 17 00:00:00 2001 From: Toby Date: Sat, 24 Aug 2024 15:03:17 +0400 Subject: [PATCH 07/10] Fixes after review. Fix multiple request --- src/api/ChatApi.ts | 26 +-- src/components/add-user-modal/user-modal.hbs | 2 +- src/components/add-user-modal/user-modal.less | 13 ++ src/components/add-user-modal/user-modal.ts | 58 +----- src/components/avatar/avatar.hbs | 2 +- src/components/button/button.hbs | 2 +- src/components/chat-body/chat-body.hbs | 21 +- src/components/chat-body/chat-body.less | 80 +++++++- src/components/chat-body/chat-body.ts | 27 ++- src/components/chat-create/chat-create.hbs | 6 +- src/components/chat-create/chat-create.less | 54 ++++++ src/components/chat-create/chat-create.ts | 16 +- src/components/chat-item/chat-item.hbs | 10 +- src/components/chat-item/chat-item.ts | 16 +- src/components/chat-list/chat-list.hbs | 2 +- src/components/chat-list/chat-list.less | 183 ++++++++++++++++++ src/components/chat-list/chat-list.ts | 22 +-- src/components/chat-menu/chat-menu.ts | 22 +-- .../chat-settings-modal.hbs | 30 +++ .../chat-settings-modal.less | 131 +++++++++++++ .../chat-settings-modal.ts | 79 ++++++++ src/components/error/error.hbs | 2 +- src/components/input/input.ts | 4 +- src/components/search/search.less | 4 +- src/components/search/search.ts | 10 +- src/controllers/AuthController.ts | 45 ++--- src/controllers/ChatController.ts | 108 +++++------ src/controllers/MessageController.ts | 24 ++- src/controllers/UserController.ts | 18 +- src/core/Block.ts | 33 ++-- src/core/HTTPTransport.ts | 38 ++-- src/core/RegistrationComponent.ts | 23 ++- src/core/Render.ts | 14 +- src/core/Socket.ts | 12 +- src/core/Store.ts | 47 ++--- src/main.ts | 11 +- src/pages/chat/chat.hbs | 2 +- src/pages/chat/chat.less | 182 ----------------- src/pages/chat/chat.ts | 2 +- src/pages/login-form/login-form.hbs | 4 +- src/style.less | 11 ++ src/variables.less | 1 + 42 files changed, 868 insertions(+), 529 deletions(-) create mode 100644 src/components/chat-create/chat-create.less create mode 100644 src/components/chat-list/chat-list.less create mode 100644 src/components/chat-settings-modal/chat-settings-modal.hbs create mode 100644 src/components/chat-settings-modal/chat-settings-modal.less create mode 100644 src/components/chat-settings-modal/chat-settings-modal.ts diff --git a/src/api/ChatApi.ts b/src/api/ChatApi.ts index 4e7fd94..6ab2569 100644 --- a/src/api/ChatApi.ts +++ b/src/api/ChatApi.ts @@ -9,12 +9,7 @@ class ChatAPI { } public async createChat(title: string): Promise { - return chats.post('/', { - data: { title }, - headers: { - 'Content-type': 'application/json; charset=UTF-8', - }, - }); + return chats.post('/', {data: { title }}); } public async getUserToken(chatId: number): Promise<{ token: string }> { @@ -26,22 +21,22 @@ class ChatAPI { return response; } - public async getChatUsers(chatId: number): Promise { + public async getChatUsers(chatId: number | undefined): Promise { return chats.get(`/${chatId}/users`); } - public async addUsers(chatId: number, users: any): Promise { + public async addUsers(data: any): Promise { return chats.put('/users', { - data: { chatId, users }, + data: data, headers: { 'Content-type': 'application/json; charset=UTF-8', }, }); } - public async removeUsers(chatId: number, users: any): Promise { + public async removeUsers(data: any): Promise { return chats.delete('/users', { - data: { chatId, users }, + data: data, headers: { 'Content-type': 'application/json; charset=UTF-8', } @@ -56,6 +51,15 @@ class ChatAPI { } }); } + + public async uploadAvatar(data: any): Promise { + return chats.put('/avatar', { + data: data, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + } } export default new ChatAPI(); diff --git a/src/components/add-user-modal/user-modal.hbs b/src/components/add-user-modal/user-modal.hbs index 1b4b41e..43caa6e 100644 --- a/src/components/add-user-modal/user-modal.hbs +++ b/src/components/add-user-modal/user-modal.hbs @@ -1,6 +1,6 @@ {{#if isAddUserOpen }}
-

Добавить пользователя в чат

+

{{#if isUserSearchEnabled}}Add user to chat{{else}}Delete user from chat{{/if}}

{{#if isUserSearchEnabled}} {{{ Search users=users class="search" placeholder="Search users" }}} {{/if}} diff --git a/src/components/add-user-modal/user-modal.less b/src/components/add-user-modal/user-modal.less index 0fe2693..d078e30 100644 --- a/src/components/add-user-modal/user-modal.less +++ b/src/components/add-user-modal/user-modal.less @@ -10,13 +10,25 @@ background: @main__background-color; padding: 3rem 2rem 1rem 2rem; display: flex; + width: 30%; flex-direction: column; justify-content: space-between; .title { + text-align: center; margin: 0 0 1rem 0; } + .user-item { + list-style: none; + border-bottom: 1px solid @secondary__background-color; + cursor: pointer; + + &:hover { + color: @main__background-hover-color; + } + } + .search { .search-results { display: block; @@ -30,6 +42,7 @@ margin-top: 0; padding-inline-start: 15px; padding-inline-end: 15px; + padding-bottom: 0.75rem; .user-item { display: flex; diff --git a/src/components/add-user-modal/user-modal.ts b/src/components/add-user-modal/user-modal.ts index 50018db..35e73ae 100644 --- a/src/components/add-user-modal/user-modal.ts +++ b/src/components/add-user-modal/user-modal.ts @@ -12,8 +12,7 @@ export class UserModalBase extends Block { super({ ...props, onClose: () => { - Store.set('isAddUserOpen', false); - Store.set('selectedUser', null); + Store.set({isAddUserOpen: false}); }, events: { click: (event: Event) => { @@ -27,34 +26,6 @@ export class UserModalBase extends Block { this.children.Search = new Search({} as any); } - protected componentDidUpdate(oldProps: any, newProps: any) { - if (oldProps.selectedUser !== newProps.selectedUser && newProps.selectedUser) { - if (this.props.isUserSearchEnabled) { - this.addUserToChat(newProps.selectedUser); - } else { - this.removeUserFromChat(newProps.selectedUser); - } - Store.set('selectedUser', null); - } - - return true; - } - - private async addUserToChat(user: any) { - const chatId = Store.getState().selectedChat?.id; - if (chatId) { - try { - await ChatController.addUsers({ id: chatId, user: user.id }); - await ChatController.getChatUsers(chatId); - Store.set('isAddUserOpen', false); - } catch (error) { - console.error('Error adding user to chat:', error); - } - } else { - console.error('No selected chat found'); - } - } - private async handleUserClick(e: Event) { const target = e.target as HTMLElement; const userItem = target.closest('.user-item'); @@ -64,28 +35,14 @@ export class UserModalBase extends Block { let user; if (this.props.isUserSearchEnabled) { user = this.props.users.find((u: User) => u.id.toString() === userId); + await ChatController.addUsers(user); + Store.set({isAddUserOpen: false}); } else { user = this.props.currentChatUsers.find((u: User) => u.id.toString() === userId); + await ChatController.removeUsers(user); + Store.set({isAddUserOpen: false}); } - if (user) { - Store.setSelectedUser(user); - } - } - } - } - - private async removeUserFromChat(user: any) { - const chatId = Store.getState().selectedChat?.id; - if (chatId) { - try { - await ChatController.removeUsers({ id: chatId, users: [user.id] }); - await ChatController.getChatUsers(chatId); - Store.set('isAddUserOpen', false); - } catch (error) { - console.error('Error removing user from chat:', error); } - } else { - console.error('No selected chat found'); } } @@ -98,9 +55,10 @@ export const UserModal = connect((state) => { return { isAddUserOpen: state?.isAddUserOpen || false, isUserSearchEnabled: state?.isUserSearchEnabled || false, - usersList: state?.usersList || false, selectedUser: state.selectedUser, selectedChatId: state.selectedChat?.id, - currentChatUsers: state?.currentChatUsers || [] + currentChatUsers: state?.currentChatUsers || [], + users: state?.users || [], + usersList: state.usersList } })(UserModalBase); diff --git a/src/components/avatar/avatar.hbs b/src/components/avatar/avatar.hbs index df606d1..c1f714c 100644 --- a/src/components/avatar/avatar.hbs +++ b/src/components/avatar/avatar.hbs @@ -1,4 +1,4 @@ -
+
{{ label }} + diff --git a/src/components/chat-body/chat-body.hbs b/src/components/chat-body/chat-body.hbs index 047aabb..0faecbc 100644 --- a/src/components/chat-body/chat-body.hbs +++ b/src/components/chat-body/chat-body.hbs @@ -1,10 +1,22 @@ {{#if selectedChat}}
-

{{selectedChat.title}}

- {{#if websocketError}} -
{{websocketError}}
- {{/if}} +
+
+ {{#if selectedChat.avatar}} + {{{ Avatar avatar=selectedChat.avatar name=selectedChat.login }}} + {{else}} +
+ {{firstLetter selectedChat.title}} +
+ {{/if}} +
+

{{selectedChat.title}}

+ {{#if isSettingsModalOpen }} + {{{ ChatSettingsModal selectedChat=selectedChat currentChatUsers=currentChatUsers }}} +
+ {{/if}} +
{{{ MenuButton isOpenChatMenu=isOpenChatMenu class="chat-menu-button" onClick=toggleMenu }}} {{#if isOpenChatMenu}} @@ -12,6 +24,7 @@ {{/if}} {{#if isAddUserOpen }} {{{ UserModal }}} +
{{/if}}
diff --git a/src/components/chat-body/chat-body.less b/src/components/chat-body/chat-body.less index ab74d29..0cfe2c1 100644 --- a/src/components/chat-body/chat-body.less +++ b/src/components/chat-body/chat-body.less @@ -27,8 +27,80 @@ background-color: @main__background-color; border-bottom: 1px solid @secondary__background-color; - h2 { + .left-section { + display: flex; + flex-direction: row; padding-inline-start: 2rem; + justify-content: space-between; + align-items: center; + width: auto; + border-right: 1px solid @secondary__background-color; + + .avatar { + display: flex; + align-items: center; + justify-content: center; + width: 1.375rem !important; + height: 1.375rem !important; + z-index: 100; + + .default-avatar { + display: flex; + align-items: center; + justify-content: center; + width: 1.375rem !important; + height: 1.375rem !important; + z-index: 100; + + .title { + position: absolute; + } + + &:after { + content: ''; + border: 30px solid #343A4F; + border-radius: 100%; + } + } + + .image { + position: absolute; + + &.uploaded { + width: 3.675rem !important; + height: 3.675rem !important; + + img { + width: 3.675rem !important; + height: 3.675rem !important; + border-radius: 50%; + } + } + } + } + + .chat-title { + padding-right: 0.5rem; + cursor: pointer; + margin-left: 3rem; + + &:hover { + color: @main__button-background-hover; + + &:after { + color: @main__button-background-hover; + } + } + + &:after { + font-family: @icon-font; + content: @icon__angle-right; + color: @main__font-color; + vertical-align: middle; + font-size: 12px; + margin-left: 0.25rem; + } + } } } @@ -78,6 +150,7 @@ border-left: 1px solid @secondary__background-color; display: flex; position: relative; + align-items: center; .sender { .input(@border-radius: 0, @width: 100%, @padding: 0, @color: @main__font-color); @@ -107,20 +180,21 @@ position: absolute; right: 0; - border: 10px solid @main__button-background-color; + border: 20px solid @main__button-background-color; border-radius: 100%; &:before { font-family: @icon-font; content: '\e901'; color: @main__icon-color; - font-size: 20px; + font-size: 30px; position: absolute; transform: translate(-50%, -50%); } &:hover { .custom-button-hover(); + border-color: @main__button-background-hover; } } } diff --git a/src/components/chat-body/chat-body.ts b/src/components/chat-body/chat-body.ts index dfd0bba..e90cf2c 100644 --- a/src/components/chat-body/chat-body.ts +++ b/src/components/chat-body/chat-body.ts @@ -19,23 +19,24 @@ class ChatBodyBase extends Block { super({ ...props, events: { - submit: (e: Event) => this.onSubmit(e), - click: (e: Event) => this.handleClick(e) + submit: (event: Event) => this.onSubmit(event), + click: (event: Event) => this.handleClick(event) }, - toggleMenu: (e: Event) => { - e.stopPropagation(); + toggleMenu: (event: Event) => { + event.stopPropagation(); const isOpen = Store.getState().isOpenChatMenu; - Store.set('isOpenChatMenu', !isOpen); + Store.set({isOpenChatMenu: !isOpen}); + }, + openSettingsModal: (event: Event) => { + if (!event) return; + event.preventDefault(); + Store.set({isSettingsModalOpen: true}); } }); } - protected componentDidUpdate(_oldProps: ChatBodyProps, _newProps: ChatBodyProps): boolean { - return true; - } - private closeMenu() { - Store.set('isOpenChatMenu', false); + Store.set({isOpenChatMenu: false}); } private handleClick(e: Event) { @@ -47,6 +48,10 @@ class ChatBodyBase extends Block { } else if (isMenuOpen && !target.closest('.menu__wrapper') && !target.closest('form')) { this.closeMenu(); } + + if (target.closest('.chat-title')) { + this.props.openSettingsModal(e); + } } private onSubmit(e: Event) { @@ -72,5 +77,7 @@ export const ChatBody = connect((state) => { websocketError: state.websocketError, isOpenChatMenu: state.isOpenChatMenu || false, isAddUserOpen: state.isAddUserOpen || false, + isSettingsModalOpen: state.isSettingsModalOpen || false, + currentChatUsers: state.currentChatUsers || [], }; })(ChatBodyBase); diff --git a/src/components/chat-create/chat-create.hbs b/src/components/chat-create/chat-create.hbs index 6398a17..a680b05 100644 --- a/src/components/chat-create/chat-create.hbs +++ b/src/components/chat-create/chat-create.hbs @@ -2,8 +2,8 @@
diff --git a/src/components/chat-create/chat-create.less b/src/components/chat-create/chat-create.less new file mode 100644 index 0000000..1e3a622 --- /dev/null +++ b/src/components/chat-create/chat-create.less @@ -0,0 +1,54 @@ +@import '../../utils.less'; + +#app { + .chat { + .create-chat-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + justify-content: center; + align-items: center; + z-index: 1000; + + &.is-open { + display: flex; + } + + .modal-content { + background-color: @main__background-color; + width: 25%; + padding: 2rem 3rem; + border-radius: 14px; + text-align: center; + + .create-chat-form { + display: flex; + flex-direction: column; + margin: 2rem 1rem; + + .chat-title { + .input(@background: @secondary__background-color); + margin-bottom: 2.5rem; + } + + .chat-create-button { + margin: 0 auto; + } + } + + .close-modal { + margin-top: 1rem; + .button(@background: none, @width: auto, @border: none); + + &:hover { + .custom-button-hover(@color: @main__button-background-color); + } + } + } + } + } +} diff --git a/src/components/chat-create/chat-create.ts b/src/components/chat-create/chat-create.ts index d4f498e..520cc99 100644 --- a/src/components/chat-create/chat-create.ts +++ b/src/components/chat-create/chat-create.ts @@ -1,3 +1,4 @@ +import './chat-create.less'; import Block from '../../core/Block'; import CreateChatFormTemplate from './chat-create.hbs?raw'; import ChatController from '../../controllers/ChatController'; @@ -19,7 +20,7 @@ export class ChatCreateBase extends Block { click: (e: Event) => this.onClick(e), }, closeModal: () => { - Store.set('isCreateChatModalOpen', false); + Store.set({isCreateChatModalOpen: false}); } }); } @@ -33,7 +34,7 @@ export class ChatCreateBase extends Block { } private closeModal() { - Store.set('isCreateChatModalOpen', false); + Store.set({isCreateChatModalOpen: false}); } private async onSubmit(e: Event) { @@ -46,20 +47,17 @@ export class ChatCreateBase extends Block { try { await ChatController.createChat(chatTitle); form.reset(); - Store.set('createChatSuccess', 'Chat created successfully'); - Store.set('createChatError', null); + Store.set({createChatSuccess: 'Chat created successfully', createChatError: null }); setTimeout(() => { this.closeModal(); - Store.set('createChatSuccess', null); + Store.set({createChatSuccess: null}); }, 2000); } catch (error) { console.error('Error creating chat:', error); - Store.set('createChatError', 'Failed to create chat'); - Store.set('createChatSuccess', null); + Store.set({createChatError: 'Failed to create chat', createChatSuccess: null}); } } else { - Store.set('createChatError', 'Chat title cannot be empty'); - Store.set('createChatSuccess', null); + Store.set({createChatError: 'Failed to create chat', createChatSuccess: null}); } } diff --git a/src/components/chat-item/chat-item.hbs b/src/components/chat-item/chat-item.hbs index 4e270fb..9aca15d 100644 --- a/src/components/chat-item/chat-item.hbs +++ b/src/components/chat-item/chat-item.hbs @@ -1,9 +1,13 @@ -
+
{{#if chat.avatar}} - {{chat.title}} +
+ {{{ Avatar avatar=chat.avatar name=chat.login }}} +
{{else}} -
{{firstLetter chat.title}}
+
+ {{firstLetter chat.title}} +
{{/if}}
diff --git a/src/components/chat-item/chat-item.ts b/src/components/chat-item/chat-item.ts index 877ccdb..b15ac75 100644 --- a/src/components/chat-item/chat-item.ts +++ b/src/components/chat-item/chat-item.ts @@ -1,28 +1,26 @@ import ChatItemTmpl from './chat-item.hbs?raw' import Block, { Props } from '../../core/Block'; -import ChatController from '../../controllers/ChatController'; +import isEqual from "../../utils/isEqual"; export class ChatItem extends Block { constructor(props: Props) { super({ ...props, - select: () => (props?.id === props?.currentChat), + select: () => (props?.chat.id === props?.currentChat), events: { click: (e: Event) => { + if (!e) return; e.preventDefault(); - const chatId = this.props.chat.id; - if (chatId !== undefined) { - ChatController.setCurrentChat(chatId); - } else { - console.error('Chat id is undefined'); + if (props.onSetCurrentChat) { + props.onSetCurrentChat.call(this, this.props?.chat.id); } }, }, }); } - protected componentDidUpdate(_oldProps: Props, _newProps: Props): boolean { - return true; + protected componentDidUpdate(oldProps: Props, newProps: Props): boolean { + return !isEqual(oldProps, newProps); } render(): string { diff --git a/src/components/chat-list/chat-list.hbs b/src/components/chat-list/chat-list.hbs index fe26277..dca26e8 100644 --- a/src/components/chat-list/chat-list.hbs +++ b/src/components/chat-list/chat-list.hbs @@ -1,7 +1,7 @@
{{#if chats.length}} {{#each chats}} - {{{ ChatItem chat=this currentChat=../currentChat }}} + {{{ ChatItem chat=this onSetCurrentChat=../onSetCurrentChat currentChat= ../currentChat }}} {{else}}

No chats available

{{/each}} diff --git a/src/components/chat-list/chat-list.less b/src/components/chat-list/chat-list.less new file mode 100644 index 0000000..291b6d1 --- /dev/null +++ b/src/components/chat-list/chat-list.less @@ -0,0 +1,183 @@ +@import '../../utils.less'; + +.sidebar { + flex: 1; + + .list { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + + .list-header { + display: flex; + flex-direction: column; + height: 5rem; + border-right: 1px solid @secondary__background-color; + border-bottom: 1px solid @secondary__background-color; + + .section { + &.buttons { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + .create-new-chat { + margin: 0; + + .new-chat { + .button(@margin: 0, @background: none, @border: none, @width: auto); + + &:hover { + .custom-button-hover(); + } + } + } + + .profile { + align-self: flex-end; + padding: 0.25rem 0.1rem; + + .profile-button { + .button(@background: none, @width: 100%, @margin: 0); + + &:hover { + background: none !important; + color: @main__button-background-hover; + + &:after { + color: @main__button-background-hover; + } + } + + &:after { + font-family: @icon-font; + content: @icon__angle-right; + color: @main__font-color; + vertical-align: middle; + font-size: 12px; + margin-left: 0.25rem; + } + } + + &:hover, &:active, &:focus-visible { + background: none !important; + color: @main__button-background-hover !important; + } + } + } + } + + } + + .chat-list { + overflow-y: scroll; + } + + .item { + display: flex; + flex-direction: column; + justify-content: center; + padding-top: 0 !important; + padding-bottom: 0 !important; + padding-inline: 1rem .75rem; + border-bottom: 1px solid @secondary__background-color; + min-height: 4.5rem; + padding-inline-start: 4.5rem !important; + text-decoration: none; + color: @main__font-color; + position: relative; + cursor: pointer; + + &.selected { + background: @main__button-background-color; + + .dialog { + &.subtitle { + color: @main__font-color; + } + } + } + + &:hover { + background: @main__background-hover-color; + } + + .avatar { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + inset-inline-start: .5625rem !important; + + .loaded-avatar { + inset-inline-start: .5625rem !important; + width: 3.575rem !important; + height: 3.575rem !important; + + .image { + &.uploaded { + img { + width: 3.575rem !important; + height: 3.575rem !important; + border-radius: 50%; + } + } + } + } + + .default-avatar { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + inset-inline-start: .1rem !important; + width: 3.375rem !important; + height: 3.375rem !important; + + .title { + position: absolute; + font-size: 25px; + } + + &:after { + content: ''; + border: 30px solid @secondary__background-color; + border-radius: 100%; + } + } + } + + .dialog { + display: flex; + justify-content: space-between; + align-items: center; + + &.title { + height: 1.375rem; + font-weight: bold; + } + + &.subtitle { + height: 1.375rem; + margin-top: .125rem; + color: @secondary__font-color; + + .unread-count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + background-color: @secondary__font-color; /* Вы можете изменить цвет на любой подходящий вашему дизайну */ + color: white; + border-radius: 10px; /* Это создаст круглую форму */ + font-size: 12px; + font-weight: bold; + } + } + } + } + } +} diff --git a/src/components/chat-list/chat-list.ts b/src/components/chat-list/chat-list.ts index e94db48..ebca84d 100644 --- a/src/components/chat-list/chat-list.ts +++ b/src/components/chat-list/chat-list.ts @@ -1,3 +1,4 @@ +import './chat-list.less'; import Block, {Props} from '../../core/Block'; import ChatListTemplate from './chat-list.hbs?raw'; import ChatController from '../../controllers/ChatController'; @@ -7,26 +8,10 @@ export class ChatListBase extends Block { constructor(props: Props) { super({ ...props, - onSetCurrentChat: (id: number) => { + onSetCurrentChat: (id: number | undefined) => { ChatController.setCurrentChat(id); }, }); - - setInterval(() => { - ChatController.getChats(); - }, 5000); - } - - init() { - this.loadChats(); - } - - private async loadChats() { - try { - await ChatController.getChats(); - } catch (error) { - console.error('Error loading chats:', error); - } } render(): string { @@ -37,6 +22,7 @@ export class ChatListBase extends Block { export const ChatList = connect((state) => { return { chats: state.chats || [], - currentChat: state.currentChat + currentChat: state.currentChat, + selectedChat: state.selectedChat, }; })(ChatListBase); diff --git a/src/components/chat-menu/chat-menu.ts b/src/components/chat-menu/chat-menu.ts index 8f85f2d..c4fae7f 100644 --- a/src/components/chat-menu/chat-menu.ts +++ b/src/components/chat-menu/chat-menu.ts @@ -10,34 +10,18 @@ export class ChatMenuBase extends Block { super({ ...props, addUser: () => { - Store.set('isAddUserOpen', true); - Store.set('isUserSearchEnabled', true); - Store.set('usersList', false); - Store.set('isOpenChatMenu', false); + Store.set({isAddUserOpen: true, isUserSearchEnabled: true, usersList: false, isOpenChatMenu: false}); }, deleteUser: () => { - Store.set('isAddUserOpen', true); - Store.set('isUserSearchEnabled', false); - Store.set('usersList', true); - Store.set('isOpenChatMenu', false); - - if (Store.getState().usersList) { - this.getChatUsers(); - } + Store.set({isAddUserOpen: true, isUserSearchEnabled: false, usersList: true, isOpenChatMenu: false}); }, deleteChat: () => { - Store.set('isOpenChatMenu', false); - Store.set('selectedChat', false); + Store.set({isOpenChatMenu: false, selectedChat: false}); ChatController.deleteChat(); } }); } - private async getChatUsers() { - const chatId = Store.getState().currentChat; - await ChatController.getChatUsers(chatId); - } - render(): string { return ChatMenuTmpl; } diff --git a/src/components/chat-settings-modal/chat-settings-modal.hbs b/src/components/chat-settings-modal/chat-settings-modal.hbs new file mode 100644 index 0000000..81f991e --- /dev/null +++ b/src/components/chat-settings-modal/chat-settings-modal.hbs @@ -0,0 +1,30 @@ +{{#if isSettingsModalOpen}} +
+
+ {{#if selectedChat.avatar}} +
+ {{{ Avatar avatar=selectedChat.avatar name=selectedChat.login }}} + {{{ Input class="avatar-uploader" style="display: none" id="avatar-uploader" type="file" accept="image/*" skipValidation=true onChange=changeChatAvatar }}} +
+ {{else}} +
+ {{firstLetter selectedChat.title}} + {{{ Input class="avatar-uploader" style="display: none" id="avatar-uploader" type="file" accept="image/*" skipValidation=true onChange=changeChatAvatar }}} +
+ {{/if}} +
+
+
    + {{#each currentChatUsers }} +
  • + {{first_name}} {{second_name}} + +
  • + {{/each}} +
+
+
+ {{{ Button class="close" type="button" label="Cancel" onClick=onClose }}} +
+
+{{/if}} diff --git a/src/components/chat-settings-modal/chat-settings-modal.less b/src/components/chat-settings-modal/chat-settings-modal.less new file mode 100644 index 0000000..fd9d3ca --- /dev/null +++ b/src/components/chat-settings-modal/chat-settings-modal.less @@ -0,0 +1,131 @@ +@import '../../utils.less'; + +.chat.messenger { + .messenger-body { + .chat-body { + .chat-header { + .chat-settings { + display: flex; + flex-direction: column; + z-index: 1000; + top: 15%; + position: absolute; + background: white; + width: 25%; + padding: 3rem 2rem; + align-items: center; + border-radius: 14px; + background: @main__background-color; + + .avatar { + margin-bottom: 2rem; + display: flex; + align-items: center; + justify-content: center; + position: relative; + width: 4.995rem !important; + height: 4.995rem !important; + + &:hover { + cursor: pointer; + background: rgba(0, 0, 0, 0.5); + border-radius: 50%; + width: 6.675rem !important; + height: 6.675rem !important; + + &:before { + content: 'Change avatar' !important; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 15px; + z-index: 1001; + } + + &:after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + } + } + + .loaded-avatar-modal { + position: absolute; + width: 6.575rem !important; + height: 6.575rem !important; + + .image { + width: 6.575rem !important; + height: 6.575rem !important; + cursor: pointer; + + &.uploaded { + width: 6.575rem !important; + height: 6.575rem !important; + + img { + width: 6.575rem !important; + height: 6.575rem !important; + } + } + } + } + + .default-avatar { + position: absolute; + font-size: 25px; + + &:after { + content: ''; + border: 53px solid @secondary__background-color; + border-radius: 100%; + } + } + } + + .chat-users { + margin-bottom: 2rem; + width: 100%; + + .users-list { + display: flex; + flex-direction: column; + padding: 0; + text-align: center; + + .user-item { + display: flex; + flex-direction: column; + border-bottom: 1px solid @secondary__background-color; + padding: 5px; + cursor: pointer; + + &:hover { + background: @main__background-hover-color; + } + } + } + } + + .close-action { + .close { + .button(@margin: 0, @width: auto, @background: none, @color: @secondary__button-background-color); + + &:hover { + .custom-button-hover(@color: @secondary__button-background-color); + opacity: 70%; + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/components/chat-settings-modal/chat-settings-modal.ts b/src/components/chat-settings-modal/chat-settings-modal.ts new file mode 100644 index 0000000..b8195f5 --- /dev/null +++ b/src/components/chat-settings-modal/chat-settings-modal.ts @@ -0,0 +1,79 @@ +import './chat-settings-modal.less'; +import ChatSettingsModalTmpl from './chat-settings-modal.hbs?raw'; +import Block, {Props} from '../../core/Block'; +import Store from '../../core/Store'; +import {connect} from "../../utils/connect"; +import ChatController from "../../controllers/ChatController"; + +export class ChatSettingsModalBase extends Block { + constructor(props: Props) { + super({ + ...props, + events: { + click: (event: Event) => { + this.handleClick(event); + } + }, + onClose: () => { + Store.set({isSettingsModalOpen: false}); + }, + changeChatAvatar: (event: Event) => { + if (!event) return; + event.preventDefault(); + + const input = event?.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + const file = input.files[0]; + this.uploadAvatar(file); + } + } + }); + } + + private handleClick(event: Event) { + const target = event.target as HTMLElement; + const defaultAvatar = target.closest('.avatar'); + const avatarLoaded = target.closest('.image'); + + if (defaultAvatar) { + this.onClickAvatar(); + } + + if (avatarLoaded) { + this.onClickAvatar(); + } + } + + onClickAvatar() { + const avatarUploader: HTMLInputElement | undefined = this._element?.querySelector('#avatar-uploader') as HTMLInputElement | undefined; + if (avatarUploader) { + avatarUploader.click(); + } + } + + async uploadAvatar(file: File) { + try { + const data = new FormData(); + data.set('chatId', this.props.selectedChat.id); + data.set('avatar', file, file.name); + + const update = await ChatController.changeAvatar(data); + + if (update) { + this.setProps({selectedChat: update}); + } + } catch (error) { + console.error('AvatarUploader: Error uploading avatar:', error); + } + } + + render(): string { + return ChatSettingsModalTmpl; + } +} + +export const ChatSettingsModal = connect((state) => { + return { + isSettingsModalOpen: state.isSettingsModalOpen || false, + } +})(ChatSettingsModalBase); diff --git a/src/components/error/error.hbs b/src/components/error/error.hbs index b294b5b..a772db2 100644 --- a/src/components/error/error.hbs +++ b/src/components/error/error.hbs @@ -1,6 +1,6 @@

{{errorTitle}}

{{errorText}} - {{{ Button class="error back-button" label="Chat" page="ChatPage" }}} + {{{ Button class="error back-button" label="Chat" }}}
diff --git a/src/components/input/input.ts b/src/components/input/input.ts index 555deab..3889989 100644 --- a/src/components/input/input.ts +++ b/src/components/input/input.ts @@ -31,7 +31,9 @@ export class Input extends Block { if (typeof this.props.onChange === 'function') { this.props.onChange(e); } - } + }, + + ...(props.onInput ? { input: props.onInput} : {}) } }); } diff --git a/src/components/search/search.less b/src/components/search/search.less index 7ffab04..5b3bba6 100644 --- a/src/components/search/search.less +++ b/src/components/search/search.less @@ -22,7 +22,9 @@ z-index: 100; list-style: none; border-radius: 5px; - width: 28.6%; + max-height: 70%; + width: 29.5%; + overflow-y: scroll; margin-top: 0; padding-inline-start: 15px; padding-inline-end: 15px; diff --git a/src/components/search/search.ts b/src/components/search/search.ts index d610151..25e7d2a 100644 --- a/src/components/search/search.ts +++ b/src/components/search/search.ts @@ -1,10 +1,10 @@ import './search.less'; -import Block from '../../core/Block'; +import Block, {Props} from '../../core/Block'; import SearchTemplate from './search.hbs?raw'; import { User, Chat } from '../../utils/types'; import UserController from '../../controllers/UserController'; import ChatController from '../../controllers/ChatController'; -import Store from '../../core/Store'; +import isEqual from "../../utils/isEqual"; interface SearchUsersProps { onUserSelect: (user: User) => void; @@ -37,6 +37,10 @@ export class Search extends Block { }); } + protected componentDidUpdate(oldProps: Props, newProps: Props): boolean { + return isEqual(oldProps, newProps); + } + private handleClick(e: Event) { const target = e.target as HTMLElement; const userItem = target.closest('.user-item'); @@ -47,7 +51,7 @@ export class Search extends Block { if (userId) { const user = this.props.users.find((u: User) => u.id.toString() === userId); if (user) { - Store.setSelectedUser(user); + // To do: add create chat with user } } } diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index e3357f2..9adc278 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -1,6 +1,7 @@ import AuthApi from "../api/AuthApi"; import {TOptionsData} from "../core/HTTPTransport"; import { goToLogin, goToMessenger, goToError500 } from "../utils/router"; +import ChatController from "./ChatController"; import Store from "../core/Store"; class AuthController { @@ -26,18 +27,13 @@ class AuthController { public async login(data: TOptionsData): Promise { try { - const { status, response } = await AuthApi.login(data); - if (status === 200) { - Store.set('auth', true); - goToMessenger(); - this.getUser(); - } else if (status === 500) { - goToError500(); - } else { - alert(JSON.parse(response).reason ?? 'Bad request'); - } - } catch (e) { - console.error(e); + await AuthApi.login(data); + const user = await this.getUser(); + const chats = await ChatController.getChats(); + Store.set({user: user, chats}); + goToMessenger(); + } catch (error) { + console.error(error); } } @@ -45,35 +41,22 @@ class AuthController { try { const response = await AuthApi.getUser(); - if (response.status === 200 && response) { - Store.set('user', response.response); - Store.set('auth', true); + if (response) { + Store.set({user: response}); return true; } else { - Store.set('auth', false); return false; } } catch (e) { - console.error('Error in getUser:', e); - Store.set('auth', false); + console.error('Error in user:', e); return false; } } public async logout(): Promise { - try { - const { status, response } = await AuthApi.logout(); - if (status === 200) { - Store.setResetState(); - goToLogin(); - } else if (status === 500) { - goToError500(); - } else { - alert(JSON.parse(response).reason ?? 'Error response'); - } - } catch (e) { - console.error(e); - } + await AuthApi.logout(); + Store.set({user: null, currentChat: null}); + goToLogin(); } } diff --git a/src/controllers/ChatController.ts b/src/controllers/ChatController.ts index aa55cd7..d371c41 100644 --- a/src/controllers/ChatController.ts +++ b/src/controllers/ChatController.ts @@ -1,6 +1,6 @@ import ChatAPI from '../api/ChatApi'; import Store from '../core/Store'; -import { Chat } from '../utils/types'; +import {Chat, User} from '../utils/types'; import { MessageController } from "./MessageController"; const messageController = new MessageController(); @@ -8,16 +8,7 @@ class ChatController { async getChats(): Promise { try { const chats = await ChatAPI.getChats(); - if (chats instanceof XMLHttpRequest) { - const chatsData = chats.response; - if (Array.isArray(chatsData)) { - Store.set('chats', chatsData); - return chatsData; - } else { - return []; - } - } else if (Array.isArray(chats)) { - Store.set('chats', chats); + if (chats) { return chats; } else { return []; @@ -32,7 +23,7 @@ class ChatController { try { const newChat = await ChatAPI.createChat(title); const currentChats = Store.getState().chats || []; - Store.set('chats', [...currentChats, newChat]); + Store.set({chats: [...currentChats, newChat]}); return newChat; } catch (error) { console.error('Error creating chat:', error); @@ -52,47 +43,42 @@ class ChatController { } } - public async getChatUsers(id: number): Promise { + public async getChatUsers(id: number | undefined): Promise { try { const chatUsers = await ChatAPI.getChatUsers(id); - if (chatUsers instanceof XMLHttpRequest) { - const usersData = chatUsers.response; - if (Array.isArray(usersData)) { - Store.set('currentChatUsers', usersData); - } - return usersData; - } + Store.set({currentChatUsers: chatUsers}); + return chatUsers; } catch (error) { console.error('Error fetching chatUsers:', error); } } - public async addUsers(data: any) { - if (!data.id || !data.user) return false; - - try { - const response= await ChatAPI.addUsers(data.id, [data.user]); - if (response instanceof XMLHttpRequest) { - return true; - } else if (Array.isArray(response)) { - return true; - } else { - return false; + public async addUsers(user: User): Promise { + const { selectedChat, currentChatUsers } = Store.getState(); + if (user && selectedChat) { + try { + await ChatAPI.addUsers({ chatId: selectedChat.id, users: [user.id] }); + } catch (error) { + console.error(error); } - } catch (e) { - console.error(e); - } - return false; + const newCurrentChatUsers = [...currentChatUsers, user]; + Store.set({currentChatUsers: newCurrentChatUsers}); + } } - async removeUsers(data: { id: number, users: number[] }) { - try { - await ChatAPI.removeUsers(data.id, data.users); - await this.getChatUsers(data.id); - } catch (error) { - console.error('Error removing users from chat:', error); - throw error; + async removeUsers(data: { id: number, users: number[] }): Promise { + const { selectedChat, currentChatUsers } = Store.getState(); + + if (data && selectedChat) { + try { + await ChatAPI.removeUsers({ users: [data.id], chatId: selectedChat.id }); + } catch (error) { + console.error(error); + } + + const newCurrentChatUsers = currentChatUsers.filter((user: { id: number; }) => user.id !== data.id); + Store.set({currentChatUsers: newCurrentChatUsers}); } } @@ -100,30 +86,34 @@ class ChatController { const { chats, currentChat } = Store.getState(); if (currentChat) { await ChatAPI.deleteChat({ chatId: currentChat }); - Store.set('chats', chats.filter((chat: { id: any; }) => (chat.id !== currentChat))); - Store.set('currentChat', null); + Store.set({ + chats: chats.filter((chat: { id: any; }) => (chat.id !== currentChat)), + currentChat: null + }) } } - public async setCurrentChat(id: number) { + public async changeAvatar(data: FormData): Promise { + try { + const response = await ChatAPI.uploadAvatar(data); + if (response) { + Store.set({selectedChat: response}); + return response; + } + } catch (error) { + console.error('Error uploading avatar:', error); + } + } + + public async setCurrentChat(id: number | undefined) { const chats = Store.getState().chats; const selectedChat = chats.find((chat: Chat) => chat.id === id); + const chatUsers = await this.getChatUsers(id); - if (selectedChat) { - Store.set('currentChat', id); - Store.set('selectedChat', selectedChat); - Store.set('currentChatMessages', []); - await messageController.disconnect(); + Store.set({ currentChat: id, selectedChat: selectedChat, currentChatUsers: chatUsers }); - try { - await messageController.connect(); - await this.getChats(); - } catch (error) { - Store.set('websocketError', 'Failed to connect to chat'); - } - } else { - console.error('Chat not found:', id); - } + messageController.disconnect(); + messageController.connect(); } } diff --git a/src/controllers/MessageController.ts b/src/controllers/MessageController.ts index fc95b01..199dc22 100644 --- a/src/controllers/MessageController.ts +++ b/src/controllers/MessageController.ts @@ -3,13 +3,27 @@ import Socket, { Message, WebSocketProps } from '../core/Socket'; import ChatAPI from '../api/ChatApi'; import Store from '../core/Store'; -const setLastMessage = (_message: MessageProps) => { - const { chats, currentChat } = Store.getState(); +const setLastMessage = (message: MessageProps) => { + const { chats, currentChat, currentChatUsers } = Store.getState(); if (chats && currentChat) { const chat = chats.find((c: { id: any; }) => c.id === currentChat); if (chat) { + const user = currentChatUsers.find((u: { id: any; }) => u.id === message.user_id); const newChat = { ...chat }; - Store.set('chats', chats.map((c: any) => (c === chat ? newChat : c))); + newChat.last_message = { + user: { + id: user?.id || 0, + login: user?.login || '', + first_name: user?.first_name || '', + second_name: user?.second_name || '', + display_name: user?.display_name || '', + avatar: user?.avatar || '', + role: user?.role || '', + }, + time: message.time, + content: message.content, + }; + Store.set({chats: chats.map((c: any) => (c === chat ? newChat : c))}) } } }; @@ -73,8 +87,8 @@ export class MessageController { newChatMessages = [...message].reverse(); } else { newChatMessages = [...currentChatMessages, message]; - setLastMessage(message); + setLastMessage(message as any); } - Store.set('currentChatMessages', newChatMessages); + Store.set({currentChatMessages: newChatMessages}); } } diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index 89f23d9..4c13665 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -10,10 +10,10 @@ class UserController { if (response && typeof response === 'object') { if (response instanceof XMLHttpRequest) { const userData = response.response; - Store.set('user', userData); + Store.set({user: userData}); return userData; } else { - Store.set('user', response); + Store.set({user: response}); return response; } } else { @@ -48,10 +48,10 @@ class UserController { if (response && typeof response === 'object') { if (response instanceof XMLHttpRequest) { const userData = response.response; - Store.set('user', userData); + Store.set({user: userData}); return userData; } else { - Store.set('user', response); + Store.set({user: response}); return response; } } else { @@ -65,14 +65,8 @@ class UserController { public async searchUsers(query: string): Promise { try { const response = await UserApi.searchUsers(query); - if (response instanceof XMLHttpRequest) { - const data = response.response; - if (Array.isArray(data)) { - return data; - } else { - return []; - } - } else if (Array.isArray(response)) { + if (response) { + Store.set({users: response}); return response; } else { return []; diff --git a/src/core/Block.ts b/src/core/Block.ts index c0a22ef..b74264e 100644 --- a/src/core/Block.ts +++ b/src/core/Block.ts @@ -98,7 +98,8 @@ class Block { }); } - public componentDidMount(): void {} + public componentDidMount(): void { + } public dispatchComponentDidMount(): void { this.eventBus().emit(Block.EVENTS.FLOW_CDM); @@ -118,7 +119,7 @@ class Block { } protected componentDidUpdate(oldProps: Props, newProps: Props) { - return isEqual(oldProps, newProps); + return !isEqual(oldProps, newProps); } protected _componentWillUnmount() { @@ -139,49 +140,41 @@ class Block { private _render() { const fragment = this._compile(); - const newElement = fragment.firstElementChild as HTMLElement; - if (this._element) { + if (this._element && this._element.parentNode) { this._removeEvents(); this._element.replaceWith(newElement); } this._element = newElement; - this._addEvents(); - - Object.values(this.children).forEach(child => { - if (child instanceof Block) { - child.forceUpdate(); - } - }); } private _compile(): DocumentFragment { const template = this.render(); const fragment = document.createElement('template'); - const context = { + if (!template) { + return document.createDocumentFragment(); + } + + fragment.innerHTML = Handlebars.compile(template)({ ...this.props, __children: this.children, - }; - - fragment.innerHTML = Handlebars.compile(template)(context); + }); Object.entries(this.children).forEach(([id, child]) => { const stub = fragment.content.querySelector(`[data-id="${id}"]`); if (!stub) { return; } - if (child instanceof Block) { const content = child.getContent(); - if (content) { - stub.replaceWith(content); + if (!content) { + return; } - } else if (child instanceof Element) { - stub.replaceWith(child); + stub.replaceWith(content); } }); diff --git a/src/core/HTTPTransport.ts b/src/core/HTTPTransport.ts index b8d6463..1730851 100644 --- a/src/core/HTTPTransport.ts +++ b/src/core/HTTPTransport.ts @@ -41,7 +41,7 @@ class HTTPTransport { }; request = (url: string, options: Options = {}, timeout = 5000) => { - const {headers = {}, method, data} = options; + const {method, data} = options; return new Promise(function(resolve, reject) { if (!method) { @@ -50,23 +50,25 @@ class HTTPTransport { } const xhr = new XMLHttpRequest(); - const isGet = method === METHODS.GET; if (method === METHODS.GET && data) { - url += queryStringify(data as Record); + // eslint-disable-next-line no-param-reassign + url += queryStringify(data as Record); } - xhr.open( - method, - isGet && !!data - ? `${url}${queryStringify(data)}` - : url, - ); - xhr.onload = function() { - if (xhr.status >= 200 && xhr.status < 300) { - resolve(xhr); + xhr.open(method || METHODS.GET, url); + + if (data instanceof FormData) { + xhr.setRequestHeader('Accept', 'application/json'); + } else { + xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); + } + + xhr.onload = () => { + if (xhr.status !== 200) { + reject(new Error(`Error ${xhr.status}: ${xhr?.response?.reason || xhr.statusText}`)); } else { - reject(xhr); + resolve(xhr.response); } }; @@ -77,14 +79,12 @@ class HTTPTransport { xhr.responseType = 'json'; xhr.withCredentials = true; - Object.keys(headers).forEach(key => { - xhr.setRequestHeader(key, headers[key]); - }); - - if (isGet || !data) { + if (method === METHODS.GET || !data) { xhr.send(); + } else if (data instanceof FormData) { + xhr.send(data); } else { - xhr.send(data instanceof FormData ? data : JSON.stringify(data)); + xhr.send(JSON.stringify(data)); } }); }; diff --git a/src/core/RegistrationComponent.ts b/src/core/RegistrationComponent.ts index 5bbbadd..f52be83 100644 --- a/src/core/RegistrationComponent.ts +++ b/src/core/RegistrationComponent.ts @@ -2,16 +2,21 @@ import Handlebars from 'handlebars'; import Block from "./Block"; export function registerComponent(name: string, Component: typeof Block) { - Handlebars.registerHelper(name, function(this: any, { hash, data }) { - const component: Block = new Component(hash); - const id: string = `${name}-${component.id}`; + if (!Handlebars.helpers[name]) { + Handlebars.registerHelper(name, function (this: any, {hash, data, fn}) { + const component: Block = new Component(hash); + const id: string = `${name}-${component.id}`; - if (hash.ref) { - (data.root.__refs = data.root.__refs || {})[hash.ref] = component; - } + if (hash.ref) { + (data.root.__refs = data.root.__refs || {})[hash.ref] = component; + } - (data.root.__children = data.root.__children || {})[id] = component; + (data.root.__children = data.root.__children || {})[id] = component; - return `
`; - }); + const contents = fn ? fn(this) : ''; + + + return `
${contents}
`; + }); + } } diff --git a/src/core/Render.ts b/src/core/Render.ts index 50406f7..0e1ef2b 100644 --- a/src/core/Render.ts +++ b/src/core/Render.ts @@ -1,9 +1,19 @@ import Block from './Block'; export function render(rootQuery: string, block: Block) { - const root = document.querySelector(rootQuery); + const root = document.querySelector(rootQuery) as HTMLElement; + if (root) { root.innerHTML = ''; - root.append(block.getContent()); + const content = block.getContent(); + if (content) { + root.appendChild(content); + } + + block.dispatchComponentDidMount(); + + return root; } + + return null; } diff --git a/src/core/Socket.ts b/src/core/Socket.ts index 81cea87..642395b 100644 --- a/src/core/Socket.ts +++ b/src/core/Socket.ts @@ -69,10 +69,14 @@ class Socket { } public message(event: MessageEvent) { - const data = JSON.parse(event.data); - - if (data.type !== 'user connected' && data.type !== 'pong') { - this.callbackMessages(data); + try { + const data = JSON.parse(event.data); + + if (data.type !== 'user connected' && data.type !== 'pong') { + this.callbackMessages(data); + } + } catch (error) { + console.error('Error receiving message', error); } } diff --git a/src/core/Store.ts b/src/core/Store.ts index e018166..10aa848 100644 --- a/src/core/Store.ts +++ b/src/core/Store.ts @@ -1,5 +1,4 @@ import { EventBus } from './EventBus'; -import { Indexed, set } from '../utils/utils' import isEqual from "../utils/isEqual"; import { User } from '../utils/types'; @@ -7,8 +6,15 @@ export enum StoreEvents { Updated = 'updated', } +interface StoreState { + notificationMessage?: string; + selectedUser?: User; + users?: User[]; + [key: string]: any; +} + class Store extends EventBus { - private state: Indexed = {}; + private state: StoreState = {}; constructor() { super(); @@ -18,24 +24,18 @@ class Store extends EventBus { return this.state; } - public set(path: string, value: unknown) { - const oldValue = this.getState()[path]; - if (!isEqual(oldValue, value)) { - set(this.state, path, value); - this.emit(StoreEvents.Updated, oldValue, value); - } - } - - public setNotificationMessage(message: string | null) { - this.set('notificationMessage', message); - } + public set(nextState: Partial) { + const prevState = { ...this.state }; + const newState = { ...this.state, ...nextState }; - public setSelectedUser(user: User | null) { - this.set('selectedUser', user); + if (!isEqual(prevState, newState)) { + this.state = newState; + this.emit(StoreEvents.Updated, prevState, newState); + } } - public setUsers(users: User[]) { - this.set('users', users); + public setNotificationMessage(message: string) { + this.set({notificationMessage: message}); } public emit(event: string, ...args: unknown[]) { @@ -47,19 +47,6 @@ class Store extends EventBus { listener(...args); }); } - - public setResetState(): void { - try { - this.state = { - auth: false, - user: null, - getPage: '/', - }; - this.emit(StoreEvents.Updated); - } catch (e) { - console.error(e); - } - } } export default new Store(); diff --git a/src/main.ts b/src/main.ts index 6d849fc..2ae3fed 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,8 @@ import { BlockType } from "./core/Block"; import './style.less'; import AuthController from './controllers/AuthController'; import './utils/helpers'; +import Store from "./core/Store"; +import ChatController from "./controllers/ChatController"; type ImportValue = Record; type ImportGlob = Record; @@ -52,8 +54,8 @@ async function initApp() { .start(); try { - const isLoggedIn = await AuthController.getUser(); - if (isLoggedIn) { + const user = await AuthController.getUser(); + if (user) { const currentRoute = router.getCurrentRoute(); if (currentRoute && ( currentRoute.match('/')) @@ -69,6 +71,9 @@ async function initApp() { console.error('Error during app initialization:', e); goToLogin(); } + + const chats = await ChatController.getChats(); + Store.set({chats}); } -initApp(); +document.addEventListener('DOMContentLoaded', () => initApp()); \ No newline at end of file diff --git a/src/pages/chat/chat.hbs b/src/pages/chat/chat.hbs index 4c8caed..10e587d 100644 --- a/src/pages/chat/chat.hbs +++ b/src/pages/chat/chat.hbs @@ -15,7 +15,7 @@
{{error}}
{{/if}}
- {{{ ChatList }}} + {{{ ChatList chats=chats }}}
diff --git a/src/pages/chat/chat.less b/src/pages/chat/chat.less index 778a5f1..c2cc101 100644 --- a/src/pages/chat/chat.less +++ b/src/pages/chat/chat.less @@ -11,155 +11,6 @@ height: 100%; } - .sidebar { - flex: 1; - - .list { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - - .list-header { - display: flex; - flex-direction: column; - height: 5rem; - border-right: 1px solid @secondary__background-color; - border-bottom: 1px solid @secondary__background-color; - - .section { - &.buttons { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - .create-new-chat { - margin: 0; - - .new-chat { - .button(@margin: 0, @background: none, @border: none, @width: auto); - - &:hover { - .custom-button-hover(); - } - } - } - - .profile { - align-self: flex-end; - padding: 0.25rem 0.1rem; - - .profile-button { - .button(@background: none, @width: 100%, @margin: 0); - - &:hover { - background: none !important; - color: @main__button-background-hover; - - &:after { - color: @main__button-background-hover; - } - } - - &:after { - font-family: @icon-font; - content: @icon__angle-right; - color: @main__font-color; - vertical-align: middle; - font-size: 12px; - margin-left: 0.25rem; - } - } - - &:hover, &:active, &:focus-visible { - background: none !important; - color: @main__button-background-hover !important; - } - } - } - } - - } - - .chat-list { - overflow-y: scroll; - } - - .item { - display: flex; - flex-direction: column; - justify-content: center; - padding-top: 0 !important; - padding-bottom: 0 !important; - padding-inline: 1rem .75rem; - border-bottom: 1px solid @secondary__background-color; - min-height: 4.5rem; - padding-inline-start: 4.5rem !important; - text-decoration: none; - color: @main__font-color; - position: relative; - - .avatar { - display: flex; - align-items: center; - justify-content: center; - position: absolute; - inset-inline-start: .5625rem !important; - width: 3.375rem !important; - height: 3.375rem !important; - - .default-avatar { - position: absolute; - font-size: 25px; - } - - &:before { - content: ''; - color: @main__font-color; - font-size: 24px; - } - - &:after { - content: ''; - border: 30px solid @secondary__background-color; - border-radius: 100%; - } - } - - .dialog { - display: flex; - justify-content: space-between; - align-items: center; - - &.title { - height: 1.375rem; - font-weight: bold; - } - - &.subtitle { - height: 1.375rem; - margin-top: .125rem; - color: @secondary__font-color; - - .unread-count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 20px; - height: 20px; - background-color: @secondary__font-color; /* Вы можете изменить цвет на любой подходящий вашему дизайну */ - color: white; - border-radius: 10px; /* Это создаст круглую форму */ - font-size: 12px; - font-weight: bold; - } - } - } - } - } - } - .messenger { flex: 2; display: flex; @@ -167,38 +18,5 @@ background: @secondary__background-color; } - .create-chat-modal { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - justify-content: center; - align-items: center; - z-index: 1000; - - &.is-open { - display: flex; - } - - .modal-content { - background-color: white; - padding: 20px; - border-radius: 5px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - } - - .create-chat-form { - display: flex; - flex-direction: column; - gap: 10px; - } - - .close-modal { - margin-top: 10px; - } - } } } diff --git a/src/pages/chat/chat.ts b/src/pages/chat/chat.ts index 7bb932a..bd435da 100644 --- a/src/pages/chat/chat.ts +++ b/src/pages/chat/chat.ts @@ -15,7 +15,7 @@ class ChatPageBase extends Block { onChatCreate: (e: Event) => { e.preventDefault(); e.stopPropagation(); - Store.set('isCreateChatModalOpen', true); + Store.set({isCreateChatModalOpen: true}); } }); } diff --git a/src/pages/login-form/login-form.hbs b/src/pages/login-form/login-form.hbs index 78c1527..f826a17 100644 --- a/src/pages/login-form/login-form.hbs +++ b/src/pages/login-form/login-form.hbs @@ -2,10 +2,10 @@

Sign In