diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 71267df38..b71dbf9d9 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -21,5 +21,7 @@ module.exports = { sourceType: "module", }, plugins: ["@typescript-eslint"], - rules: {}, + rules: { + "@typescript-eslint/no-namespace": "off", + }, }; diff --git a/.stylelintcache b/.stylelintcache index 5b651dc9c..30e9588a5 100644 --- a/.stylelintcache +++ b/.stylelintcache @@ -1 +1 @@ -[{"C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\chat\\styles.scss":"1","C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\registration\\styles.scss":"2","C:\\1\\middle.messenger.praktikum.yandex\\src\\components\\Input\\styles.scss":"3","C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\login\\styles.scss":"4","C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\account\\styles.scss":"5","C:\\1\\middle.messenger.praktikum.yandex\\src\\components\\Form\\styles.scss":"6","C:\\1\\middle.messenger.praktikum.yandex\\src\\components\\Link\\styles.scss":"7","C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\error\\styles.scss":"8","C:\\1\\middle.messenger.praktikum.yandex\\src\\components\\Modal\\styles.scss":"9","C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\account\\components\\AccountPhoto\\styles.scss":"10"},{"size":13372,"mtime":1708907856866,"hashOfConfig":"11"},{"size":135,"mtime":1708892428481,"hashOfConfig":"11"},{"size":853,"mtime":1708907080241,"hashOfConfig":"11"},{"size":128,"mtime":1708892428478,"hashOfConfig":"11"},{"size":3868,"mtime":1709391740708,"hashOfConfig":"11"},{"size":851,"mtime":1708907078157,"hashOfConfig":"11"},{"size":390,"mtime":1708907040933,"hashOfConfig":"11"},{"size":395,"mtime":1708892428472,"hashOfConfig":"11"},{"size":1122,"mtime":1709391740708,"hashOfConfig":"11"},{"size":390,"mtime":1708907040933,"hashOfConfig":"11"},"1v6j01t"] \ No newline at end of file +[{"C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\chat\\styles.scss":"1","C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\registration\\styles.scss":"2","C:\\1\\middle.messenger.praktikum.yandex\\src\\components\\Input\\styles.scss":"3","C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\login\\styles.scss":"4","C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\account\\styles.scss":"5","C:\\1\\middle.messenger.praktikum.yandex\\src\\components\\Form\\styles.scss":"6","C:\\1\\middle.messenger.praktikum.yandex\\src\\components\\Link\\styles.scss":"7","C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\error\\styles.scss":"8","C:\\1\\middle.messenger.praktikum.yandex\\src\\components\\Modal\\styles.scss":"9","C:\\1\\middle.messenger.praktikum.yandex\\src\\pages\\account\\components\\AccountPhoto\\styles.scss":"10","C:\\1\\middle.messenger.praktikum.yandex\\src\\components\\Button\\styles.scss":"11","C:\\1\\middle.messenger.praktikum.yandex\\src\\components\\Message\\styles.scss":"12"},{"size":11756,"mtime":1710702679458,"hashOfConfig":"13"},{"size":135,"mtime":1708892428481,"hashOfConfig":"13"},{"size":853,"mtime":1708907080241,"hashOfConfig":"13"},{"size":128,"mtime":1708892428478,"hashOfConfig":"13"},{"size":3868,"mtime":1709391740708,"hashOfConfig":"13"},{"size":851,"mtime":1708907078157,"hashOfConfig":"13"},{"size":390,"mtime":1708907040933,"hashOfConfig":"13"},{"size":395,"mtime":1708892428472,"hashOfConfig":"13"},{"size":1152,"mtime":1710584696300,"hashOfConfig":"13"},{"size":895,"mtime":1710102023778,"hashOfConfig":"13"},{"size":1159,"mtime":1709970950492,"hashOfConfig":"13"},{"size":1167,"mtime":1710702879251,"hashOfConfig":"13"},"1v6j01t"] \ No newline at end of file diff --git a/package.json b/package.json index 18cd11773..df74d2628 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,12 @@ "stylelint-config-standard": "^36.0.0", "stylelint-config-standard-scss": "^13.0.0", "typescript": "^5.3.3", - "vite": "^5.1.0" + "vite": "^5.1.0", + "vite-plugin-mkcert": "^1.17.4" }, "dependencies": { "@types/uuid": "^9.0.8", + "autoprefixer": "^10.4.18", "express": "^4.18.2", "uuid": "^9.0.1", "vite-express": "^0.15.0", diff --git a/src/assets/scss/reset.scss b/src/assets/scss/reset.scss index b3a87c4f0..df79d81af 100644 --- a/src/assets/scss/reset.scss +++ b/src/assets/scss/reset.scss @@ -57,6 +57,11 @@ textarea { white-space: revert; } +select { + -webkit-appearance: none; + appearance: none; +} + /* minimum style to allow to style meter element */ meter { appearance: revert; diff --git a/src/assets/scss/shared.scss b/src/assets/scss/shared.scss index 354c12770..085f82219 100644 --- a/src/assets/scss/shared.scss +++ b/src/assets/scss/shared.scss @@ -7,6 +7,11 @@ flex-wrap: wrap; } +section { + max-width: 1800px; + margin: 0 auto; +} + h2 { font-size: 16px; } @@ -15,26 +20,6 @@ h3 { font-size: 15px; } -.btn { - transition: 0.2s; - cursor: pointer; - width: 280px; - max-width: 100%; - height: 37px; - line-height: 37px; - padding: 0 10px; - border-radius: 8px; - font-size: 13px; - text-align: center; - color: #fff; - background-color: $primary-color; - - &:hover { - transition: 0.2s; - background-color: $dark-primary-color; - } -} - .title { font-size: 20px; width: 100%; diff --git a/src/components/Button/index.ts b/src/components/Button/index.ts index 7bfacb778..d7e0d2e48 100644 --- a/src/components/Button/index.ts +++ b/src/components/Button/index.ts @@ -1,10 +1,19 @@ -import Block, { BlockProps, setDefaultClassName } from "../../services/Block"; +import Block, { BlockProps } from "../../services/Block"; import tpl from "./tpl.hbs?raw"; import "./styles.scss"; export default class Button extends Block { constructor(props: BlockProps) { - super(setDefaultClassName(props, "btn", { type: "submit" }), "button"); + super( + { + ...props, + attrs: { + class: props.attrs?.class ?? "btn", + type: "submit", + }, + }, + "button" + ); } render() { return this.compile(tpl, this.props); diff --git a/src/components/Button/tpl.hbs b/src/components/Button/tpl.hbs index 8d15644bd..84230c7c1 100644 --- a/src/components/Button/tpl.hbs +++ b/src/components/Button/tpl.hbs @@ -1 +1,2 @@ {{text}} +{{!--
--}} diff --git a/src/components/Dropdown/index.ts b/src/components/Dropdown/index.ts index 50d750bd1..136440fb1 100644 --- a/src/components/Dropdown/index.ts +++ b/src/components/Dropdown/index.ts @@ -2,8 +2,12 @@ import Block, { BlockProps, setDefaultClassName } from "../../services/Block"; import tpl from "./tpl.hbs?raw"; import "./styles.scss"; +type DropdownProps = BlockProps & { + icon: Block | string; + dropdown: Block; +}; export default class Dropdown extends Block { - constructor(props: BlockProps) { + constructor(props: DropdownProps) { super(setDefaultClassName(props, "dropdown"), "div"); } render() { diff --git a/src/components/Form/styles.scss b/src/components/Form/styles.scss index 512cab37c..6181ca80e 100644 --- a/src/components/Form/styles.scss +++ b/src/components/Form/styles.scss @@ -27,7 +27,7 @@ margin-top: 60px; } - .error { + .error-message { margin: 0 auto; margin-top: 14px; text-align: center; diff --git a/src/components/Image/index.ts b/src/components/Image/index.ts index 5977f6b7a..24703f1b0 100644 --- a/src/components/Image/index.ts +++ b/src/components/Image/index.ts @@ -5,6 +5,6 @@ export default class Image extends Block { super(props, "img"); } render() { - return this._createDocumentElement("img"); + return this.createDocumentElement("img"); } } diff --git a/src/components/Input/index.ts b/src/components/Input/index.ts index d58d04e8b..fcc5432b1 100644 --- a/src/components/Input/index.ts +++ b/src/components/Input/index.ts @@ -12,7 +12,7 @@ export default class Input extends Block { this.element .querySelector("input") ?.addEventListener("blur", (e) => { - (this.props.onChange as (e?: EventTarget) => void)( + (this.props.onChange as (target?: EventTarget) => void)( e.target ?? undefined ); }); diff --git a/src/components/Message/index.ts b/src/components/Message/index.ts index 58d27cc2a..a943e91c9 100644 --- a/src/components/Message/index.ts +++ b/src/components/Message/index.ts @@ -1,8 +1,8 @@ -import Block from "../../services/Block"; -import tpl from "./tpl.hbs?raw"; +import IndexPage from "./indexPage"; +import { Connect } from "../../services/Store"; -export default class Message extends Block { - render() { - return this.compile(tpl, this.props); - } -} +const Message = Connect(IndexPage, (state) => { + return { currentUserId: state.user.id }; +}); + +export default Message; diff --git a/src/components/Message/tpl.hbs b/src/components/Message/tpl.hbs index aa1e1fbf6..e311dae6a 100644 --- a/src/components/Message/tpl.hbs +++ b/src/components/Message/tpl.hbs @@ -1,9 +1,8 @@ {{!-- TODO: paste date here --}} {{!--
19 июня
--}} {{!-- {{{date}}} --}} -
-
- {{{message}}} -
-
{{date}}
+
+ {{message}}
+
{{time}}
+ diff --git a/src/components/MessagePreview/index.ts b/src/components/MessagePreview/index.ts deleted file mode 100644 index 56501d82c..000000000 --- a/src/components/MessagePreview/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Block from "../../services/Block"; -import tpl from "./tpl.hbs?raw"; - -export default class Input extends Block { - render() { - return this.compile(tpl, this.props); - } -} diff --git a/src/components/MessagePreview/tpl.hbs b/src/components/MessagePreview/tpl.hbs deleted file mode 100644 index 08159d81f..000000000 --- a/src/components/MessagePreview/tpl.hbs +++ /dev/null @@ -1,11 +0,0 @@ -
  • -
    -
    -

    {{name}}

    -
    - {{message}} -
    -
    -
    {{date}}
    -
    {{countMessages}}
    -
  • diff --git a/src/components/Modal/styles.scss b/src/components/Modal/styles.scss index 707013597..0f6276f0e 100644 --- a/src/components/Modal/styles.scss +++ b/src/components/Modal/styles.scss @@ -40,6 +40,7 @@ .title { font-size: 15px; + margin-bottom: 16px; &.error { color: $red-color; diff --git a/src/index.html b/src/index.html index a9110eeb9..71ec38b5e 100644 --- a/src/index.html +++ b/src/index.html @@ -8,7 +8,6 @@
    - diff --git a/src/main.ts b/src/main.ts index 060b8aaf7..9bac7e9db 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,35 +1,52 @@ -import Block from "./services/Block"; import LoginPage from "./pages/login"; import RegistrationPage from "./pages/registration"; import ChatPage from "./pages/chat"; +import AccountPage from "./pages/account"; import ErrorPage from "./pages/error"; import "./assets/scss/styles.scss"; +import router from "./services/Router/Router"; +import { getUserInfo } from "./api/auth"; +import { setCurrentUser } from "./services/Store"; -const routes: Record = { - homepage: LoginPage, - registration: RegistrationPage, - chat: ChatPage, - error500: new ErrorPage({ - errorCode: "500 ", +router + .use("*", ErrorPage, { + justification: "Не туда попали", + errorCode: "404", + }) + .use("/", LoginPage) + .use("/sign-up", RegistrationPage) + .use("/messenger", ChatPage) + .use("/settings", AccountPage) + .use("/error500", ErrorPage, { justification: "Мы уже фиксим", - }), -}; + errorCode: "500", + }) + .start(); -const pathName = window.location.pathname.slice(1); +document.addEventListener("click", (e) => { + if ((e.target as HTMLElement).tagName.toLowerCase() === "a") { + e.preventDefault(); + const link = (e.target as HTMLElement).getAttribute("href"); + if (link) { + router.go(link); + } + } +}); -const res = pathName - ? routes[pathName] - ? routes[pathName] - : new ErrorPage({ - errorCode: "404", - justification: "Не туда попали", - }) - : routes.homepage; +getUserInfo() + .then((user) => { + if ( + user && + (router._currentRoute?._pathname === "/" || + router._currentRoute?._pathname === "/sign-up") + ) { + router.go("/messenger"); + } -function render(query: string, block: Block) { - const root = document.querySelector(query); - root?.appendChild(block.getContent()); - return root; -} - -render("#app", res); + setCurrentUser(user); + }) + .catch(() => { + if (router._currentRoute?._pathname !== "/sign-up") { + router.go("/"); + } + }); diff --git a/src/mockData.json b/src/mockData.json deleted file mode 100644 index febeb8a0e..000000000 --- a/src/mockData.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "chat": { - "chatList": [ - { - "name": "Design Destroyer2", - "message": "Миллионы россиян ежедневно проводят десятки часов своевременно прибегая к нуждам самоорганизации через непосильный труд копьем", - "date": "2024-02-05T18:47:05.060Z", - "countMessages": "2" - }, - { - "name": "Design Destroyer", - "message": "Миллионы россиян ежедневно проводят десятки часов своевременно прибегая к нуждам самоорганизации через непосильный труд копьем", - "date": "2024-02-08T18:47:05.060Z", - "countMessages": "2" - }, - { - "name": "Design Destroyer", - "message": "Миллионы россиян ежедневно проводят десятки часов своевременно прибегая к нуждам самоорганизации через непосильный труд копьем", - "date": "2024-02-07T18:47:05.060Z", - "countMessages": "2" - }, - { - "name": "Design Destroyer", - "message": "Миллионы россиян ежедневно проводят десятки часов своевременно прибегая к нуждам самоорганизации через непосильный труд копьем", - "date": "2024-02-08T18:47:05.060Z", - "countMessages": "2" - } - ], - "currentChat": [ - { - "userId": "1", - "message": "

    Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatem dignissimos placeat, aliquam consequatur modi labore mollitia eum incidunt, adipisci magnam possimus nesciunt provident ea, quos aliquid blanditiis repudiandae minus vel unde exercitationem explicabo corporis? Vel blanditiis ducimus hic ipsam est.

    ", - "date": "2024-02-08T18:47:05.060Z", - "checked": true - }, - { - "userId": "current-user", - "message": "

    Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatem dignissimos placeat, aliquam consequatur modi labore mollitia eum incidunt, adipisci magnam possimus nesciunt provident ea, quos aliquid blanditiis repudiandae minus vel unde exercitationem explicabo corporis? Vel blanditiis ducimus hic ipsam est.

    ", - "date": "2024-02-08T18:47:05.060Z", - "checked": true - }, - { - "userId": "1", - "message": "

    Привет! Смотри, тут всплыл интересный кусок лунной космической истории — НАСА в какой-то момент попросила Хассельблад адаптировать модель SWC для полетов на Луну. Сейчас мы все знаем что астронавты летали с моделью 500 EL — и к слову говоря, все тушки этих камер все еще находятся на поверхности Луны, так как астронавты с собой забрали только кассеты с пленкой.

    Хассельблад в итоге адаптировал SWC для космоса, но что-то пошло не так и на ракету они так никогда и не попали. Всего их было произведено 25 штук, одну из них недавно продали на аукционе за 45000 евро.

    ", - "date": "2024-02-05T18:47:05.060Z", - "checked": true - }, - { - "userId": "1", - "message": "\"Фотография\"", - "date": "2024-02-07T18:47:05.060Z", - "checked": true - }, - { - "userId": "current-user", - "message": "

    Круто!

    ", - "date": "2024-02-08T18:47:05.060Z", - "checked": true - }, - { - "userId": "current-user", - "message": "

    Lorem ipsum dolor sit amet consectetur adip

    ", - "date": "2024-02-08T18:47:05.060Z", - "checked": false - } - ] - } -} diff --git a/src/pages/account/components/AccountPhoto/index.ts b/src/pages/account/components/AccountPhoto/index.ts index 2a8273a96..b19f92a66 100644 --- a/src/pages/account/components/AccountPhoto/index.ts +++ b/src/pages/account/components/AccountPhoto/index.ts @@ -1,15 +1,18 @@ import Block, { BlockProps } from "../../../../services/Block"; import tpl from "./tpl.hbs?raw"; import "./styles.scss"; +import { RESOURCES_URL } from "../../../../api/HTTPTransportYaPraktikum"; export default class AccountPhoto extends Block { constructor(props: BlockProps) { super( { ...props, - src: "img/noImage.svg", + avatar: props.avatar + ? `${RESOURCES_URL}${props.avatar}` + : "img/noImage.svg", attrs: { - class: "image", + class: "user-avatar", }, }, "div" diff --git a/src/pages/account/components/AccountPhoto/styles.scss b/src/pages/account/components/AccountPhoto/styles.scss index 65e78c623..5d6f3abb9 100644 --- a/src/pages/account/components/AccountPhoto/styles.scss +++ b/src/pages/account/components/AccountPhoto/styles.scss @@ -1,19 +1,42 @@ -.btn { - transition: 0.2s; +.user-avatar { cursor: pointer; - width: 280px; - max-width: 100%; - height: 37px; - line-height: 37px; - padding: 0 10px; - border-radius: 8px; - font-size: 13px; - text-align: center; - color: #fff; - background-color: $primary-color; + position: relative; + margin-bottom: 20px; + border-radius: 50%; + overflow: hidden; + + &__image { + width: 130px; + height: 130px; + min-width: 130px; + min-height: 130px; + object-fit: cover; + object-position: center; + } + + .change-image { + transition: 0.4s; + opacity: 0; + visibility: hidden; + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + background-color: #000; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + font-size: 13px; + color: #fff; + } &:hover { - transition: 0.2s; - background-color: $dark-primary-color; + .change-image { + transition: 0.4s; + opacity: 0.5; + visibility: visible; + } } } diff --git a/src/pages/account/components/AccountPhoto/tpl.hbs b/src/pages/account/components/AccountPhoto/tpl.hbs index 25bf31891..6523431a3 100644 --- a/src/pages/account/components/AccountPhoto/tpl.hbs +++ b/src/pages/account/components/AccountPhoto/tpl.hbs @@ -1,4 +1,4 @@ - +Поменять аватар
    Поменять
    аватар
    diff --git a/src/pages/account/components/ModalChangeAvatar/index.ts b/src/pages/account/components/ModalChangeAvatar/index.ts index 30028c7b7..b1750ed24 100644 --- a/src/pages/account/components/ModalChangeAvatar/index.ts +++ b/src/pages/account/components/ModalChangeAvatar/index.ts @@ -3,14 +3,14 @@ import Form from "../../../../components/Form"; import Modal from "../../../../components/Modal"; import Title from "../../../../components/Title"; import InputFile from "../../../../components/InputFile"; +import { changeUserAvatar } from "../../../../api/users"; +import Unit from "../../../../components/Unit"; const DEFAULT_TITLE_TEXT = "Загрузите файл"; const DEFAULT_INPUT_FILE_TEXT = "Выбрать файл на
    компьютере"; const TitleModal = new Title({ text: DEFAULT_TITLE_TEXT }, "h3"); -let inputFileValue: FileList | null = null; - const InputFileModal = new InputFile( { name: "avatar", @@ -23,10 +23,6 @@ const InputFileModal = new InputFile( const target = e.target as HTMLInputElement; const files = target?.files; - if (files && files.length > 0) { - inputFileValue = files; - } - TitleModal.setProps({ text: "Файл загружен", }); @@ -52,38 +48,48 @@ const InputFileModal = new InputFile( "label" ); -const ModalChangeAvatar = new Modal({ - content: [ - TitleModal, - new Form({ - fields: [InputFileModal], - button: new Button({ - text: "Сохранить", - }), - events: { - submit: (e) => { - e.preventDefault(); +const formChangeAvatar = new Form({ + fields: [InputFileModal], + button: new Button({ + text: "Сохранить", + }), + events: { + submit: async (e: Event) => { + e.preventDefault(); + const formData = new FormData(e?.target as HTMLFormElement); - const fr = new FileReader(); - fr.onload = function () { - const image = - document.querySelector( - ".account-image" - ); + try { + await changeUserAvatar(formData); + // TODO: set store to update avatar here + formChangeAvatar.setProps({ + footer: new Unit(), + }); + ModalChangeAvatar.hide(); - if (image) { - image.src = fr.result as string; - } - }; - if (inputFileValue) { - fr.readAsDataURL(inputFileValue[0]); - } + (formChangeAvatar._element as HTMLFormElement).reset(); + TitleModal.setProps({ + text: DEFAULT_TITLE_TEXT, + }); + InputFileModal.setProps({ + text: DEFAULT_INPUT_FILE_TEXT, + }); + } catch (e) { + formChangeAvatar.setProps({ + footer: new Unit( + { + content: "Файл не загружен, попробуйте другой", + attrs: { class: "error-message red" }, + }, + "span" + ), + }); + } + }, + }, +}); - ModalChangeAvatar.hide(); - }, - }, - }), - ], +const ModalChangeAvatar = new Modal({ + content: [TitleModal, formChangeAvatar], attrs: { class: "form change-avatar", }, diff --git a/src/pages/account/index.ts b/src/pages/account/index.ts index 977909f48..e126344e1 100644 --- a/src/pages/account/index.ts +++ b/src/pages/account/index.ts @@ -9,16 +9,29 @@ import Form from "../../components/Form"; import Input from "../../components/Input"; import Unit from "../../components/Unit"; import ModalChangeAvatar from "./components/ModalChangeAvatar"; +import { + ChangeUserPasswordData, + changeUserPassword, + changeUserProfile, +} from "../../api/users"; +import { getUserInfo, logOut } from "../../api/auth"; +import { User } from "../../api/types"; import "./styles.scss"; +import Title from "../../components/Title"; +import router from "../../services/Router/Router"; +import { Store } from "../../services/Store"; -const defaultContent = [ +const store = new Store(); + +const currentUser = store.getStateEl("user"); + +const defaultFieldsList = [ new Input( { type: "text", name: "email", isRequired: true, text: "Почта", - value: "pochta@yandex.ru", onChange: (target: EventTarget) => { validateInput( (val) => Boolean(val?.match(REGEX.EMAIL_REGEX)), @@ -35,7 +48,6 @@ const defaultContent = [ name: "login", isRequired: true, text: "Логин", - value: "lolov", onChange: (target: EventTarget) => { validateInput( (val) => Boolean(val?.match(REGEX.LOGIN_REGEX)), @@ -52,7 +64,6 @@ const defaultContent = [ name: "first_name", isRequired: true, text: "Имя", - value: "Иван", onChange: (target: EventTarget) => { validateInput( (val) => Boolean(val?.match(REGEX.NAME_REGEX)), @@ -69,7 +80,6 @@ const defaultContent = [ name: "second_name", isRequired: true, text: "Фамилия", - value: "Иванович", onChange: (target: EventTarget) => { validateInput( (val) => Boolean(val?.match(REGEX.NAME_REGEX)), @@ -86,7 +96,6 @@ const defaultContent = [ name: "display_name", isRequired: true, text: "Имя в чате", - value: "Иван", }, "li" ), @@ -96,7 +105,6 @@ const defaultContent = [ name: "phone", isRequired: true, text: "Телефон", - value: "+79099673030", onChange: (target: EventTarget) => { validateInput( (val) => Boolean(val?.match(REGEX.PHONE_REGEX)), @@ -109,6 +117,32 @@ const defaultContent = [ ), ]; +const AccountPhotoComponent = new AccountPhoto({ + avatar: currentUser.avatar, + events: { + click: () => { + ModalChangeAvatar.show(); + }, + }, +}); + +const setAccountProps = async (userInfo: User) => { + defaultFieldsList.forEach((field) => { + field.setProps({ value: userInfo[field.props.name as keyof User] }); + }); + AccountPhotoComponent.setProps({ src: userInfo.avatar }); + formAccountInfoTitle.setProps({ text: userInfo.display_name }); +}; + +const setInitialUserInfo = async () => { + try { + const res = await getUserInfo(); + setAccountProps(res); + } catch (e) { + console.error(e); + } +}; + const passwordContent = [ new Input( { @@ -164,7 +198,9 @@ const passwordContent = [ ), ]; -const defaultFields = new Unit({ content: defaultContent }, "ul"); +const defaultFields = new Unit({ content: defaultFieldsList }, "ul"); +setInitialUserInfo(); + const passwordFields = new Unit( { content: passwordContent, @@ -172,200 +208,247 @@ const passwordFields = new Unit( "ul" ); -const formAccountInfo = new Form({ - fields: defaultFields, - button: new Button({ text: "Сохранить" }), - attrs: { - class: "user-info", - }, - events: { - submit: (e) => { - e.preventDefault(); - let isFormValid; +const handleFormAccountSubmit = async (e: Event) => { + e.preventDefault(); + let isFormValid; + let data; + const formData = new FormData(e?.target as HTMLFormElement); - if (formAccountInfo.children.fields === defaultFields) { - isFormValid = - [ - validateInput( - (val) => Boolean(val?.match(REGEX.EMAIL_REGEX)), - "Неверная почта", - (e.target as HTMLElement)?.querySelector( - `[name="email"]` - ) as HTMLInputElement - ), - validateInput( - (val) => Boolean(val?.match(REGEX.LOGIN_REGEX)), - "Неверный логин", - (e.target as HTMLElement)?.querySelector( - `[name="login"]` - ) as HTMLInputElement - ), - validateInput( - (val) => Boolean(val?.match(REGEX.NAME_REGEX)), - "Неверное имя", - (e.target as HTMLElement)?.querySelector( - `[name="first_name"]` - ) as HTMLInputElement - ), - validateInput( - (val) => Boolean(val?.match(REGEX.NAME_REGEX)), - "Неверная Фамилия", - (e.target as HTMLElement)?.querySelector( - `[name="second_name"]` - ) as HTMLInputElement - ), - validateInput( - (val) => Boolean(val?.match(REGEX.PHONE_REGEX)), - "Неверный телефон", - (e.target as HTMLElement)?.querySelector( - `[name="phone"]` - ) as HTMLInputElement - ), - ].filter((val) => !val).length === 0; - } else { - isFormValid = - [ - validateInput( - (val) => Boolean(val?.match(REGEX.PASSWORD_REGEX)), - "Неверный пароль", - (e.target as HTMLElement)?.querySelector( - `[name="oldPassword"]` - ) as HTMLInputElement - ), - validateInput( - (val) => Boolean(val?.match(REGEX.PASSWORD_REGEX)), - "Минимум 8 символов", + if (formAccountInfo.children.fields === defaultFields) { + isFormValid = + [ + validateInput( + (val) => Boolean(val?.match(REGEX.EMAIL_REGEX)), + "Неверная почта", + (e.target as HTMLElement)?.querySelector( + `[name="email"]` + ) as HTMLInputElement + ), + validateInput( + (val) => Boolean(val?.match(REGEX.LOGIN_REGEX)), + "Неверный логин", + (e.target as HTMLElement)?.querySelector( + `[name="login"]` + ) as HTMLInputElement + ), + validateInput( + (val) => Boolean(val?.match(REGEX.NAME_REGEX)), + "Неверное имя", + (e.target as HTMLElement)?.querySelector( + `[name="first_name"]` + ) as HTMLInputElement + ), + validateInput( + (val) => Boolean(val?.match(REGEX.NAME_REGEX)), + "Неверная Фамилия", + (e.target as HTMLElement)?.querySelector( + `[name="second_name"]` + ) as HTMLInputElement + ), + validateInput( + (val) => Boolean(val?.match(REGEX.PHONE_REGEX)), + "Неверный телефон", + (e.target as HTMLElement)?.querySelector( + `[name="phone"]` + ) as HTMLInputElement + ), + ].filter((val) => !val).length === 0; + + data = [ + "first_name", + "second_name", + "display_name", + "login", + "email", + "phone", + ].reduce( + (acc, curr) => ({ + ...acc, + [curr]: formData.get(curr) as string, + }), + {} as User + ); + + try { + const res = await changeUserProfile(data); + setAccountProps(res); + } catch (e) { + console.error(e); + } + } else { + isFormValid = + [ + validateInput( + (val) => Boolean(val?.match(REGEX.PASSWORD_REGEX)), + "Неверный пароль", + (e.target as HTMLElement)?.querySelector( + `[name="oldPassword"]` + ) as HTMLInputElement + ), + validateInput( + (val) => Boolean(val?.match(REGEX.PASSWORD_REGEX)), + "Минимум 8 символов", + (e.target as HTMLElement)?.querySelector( + `[name="newPassword"]` + ) as HTMLInputElement + ), + validateInput( + (val) => + val === + ( (e.target as HTMLElement)?.querySelector( `[name="newPassword"]` ) as HTMLInputElement - ), - validateInput( - (val) => - val === - ( - (e.target as HTMLElement)?.querySelector( - `[name="newPassword"]` - ) as HTMLInputElement - )?.value, - "Пароли не совпадают", - (e.target as HTMLElement)?.querySelector( - `[name="newPassword_2"]` - ) as HTMLInputElement - ), - ].filter((val) => !val).length === 0; - } + )?.value, + "Пароли не совпадают", + (e.target as HTMLElement)?.querySelector( + `[name="newPassword_2"]` + ) as HTMLInputElement + ), + ].filter((val) => !val).length === 0; - if (isFormValid) { - const formData = new FormData(e.target as HTMLFormElement); + data = ["oldPassword", "newPassword"].reduce( + (acc, curr) => ({ + ...acc, + [curr]: formData.get(curr) as string, + }), + {} as ChangeUserPasswordData + ); - const data: { [key: string]: FormDataEntryValue } = {}; - formData.forEach((val, key) => { - data[key] = val; - }); - console.log(data); + try { + await changeUserPassword(data); + } catch (e) { + console.error(e); + } + } - document - .querySelector("section#account") - ?.removeAttribute("data-edit"); - formAccountInfo.setProps({ - fields: defaultFields, - footer: null, - }); - } else { - formAccountInfo.setProps({ - footer: new Unit( - { - content: "Неверно заполнены поля", - attrs: { class: "error red" }, - }, - "span" - ), - }); - } - }, - }, -}); + if (isFormValid) { + document.querySelector("section#account")?.removeAttribute("data-edit"); -class Account extends Block { - render() { - return this.compile(tpl, this.props); + formAccountInfo.setProps({ + fields: defaultFields, + footer: new Unit(), + }); + } else { + formAccountInfo.setProps({ + footer: new Unit( + { + content: "Неверно заполнены поля", + attrs: { class: "error-message red" }, + }, + "span" + ), + }); } -} +}; -const AccountComponent = new Account( - { - backBtn: new Unit( +const formAccountInfoTitle = new Title({ text: "" }, "h2"); + +const formAccountInfo = new Form({ + title: formAccountInfoTitle, + fields: defaultFields, + button: new Button({ text: "Сохранить" }), + attrs: { + class: "user-info", + }, + events: { + submit: handleFormAccountSubmit, + }, +}); + +export default class Account extends Block { + constructor() { + super( { - attrs: { - class: "back", - }, - events: { - click: () => { - AccountComponent.setProps({ - attrs: { - class: "", + backBtn: new Unit( + { + attrs: { + class: "back", + }, + events: { + click: () => { + router.back(); }, - }); - }, - }, - }, - "div" - ), - - accountPhoto: new AccountPhoto({ - events: { - click: () => { - ModalChangeAvatar.show(); - }, - }, - }), - form: formAccountInfo, - links: [ - new Link( - { - text: "Изменить данные", - events: { - click: () => { - document - .querySelector("section#account") - ?.setAttribute("data-edit", ""); - formAccountInfo.setProps({ fields: defaultFields }); }, }, - }, - "li" - ), - new Link( - { - text: "Изменить пароль", - events: { - click: () => { - document - .querySelector("section#account") - ?.setAttribute("data-edit", ""); - formAccountInfo.setProps({ - fields: passwordFields, - }); + "div" + ), + accountPhoto: AccountPhotoComponent, + form: formAccountInfo, + links: new Unit( + { + content: [ + new Link( + { + text: "Изменить данные", + events: { + click: () => { + document + .querySelector( + "section#account" + ) + ?.setAttribute("data-edit", ""); + formAccountInfo.setProps({ + fields: defaultFields, + }); + }, + }, + }, + "li" + ), + new Link( + { + text: "Изменить пароль", + events: { + click: () => { + document + .querySelector( + "section#account" + ) + ?.setAttribute("data-edit", ""); + formAccountInfo.setProps({ + fields: passwordFields, + }); + }, + }, + }, + "li" + ), + new Link( + { + text: "Выйти", + events: { + click: async () => { + try { + await logOut(); + router.go("/"); + } catch (e) { + console.error(e); + } + }, + }, + attrs: { + class: "red", + }, + }, + "li" + ), + ], + attrs: { + class: "links", }, }, - }, - "li" - ), - new Link( - { - text: "Выйти", - attrs: { - class: "red", - }, - }, - "li" - ), - ], - modals: ModalChangeAvatar, + "ul" + ), + modals: ModalChangeAvatar, - attrs: { id: "account" }, - }, - "section" -); + attrs: { id: "account", class: "active" }, + }, + "section" + ); + } -export default AccountComponent; + render() { + return this.compile(tpl, this.props); + } +} diff --git a/src/pages/account/styles.scss b/src/pages/account/styles.scss index f3191817b..a2debd596 100644 --- a/src/pages/account/styles.scss +++ b/src/pages/account/styles.scss @@ -36,47 +36,6 @@ section#account { align-items: center; } - .image { - cursor: pointer; - position: relative; - margin-bottom: 20px; - border-radius: 50%; - overflow: hidden; - - .account-image { - width: 130px; - height: 130px; - object-fit: cover; - object-position: center; - } - - .change-image { - transition: 0.4s; - opacity: 0; - visibility: hidden; - width: 100%; - height: 100%; - position: absolute; - left: 0; - top: 0; - background-color: #000; - display: flex; - justify-content: center; - align-items: center; - text-align: center; - font-size: 13px; - color: #fff; - } - - &:hover { - .change-image { - transition: 0.4s; - opacity: 0.5; - visibility: visible; - } - } - } - .links { width: 100%; margin-top: 40px; diff --git a/src/pages/account/tpl.hbs b/src/pages/account/tpl.hbs index ff9e92419..9e383d7a8 100644 --- a/src/pages/account/tpl.hbs +++ b/src/pages/account/tpl.hbs @@ -1,13 +1,9 @@ {{{backBtn}}}
    - {{title}} {{{accountPhoto}}} -

    Иван

    {{{form}}} - + {{{links}}}
    {{{modals}}} diff --git a/src/pages/chat/index.ts b/src/pages/chat/index.ts index 425ded495..36e90173b 100644 --- a/src/pages/chat/index.ts +++ b/src/pages/chat/index.ts @@ -1,181 +1,7 @@ -import tpl from "./tpl.hbs?raw"; -import * as REGEX from "../../constants/constants"; -import Block from "../../services/Block"; -import Message from "../../components/Message"; -import MessagePreview from "../../components/MessagePreview"; -import Modal from "../../components/Modal"; -import Title from "../../components/Title"; -import Form from "../../components/Form"; -import Input from "../../components/Input"; -import Button from "../../components/Button"; -import Unit from "../../components/Unit"; -import Dropdown from "../../components/Dropdown"; -import Image from "../../components/Image"; -import validateInput from "../../utils/validateInput"; -import AccountComponent from "../account"; -import mockData from "../../mockData.json"; -import "./styles.scss"; +import IndexPage from "./indexPage"; +import { Connect } from "../../services/Store"; -class Chat extends Block { - render() { - return this.compile(tpl, this.props); - } -} - -const modalAddUser = new Modal({ - content: [ - new Title({ text: "Добавить пользователя" }, "h3"), - new Form({ - fields: [ - new Input({ - type: "text", - name: "login", - isRequired: true, - text: "Логин", - onChange: (e: Event) => { - validateInput( - (val) => Boolean(val?.match(REGEX.LOGIN_REGEX)), - "Неверный логин", - e.target as HTMLInputElement - ); - }, - }), - ], - button: new Button({ - text: "Добавить", - }), - }), - ], -}); - -const modalRemoveUser = new Modal({ - content: [ - new Title({ text: "Удалить пользователя" }, "h3"), - new Form({ - fields: [ - new Input({ - type: "text", - name: "login", - isRequired: true, - text: "Логин", - onChange: (e: Event) => { - validateInput( - (val) => Boolean(val?.match(REGEX.LOGIN_REGEX)), - "Неверный логин", - e.target as HTMLInputElement - ); - }, - }), - ], - button: new Button({ - text: "Удалить", - }), - }), - ], +const Page = Connect(IndexPage, (state) => { + return state; }); - -const ChatPage = new Chat({ - accountBtn: new Unit({ - content: "Профиль", - attrs: { - class: "profile", - }, - events: { - click: () => { - AccountComponent.setProps({ - attrs: { - class: "active", - }, - }); - }, - }, - }), - account: AccountComponent, - chatList: mockData.chat.chatList.map( - (item) => - new MessagePreview({ - ...item, - date: /\d\d:\d\d/.exec(item.date)?.[0] ?? "", - }) - ), - userActions: new Dropdown({ - icon: "Добавить/Удалить пользователя", - dropdown: new Unit( - { - content: [ - new Unit( - { - content: [ - new Image({ - attrs: { - src: "img/add.svg", - alt: "Добавить пользователя", - }, - }), - new Unit( - { content: "Добавить пользователя" }, - "span" - ), - ], - events: { - click: () => { - modalAddUser.show(); - }, - }, - }, - "li" - ), - new Unit( - { - content: [ - new Image({ - attrs: { - src: "img/remove.svg", - alt: "Удалить пользователя", - }, - }), - new Unit( - { content: "Удалить пользователя" }, - "span" - ), - ], - events: { - click: () => { - modalRemoveUser.show(); - }, - }, - }, - "li" - ), - ], - }, - "ul" - ), - }), - currentChat: mockData.chat.currentChat.map( - (item) => - new Message({ - ...item, - date: /\d\d:\d\d/.exec(item.date)?.[0] ?? "", - }) - ), - modals: [modalAddUser, modalRemoveUser], - textMessage: new Unit( - { - attrs: { - name: "message", - placeholder: "Сообщение", - }, - events: { - input: (e) => { - const target = e.target as HTMLElement; - target.style.height = "5px"; - target.style.height = target.scrollHeight + "px"; - }, - }, - }, - "textarea" - ), -}); - -export default ChatPage; +export default Page; diff --git a/src/pages/chat/styles.scss b/src/pages/chat/styles.scss index bddad1724..ebfca6d67 100644 --- a/src/pages/chat/styles.scss +++ b/src/pages/chat/styles.scss @@ -90,6 +90,9 @@ section#chat { .head { text-align: right; + display: flex; + flex-wrap: wrap; + justify-content: space-between; .profile { transition: 0.2s; @@ -117,6 +120,7 @@ section#chat { line-height: 32px; border-radius: 5px; background-color: $bg-color; + width: 100%; span { position: absolute; @@ -165,6 +169,14 @@ section#chat { height: 47px; border-radius: 50%; margin-right: 10px; + overflow: hidden; + + img { + object-position: center; + object-fit: cover; + width: 100%; + height: 100%; + } &.no-image { background-image: url("../../assets/img/noImage.svg"); @@ -282,6 +294,7 @@ section#chat { height: 34px; border-radius: 50%; margin-right: 10px; + overflow: hidden; &.no-image { background-image: url("../../assets/img/noImage.svg"); @@ -303,70 +316,11 @@ section#chat { } .date { + text-align: center; + width: 100%; color: $light-text-color; - margin: 0 auto; margin-bottom: 30px; } - - .message { - position: relative; - margin-bottom: 10px; - padding: 10px 10px 20px 20px; - max-width: 410px; - border-radius: 12px; - - .content { - display: flex; - flex-flow: column wrap; - } - - p { - margin-bottom: 20px; - - &:last-of-type { - margin-bottom: 0; - } - } - - .time { - position: absolute; - right: 10px; - bottom: 10px; - font-size: 9px; - } - - &.current-user { - background-color: $light-primary-color; - padding-right: 60px; - margin-left: auto; - - .time { - display: flex; - color: $primary-color; - } - - &.checked { - .time::before { - content: url("../../assets/img/checked.svg"); - margin-right: 4px; - } - } - } - - &:has(.content img) { - padding: 0; - border: 1px solid $bg-color; - - .time { - height: 13px; - line-height: 13px; - border-radius: 30px; - padding: 0 7px; - color: #fff; - background: #52525280; - } - } - } } .footer { diff --git a/src/pages/chat/tpl.hbs b/src/pages/chat/tpl.hbs index 12a83a044..6a20b103e 100644 --- a/src/pages/chat/tpl.hbs +++ b/src/pages/chat/tpl.hbs @@ -1,103 +1,22 @@ -
    -
    -{{{account}}} - -{{{modals}}} -{{!-- - --}} +
    + {{!-- TODO: Add when api will be ready --}} + + {{{chat}}} +
    +{{{modals}}} diff --git a/src/pages/error/index.ts b/src/pages/error/index.ts index 513fe8644..e0a562cc7 100644 --- a/src/pages/error/index.ts +++ b/src/pages/error/index.ts @@ -1,6 +1,7 @@ import Block from "../../services/Block"; import tpl from "./tpl.hbs?raw"; import "./styles.scss"; + export default class Error extends Block { render() { return this.compile(tpl, this.props); diff --git a/src/pages/error/tpl.hbs b/src/pages/error/tpl.hbs index 2caa657ec..71f859bbd 100644 --- a/src/pages/error/tpl.hbs +++ b/src/pages/error/tpl.hbs @@ -2,6 +2,7 @@

    {{errorCode}}

    {{justification}}

    - Назад к чатам +

    {{email}}

    + Назад к чатам
    - \ No newline at end of file + diff --git a/src/pages/login/index.ts b/src/pages/login/index.ts index 56c924c0e..278ce49f0 100644 --- a/src/pages/login/index.ts +++ b/src/pages/login/index.ts @@ -8,6 +8,8 @@ import Input from "../../components/Input"; import Link from "../../components/Link"; import Title from "../../components/Title"; import "./styles.scss"; +import { SignInData, signIn } from "../../api/auth"; +import router from "../../services/Router/Router"; const mockCredentials = { login: "login", @@ -60,68 +62,88 @@ const fields = [ ]; class Login extends Block { - render() { - return this.compile(tpl, this.props); - } -} + constructor() { + super( + { + form: new Form({ + title: new Title({ text: "Вход" }, "h1"), + fields: fields, + button: new Button({ text: "Вход" }), + footer: new Link( + { + text: "Нет аккаунта?", + attrs: { href: "/sign-up" }, + }, + "a" + ), + events: { + submit: async (e: Event) => { + e.preventDefault(); -const LoginPage = new Login( - { - form: new Form({ - title: new Title({ text: "Вход" }, "h1"), - fields: fields, - button: new Button({ text: "Вход" }), - footer: new Link( - { - text: "Нет аккаунта?", - attrs: { href: "/registration" }, - }, - "a" - ), - events: { - submit: (e: Event) => { - e.preventDefault(); + const isFormValid = + [ + validateInput( + (val) => + Boolean( + val?.match(REGEX.LOGIN_REGEX) + ), + "Неверный логин", + ( + e.target as HTMLElement + )?.querySelector( + `[name="login"]` + ) as HTMLInputElement + ), + validateInput( + (val) => + Boolean( + val?.match(REGEX.PASSWORD_REGEX) + ), + "Неверный пароль", + ( + e.target as HTMLElement + )?.querySelector( + `[name="password"]` + ) as HTMLInputElement + ), + ].filter((val) => !val).length === 0; - const isFormValid = - [ - validateInput( - (val) => Boolean(val?.match(REGEX.LOGIN_REGEX)), - "Неверный логин", - (e.target as HTMLElement)?.querySelector( - `[name="login"]` - ) as HTMLInputElement - ), - validateInput( - (val) => - Boolean(val?.match(REGEX.PASSWORD_REGEX)), - "Неверный пароль", - (e.target as HTMLElement)?.querySelector( - `[name="password"]` - ) as HTMLInputElement - ), - ].filter((val) => !val).length === 0; + if (isFormValid) { + const formData = new FormData( + e?.target as HTMLFormElement + ); - if (isFormValid) { - const formData = new FormData( - e.target as HTMLFormElement - ); + const data = ["login", "password"].reduce( + (acc, curr) => ({ + ...acc, + [curr]: formData.get(curr) as string, + }), + {} as SignInData + ); - const data: { [key: string]: FormDataEntryValue } = {}; - formData.forEach((val, key) => { - data[key] = val; - }); - } + try { + await signIn(data); + router.go("/messenger"); + } catch (e) { + console.error(e); + } + } + }, + }, + attrs: { + class: "form", + }, + }), + attrs: { + id: "login", }, }, - attrs: { - class: "form", - }, - }), - attrs: { - id: "login", - }, - }, - "section" -); + "section" + ); + } + render() { + return this.compile(tpl, this.props); + } +} -export default LoginPage; +export default Login; diff --git a/src/pages/login/styles.scss b/src/pages/login/styles.scss index 088ecfdbf..87a331a32 100644 --- a/src/pages/login/styles.scss +++ b/src/pages/login/styles.scss @@ -1,7 +1,10 @@ section#login { - min-height: 100vh; - padding: 30px 0; - display: flex; - flex-wrap: wrap; - place-content: center center; + .container { + min-height: 100vh; + display: flex; + flex-wrap: wrap; + place-content: center center; + padding-top: 30px; + padding-bottom: 30px; + } } diff --git a/src/pages/login/tpl.hbs b/src/pages/login/tpl.hbs index c8dbbbef1..a5ee757f5 100644 --- a/src/pages/login/tpl.hbs +++ b/src/pages/login/tpl.hbs @@ -1 +1,3 @@ -{{{form}}} +
    + {{{form}}} +
    diff --git a/src/pages/registration/index.ts b/src/pages/registration/index.ts index 86653dacd..dbdbe4943 100644 --- a/src/pages/registration/index.ts +++ b/src/pages/registration/index.ts @@ -7,12 +7,9 @@ import Form from "../../components/Form"; import Input from "../../components/Input"; import Title from "../../components/Title"; import "./styles.scss"; - -class Registration extends Block { - render() { - return this.compile(tpl); - } -} +import { SignUpData, signUp } from "../../api/auth"; +import router from "../../services/Router/Router"; +import Link from "../../components/Link"; const fields = [ new Input( @@ -21,11 +18,11 @@ const fields = [ name: "email", isRequired: true, text: "Почта", - onChange: (e: Event) => { + onChange: (target: EventTarget) => { validateInput( (val) => Boolean(val?.match(REGEX.EMAIL_REGEX)), "Неверная почта", - e.target as HTMLInputElement + target as HTMLInputElement ); }, attrs: { @@ -40,11 +37,11 @@ const fields = [ name: "login", isRequired: true, text: "Логин", - onChange: (e: Event) => { + onChange: (target: EventTarget) => { validateInput( (val) => Boolean(val?.match(REGEX.LOGIN_REGEX)), "Неверный логин", - e.target as HTMLInputElement + target as HTMLInputElement ); }, attrs: { @@ -59,11 +56,11 @@ const fields = [ name: "first_name", isRequired: true, text: "Имя", - onChange: (e: Event) => { + onChange: (target: EventTarget) => { validateInput( (val) => Boolean(val?.match(REGEX.NAME_REGEX)), "Неверное имя", - e.target as HTMLInputElement + target as HTMLInputElement ); }, @@ -79,11 +76,11 @@ const fields = [ name: "second_name", isRequired: true, text: "Фамилия", - onChange: (e: Event) => { + onChange: (target: EventTarget) => { validateInput( (val) => Boolean(val?.match(REGEX.NAME_REGEX)), "Неверная Фамилия", - e.target as HTMLInputElement + target as HTMLInputElement ); }, @@ -99,11 +96,11 @@ const fields = [ name: "phone", isRequired: true, text: "Телефон", - onChange: (e: Event) => { + onChange: (target: EventTarget) => { validateInput( (val) => Boolean(val?.match(REGEX.PHONE_REGEX)), "Неверный телефон", - e.target as HTMLInputElement + target as HTMLInputElement ); }, @@ -119,11 +116,11 @@ const fields = [ name: "password", isRequired: true, text: "Пароль", - onChange: (e: Event) => { + onChange: (target: EventTarget) => { validateInput( (val) => Boolean(val?.match(REGEX.PASSWORD_REGEX)), "Минимум 8 символов", - e.target as HTMLInputElement + target as HTMLInputElement ); }, @@ -139,7 +136,7 @@ const fields = [ name: "password_2", isRequired: true, text: "Пароль", - onChange: (e: Event) => { + onChange: (target: EventTarget) => { validateInput( (val) => val === @@ -147,7 +144,7 @@ const fields = [ 'input[name="password"]' )?.value, "Пароли не совпадают", - e.target as HTMLInputElement + target as HTMLInputElement ); }, @@ -159,104 +156,129 @@ const fields = [ ), ]; -const RegistrationPage = new Registration( - { - form: new Form({ - title: new Title({ text: "Регистрация" }, "h1"), - fields: fields, - button: new Button({ - text: "Регистрация", - }), - footer: 'Войти?', - events: { - submit: (e: Event) => { - e.preventDefault(); +const handleSubmit = async (e: Event) => { + e.preventDefault(); + + const isFormValid = + [ + validateInput( + (val) => Boolean(val?.match(REGEX.EMAIL_REGEX)), + "Неверная почта", + (e.target as HTMLElement)?.querySelector( + `[name="email"]` + ) as HTMLInputElement + ), + validateInput( + (val) => Boolean(val?.match(REGEX.LOGIN_REGEX)), + "Неверный логин", + (e.target as HTMLElement)?.querySelector( + `[name="login"]` + ) as HTMLInputElement + ), + validateInput( + (val) => Boolean(val?.match(REGEX.NAME_REGEX)), + "Неверное имя", + (e.target as HTMLElement)?.querySelector( + `[name="first_name"]` + ) as HTMLInputElement + ), + validateInput( + (val) => Boolean(val?.match(REGEX.NAME_REGEX)), + "Неверная Фамилия", + (e.target as HTMLElement)?.querySelector( + `[name="second_name"]` + ) as HTMLInputElement + ), + validateInput( + (val) => Boolean(val?.match(REGEX.PHONE_REGEX)), + "Неверный телефон", + (e.target as HTMLElement)?.querySelector( + `[name="phone"]` + ) as HTMLInputElement + ), + validateInput( + (val) => Boolean(val?.match(REGEX.PASSWORD_REGEX)), + "Минимум 8 символов", + (e.target as HTMLElement)?.querySelector( + `[name="password"]` + ) as HTMLInputElement + ), + validateInput( + (val) => + val === + ( + (e.target as HTMLElement)?.querySelector( + `[name="password"]` + ) as HTMLInputElement + )?.value, + "Пароли не совпадают", + (e.target as HTMLElement)?.querySelector( + `[name="password_2"]` + ) as HTMLInputElement + ), + ].filter((val) => !val).length === 0; - const isFormValid = - [ - validateInput( - (val) => Boolean(val?.match(REGEX.EMAIL_REGEX)), - "Неверная почта", - (e.target as HTMLElement)?.querySelector( - `[name="email"]` - ) as HTMLInputElement - ), - validateInput( - (val) => Boolean(val?.match(REGEX.LOGIN_REGEX)), - "Неверный логин", - (e.target as HTMLElement)?.querySelector( - `[name="login"]` - ) as HTMLInputElement - ), - validateInput( - (val) => Boolean(val?.match(REGEX.NAME_REGEX)), - "Неверное имя", - (e.target as HTMLElement)?.querySelector( - `[name="first_name"]` - ) as HTMLInputElement - ), - validateInput( - (val) => Boolean(val?.match(REGEX.NAME_REGEX)), - "Неверная Фамилия", - (e.target as HTMLElement)?.querySelector( - `[name="second_name"]` - ) as HTMLInputElement - ), - validateInput( - (val) => Boolean(val?.match(REGEX.PHONE_REGEX)), - "Неверный телефон", - (e.target as HTMLElement)?.querySelector( - `[name="phone"]` - ) as HTMLInputElement - ), - validateInput( - (val) => - Boolean(val?.match(REGEX.PASSWORD_REGEX)), - "Минимум 8 символов", - (e.target as HTMLElement)?.querySelector( - `[name="password"]` - ) as HTMLInputElement - ), - validateInput( - (val) => - val === - ( - ( - e.target as HTMLElement - )?.querySelector( - `[name="password"]` - ) as HTMLInputElement - )?.value, - "Пароли не совпадают", - (e.target as HTMLElement)?.querySelector( - `[name="password_2"]` - ) as HTMLInputElement - ), - ].filter((val) => !val).length === 0; + if (isFormValid) { + const formData = new FormData(e?.target as HTMLFormElement); + + const data = [ + "first_name", + "second_name", + "login", + "email", + "phone", + "password", + ].reduce( + (acc, curr) => ({ + ...acc, + [curr]: formData.get(curr) as string, + }), + {} as SignUpData + ); - if (isFormValid) { - alert("Вы успешно зарегестрированы!"); - const formData = new FormData( - e?.target as HTMLFormElement - ); + try { + await signUp(data); + router.go("/messenger"); + } catch (e) { + console.error(e); + } + } +}; - const data: { [key: string]: FormDataEntryValue } = {}; - formData.forEach((val, key) => { - data[key] = val; - }); - console.log(data); - } +class Registration extends Block { + constructor() { + super( + { + form: new Form({ + title: new Title({ text: "Регистрация" }, "h1"), + fields: fields, + button: new Button({ + text: "Регистрация", + }), + footer: new Link( + { + text: "Войти?", + attrs: { href: "/" }, + }, + "a" + ), + events: { + submit: handleSubmit, + }, + attrs: { + class: "form", + }, + }), + attrs: { + id: "registration", }, }, - attrs: { - class: "form", - }, - }), - attrs: { - id: "registration", - }, - }, - "section" -); + "section" + ); + } + render() { + return this.compile(tpl); + } +} -export default RegistrationPage; +export default Registration; diff --git a/src/pages/registration/styles.scss b/src/pages/registration/styles.scss index de911ab86..6e560c688 100644 --- a/src/pages/registration/styles.scss +++ b/src/pages/registration/styles.scss @@ -1,7 +1,10 @@ section#registration { - min-height: 100vh; - padding: 30px 0; - display: flex; - flex-wrap: wrap; - place-content: center center; + .container { + min-height: 100vh; + display: flex; + flex-wrap: wrap; + place-content: center center; + padding-top: 30px; + padding-bottom: 30px; + } } diff --git a/src/pages/registration/tpl.hbs b/src/pages/registration/tpl.hbs index c8dbbbef1..b9b5db5c8 100644 --- a/src/pages/registration/tpl.hbs +++ b/src/pages/registration/tpl.hbs @@ -1 +1 @@ -{{{form}}} +
    {{{form}}}
    diff --git a/src/services/Block.ts b/src/services/Block.ts index 6d1b4dbfb..15c87bbb6 100644 --- a/src/services/Block.ts +++ b/src/services/Block.ts @@ -64,14 +64,14 @@ export default class Block { return this._element; } - _registerEvents(eventBus: EventBus) { + private _registerEvents(eventBus: EventBus) { eventBus.on(Block.EVENTS.INIT, this.init.bind(this)); eventBus.on(Block.EVENTS.FLOW_CDM, this._componentDidMount.bind(this)); eventBus.on(Block.EVENTS.FLOW_RENDER, this._render.bind(this)); eventBus.on(Block.EVENTS.FLOW_CDU, this._componentDidUpdate.bind(this)); } - _getChildren(propsAndChildren: ObjectT) { + private _getChildren(propsAndChildren: ObjectT) { const props: ObjectT = {}; const children: Record = {}; const lists: ObjectT = {}; @@ -79,7 +79,11 @@ export default class Block { Object.entries(propsAndChildren).forEach(([key, value]) => { if (value instanceof Block) { children[key] = value; - } else if (Array.isArray(value)) { + } else if ( + Array.isArray(value) && + value.find((val) => val instanceof Block) + ) { + // если массив пустой то он пойдет в пропсы, по этому надо оборачивать в Unit пустой список компонентов lists[key] = value; } else { props[key] = value; @@ -101,7 +105,7 @@ export default class Block { propsAndStubs[key] = `
    `; }); - const fragment = this._createDocumentElement( + const fragment = this.createDocumentElement( "template" ) as HTMLTemplateElement; @@ -122,7 +126,7 @@ export default class Block { return; } - const listContent = this._createDocumentElement( + const listContent = this.createDocumentElement( "template" ) as HTMLTemplateElement; item.forEach((item) => { @@ -144,17 +148,29 @@ export default class Block { } } - _createResources() { - const el = this._createDocumentElement(this.tagName); + private _createResources() { + const el = this.createDocumentElement(this.tagName); this._element = el; } init() { this._createResources(); this.eventBus.emit(Block.EVENTS.FLOW_RENDER); + + setTimeout(() => { + this.eventBus.emit(Block.EVENTS.FLOW_CDM); + }); } - _componentDidMount() { + refresh() { + this.eventBus.emit(Block.EVENTS.FLOW_RENDER); + + setTimeout(() => { + this.eventBus.emit(Block.EVENTS.FLOW_CDM); + }); + } + + private _componentDidMount() { this.componentDidMount(); Object.values(this.children).forEach((child: Block) => { @@ -162,21 +178,15 @@ export default class Block { }); } - // Может переопределять пользователь, необязательно трогать componentDidMount() {} - dispatchComponentDidMount() { + private dispatchComponentDidMount() { this.eventBus.emit(Block.EVENTS.FLOW_CDM); } - _componentDidUpdate(oldProps: BlockProps, newProps: BlockProps) { - const response = this.componentDidUpdate(oldProps, newProps); - return response; - } - - // Может переопределять пользователь, необязательно трогать - componentDidUpdate(oldProps: BlockProps, newProps: BlockProps) { + private _componentDidUpdate(oldProps: BlockProps, newProps: BlockProps) { if (oldProps !== newProps) { + this.componentDidUpdate(); this.eventBus.emit(Block.EVENTS.FLOW_RENDER); return true; } @@ -184,6 +194,9 @@ export default class Block { return false; } + // Может переопределять пользователь, необязательно трогать + componentDidUpdate() {} + setProps = (newProps: BlockProps) => { if (!newProps) { return; @@ -201,7 +214,7 @@ export default class Block { } }; - _render() { + private _render() { const block = this.render(); this._removeEvents(); @@ -215,10 +228,6 @@ export default class Block { this.setAttributes(); this._addEvents(); - - setTimeout(() => { - this.dispatchComponentDidMount(); - }); } // Может переопределять пользователь, необязательно трогать @@ -228,7 +237,7 @@ export default class Block { removeEvents() {} - _removeEvents() { + private _removeEvents() { const { events } = this.props; if (events) { Object.keys(events).forEach((eventName) => { @@ -239,7 +248,7 @@ export default class Block { addEvents() {} - _addEvents() { + private _addEvents() { const { events = {} } = this.props; if (events) { Object.keys(events).forEach((eventName) => { @@ -250,7 +259,7 @@ export default class Block { this.addEvents(); } - _makeProxy(props: ObjectT) { + private _makeProxy(props: ObjectT) { const handleEventBus = (oldValue: ObjectT, newValue: ObjectT) => { this.eventBus.emit(Block.EVENTS.FLOW_CDU, [oldValue, newValue]); }; @@ -274,11 +283,19 @@ export default class Block { return proxy; } - _createDocumentElement(tagName?: keyof HTMLElementTagNameMap): HTMLElement { + createDocumentElement(tagName?: keyof HTMLElementTagNameMap): HTMLElement { return document.createElement(tagName ?? "div"); } getContent() { return this.element; } + + show() { + this.element.style.display = "block"; + } + + hide() { + this.element.style.display = "none"; + } } diff --git a/src/utils/HTTPTransport.ts b/src/utils/HTTPTransport.ts index 122bdd4c8..3a7996599 100644 --- a/src/utils/HTTPTransport.ts +++ b/src/utils/HTTPTransport.ts @@ -1,3 +1,5 @@ +import queryString from "../helpers/queryStringify"; + enum METHODS { GET = "GET", POST = "POST", @@ -5,101 +7,81 @@ enum METHODS { DELETE = "DELETE", } -type Options = { method: METHODS; data?: Record }; - -const convertToParamsUrl = (data: Record = {}) => { - return Object.keys(data).reduce((acc, key, index) => { - return acc + (index === 0 ? "?" : "&") + `${key}=${data[key]}`; - }, ""); +type Options = { + method: METHODS; + data?: Record | FormData; + withCredentials?: boolean; + timeout?: number; }; -function queryStringify(data?: Record) { - return data ? JSON.stringify(data) : ""; -} +export default class HTTPTransport { + baseUrl?: string; -type HTTPMethod = ( - url: string, - options: Options, - timeout?: number -) => Promise; + constructor(baseUrl?: string) { + this.baseUrl = baseUrl; + } -export default class HTTPTransport { - request: HTTPMethod = (url, options, timeout = 2000) => { - const { method, data } = options; + private request = (url: string, options: Options): Promise => { + const { + method, + data, + withCredentials = true, + timeout = 60000, + } = options; return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); + xhr.timeout = timeout; + let finalUrl = this.baseUrl + url; if (method === METHODS.GET && data) { - xhr.open(method, url + convertToParamsUrl(data)); - } else { - xhr.open(method, url); + finalUrl += queryString(data as Record); } + xhr.open(method, finalUrl); xhr.onload = function () { - resolve(xhr); + const status = xhr.status || 0; + if (status >= 200 && status < 300) { + resolve( + xhr.response === "OK" + ? xhr.response + : JSON.parse(xhr.response) + ); + } else { + reject(JSON.parse(xhr.response)); + } }; xhr.onabort = reject; xhr.onerror = reject; xhr.ontimeout = reject; - if (method === METHODS.GET && data) { + xhr.withCredentials = withCredentials; + + if (method === METHODS.GET || !data) { xhr.send(); + } else if (data instanceof FormData) { + xhr.send(data); } else { - xhr.send(queryStringify(data)); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.send(JSON.stringify(data)); } }); }; - get: HTTPMethod = (url, options, timeout) => { - return this.request(url, { ...options, method: METHODS.GET }, timeout); - }; - post: HTTPMethod = (url, options, timeout) => { - return this.request(url, { ...options, method: METHODS.POST }, timeout); + get = (url: string, options?: Omit): Promise => { + return this.request(url, { ...options, method: METHODS.GET }); }; - put: HTTPMethod = (url, options, timeout) => { - return this.request(url, { ...options, method: METHODS.PUT }, timeout); + post = (url: string, options?: Omit): Promise => { + return this.request(url, { ...options, method: METHODS.POST }); }; - delete: HTTPMethod = (url, options, timeout) => { - return this.request( - url, - { ...options, method: METHODS.DELETE }, - timeout - ); + put = (url: string, options?: Omit): Promise => { + return this.request(url, { ...options, method: METHODS.PUT }); }; -} - -export function fetchWithRetry( - url: string, - options: Options & { retries: number }, - timeout: number -) { - const { method, data, retries = 2 } = options; - const fetchInstance = new HTTPTransport(); - let i = retries; - - const fetchLoop = () => { - return fetchInstance - .get(url, { method, data }, timeout) - .then((res) => { - return Promise.resolve(res); - }) - .catch((e) => { - i = -1; - if (i === 0) { - fetchLoop(); - } else { - throw new Error(e); - } - }); - - // return response; + delete = ( + url: string, + options?: Omit + ): Promise => { + return this.request(url, { ...options, method: METHODS.DELETE }); }; - try { - const res = fetchLoop(); - return res; - } catch (e) { - return e; - } } diff --git a/src/utils/validateInput.ts b/src/utils/validateInput.ts index f7b9d78f9..c868fd361 100644 --- a/src/utils/validateInput.ts +++ b/src/utils/validateInput.ts @@ -15,9 +15,9 @@ const validateInput = ( inputWrap?.appendChild(errorElem); } - if (validateFc(input?.value)) { + if (input?.value && validateFc(input?.value)) { inputWrap?.classList.remove(VISIBLE_ERROR_CLASSNAME); - return true; + return input.value; } else { inputWrap?.classList.add(VISIBLE_ERROR_CLASSNAME); return false; diff --git a/tsconfig.json b/tsconfig.json index 710c22a28..c6ea6d473 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "noImplicitAny": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, }, "include": ["src"] } diff --git a/vite.config.js b/vite.config.js index 6300cfe94..1d950f8ec 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,6 +1,7 @@ import { defineConfig } from "vite"; import eslint from "vite-plugin-eslint"; import stylelint from "vite-plugin-stylelint"; +import autoprefixer from "autoprefixer"; export default defineConfig({ root: "src", @@ -20,6 +21,7 @@ export default defineConfig({ stylelint({ fix: true, }), + autoprefixer({}), ], css: { preprocessorOptions: {