diff --git a/src/components/avatar/avatar.css b/src/components/avatar/avatar.css index 812f1d7af..31509ebc6 100644 --- a/src/components/avatar/avatar.css +++ b/src/components/avatar/avatar.css @@ -7,11 +7,11 @@ height: 130px; overflow: hidden; border-radius: 50%; - cursor: pointer; &__text { display: none; position: absolute; + cursor: pointer; } &__img { @@ -19,18 +19,24 @@ height: 100%; object-fit: cover; object-position: center; + background: var(--hover-bg); } &:hover { - &__text { + .avatar__text { background: var(--hover-bg); display: flex; justify-content: center; align-items: center; height: 100%; + + span { + font-size: 18px; + text-align: center; + } } - &__img { + .avatar__img { width: 100%; height: 100%; object-fit: cover; diff --git a/src/components/avatar/avatar.ts b/src/components/avatar/avatar.ts index baa678313..afbc15ce3 100644 --- a/src/components/avatar/avatar.ts +++ b/src/components/avatar/avatar.ts @@ -5,9 +5,11 @@ import './avatar.css' const avatarTemplate = `
{{alt}} -
- Сменить аватар -
+ {{#if canChange}} +
+ Поменять аватар +
+ {{/if}}
` @@ -16,20 +18,16 @@ type AvatarProps = { alt: string size?: string className?: string + canChange?: boolean } & Props export default class Avatar extends Block { constructor(props: AvatarProps) { super(props) - const element = this.element as HTMLElement - - if (props.src === '') { - const img = element.querySelector('img') - if (img) { - img.src = - 'https://i2.wp.com/vdostavka.ru/wp-content/uploads/2019/05/no-avatar.png?fit=512%2C512&ssl=1' - } + if (!this.props.canChange) { + this.props.canChange = false } + const element = this.element as HTMLElement if (props.size) { element.style.width = props.size @@ -40,6 +38,13 @@ export default class Avatar extends Block { } } + componentDidUpdate(oldProps: Props, newProps: Partial): boolean { + if (!newProps.src) { + newProps.src = 'https://i2.wp.com/vdostavka.ru/wp-content/uploads/2019/05/no-avatar.png?fit=512%2C512&ssl=1' + } + return super.componentDidUpdate(oldProps, newProps) + } + render() { return this.compile(avatarTemplate, this.props) } diff --git a/src/components/chat/chat.css b/src/components/chat/chat.css deleted file mode 100644 index 834176a98..000000000 --- a/src/components/chat/chat.css +++ /dev/null @@ -1,42 +0,0 @@ -.chat { - display: grid; - grid-template-rows: 44px auto 44px; - padding: 10px 0; - gap: 10px; - max-height: 100%; - overflow-y: hidden; - scrollbar-width: thin; - box-sizing: border-box; - - &__user { - display: grid; - grid-template-columns: 34px auto 30px; - padding: 0 20px 10px; - gap: 10px; - align-items: center; - border-bottom: 1px solid var(--border); - } - - &__messages { - display: flex; - flex-direction: column; - gap: 20px; - height: 100%; - padding: 0 20px; - overflow-y: scroll; - scrollbar-width: thin; - } - - &__input { - display: flex; - align-items: center; - padding: 10px 20px 0; - border-top: 1px solid var(--border); - - .lni-paperclip { - color: var(--color-gray); - font-size: 20px; - transform: rotate(45deg); - } - } -} diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts deleted file mode 100644 index a58a19a0e..000000000 --- a/src/components/chat/chat.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { User } from '@/constants/types' -import Block from '@/core/Block' -import { Message, MessageProps } from '../message/message' -import './chat.css' - -// language=hbs -const ChatTemplate: string = ` -
-
- {{{ avatar }}} - {{{user.displayName}}} -
- -
- {{{ messages }}} -
- -
- - - -
-
-` - -type ChatProps = { - user: User - messages: MessageProps[] -} - -export class ChatWindow extends Block { - constructor(props: ChatProps) { - const messagesArr: Message[] = [] - props.messages.map((message) => { - messagesArr.push(new Message(message)) - }) - super({ ...props, avatar: props.user.avatar, messages: messagesArr }) - } - - render() { - return this.compile(ChatTemplate, this.props) - } -} diff --git a/src/components/chatItem/chatItem.css b/src/components/chatItem/chatItem.css index fe9ed08ee..31d1c9b44 100644 --- a/src/components/chatItem/chatItem.css +++ b/src/components/chatItem/chatItem.css @@ -28,6 +28,7 @@ color: var(--color-gray); font-weight: 400; font-size: 12px; + place-self: center center; } &__message { @@ -37,7 +38,21 @@ max-width: 100%; overflow: hidden; text-overflow: ellipsis; - font-size: 14px; + font-size: 13px; font-weight: 400; } + + &__unreaded { + width: 20px; + height: 20px; + justify-self: center; + display: flex; + align-items: center; + justify-content: center; + padding: 2px; + background: var(--color-blue); + border-radius: 50%; + color: var(--color-white); + font-size: 12px; + } } diff --git a/src/components/chatItem/chatItem.ts b/src/components/chatItem/chatItem.ts index 275f35a21..6565dc6ef 100644 --- a/src/components/chatItem/chatItem.ts +++ b/src/components/chatItem/chatItem.ts @@ -3,8 +3,8 @@ import { Chat } from '@/constants/types.ts' import Block, { Props } from '@/core/Block' import './chatItem.css' import store from '@/core/Store.ts' -import getResourceURL from '@/utils/getResourceURL.ts' import formatMessageDate from '@/utils/formatMessageDate.ts' +import getResourceURL from '@/utils/getResourceURL.ts' // language=hbs const ChatItemTemplate = ` @@ -13,6 +13,9 @@ const ChatItemTemplate = ` {{{ title }}} {{{ time }}} {{{ last_message.content }}} + {{#if unreaded}} + {{{ unreaded }}} + {{/if}} ` @@ -23,9 +26,14 @@ export default class ChatItem extends Block { super({ ...props, events: { - click: () => store.set('selectedChat', { id: props.id }), + click: () => { + store.set('selectedChat', props.id) + }, }, - time: props.last_message ? formatMessageDate(props.last_message.time) : '', + time: props.last_message + ? formatMessageDate(props.last_message.time) + : '', + unreaded: props.unread_count ? props.unread_count : '', avatar: new Avatar({ src: props.avatar ? getResourceURL(props.avatar) : '', alt: 'avatar', diff --git a/src/components/chatWindow/chatWindow.css b/src/components/chatWindow/chatWindow.css new file mode 100644 index 000000000..3914e9800 --- /dev/null +++ b/src/components/chatWindow/chatWindow.css @@ -0,0 +1,111 @@ +.chat { + display: grid; + grid-template-rows: 44px auto 64px; + padding: 10px 0; + gap: 4px; + max-height: 100%; + overflow-y: hidden; + scrollbar-width: thin; + box-sizing: border-box; + + &__header { + display: grid; + grid-template-columns: 34px auto 44px; + padding: 0 20px 10px; + gap: 16px; + align-items: center; + border-bottom: 1px solid var(--border); + z-index: 1000; + + .chat-menu { + position: relative; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + + &__actions { + display: none; + position: absolute; + width: 240px; + background: var(--color-white); + box-shadow: var(--box-shadow); + padding: 12px; + border-radius: 10px; + + i { + color: var(--color-blue); + } + } + + &__action { + display: flex; + align-items: center; + padding: 8px; + gap: 8px; + cursor: pointer; + border-radius: 6px; + background: none; + border: none; + font-size: 14px; + + &:hover { + background: var(--color1); + } + } + } + + .chat-menu:hover .chat-menu__actions { + display: flex; + flex-direction: column; + gap: 4px; + top: 30px; + right: 12px; + } + } + + &__messages { + display: flex; + flex-direction: column-reverse; + gap: 20px; + height: 100%; + padding: 0 20px; + overflow-y: scroll; + scrollbar-width: thin; + } + + &__input-block { + display: grid; + grid-template-columns: 30px auto 30px; + gap: 12px; + align-items: center; + padding: 10px 20px 0; + border-top: 1px solid var(--border); + + .message-label { + margin: 0; + } + + .message-input { + padding: 12px 10px; + background: var(--color1); + border: none; + border-radius: 100px; + box-sizing: border-box; + } + + .lni-paperclip { + color: var(--color-gray); + font-size: 20px; + transform: rotate(45deg); + } + + .send-btn { + height: 40px; + width: 40px; + background: var(--color1); + border-radius: 50%; + cursor: pointer; + } + } +} diff --git a/src/components/chatWindow/chatWindow.ts b/src/components/chatWindow/chatWindow.ts new file mode 100644 index 000000000..edf964c87 --- /dev/null +++ b/src/components/chatWindow/chatWindow.ts @@ -0,0 +1,233 @@ +import Avatar from '@/components/avatar/avatar.ts' +import Button from '@/components/button/button.ts' +import Input from '@/components/input/input.ts' +import { MessageItem, MessageItemProps } from '@/components/message/message.ts' +import { Modal } from '@/components/modal/modal.ts' +import { Chat, User } from '@/constants/types.ts' +import { ChatController } from '@/controllers/ChatController.ts' +import Block, { Props } from '@/core/Block' +import store from '@/core/Store.ts' +import connect from '@/utils/connect.ts' +import connectToMessageSocket from '@/utils/connectToMessageSocket.ts' +import getResourceURL from '@/utils/getResourceURL.ts' +import './chatWindow.css' + +// language=hbs +const ChatTemplate: string = ` +
+ {{#if selectedChat}} +
+ {{{ avatar }}} + + {{ title }} + +
+ +
+ {{{ addUserBtn }}} + {{{ deleteUserBtn }}} +
+
+
+ + +
+ {{{ messages }}} +
+ + +
+ + {{{ messageInput }}} + +
+ {{/if}} +
+` + +type ChatWindowProps = { + selectedChat: number +} & Props + +const chatController = new ChatController() + +export class ChatWindow extends Block { + private socket: WebSocket | null = null + private chat: Chat | null = null + private modal: Modal + + constructor(props: ChatWindowProps) { + super(props) + this.modal = new Modal() + this.children.addUserBtn = new Button({ + label: 'Добавить пользователя', + className: 'chat-menu__action', + withId: true, + events: { + click: () => { + this.showAddUserModal() + }, + }, + }) + this.children.deleteUserBtn = new Button({ + label: 'Удалить пользователя', + className: 'chat-menu__action', + withId: true, + events: { + click: () => { + this.showDeleteUserModal() + }, + }, + }) + } + + createSocket() { + const userId = store.getState().userdata.id + const chatId = store.getState().selectedChat + + connectToMessageSocket(userId, chatId).then((resp) => { + if (resp !== undefined) { + this.socket = resp + chatController.getChatUsers(chatId) + + this.chat = store + .getState() + .chats.filter((chat) => chat.id === this.props.selectedChat)[0] + this.props.title = this.chat.title + this.children.avatar = new Avatar({ + src: this.chat.avatar ? getResourceURL(this.chat.avatar) : '', + alt: this.chat.avatar ? (this.props.title as string) : '', + size: '40px', + withId: true, + }) + + this.children.messageInput = new Input({ + type: 'text', + name: 'message', + label: '', + placeholder: 'Сообщение...', + className: 'message-label', + classNameInput: 'message-input', + events: { + keyup: (event) => { + if ( + event instanceof KeyboardEvent && + event.target instanceof HTMLInputElement && + event.key === 'Enter' + ) { + const message = { content: event.target.value, type: 'message' } + this.socket?.send(JSON.stringify(message)) + event.target.value = '' + } + }, + }, + }) + } + }) + } + + showAddUserModal() { + const content = document.createElement('div') + const input = new Input({ + type: 'text', + label: 'ID юзера', + placeholder: 'ID...', + name: 'id', + }) + const btn = new Button({ + label: 'Добавить', + className: 'button input-submit', + events: { + click: () => { + chatController.addUserToChat({ + users: [Number(input.getValue())], + chatId: Number(this.props.selectedChat), + }) + this.closeModal() + }, + }, + }) + + content.appendChild(input.element) + content.appendChild(btn.element) + + this.modal.setContent('Добавить пользователя', content) + this.modal.open() + } + + showDeleteUserModal() { + const content = document.createElement('div') + content.className = 'users' + + ;(this.props.chatUsers as User[]).map((user: User) => { + const userBlock = document.createElement('div') + const userLogin = document.createElement('span') + const deleteBtn = document.createElement('button') + + userBlock.className = 'user' + userLogin.innerHTML = `${user.login}` + userLogin.className = 'user__login' + deleteBtn.innerHTML = `☒` + deleteBtn.className = 'user__delete-btn' + deleteBtn.addEventListener('click', () => { + chatController.deleteUserFromChat({ + users: [user.id], + chatId: Number(this.props.selectedChat), + }) + console.log(store.getState().chatUsers) + this.closeModal() + }) + + userBlock.appendChild(userLogin) + userBlock.appendChild(deleteBtn) + + content.appendChild(userBlock) + }) + + this.modal.setContent('Удалить пользователя', content) + this.modal.open() + } + + closeModal() { + this.modal.close() + } + + showMessages(messages: MessageItemProps[]) { + return messages.map((message) => { + return new MessageItem({ + ...message, + isMy: message.user_id === store.getState().userdata.id, + user: store + .getState() + .chatUsers.filter((user) => user.id === message.user_id)[0], + }) + }) + } + + componentDidUpdate(oldProps: Props, newProps: Partial): boolean { + if (this.socket && oldProps.selectedChat !== newProps.selectedChat) { + this.socket.close() + } + if (oldProps.selectedChat !== newProps.selectedChat) { + this.createSocket() + } + + this.blockArrays.messages = this.showMessages(store.getState().messages) + + return super.componentDidUpdate(oldProps, newProps) + } + + render() { + return this.compile(ChatTemplate, this.props) + } +} + +export const chatWindow = connect((state) => ({ + selectedChat: state.selectedChat, + messages: state.messages, + chatUsers: state.chatUsers, +}))(ChatWindow) diff --git a/src/components/input/input.ts b/src/components/input/input.ts index 03850882f..bd073d629 100644 --- a/src/components/input/input.ts +++ b/src/components/input/input.ts @@ -18,7 +18,7 @@ type InputValidation = { type InputProps = { name: string - type: string + type: 'text' | 'password' | 'submit' label: string value?: string placeholder?: string diff --git a/src/components/message/message.css b/src/components/message/message.css index 40ec7a72f..f08dcb497 100644 --- a/src/components/message/message.css +++ b/src/components/message/message.css @@ -1,16 +1,44 @@ .message { + position: relative; + display: grid; + grid-template-columns: 30px auto; + grid-auto-rows: 16px auto; + gap: 4px 12px; + padding: 2px 12px 12px; width: calc(50% - 40px); - padding: 12px; - background: var(--message-bg); border-radius: 10px; + background: var(--message-bg); &_my { background: var(--blue-bg); align-self: flex-end; } + &__nickname { + padding: 4px 0 0; + position: relative; + grid-column: 1/3; + font-size: 12px; + font-weight: 600; + text-transform: capitalize; + } + + &__avatar { + position: relative; + } + &__text { font-size: 15px; font-weight: 400; } + + &__date { + position: absolute; + bottom: 4px; + right: 4px; + color: var(--color-gray); + font-weight: 400; + font-size: 12px; + place-self: end end; + } } diff --git a/src/components/message/message.ts b/src/components/message/message.ts index e04126b2b..5232eddb5 100644 --- a/src/components/message/message.ts +++ b/src/components/message/message.ts @@ -1,22 +1,47 @@ +import Avatar from '@/components/avatar/avatar.ts' +import { Message, User } from '@/constants/types.ts' import Block from '@/core/Block' import './message.css' +import formatMessageDate from '@/utils/formatMessageDate.ts' +import getResourceURL from '@/utils/getResourceURL.ts' // language=hbs const MessageTemplate = ` -
-
{{ body }}
-
+ {{#if isMessage}} +
+
{{ nickname }}
+
{{{ avatar }}}
+
{{ content }}
+
{{ date }}
+
+ {{else}} +
{{content}}
+ {{/if}} ` -export type MessageProps = { - id: string - body: string -} +export type MessageItemProps = { + isMy: boolean + isMessage?: boolean + user: User +} & Message -export class Message extends Block { - constructor(props: MessageProps) { - super(props) - if (this.props.id === '1') { +export class MessageItem extends Block { + constructor(props: MessageItemProps) { + super({ + ...props, + avatar: new Avatar({ + src: + props.user && props.user.avatar + ? getResourceURL(props.user.avatar) + : '', + alt: props.user ? props.user.display_name : '', + size: '32px', + }), + nickname: props.user ? props.user.display_name : '', + isMessage: props.type === 'message', + date: formatMessageDate(props.time), + }) + if (props.isMy) { this.element.classList.add('message_my') } } diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css new file mode 100644 index 000000000..194e7801a --- /dev/null +++ b/src/components/modal/modal.css @@ -0,0 +1,47 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgb(200 200 200 / 50%); + z-index: 1000; + display: none; +} + +.modal { + position: fixed; + top: 50%; + left: 50%; + min-width: 300px; + padding: 40px 30px; + transform: translate(-50%, -50%); + background-color: var(--color-white); + border-radius: 8px; + box-shadow: var(--box-shadow); + z-index: 1001; + display: none; + font-family: var(--font-family); +} + +.modal-content { + position: relative; + + &__title { + margin-bottom: 32px; + font-size: 18px; + text-align: center; + } + + &__close-button { + position: absolute; + right: -20px; + top: -34px; + font-size: 24px; + cursor: pointer; + } + + button { + margin-top: 16px; + } +} \ No newline at end of file diff --git a/src/components/modal/modal.ts b/src/components/modal/modal.ts new file mode 100644 index 000000000..d2c07dc64 --- /dev/null +++ b/src/components/modal/modal.ts @@ -0,0 +1,60 @@ +import './modal.css' + +export class Modal { + private modalElement: HTMLElement + private overlayElement: HTMLElement + + constructor() { + this.modalElement = document.createElement('div') + this.modalElement.className = 'modal' + this.modalElement.innerHTML = ` + + ` + + this.overlayElement = document.createElement('div') + this.overlayElement.className = 'overlay' + + document.body.appendChild(this.overlayElement) + document.body.appendChild(this.modalElement) + + this.bindEvents() + } + + setContent(title: string, content: HTMLElement | DocumentFragment): void { + const titleElement = this.modalElement.querySelector('h2') + const contentElement = this.modalElement.querySelector( + '.modal-content__content' + ) + + if (titleElement) { + titleElement.textContent = title + } + + if (contentElement) { + contentElement.innerHTML = '' // Clear existing content + contentElement.appendChild(content) // Append the new content + } + } + + private bindEvents(): void { + const closeButton = this.modalElement.querySelector( + '.modal-content__close-button' + ) as HTMLElement + closeButton.addEventListener('click', () => this.close()) + this.overlayElement.addEventListener('click', () => this.close()) + } + + open(): void { + this.modalElement.style.display = 'block' + this.overlayElement.style.display = 'block' + } + + close(): void { + this.modalElement.style.display = 'none' + this.overlayElement.style.display = 'none' + } +} \ No newline at end of file diff --git a/src/constants/initialState.ts b/src/constants/initialState.ts index a8914b7d9..c7afc8186 100644 --- a/src/constants/initialState.ts +++ b/src/constants/initialState.ts @@ -4,11 +4,13 @@ export const initialState = { avatar: '', email: '', login: '', - firstName: '', - secondName: '', - displayName: '', + first_name: '', + second_name: '', + display_name: '', phone: '', }, chats: [], selectedChat: 0, + messages: [], + chatUsers: [] } diff --git a/src/constants/types.ts b/src/constants/types.ts index 21ef3f6c4..c01d24861 100644 --- a/src/constants/types.ts +++ b/src/constants/types.ts @@ -3,16 +3,16 @@ export type User = { avatar: string email: string login: string - firstName: string - secondName: string - displayName: string + first_name: string + second_name: string + display_name: string phone: string } export type Chat = { id: number title: string - avatar: string + avatar: string | null unread_count: number created_by: number last_message: { @@ -23,6 +23,17 @@ export type Chat = { } } +export type Message = { + chat_id: number + content: string + file: null + id: number + is_read: boolean + time: string + type: "message" + user_id: number +} + export type EditPasswordData = { oldPassword: string newPassword: string diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index efd74b15e..03e5279c9 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -29,7 +29,7 @@ export class AuthController { }) .then((resp) => { if (resp.status === 200) { - store.set('userdata', { id: resp.response }) + store.set('userdata', { ...store.getState().userdata, id: resp.response }) } return resp }) @@ -45,7 +45,9 @@ export class AuthController { .then((resp) => { if (resp.status === 200) { const data = JSON.parse(resp.response) - data.avatar = getResourceURL(data.avatar) + if (data.avatar) { + data.avatar = getResourceURL(data.avatar) + } store.set('userdata', data) } return resp diff --git a/src/controllers/ChatController.ts b/src/controllers/ChatController.ts index 3896f8ddd..78cc2d1d9 100644 --- a/src/controllers/ChatController.ts +++ b/src/controllers/ChatController.ts @@ -1,5 +1,5 @@ import store from '@/core/Store.ts' -import { ChatService } from '@/services/ChatService.ts' +import { ChatService, ChatUsersRequest } from '@/services/ChatService.ts' const chatService = new ChatService() @@ -7,7 +7,40 @@ export class ChatController { public async getChats() { return chatService.getChats().then((resp) => { store.set('chats', JSON.parse(resp.response)) + if (store.getState().chats.length) { + store.set('selectedChat', store.getState().chats[0].id) + } return resp }) } + + public async getToken(chatId: number) { + return chatService.getToken(chatId).then((resp) => { + return JSON.parse(resp.response) + }) + } + + public async getChatUsers(chatId: number) { + return chatService.getChatUsers(chatId).then((resp) => { + store.set('chatUsers', JSON.parse(resp.response)) + }) + } + + public async createChat(title: string) { + return chatService.createChat(title).then(() => { + this.getChats() + }) + } + + public async addUserToChat(data: ChatUsersRequest) { + return chatService.addUserToChat(data).then(() => { + this.getChatUsers(data.chatId) + }) + } + + public async deleteUserFromChat(data: ChatUsersRequest) { + return chatService.deleteUserFromChat(data).then(() => { + this.getChatUsers(data.chatId) + }) + } } diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index 2fb1957c6..108979071 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -1,5 +1,6 @@ import { EditPasswordData, User } from '@/constants/types.ts' import { AuthController } from '@/controllers/AuthController.ts' +import store from '@/core/Store.ts' import { UserService } from '@/services/UserService.ts' const authController = new AuthController() @@ -10,8 +11,7 @@ export class UserController { return userService .editProfile(userdata) .then((resp) => { - authController.getUser() - return resp + store.set('userdata', resp.response) }) .catch((error) => { return error diff --git a/src/core/Store.ts b/src/core/Store.ts index 414e3758e..e9cc3a7a7 100644 --- a/src/core/Store.ts +++ b/src/core/Store.ts @@ -1,3 +1,4 @@ +import { MessageItemProps } from '@/components/message/message.ts' import { initialState } from '@/constants/initialState.ts' import { Chat, User } from '@/constants/types.ts' import EventBus from './EventBus.ts' @@ -6,29 +7,14 @@ export enum StoreEvents { UPDATED = 'updated', } -function set( +function set( object: StateType, - path: string, - value: Partial + path: K, + value: StateType[K] ): StateType { - const parts = path.split('.') - let current = object - - for (let i = 0; i < parts.length - 1; i++) { - const part = parts[i] - - if (!(part in current)) { - current[part] = {} - } - - current = current[part] as StateType - - if (typeof current !== 'object' || current === null) { - return object - } + if (path in object) { + object[path] = value } - - current[parts[parts.length - 1]] = value return object } @@ -36,7 +22,8 @@ export type StateType = { userdata: User chats: Chat[] selectedChat: number - [key: string]: unknown + messages: MessageItemProps[] + chatUsers: User[] } class Store extends EventBus { @@ -46,7 +33,10 @@ class Store extends EventBus { return this.state } - public set(path: string, value: Partial) { + public set( + path: K, + value: StateType[K] + ) { set(this.state, path, value) this.emit(StoreEvents.UPDATED) } diff --git a/src/mockData.ts b/src/mockData.ts deleted file mode 100644 index 8e30f8406..000000000 --- a/src/mockData.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { MessageProps } from './components/message/message' - -export const mockMessages: MessageProps[] = [ - { - id: '0', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '1', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '0', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '1', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '0', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '1', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '0', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '1', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '0', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '1', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '0', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '1', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '0', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '1', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '0', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, - { - id: '1', - body: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века.', - }, -] diff --git a/src/pages/loginPage/loginPage.ts b/src/pages/loginPage/loginPage.ts index b19407107..b7d524692 100644 --- a/src/pages/loginPage/loginPage.ts +++ b/src/pages/loginPage/loginPage.ts @@ -7,8 +7,8 @@ import { AuthController } from '@/controllers/AuthController.ts' import Block, { Props } from '@/core/Block' import router from '@/router.ts' import { RegisterData } from '@/services/AuthService.ts' -import { withUserdata } from '@/utils/connect.ts' import './loginPage.css' +import connect from '@/utils/connect.ts' // language=hbs const loginPageTemplate = ` @@ -55,7 +55,8 @@ const submitHandler = (e: Event) => { if (resp instanceof XMLHttpRequest && resp.status === 200) { router.go(routes.messenger) } else { - alert('kek') + loginForm.showInputError('login', 'Неверные данные') + loginForm.showInputError('password', 'Неверные данные') } }) } @@ -99,9 +100,9 @@ const loginForm = new Form({ }), }) -const connectedLoginPage = withUserdata(LoginPage) +const withUserdata = connect((state) => ({ userdata: state.userdata }))(LoginPage) -export const loginPage = new connectedLoginPage({ +export const loginPage = new withUserdata({ loginForm: loginForm, registerLink: new Link({ to: routes.register, diff --git a/src/pages/messengerPage/messengerPage.css b/src/pages/messengerPage/messengerPage.css index 8f7bd7daa..f1fb37661 100644 --- a/src/pages/messengerPage/messengerPage.css +++ b/src/pages/messengerPage/messengerPage.css @@ -4,6 +4,7 @@ height: 100%; .sidebar { + position: relative; height: 100%; overflow-y: scroll; background: var(--sidebar-bg); @@ -22,5 +23,20 @@ color: var(--color-gray); } } + + .create-chat-btn { + position: absolute; + bottom: 8px; + right: 8px; + width: 40px; + height: 40px; + padding: 12px; + border-radius: 50%; + border: none; + background: var(--color-blue); + color: var(--color-white); + font-weight: 600; + cursor: pointer; + } } } diff --git a/src/pages/messengerPage/messengerPage.ts b/src/pages/messengerPage/messengerPage.ts index 0029ca63c..8f0c4545d 100644 --- a/src/pages/messengerPage/messengerPage.ts +++ b/src/pages/messengerPage/messengerPage.ts @@ -1,15 +1,16 @@ import Button from '@/components/button/button.ts' -import { ChatWindow } from '@/components/chat/chat' import ChatItem from '@/components/chatItem/chatItem.ts' +import { ChatWindow, chatWindow } from '@/components/chatWindow/chatWindow.ts' +import Input from '@/components/input/input.ts' +import { Modal } from '@/components/modal/modal.ts' import { routes } from '@/constants/routes.ts' import { Chat } from '@/constants/types.ts' import { AuthController } from '@/controllers/AuthController.ts' import { ChatController } from '@/controllers/ChatController.ts' import Block, { Props } from '@/core/Block' import store from '@/core/Store.ts' -import { mockMessages } from '@/mockData' import router from '@/router.ts' -import { withChats } from '@/utils/connect.ts' +import connect from '@/utils/connect.ts' import './messengerPage.css' // language=hbs @@ -22,6 +23,7 @@ const MessengerPageTemplate = ` {{{ chats }}} + {{{ createChatBtn }}} {{{ chat }}} @@ -30,7 +32,6 @@ const MessengerPageTemplate = ` type MessengerPageProps = { profileBtn: Button - chats: Chat[] chat: ChatWindow } & Props @@ -38,8 +39,47 @@ const authController = new AuthController() const chatController = new ChatController() export class MessengerPage extends Block { + private modal: Modal + constructor(props: MessengerPageProps) { super(props) + this.modal = new Modal() + this.children.createChatBtn = new Button({ + label: '', + className: 'create-chat-btn', + withId: true, + events: { + click: () => { + this.showCreateChatModal() + }, + }, + }) + } + + showCreateChatModal() { + const content = document.createElement('div') + const input = new Input({ + type: 'text', + label: 'Название чата', + placeholder: 'Название...', + name: 'create-chat' + }) + const btn = new Button({ + label: 'Создать чат', + className: 'button input-submit', + events: { + click: () => { + chatController.createChat(input.getValue()) + this.modal.close() + } + } + }) + + content.appendChild(input.element) + content.appendChild(btn.element) + + this.modal.setContent('Создать чат', content); + this.modal.open(); } createChatItems(chats: Chat[]) { @@ -66,9 +106,11 @@ export class MessengerPage extends Block { } } -const connectedMessengerPage = withChats(MessengerPage) +export const withChats = connect((state) => ({ + chats: state.chats, +}))(MessengerPage) -export const messengerPage = new connectedMessengerPage({ +export const messengerPage = new withChats({ profileBtn: new Button({ label: 'Профиль>', className: 'button-icon back-btn', @@ -78,9 +120,7 @@ export const messengerPage = new connectedMessengerPage({ }, }, }), - chats: store.getState().chats, - chat: new ChatWindow({ - user: store.getState().userdata, - messages: mockMessages, + chat: new chatWindow({ + selectedChat: store.getState().selectedChat, }), }) diff --git a/src/pages/profilePage/profilePage.ts b/src/pages/profilePage/profilePage.ts index 2e59d1f0b..5aab58e8f 100644 --- a/src/pages/profilePage/profilePage.ts +++ b/src/pages/profilePage/profilePage.ts @@ -8,7 +8,7 @@ import { UserController } from '@/controllers/UserController.ts' import Block from '@/core/Block' import store from '@/core/Store.ts' import router from '@/router.ts' -import { withUserAvatar, withUserdata } from '@/utils/connect.ts' +import connect from '@/utils/connect.ts' import './profilePage.css' // language=hbs @@ -116,10 +116,12 @@ const logoutBtnHandler = () => { }) } -const connectedProfilePage = withUserdata(ProfilePage) -const connectedAvatar = withUserAvatar(Avatar) +const withUserdata = connect((state) => ({ userdata: state.userdata }))(ProfilePage) +export const withUserAvatar = connect((state) => ({ + src: state.userdata.avatar, +}))(Avatar) -export const profilePage = new connectedProfilePage({ +export const profilePage = new withUserdata({ userdata: store.getState().userdata, backBtn: new Button({ label: '', @@ -130,9 +132,10 @@ export const profilePage = new connectedProfilePage({ }, }, }), - avatar: new connectedAvatar({ + avatar: new withUserAvatar({ src: store.getState().userdata.avatar, alt: 'avatar', + canChange: true, events: { click: avatarUploadHandler, }, diff --git a/src/pages/registerPage/registerPage.ts b/src/pages/registerPage/registerPage.ts index 8c4897975..f5b0361a0 100644 --- a/src/pages/registerPage/registerPage.ts +++ b/src/pages/registerPage/registerPage.ts @@ -7,8 +7,8 @@ import { AuthController } from '@/controllers/AuthController.ts' import Block from '@/core/Block' import router from '@/router.ts' import { RegisterData } from '@/services/AuthService.ts' -import { withUserdata } from '@/utils/connect.ts' import './registerPage.css' +import connect from '@/utils/connect.ts' // language=hbs const registerPageTemplate = ` @@ -146,9 +146,9 @@ const registerForm = new Form({ }), }) -const connectedRegisterPage = withUserdata(RegisterPage) +const withUserdata = connect((state) => ({ userdata: state.userdata }))(RegisterPage) -export const registerPage = new connectedRegisterPage({ +export const registerPage = new withUserdata({ registerForm: registerForm, loginLink: new Link({ to: routes.login, diff --git a/src/services/ChatService.ts b/src/services/ChatService.ts index 524fc0677..0d00889a9 100644 --- a/src/services/ChatService.ts +++ b/src/services/ChatService.ts @@ -12,6 +12,14 @@ export class ChatService { return HTTPTransport.get(`${this.baseURL}`, {}) } + getChatUsers(chatId: number) { + return HTTPTransport.get(`${this.baseURL}/${chatId}/users`, {}) + } + + getToken(chatId: number) { + return HTTPTransport.post(`${this.baseURL}/token/${chatId}`, {}) + } + createChat(title: string) { return HTTPTransport.post(`${this.baseURL}`, { body: { title }, diff --git a/src/styles/base.css b/src/styles/base.css index 9902de775..cc36b9e85 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -57,3 +57,29 @@ h4 { width: 100%; padding-bottom: 16px; } + +.modal-content { + .users { + display: flex; + flex-direction: column; + gap: 8px; + + .user { + display: flex; + align-items: center; + + &__login { + flex-grow: 1; + } + + &__delete-btn { + margin: 0; + border: none; + background: none; + cursor: pointer; + font-size: 20px; + line-height: 1; + } + } + } +} diff --git a/src/styles/variables.css b/src/styles/variables.css index 50b93df77..13c952476 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -13,7 +13,7 @@ --blue-bg: #e4edfd; --hover-bg: #ccc8; --border: #eaeaea; - --box-shadow: 0 0 3px 3px var(--color1); + --box-shadow: 0 0 3px 3px var(--hover-bg); --border-radius: 8px; --font-family: 'Inter', sans-serif; } diff --git a/src/utils/connect.ts b/src/utils/connect.ts index f25135fbb..4207478c7 100644 --- a/src/utils/connect.ts +++ b/src/utils/connect.ts @@ -26,10 +26,3 @@ export default function connect

( } } -export const withUserdata = connect((state) => ({ userdata: state.userdata })) -export const withUserAvatar = connect((state) => ({ - src: state.userdata.avatar, -})) -export const withChats = connect((state) => ({ - chats: state.chats, -})) diff --git a/src/utils/connectToMessageSocket.ts b/src/utils/connectToMessageSocket.ts new file mode 100644 index 000000000..550fdade6 --- /dev/null +++ b/src/utils/connectToMessageSocket.ts @@ -0,0 +1,65 @@ +import { MessageItemProps } from '@/components/message/message.ts' +import { ChatController } from '@/controllers/ChatController.ts' +import store from '@/core/Store.ts' + +const chatController = new ChatController() + +export default async (userId: number, chatId: number) => { + let token = '' + + await chatController.getToken(chatId).then((resp) => { + token = resp.token + }) + + const socket = new WebSocket( + `wss://ya-praktikum.tech/ws/chats/${userId}/${chatId}/${token}` + ) + + socket.addEventListener('open', () => { + console.log(`Соединение установлено c чатом ${chatId}`) + + if (socket) { + socket.send( + JSON.stringify({ + content: '0', + type: 'get old', + }) + ) + } + }) + + const ping = setInterval(() => { + socket?.send(JSON.stringify({ type: 'ping' })) + }, 10000) + + socket.addEventListener('message', (event) => { + const data = JSON.parse(event.data) + + if (Array.isArray(data)) { + store.set('messages', JSON.parse(event.data)) + } + if (data.type === 'message') { + const messages = store.getState().messages + const message: MessageItemProps = JSON.parse(event.data) + + message.time = new Date().toISOString() + + store.set('messages', [message, ...messages]) + } + if (data.type === 'user connected') { + const messages = store.getState().messages + const message: MessageItemProps = JSON.parse(event.data) + + message.time = new Date().toISOString() + message.content = `Юзер с id: ${message.content} присоединился к чату` + + store.set('messages', [message, ...messages]) + } + }) + + socket.addEventListener('close', () => { + clearInterval(ping) + }) + + return socket +} diff --git a/src/utils/formatMessageDate.ts b/src/utils/formatMessageDate.ts index 427aea1dd..2e69576d1 100644 --- a/src/utils/formatMessageDate.ts +++ b/src/utils/formatMessageDate.ts @@ -5,7 +5,6 @@ export default (date: string) => { const offset = now - messageDate let options: Intl.DateTimeFormatOptions - console.log(offset) if (offset < 24) { options = { diff --git a/vite.config.js b/vite.config.js index 57676653d..6de0d954f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -14,6 +14,7 @@ export default defineConfig({ }, }, server: { + host: true, port: 3000, open: true, },