diff --git a/.eslintrc.json b/.eslintrc.json index 9061692da..165b37538 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,6 +15,14 @@ "no-console": 0, "import/extensions": 0, "quotes": 0, - "semi": 0 + "semi": 0, + "no-underscore-dangle": 0, + "no-tabs": 0, + "lines-between-class-members": 0, + "class-methods-use-this": 0, + "no-undef": 0, + "no-param-reassign": 0, + "import/prefer-default-export": 0, + "import/no-extraneous-dependencies": 0 } } diff --git a/.stylelintrc.json b/.stylelintrc.json index eff256099..5d5ff9fcf 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,3 +1,4 @@ { - "extends": "stylelint-config-standard-scss" + "extends": "stylelint-config-standard-scss", + "ignoreFiles": ["build/*", "node_modules/*", "static/*"] } diff --git a/_proxyProps.js b/_proxyProps.js deleted file mode 100644 index d28d8bbac..000000000 --- a/_proxyProps.js +++ /dev/null @@ -1,68 +0,0 @@ -const props = { - name: 'Abby', - chat: 'the last of us. Part II', - getChat() { - this._privateMethod(); - }, - _privateMethod() { - console.log(this._privateProp); - }, - __privateMethodToo() {}, - _privateProp: 'Нельзя получить просто так', -}; - -const proxyProps = new Proxy(props, { - get(o, name) { - if (name.search('_') !== -1) { - // console.log('Нет прав'); - // return false; - } - - return o[name]; - // return o[name]; - }, - set(o, name, newValue) { - if (name.search('_') !== -1) { - throw new Error('Нет прав'); - } - - o[name] = newValue; - - return true; - }, - deleteProperty(o, name) { - if (name.search('_') !== -1) { - throw new Error('Нет прав'); - } - - return true; - }, -}); - -// proxyProps.getChat(); -// delete proxyProps.chat; - -// proxyProps.newProp = 2; -// console.log(proxyProps.newProp); - -try { - proxyProps._newPrivateProp = 'Super game'; -} catch (error) { - console.log(error); -} - -console.log(props); - -// try { -// delete proxyProps._privateProp; -// } catch (error) { -// console.log(error); // Error: Нет прав -// } - -/* - * Вывод в консоль следующий: -Нельзя получить просто так -2 -Error: Нет прав -Error: Нет прав -*/ diff --git a/package-lock.json b/package-lock.json index 2b92ab99c..5a0c0ac7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,13 @@ "name": "messenger", "version": "0.0.0", "dependencies": { - "pug": "^3.0.2" + "pug": "^3.0.2", + "uuid": "^9.0.1" }, "devDependencies": { "@types/express": "^4.17.17", "@types/pug": "^2.0.6", + "@types/uuid": "^9.0.4", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", "concurrently": "^8.2.1", @@ -22,7 +24,6 @@ "sass": "^1.66.1", "stylelint": "^15.10.3", "stylelint-config-standard-scss": "^11.0.0", - "uuid": "^9.0.1", "vite": "^4.4.5" }, "engines": { @@ -1069,6 +1070,12 @@ "@types/node": "*" } }, + "node_modules/@types/uuid": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz", + "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.0.tgz", @@ -6448,7 +6455,6 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -7409,6 +7415,12 @@ "@types/node": "*" } }, + "@types/uuid": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz", + "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.0.tgz", @@ -11360,8 +11372,7 @@ "uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, "validate-npm-package-license": { "version": "3.0.4", diff --git a/package.json b/package.json index 93d1234a9..3b0406ca9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "devDependencies": { "@types/express": "^4.17.17", "@types/pug": "^2.0.6", + "@types/uuid": "^9.0.4", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", "concurrently": "^8.2.1", @@ -25,10 +26,10 @@ "sass": "^1.66.1", "stylelint": "^15.10.3", "stylelint-config-standard-scss": "^11.0.0", - "uuid": "^9.0.1", "vite": "^4.4.5" }, "dependencies": { - "pug": "^3.0.2" + "pug": "^3.0.2", + "uuid": "^9.0.1" } } diff --git a/src/404/404.ts b/src/404/404.ts new file mode 100644 index 000000000..2433ffdea --- /dev/null +++ b/src/404/404.ts @@ -0,0 +1,15 @@ +import { render } from "../utils/index"; +import { ErrorPage } from "../pages/index"; + +import "../scss/index.scss"; + +const props = { + title: '404', + text: 'who seeks will always find', + linkText: 'go to chats page', + linkHref: '/chats-and-chat/', +} + +const page = new ErrorPage(props); + +render(page); diff --git a/src/404/index.html b/src/404/index.html new file mode 100644 index 000000000..cb37bcf87 --- /dev/null +++ b/src/404/index.html @@ -0,0 +1,12 @@ + + + + + + 404 page + + +
+ + + diff --git a/src/500/500.ts b/src/500/500.ts new file mode 100644 index 000000000..4b36001e8 --- /dev/null +++ b/src/500/500.ts @@ -0,0 +1,15 @@ +import { render } from "../utils/index"; +import { ErrorPage } from "../pages/index"; + +import "../scss/index.scss"; + +const props = { + title: '500', + text: 'something went wrong', + linkText: 'go to chats page', + linkHref: '/chats-and-chat/', +} + +const page = new ErrorPage(props); + +render(page); diff --git a/src/500/index.html b/src/500/index.html new file mode 100644 index 000000000..96bda3a4c --- /dev/null +++ b/src/500/index.html @@ -0,0 +1,12 @@ + + + + + + 500 page + + +
+ + + diff --git a/src/chats-and-chat/chats-and-chat.ts b/src/chats-and-chat/chats-and-chat.ts new file mode 100644 index 000000000..2db5caf47 --- /dev/null +++ b/src/chats-and-chat/chats-and-chat.ts @@ -0,0 +1,8 @@ +import { render } from "../utils/index"; +import { ChatsAndChat } from "../pages/index"; + +import "../scss/index.scss"; + +const page = new ChatsAndChat(); + +render(page); diff --git a/src/chats-and-chat/index.html b/src/chats-and-chat/index.html new file mode 100644 index 000000000..9955f2bb3 --- /dev/null +++ b/src/chats-and-chat/index.html @@ -0,0 +1,12 @@ + + + + + + Chats and chat page + + +
+ + + diff --git a/src/components/AuthorizationForm/AuthorizationForm.tmp.pug b/src/components/AuthorizationForm/AuthorizationForm.tmp.pug new file mode 100644 index 000000000..034d78a4b --- /dev/null +++ b/src/components/AuthorizationForm/AuthorizationForm.tmp.pug @@ -0,0 +1,7 @@ +form.form + if (title) + h1.form__title= title + .form__field!= fieldLogin + .form__field!= fieldPassword + .form__field.form__field--accent!= sendBtn + .form__field.t-right!= link diff --git a/src/components/AuthorizationForm/AuthorizationForm.ts b/src/components/AuthorizationForm/AuthorizationForm.ts new file mode 100644 index 000000000..346ae2484 --- /dev/null +++ b/src/components/AuthorizationForm/AuthorizationForm.ts @@ -0,0 +1,20 @@ +import { Block } from "../../core/index"; +import template from "./AuthorizationForm.tmp.pug"; +import { getValuesFromForm } from "../utils/index"; + +export default class AuthorizationForm extends Block { + constructor(props?: object) { + const newProps = { + ...props, + events: new Map([ + ['submit', (event: object) => getValuesFromForm(event, this)], + ]), + } + + super('div', newProps); + } + + render() { + return this.compile(template, this.props); + } +} diff --git a/src/components/Button/Button.tmp.pug b/src/components/Button/Button.tmp.pug index bd2c8044a..05c97b708 100644 --- a/src/components/Button/Button.tmp.pug +++ b/src/components/Button/Button.tmp.pug @@ -1 +1 @@ -button.btn text +button(class=className, type=type, form=form)= text diff --git a/src/components/Button/Button.ts b/src/components/Button/Button.ts index c3ad80a4a..a4c786a57 100644 --- a/src/components/Button/Button.ts +++ b/src/components/Button/Button.ts @@ -1,9 +1,11 @@ import { Block } from "../../core/index"; import template from "./Button.tmp.pug"; +import "./Button.scss"; + export default class Button extends Block { - constructor(props) { - super('button', props); + constructor(props?: object) { + super('div', props); } render() { diff --git a/src/components/EditingPasswordForm/EditingPasswordForm.tmp.pug b/src/components/EditingPasswordForm/EditingPasswordForm.tmp.pug new file mode 100644 index 000000000..c99ac1ada --- /dev/null +++ b/src/components/EditingPasswordForm/EditingPasswordForm.tmp.pug @@ -0,0 +1,5 @@ +form#userSettingsForm.user-settings__form + ul.user-settings__list + li.user-settings__list-item!= currentPassword + li.user-settings__list-item!= newPassword + li.user-settings__list-item!= repeatNewPassword diff --git a/src/components/EditingPasswordForm/EditingPasswordForm.ts b/src/components/EditingPasswordForm/EditingPasswordForm.ts new file mode 100644 index 000000000..facbb6277 --- /dev/null +++ b/src/components/EditingPasswordForm/EditingPasswordForm.ts @@ -0,0 +1,20 @@ +import { Block } from "../../core/index"; +import template from "./EditingPasswordForm.tmp.pug"; +import { getValuesFromForm } from "../utils/index"; + +export default class EditingPasswordForm extends Block { + constructor(props?: object) { + const newProps = { + ...props, + events: new Map([ + ['submit', (event: object) => getValuesFromForm(event, this)], + ]), + } + + super('div', newProps); + } + + render() { + return this.compile(template, this.props); + } +} diff --git a/src/components/EditingSettingsForm/EditingSettingsForm.tmp.pug b/src/components/EditingSettingsForm/EditingSettingsForm.tmp.pug new file mode 100644 index 000000000..63528fb32 --- /dev/null +++ b/src/components/EditingSettingsForm/EditingSettingsForm.tmp.pug @@ -0,0 +1,25 @@ +form#userSettingsForm.user-settings__form + ul.user-settings__list + li.user-settings__list-item!= nickname + li.user-settings__list-item!= firstName + li.user-settings__list-item!= secondName + li.user-settings__list-item!= email + li.user-settings__list-item!= phone + + //ul.user-settings__list + li.user-settings__list-item + label.user-settings__list-item-name(for="display_name") Nickname: + input.user-settings__list-item-value.t-right(id="display_name" type="text" name="display_name" value="user name") + li.user-settings__list-item + label.user-settings__list-item-name(for="first_name") First name: + input.user-settings__list-item-value.t-right(id="first_name" type="text" name="first_name" value="first name") + li.user-settings__list-item + label.user-settings__list-item-name(for="second_name") Second name: + input.user-settings__list-item-value.t-right(id="second_name" type="text" name="second_name" value="second name") + li.user-settings__list-item + label.user-settings__list-item-name(for="email") Email: + input.user-settings__list-item-value.t-right(id="email" type="text" name="email" value="test@test.ru") + li.user-settings__list-item + label.user-settings__list-item-name(for="phone") Phone: + input.user-settings__list-item-value.t-right(id="phone" type="text" name="phone" value="+7 (000) 000-00-00") + //input(type="file", name="avatar", hidden) diff --git a/src/components/EditingSettingsForm/EditingSettingsForm.ts b/src/components/EditingSettingsForm/EditingSettingsForm.ts new file mode 100644 index 000000000..199368c04 --- /dev/null +++ b/src/components/EditingSettingsForm/EditingSettingsForm.ts @@ -0,0 +1,20 @@ +import { Block } from "../../core/index"; +import template from "./EditingSettingsForm.tmp.pug"; +import { getValuesFromForm } from "../utils/index"; + +export default class EditingSettingsForm extends Block { + constructor(props?: object) { + const newProps = { + ...props, + events: new Map([ + ['submit', (event: object) => getValuesFromForm(event, this)], + ]), + } + + super('div', newProps); + } + + render() { + return this.compile(template, this.props); + } +} diff --git a/src/components/field-text/field-text.scss b/src/components/FieldText/FieldText.scss similarity index 72% rename from src/components/field-text/field-text.scss rename to src/components/FieldText/FieldText.scss index 27435d690..23ff0d704 100644 --- a/src/components/field-text/field-text.scss +++ b/src/components/FieldText/FieldText.scss @@ -24,6 +24,10 @@ } &__help-text { + display: none; + margin-top: 8px; + font-size: 12px; + color: #333; } &--white { @@ -44,4 +48,15 @@ border-radius: 50px; } } + + &--invalid { + #{$block-name}__input { + box-shadow: var(--on-error); + color: tomato; + } + + #{$block-name}__help-text { + display: block; + } + } } diff --git a/src/components/FieldText/FieldText.tmp.pug b/src/components/FieldText/FieldText.tmp.pug new file mode 100644 index 000000000..1523c9432 --- /dev/null +++ b/src/components/FieldText/FieldText.tmp.pug @@ -0,0 +1,7 @@ +label.field-text(class=mods) + if(typeof(title) !== 'undefined' && title) + span.field-text__name= title + span.field-text__input-wrap + input.field-text__input(type=type, name=name, value=value, data-pattern=pattern placeholder=placeholder, autocomplete="off") + if(typeof(helpText) !== 'undefined' && helpText) + span.field-text__help-text= helpText diff --git a/src/components/FieldText/FieldText.ts b/src/components/FieldText/FieldText.ts new file mode 100644 index 000000000..b0b0f5e0e --- /dev/null +++ b/src/components/FieldText/FieldText.ts @@ -0,0 +1,34 @@ +import { Block } from "../../core/index"; +import template from "./FieldText.tmp.pug"; +import templateSetting from "./FieldTextSetting.tmp.pug"; +import { validateField } from "../utils/index"; + +import "./FieldText.scss"; + +export default class FieldText extends Block { + constructor(props?: object) { + const newProps = { + ...props, + events: new Map([ + ['blur', validateField], + ]), + } + + super('div', newProps); + } + + public value() { + return this.element.querySelector('input').value; + } + + public validate() { + const input = this.element.querySelector('input'); + validateField(input); + } + + render() { + const isSettingTmp = this.props.tmp; + + return this.compile(isSettingTmp ? templateSetting : template, this.props); + } +} diff --git a/src/components/FieldText/FieldTextSetting.tmp.pug b/src/components/FieldText/FieldTextSetting.tmp.pug new file mode 100644 index 000000000..8ecad9bd5 --- /dev/null +++ b/src/components/FieldText/FieldTextSetting.tmp.pug @@ -0,0 +1,6 @@ +.field-text + if(typeof(title) !== 'undefined' && title) + label.user-settings__list-item-name(for=name)= title + input.user-settings__list-item-value.t-right(id=name, type=type, name=name, value=value, data-pattern=pattern, placeholder=placeholder) + if(typeof(helpText) !== 'undefined' && helpText) + span.field-text__help-text= helpText diff --git a/src/components/Link/Link.tmp.pug b/src/components/Link/Link.tmp.pug new file mode 100644 index 000000000..205d5c9ba --- /dev/null +++ b/src/components/Link/Link.tmp.pug @@ -0,0 +1 @@ +a.link(class=className, href=href)= text diff --git a/src/components/Link/Link.ts b/src/components/Link/Link.ts new file mode 100644 index 000000000..b63fcbc4d --- /dev/null +++ b/src/components/Link/Link.ts @@ -0,0 +1,12 @@ +import { Block } from "../../core/index"; +import template from "./Link.tmp.pug"; + +export default class Link extends Block { + constructor(props?: object) { + super('div', props); + } + + render() { + return this.compile(template, this.props); + } +} diff --git a/src/components/RegistrationForm/RegistrationForm.tmp.pug b/src/components/RegistrationForm/RegistrationForm.tmp.pug new file mode 100644 index 000000000..0448cc793 --- /dev/null +++ b/src/components/RegistrationForm/RegistrationForm.tmp.pug @@ -0,0 +1,11 @@ +form.form + if (title) + h1.form__title= title + .form__field!= fieldFirstName + .form__field!= fieldSecondName + .form__field!= fieldEmail + .form__field!= fieldPhone + .form__field!= fieldLogin + .form__field!= fieldPassword + .form__field.form__field--accent!= sendBtn + .form__field.t-right!= link diff --git a/src/components/RegistrationForm/RegistrationForm.ts b/src/components/RegistrationForm/RegistrationForm.ts new file mode 100644 index 000000000..7979493bc --- /dev/null +++ b/src/components/RegistrationForm/RegistrationForm.ts @@ -0,0 +1,20 @@ +import { Block } from "../../core/index"; +import template from "./RegistrationForm.tmp.pug"; +import { getValuesFromForm } from "../utils/index"; + +export default class RegistrationForm extends Block { + constructor(props?: object) { + const newProps = { + ...props, + events: new Map([ + ['submit', (event: object) => getValuesFromForm(event, this)], + ]), + } + + super('div', newProps); + } + + render() { + return this.compile(template, this.props); + } +} diff --git a/src/components/field-text/field-text.pug b/src/components/field-text/field-text.pug deleted file mode 100644 index 23345c704..000000000 --- a/src/components/field-text/field-text.pug +++ /dev/null @@ -1,47 +0,0 @@ -mixin field-text(props) - - //- Принимает: - //- props { - //- title: '' {string} - текст с названием (выводится над полем) - //- isTextarea: false {bool} - флаг input/textarea - //- helpText: '' {string} - пояснение под полем - //- mods: '' {string} - модификаторы блока - //- val: '' {string} - текст в поле - //- attrs: {object} - любые атрибуты для input/textarea - //- type: {string} - //- placeholder: {string} - //- Вызов: - +field-text({ - title: 'Название', - isTextarea: true, - helpText: 'Подсказка', - mods: '', - val: '', - attrs: { - name: 'comment', - } - }) - - - - if(typeof(props) === 'undefined') { - var props = {}; - } - var allMods = ''; - if(typeof(props.mods) !== 'undefined' && props.mods) { - var modsList = props.mods.split(','); - for (var i = 0; i < modsList.length; i++) { - allMods = allMods + ' field-text--' + modsList[i].trim(); - } - } - - label.field-text(class=allMods)&attributes(attributes) - if(typeof(props.title) !== 'undefined' && props.title) - span.field-text__name!= props.title - span.field-text__input-wrap - if(typeof(props.isTextarea) !== 'undefined' && props.isTextarea) - textarea.field-text__input&attributes(props.attrs)= props.val - else - input.field-text__input(type=(typeof(props.attrs) !== 'undefined' && props.attrs.type) ? props.attrs.type : 'text', value=props.val)&attributes(props.attrs) - if(typeof(props.helpText) !== 'undefined' && props.helpText) - span.field-text__help-text!= props.helpText - block diff --git a/src/components/form/form.pug b/src/components/form/form.pug deleted file mode 100644 index a3c425577..000000000 --- a/src/components/form/form.pug +++ /dev/null @@ -1,45 +0,0 @@ -mixin form(title, mods) - - //- Принимает: - //- title {string} - form title - //- mods {string} - список модификаторов - //- Вызов: - +form('title', 'mods') - +form-field() - some html - +form-field() - some html - - - - var allMods = ''; - if(typeof(mods) !== 'undefined' && mods) { - var modsList = mods.split(','); - for (var i = 0; i < modsList.length; i++) { - allMods = allMods + ' form--' + modsList[i].trim(); - } - } - - form.form(class=allMods)&attributes(attributes) - if (title) - h1.form__title= title - block - -mixin form-field(mods) - - //- Принимает: - //- mods {string} - список модификаторов - //- Вызов: - +form-field() - some html - - - - var allMods = ''; - if(typeof(mods) !== 'undefined' && mods) { - var modsList = mods.split(','); - for (var i = 0; i < modsList.length; i++) { - allMods = allMods + ' form__field--' + modsList[i].trim(); - } - } - - .form__field(class=allMods)&attributes(attributes) - block diff --git a/src/components/form/form.scss b/src/components/form/form.scss deleted file mode 100644 index c6c755485..000000000 --- a/src/components/form/form.scss +++ /dev/null @@ -1,28 +0,0 @@ -.form { - $block-name: &; - - margin-left: auto; - margin-right: auto; - padding: 50px; - width: 400px; - max-width: 100%; - border-radius: var(--b-radius); - background-color: var(--bg-whiteSmoke); - - &__title { - margin-bottom: 70px; - font-size: 35px; - font-weight: 500; - text-align: center; - } - - &__field { - & + & { - margin-top: 20px; - } - - & + &--accent { - margin-top: 40px; - } - } -} diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 000000000..bfd40e712 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,17 @@ +import AuthorizationForm from "./AuthorizationForm/AuthorizationForm"; +import RegistrationForm from "./RegistrationForm/RegistrationForm"; +import EditingSettingsForm from "./EditingSettingsForm/EditingSettingsForm" +import EditingPasswordForm from "./EditingPasswordForm/EditingPasswordForm"; +import FieldText from "./FieldText/FieldText"; +import Button from "./Button/Button"; +import Link from "./Link/Link"; + +export { + AuthorizationForm, + RegistrationForm, + EditingSettingsForm, + EditingPasswordForm, + FieldText, + Button, + Link, +} diff --git a/src/components/utils/getValuesFromForm.ts b/src/components/utils/getValuesFromForm.ts new file mode 100644 index 000000000..973d954b4 --- /dev/null +++ b/src/components/utils/getValuesFromForm.ts @@ -0,0 +1,21 @@ +type Event = { + preventDefault: Function +} + +type Instance = { + children: PropertyKey, +} + +export default function getValuesFromForm(event: Event, instance: Instance) { + event.preventDefault(); + const formValues: Record = {}; + + Object.entries(instance.children).forEach(([name, child]: [string, any]) => { + if (child.value) { + formValues[name] = child.value(); + child.validate(); + } + }) + + console.log(formValues); +} diff --git a/src/components/utils/index.ts b/src/components/utils/index.ts new file mode 100644 index 000000000..4674dd817 --- /dev/null +++ b/src/components/utils/index.ts @@ -0,0 +1,7 @@ +import validateField from "./validateField"; +import getValuesFromForm from "./getValuesFromForm"; + +export { + validateField, + getValuesFromForm, +}; diff --git a/src/components/utils/validateField.ts b/src/components/utils/validateField.ts new file mode 100644 index 000000000..e7290dbc4 --- /dev/null +++ b/src/components/utils/validateField.ts @@ -0,0 +1,33 @@ +interface DOMStringMap { + [name: string]: string | undefined +} + +interface HTMLInputElement { + value: string, + dataset: DOMStringMap, + closest: Function, +} + +interface EventTarget { + target: HTMLInputElement, +} + +export default function validateField(event: EventTarget): void { + const input = event.target || event; + const { value } = input; + const { pattern } = input.dataset; + + if (!pattern) { + return; + } + + const parent = input.closest('.field-text'); + const errClass = 'field-text--invalid'; + const regExp = new RegExp(pattern, 'g'); + + if (!regExp.test(value)) { + parent?.classList.add(errClass); + } else { + parent?.classList.remove(errClass); + } +} diff --git a/src/core/Block.js b/src/core/Block.js deleted file mode 100644 index 779eb2815..000000000 --- a/src/core/Block.js +++ /dev/null @@ -1,217 +0,0 @@ -/* eslint-disable */ - -import EventBus from "./EventBus"; -import { v4 as uuid } from "uuid"; - -export default class Block { - static EVENTS = { - INIT: "init", - FLOW_CDM: "flow:component-did-mount", - FLOW_CDU: "flow:component-did-update", - FLOW_RENDER: "flow:render", - }; - - _element = null; - _meta = null; - _id = null; - - constructor(tagName = "div", propsAndChildren = {}) { - const { children, props } = this._getChildren(propsAndChildren); - const eventBus = new EventBus(); - - this.children = children; - this._meta = { - tagName, - props, - }; - - if (props.withId) { - this._id = uuid(); - this.props = this._makePropsProxy({...props, __id: this._id}); - } else { - this.props = this._makePropsProxy(props); - } - - this.eventBus = () => eventBus; - this._registerEvents(eventBus); - - eventBus.emit(Block.EVENTS.INIT); - } - - _registerEvents(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_CDU, this._componentDidUpdate.bind(this)); - eventBus.on(Block.EVENTS.FLOW_RENDER, this._render.bind(this)); - } - - _unregisterEvents(eventBus) { - eventBus.off(Block.EVENTS.INIT, this.init.bind(this)); - eventBus.off(Block.EVENTS.FLOW_CDM, this._componentDidMount.bind(this)); - eventBus.off(Block.EVENTS.FLOW_CDU, this._componentDidUpdate.bind(this)); - eventBus.off(Block.EVENTS.FLOW_RENDER, this._render.bind(this)); - } - - _getChildren(propsAndChildren) { - const children = {}; - const props = {}; - - Object.entries(propsAndChildren).forEach(([key, value]) => { - if (value instanceof Block) { - children[key] = value; - } else { - props[key] = value; - } - }) - - return { children, props }; - } - - init() { - console.log('init'); - this._createResources(); - this.eventBus().emit(Block.EVENTS.FLOW_RENDER); - } - - _createResources() { - const { tagName } = this._meta; - this._element = this._createDocumentElement(tagName); - } - - _createDocumentElement(tagName) { - const element = document.createElement(tagName); - if (this._id) { - element.dataset.id = this._id; - } - - return element; - } - - _componentDidMount() { - console.log('_componentDidMount'); - this.componentDidMount(); - // this.eventBus().emit(Block.EVENTS.FLOW_RENDER); - } - - // Может переопределять пользователь, необязательно трогать - componentDidMount(oldProps) { - console.log('componentDidMount'); - - dispatchComponentDidMoun(); - } - - dispatchComponentDidMoun() { - console.log('dispatchComponentDidMoun'); - this.eventBus().emit(Block.EVENTS.FLOW_CDM); - } - - _componentDidUpdate(oldProps, newProps) { - console.log('_componentDidUpdate'); - const response = this.componentDidUpdate(oldProps, newProps); - } - - // Может переопределять пользователь, необязательно трогать - componentDidUpdate(oldProps, newProps) { - console.log('componentDidUpdate'); - console.log(oldProps); - console.log(newProps); - // this.eventBus().emit(Block.EVENTS.FLOW_RENDER); - // return true; - - - if (true) { - return true; - } - - return false; - } - - setProps = newProps => { - console.log('setProps'); - if (!newProps) { - return; - } - - // this.eventBus().emit(Block.EVENTS.FLOW_CDU, this.props, newProps); - // Object.assign(this.props, newProps); - // this.eventBus().emit(Block.EVENTS.FLOW_CDU); - }; - - get element() { - return this._element; - } - - _render() { - const block = this.render(); - - this._removeEvents(); - - this._element.innerHTML = block; - - this._addEvents(); - } - - // Может переопределять пользователь, необязательно трогать - render() { - console.log('render'); - } - - compile(template, props) { - const propsAndStubs = {...props}; - - Object.entries(this.children).forEach(([key, child]) => { - propsAndStubs[key] = `
`; - }) - - console.log(propsAndStubs); - - return template(propsAndStubs); - } - - _addEvents() { - const {events = new Map()} = this.props; - - events.forEach((eventFun, eventName) => { - this._element.addEventListener(eventName, eventFun); - }) - } - - _removeEvents() { - const {events = new Map()} = this.props; - - events.forEach((eventFun, eventName) => { - this._element.removeEventListener(eventName, eventFun); - }) - } - - getContent() { - return this.element; - } - - _makePropsProxy(props) { - // Можно и так передать this - // Такой способ больше не применяется с приходом ES6+ - const self = this; - const proxyProps = new Proxy(props, { - set(target, prop, newValue) { - if (prop.indexOf('_') === 0) { - throw new Error('Permission denied'); - } - - target[prop] = newValue; - self.eventBus().emit(Block.EVENTS.FLOW_CDU); - - return true; - }, - deleteProperty() { - throw new Error('Permission denied'); - }, - }) - - return proxyProps; - } - - show() {} - - hide() {} -} diff --git a/src/core/Block.ts b/src/core/Block.ts new file mode 100644 index 000000000..3ee89534b --- /dev/null +++ b/src/core/Block.ts @@ -0,0 +1,228 @@ +import { v4 as uuid } from "uuid"; +import EventBus from "./EventBus"; + +// использую any, потому что не получилось типизировать по хорошему, +// а время на исходе) + +export default class Block { + children: any; + props: any; + eventBus: () => EventBus; + + static EVENTS = { + INIT: "init", + FLOW_CDM: "flow:component-did-mount", + FLOW_CDU: "flow:component-did-update", + FLOW_RENDER: "flow:render", + }; + + _element: any = null; + _meta: any = null; + _id: any = null; + + constructor(tagName = "div", propsAndChildren = {}) { + const { children, props } = this._getChildren(propsAndChildren); + const eventBus = new EventBus(); + + this.children = children; + + this._meta = { + tagName, + props, + }; + + if (props.withId) { + this._id = uuid(); + this.props = this._makePropsProxy({ ...props, __id: this._id }); + } else { + this.props = this._makePropsProxy(props); + } + + this.eventBus = () => eventBus; + + this._registerEvents(eventBus); + + eventBus.emit(Block.EVENTS.INIT); + } + + _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_CDU, this._componentDidUpdate.bind(this)); + eventBus.on(Block.EVENTS.FLOW_RENDER, this._render.bind(this)); + } + + _getChildren(propsAndChildren: any) { + const children: any = {}; + const props: any = {}; + + Object.entries(propsAndChildren).forEach(([key, value]) => { + if (value instanceof Block) { + children[key] = value; + } else { + props[key] = value; + } + }) + + return { children, props }; + } + + init() { + this._createResources(); + this.eventBus().emit(Block.EVENTS.FLOW_RENDER); + } + + _createResources() { + const { tagName } = this._meta; + this._element = this._createDocumentElement(tagName); + } + + _createDocumentElement(tagName: string): any { + const element = document.createElement(tagName); + if (this._id) { + element.dataset.id = this._id; + } + + return element; + } + + _componentDidMount() { + this.componentDidMount(); + + Object.values(this.children).forEach((child: any) => { + child.dispatchComponentDidMount(); + }); + } + + // Может переопределять пользователь, необязательно трогать + componentDidMount(oldProps?: any) { + console.log(oldProps); + } + + dispatchComponentDidMount() {} + + setProps = (newProps: any) => { + if (!newProps) { + return; + } + + this._componentDidUpdate(this.props, newProps); + // this.eventBus().emit(Block.EVENTS.FLOW_CDU, this.props, newProps); + // Object.assign(this.props, newProps); + // this.eventBus().emit(Block.EVENTS.FLOW_CDU); + }; + + _componentDidUpdate(oldProps: any, newProps: any) { + const response = this.componentDidUpdate(oldProps, newProps); + + if (response) { + Object.assign(oldProps, newProps); + } + } + + // Может переопределять пользователь, необязательно трогать + componentDidUpdate(oldProps: any, newProps: any) { + let update = false; + Object.keys(newProps).forEach((key) => { + if (oldProps[key] !== newProps[key]) { + update = true; + } + }) + + return update; + } + + _render() { + const block = this.render(); + + this._removeEvents(); + this._element.innerHTML = ''; + + this._element.appendChild(block); + this._addEvents(); + } + + // Может переопределять пользователь, необязательно трогать + render(): any {} + + compile(template: Function, props: any) { + if (this.children) { + const propsAndStubs = { ...props }; + + Object.entries(this.children).forEach(([key, child]: [string, any]) => { + propsAndStubs[key] = `
`; + }) + + const fragment = this._createDocumentElement('template'); + fragment.innerHTML = template(propsAndStubs); + + Object.values(this.children).forEach((child: any) => { + const stub = fragment.content.querySelector(`[data-id="${child._id}"]`); + stub.replaceWith(child.getContent()); + }); + + return fragment.content; + } + + return template(props); + } + + _addEvents() { + const { events = new Map() } = this.props; + + events.forEach((eventFun: Function, eventName: string) => { + if (eventName === 'blur') { + this._element.querySelector('input')?.addEventListener(eventName, eventFun); + } else { + this._element.addEventListener(eventName, eventFun); + } + }) + } + + _removeEvents() { + const { events = new Map() } = this.props; + + events.forEach((eventFun: Function, eventName: string) => { + if (eventName === 'blur') { + this._element.querySelector('input')?.removeEventListener(eventName, eventFun); + } else { + this._element.removeEventListener(eventName, eventFun); + } + + // if (eventName === 'submit') { + // this._element.querySelector('form')?.removeEventListener(eventName, eventFun); + // } else { + // this._element.removeEventListener(eventName, eventFun); + // } + }) + } + + getContent() { + return this.element; + } + + get element() { + return this._element; + } + + _makePropsProxy(props: any) { + const self = this; + const proxyProps = new Proxy(props, { + set(target: any, prop: string, newValue) { + if (prop.indexOf('_') === 0) { + throw new Error('Permission denied'); + } + + target[prop] = newValue; + self.eventBus().emit(Block.EVENTS.INIT); + + return true; + }, + deleteProperty() { + throw new Error('Permission denied'); + }, + }) + + return proxyProps; + } +} diff --git a/src/core/index.js b/src/core/index.ts similarity index 100% rename from src/core/index.js rename to src/core/index.ts diff --git a/src/editing-password/editing-password.ts b/src/editing-password/editing-password.ts new file mode 100644 index 000000000..012a7c9c8 --- /dev/null +++ b/src/editing-password/editing-password.ts @@ -0,0 +1,8 @@ +import { render } from "../utils/index"; +import { EditingPassword } from "../pages/index"; + +import "../scss/index.scss"; + +const page = new EditingPassword(); + +render(page); diff --git a/src/editing-password/index.html b/src/editing-password/index.html new file mode 100644 index 000000000..5bdc0f74b --- /dev/null +++ b/src/editing-password/index.html @@ -0,0 +1,12 @@ + + + + + + Editing password page + + +
+ + + diff --git a/src/editing-settings/editing-settings.ts b/src/editing-settings/editing-settings.ts new file mode 100644 index 000000000..94e454f26 --- /dev/null +++ b/src/editing-settings/editing-settings.ts @@ -0,0 +1,8 @@ +import { render } from "../utils/index"; +import { EditingSettings } from "../pages/index"; + +import "../scss/index.scss"; + +const page = new EditingSettings(); + +render(page); diff --git a/src/editing-settings/index.html b/src/editing-settings/index.html new file mode 100644 index 000000000..c4daa207a --- /dev/null +++ b/src/editing-settings/index.html @@ -0,0 +1,12 @@ + + + + + + Editing settings page + + +
+ + + diff --git a/src/index.html b/src/index.html index a4251a4ba..1e268a943 100644 --- a/src/index.html +++ b/src/index.html @@ -1,8 +1,9 @@ + - Chats + Authorization page
diff --git a/src/index.ts b/src/index.ts index 2fdd32322..de1c14736 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,8 @@ -import Button from "./components/Button/Button"; +import { render } from "./utils/index"; +import { Authorization } from "./pages/index"; -const button = new Button({ - text: 'Click me', -}) +import "./scss/index.scss"; -document.addEventListener('DOMContentLoaded', () => { - const app = document.getElementById('app'); +const page = new Authorization(); - app.innerHTML = button; -}) +render(page); diff --git a/src/pages/404.pug b/src/pages/404.pug deleted file mode 100644 index 2b60f3bbe..000000000 --- a/src/pages/404.pug +++ /dev/null @@ -1,16 +0,0 @@ -extends ../layout/main.pug - -block meta - - const pageName = '404 page'; - - title= pageName - meta(name='description', content=pageName) - -block content - - section.error-page - h1.error-page__title 404 - p.error-page__description who seeks will always find - .error-page__bottom - a.error-page__link.link(href="chats-and-chat.html") go to chats page - diff --git a/src/pages/500.pug b/src/pages/500.pug deleted file mode 100644 index 7c9677bf0..000000000 --- a/src/pages/500.pug +++ /dev/null @@ -1,16 +0,0 @@ -extends ../layout/main.pug - -block meta - - const pageName = '500 page'; - - title= pageName - meta(name='description', content=pageName) - -block content - - section.error-page - h1.error-page__title 500 - p.error-page__description something went wrong - .error-page__bottom - a.error-page__link.link(href="chats-and-chat.html") go to chats page - diff --git a/src/pages/Authorization/Authorization.tmp.pug b/src/pages/Authorization/Authorization.tmp.pug new file mode 100644 index 000000000..d6145b8c0 --- /dev/null +++ b/src/pages/Authorization/Authorization.tmp.pug @@ -0,0 +1 @@ +.page__content!= authForm diff --git a/src/pages/Authorization/Authorization.ts b/src/pages/Authorization/Authorization.ts new file mode 100644 index 000000000..84ebb16fa --- /dev/null +++ b/src/pages/Authorization/Authorization.ts @@ -0,0 +1,52 @@ +import { Block } from "../../core/index"; +import template from "./Authorization.tmp.pug"; +import { + AuthorizationForm, + FieldText, + Button, + Link, +} from "../../components/index"; + +const authForm = new AuthorizationForm({ + withId: true, + title: 'Sign in', + fieldLogin: new FieldText({ + withId: true, + title: 'Login:', + type: 'text', + name: 'login', + helpText: ` + от 3 до 20 символов, латиница, может содержать цифры, + но не состоять из них, без пробелов, без спецсимволов (допустимы дефис и нижнее подчёркивание) + `, + pattern: '(?=^.{3,20}$)[a-zA-Z_-]+[0-9_-a-zA-Z]*', + }), + fieldPassword: new FieldText({ + withId: true, + title: 'Password:', + type: 'password', + name: 'password', + value: '', + helpText: 'от 8 до 40 символов, обязательно хотя бы одна заглавная буква и цифра', + pattern: '(?=^.{8,40}$)(?=.*[A-Z])(?=.*[0-9]).*', + }), + sendBtn: new Button({ + className: 'btn btn--w-100 btn--big', + type: 'submit', + text: 'Sign in', + }), + link: new Link({ + href: '/registration/', + text: 'Sign up', + }), +}) + +export default class Authorization extends Block { + constructor(props?: object) { + super('div', { ...props, authForm }); + } + + render() { + return this.compile(template, this.props); + } +} diff --git a/src/pages/ChatsAndChat/ChatsAndChat.tmp.pug b/src/pages/ChatsAndChat/ChatsAndChat.tmp.pug new file mode 100644 index 000000000..9533c251c --- /dev/null +++ b/src/pages/ChatsAndChat/ChatsAndChat.tmp.pug @@ -0,0 +1,64 @@ +.chats-and-chat + aside.sidebar + .sidebar__f + form.search!= fieldSearch + .sidebar__s + .sidebar__s-inner + .chats + each val in [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15] + article.chat(tabindex="0") + .chat__f + .avatar + img.avatar__img(src="img/avatar-dummy-1.jpg", alt="user name avatar", loading="lazy") + .chat__s + .chat__s-f + h6.name Chat name + time.chat__time 10:45 + .chat__s-s + p.chat__last-message last message last message last message last message last message last message + + .sidebar__t + .user-information + .user-information__f + .user-information__f-f + .avatar + img.avatar__img(src="img/avatar-dummy-1.jpg", alt="user name avatar", loading="lazy") + .user-information__f-s + h6.name User name + + .user-information__s + button.settings(type="button") ... + + .chat-area + .chat-area__f + .chat-area__f-f + .chat-area__info + .chat-area__info-l + .avatar + img.avatar__img(src="img/avatar-dummy-1.jpg", alt="user name avatar", loading="lazy") + .chat-area__info-r + h6.name Chat name + + .chat-area__f-s + button.settings(type="button") ... + + .chat-area__s + .messages-area + .messages-area__f + .messages-area__conversation + each val in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + .messages-area__conversation-item.messages-area__conversation-received + .messages-area__conversation-message + | Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum iste non corrupti molestias. Provident dolorum nostrum quasi id modi cumque doloribus nemo, beatae veritatis rem. Ducimus, amet dicta? Adipisci, corrupti? + .messages-area__conversation-message + | Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum iste non corrupti molestias. Provident dolorum nostrum quasi id modi cumque doloribus nemo, beatae veritatis rem. Ducimus, amet dicta? Adipisci, corrupti? + + .messages-area__conversation-item.messages-area__conversation-sent + .messages-area__conversation-message + | Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum iste non corrupti molestias. Provident dolorum nostrum quasi id modi cumque doloribus nemo, beatae veritatis rem. Ducimus, amet dicta? Adipisci, corrupti? + + .messages-area__s + .messages-area__s-inner!= fieldMessage + .messages-area__buttons + button.settings(type="button") ... + button.settings(type="button") ... diff --git a/src/pages/ChatsAndChat/ChatsAndChat.ts b/src/pages/ChatsAndChat/ChatsAndChat.ts new file mode 100644 index 000000000..6de8f8968 --- /dev/null +++ b/src/pages/ChatsAndChat/ChatsAndChat.ts @@ -0,0 +1,33 @@ +import { Block } from "../../core/index"; +import template from "./ChatsAndChat.tmp.pug"; +import { + FieldText, +} from "../../components/index"; + +const fieldSearch = new FieldText({ + withId: true, + mods: 'field-text--center field-text--white', + type: 'text', + name: 'search', + value: '', + placeholder: 'search', +}) + +const fieldMessage = new FieldText({ + withId: true, + mods: 'field-text--main', + type: 'text', + name: 'message', + value: '', + placeholder: 'Type a message', +}) + +export default class ChatsAndChat extends Block { + constructor(props?: object) { + super('div', { ...props, fieldSearch, fieldMessage }); + } + + render() { + return this.compile(template, this.props); + } +} diff --git a/src/pages/EditingPassword/EditingPassword.tmp.pug b/src/pages/EditingPassword/EditingPassword.tmp.pug new file mode 100644 index 000000000..534777381 --- /dev/null +++ b/src/pages/EditingPassword/EditingPassword.tmp.pug @@ -0,0 +1,11 @@ +.modal + .modal__inner + button.modal__close(title="Close modal") + .modal__content + section.user-settings + .user-settings__f + .user-settings__avatar + h2.user-settings__name= userName + + .user-settings__s!= userSettings + .user-settings__t!= saveBtn diff --git a/src/pages/EditingPassword/EditingPassword.ts b/src/pages/EditingPassword/EditingPassword.ts new file mode 100644 index 000000000..4d7572fbb --- /dev/null +++ b/src/pages/EditingPassword/EditingPassword.ts @@ -0,0 +1,60 @@ +import { Block } from "../../core/index"; +import template from "./EditingPassword.tmp.pug"; +import { + EditingPasswordForm, + FieldText, + Button, +} from "../../components/index"; + +export default class EditingPassword extends Block { + constructor(props?: object) { + const newProps = { + ...props, + userName: 'User name', + userSettings: new EditingPasswordForm({ + currentPassword: new FieldText({ + withId: true, + title: 'Current password:', + type: 'password', + name: 'old_password', + value: 'jzuXon8ZOT', + helpText: 'от 8 до 40 символов, обязательно хотя бы одна заглавная буква и цифра', + pattern: '(?=^.{8,40}$)(?=.*[A-Z])(?=.*\\d).*', + tmp: 'setting', + }), + newPassword: new FieldText({ + withId: true, + title: 'New password:', + type: 'password', + name: 'new_password', + value: '', + helpText: 'от 8 до 40 символов, обязательно хотя бы одна заглавная буква и цифра', + pattern: '(?=^.{8,40}$)(?=.*[A-Z])(?=.*\\d).*', + tmp: 'setting', + }), + repeatNewPassword: new FieldText({ + withId: true, + title: 'Repeat new password:', + type: 'password', + name: 'repeat_password', + value: '', + helpText: 'от 8 до 40 символов, обязательно хотя бы одна заглавная буква и цифра', + pattern: '(?=^.{8,40}$)(?=.*[A-Z])(?=.*\\d).*', + tmp: 'setting', + }), + }), + saveBtn: new Button({ + className: 'link', + type: 'submit', + form: 'userSettingsForm', + text: 'Save password', + }), + } + + super('div', newProps); + } + + render() { + return this.compile(template, this.props); + } +} diff --git a/src/pages/EditingSettings/EditingSettings.tmp.pug b/src/pages/EditingSettings/EditingSettings.tmp.pug new file mode 100644 index 000000000..534777381 --- /dev/null +++ b/src/pages/EditingSettings/EditingSettings.tmp.pug @@ -0,0 +1,11 @@ +.modal + .modal__inner + button.modal__close(title="Close modal") + .modal__content + section.user-settings + .user-settings__f + .user-settings__avatar + h2.user-settings__name= userName + + .user-settings__s!= userSettings + .user-settings__t!= saveBtn diff --git a/src/pages/EditingSettings/EditingSettings.ts b/src/pages/EditingSettings/EditingSettings.ts new file mode 100644 index 000000000..fbce677dc --- /dev/null +++ b/src/pages/EditingSettings/EditingSettings.ts @@ -0,0 +1,97 @@ +import { Block } from "../../core/index"; +import template from "./EditingSettings.tmp.pug"; +import { + EditingSettingsForm, + FieldText, + Button, +} from "../../components/index"; + +export default class EditingSettings extends Block { + constructor(props?: object) { + const newProps = { + ...props, + userName: 'User name', + userSettings: new EditingSettingsForm({ + nickname: new FieldText({ + withId: true, + title: 'Nickname:', + type: 'text', + name: 'display_name', + value: 'userNickName', + helpText: ` + латиница или кириллица, без пробелов и без цифр, + нет спецсимволов (допустим только дефис) + `, + pattern: '^[a-zA-Zа-яА-Я-]+$', + tmp: 'setting', + }), + firstName: new FieldText({ + withId: true, + title: 'First name:', + type: 'text', + name: 'first_name', + value: 'Firstname', + helpText: ` + латиница или кириллица, первая буква должна быть заглавной, + без пробелов и без цифр, нет спецсимволов (допустим только дефис) + `, + pattern: '(?=^[A-ZА-Я])[a-zA-Zа-яА-Я-]+$', + tmp: 'setting', + }), + secondName: new FieldText({ + withId: true, + title: 'Second name:', + type: 'text', + name: 'second_name', + value: 'Sirstname', + helpText: ` + латиница или кириллица, первая буква должна быть заглавной, + без пробелов и без цифр, нет спецсимволов (допустим только дефис) + `, + pattern: '(?=^[A-ZА-Я])[a-zA-Zа-яА-Я-]+$', + tmp: 'setting', + }), + email: new FieldText({ + withId: true, + title: 'Email:', + type: 'text', + inputmode: 'email', + name: 'email', + value: 'test@test.ru', + helpText: ` + латиница, может включать цифры и спецсимволы вроде дефиса и подчёркивания, + обязательно должна быть «собака» (@) и точка после неё, + но перед точкой обязательно должны быть буквы + `, + pattern: '^[a-zA-Z\\d_-]+@[a-z]+\\.[a-z]{2,3}$', + tmp: 'setting', + }), + phone: new FieldText({ + withId: true, + title: 'Phone:', + type: 'text', + inputmode: 'tel', + name: 'phone', + value: '+70000000000', + helpText: ` + от 10 до 15 символов, состоит из цифр, может начинается с плюса. + `, + pattern: '(?=^.{10,15}$)\\+?[0-9]+$', + tmp: 'setting', + }), + }), + saveBtn: new Button({ + className: 'link', + type: 'submit', + form: 'userSettingsForm', + text: 'Save settings', + }), + } + + super('div', newProps); + } + + render() { + return this.compile(template, this.props); + } +} diff --git a/src/pages/ErrorPage/ErrorPage.tmp.pug b/src/pages/ErrorPage/ErrorPage.tmp.pug new file mode 100644 index 000000000..342bbab9d --- /dev/null +++ b/src/pages/ErrorPage/ErrorPage.tmp.pug @@ -0,0 +1,7 @@ +.page__content + section.error-page + h1.error-page__title= title + p.error-page__description= text + .error-page__bottom + a.error-page__link.link(href=linkHref)= linkText + diff --git a/src/pages/ErrorPage/ErrorPage.ts b/src/pages/ErrorPage/ErrorPage.ts new file mode 100644 index 000000000..1796546d7 --- /dev/null +++ b/src/pages/ErrorPage/ErrorPage.ts @@ -0,0 +1,12 @@ +import { Block } from "../../core/index"; +import template from "./ErrorPage.tmp.pug"; + +export default class ErrorPage extends Block { + constructor(props?: object) { + super('div', props); + } + + render() { + return this.compile(template, this.props); + } +} diff --git a/src/pages/UserSettings/UserSettings.tmp.pug b/src/pages/UserSettings/UserSettings.tmp.pug new file mode 100644 index 000000000..79f236a9c --- /dev/null +++ b/src/pages/UserSettings/UserSettings.tmp.pug @@ -0,0 +1,33 @@ +.modal + .modal__inner + button.modal__close(title="Close modal") + .modal__content + section.user-settings + .user-settings__f + .user-settings__avatar + h2.user-settings__name= userName + + .user-settings__s + ul.user-settings__list + li.user-settings__list-item + span.user-settings__list-item-name Nickname: + span.user-settings__list-item-value user name + li.user-settings__list-item + span.user-settings__list-item-name First name: + span.user-settings__list-item-value first name + li.user-settings__list-item + span.user-settings__list-item-name Second name: + span.user-settings__list-item-value second name + li.user-settings__list-item + span.user-settings__list-item-name Email: + span.user-settings__list-item-value test@test.ru + li.user-settings__list-item + span.user-settings__list-item-name Phone: + span.user-settings__list-item-value +7 (000) 000-00-00 + li.user-settings__list-item + span.user-settings__list-item-name Login: + span.user-settings__list-item-value mylogin + + .user-settings__t + | !{linkToSettings} + | !{linkToPassword} diff --git a/src/pages/UserSettings/UserSettings.ts b/src/pages/UserSettings/UserSettings.ts new file mode 100644 index 000000000..9775596bb --- /dev/null +++ b/src/pages/UserSettings/UserSettings.ts @@ -0,0 +1,25 @@ +import { Block } from "../../core/index"; +import template from "./UserSettings.tmp.pug"; +import { Link } from "../../components/index"; + +export default class ChatsAndChat extends Block { + constructor(props?: object) { + const newProps = { + ...props, + userName: 'User name', + linkToSettings: new Link({ + href: '/editing-settings/', + text: 'Change settings', + }), + linkToPassword: new Link({ + href: '/editing-password/', + text: 'Change password', + }), + } + super('div', newProps); + } + + render() { + return this.compile(template, this.props); + } +} diff --git a/src/pages/chats-and-chat.pug b/src/pages/chats-and-chat.pug deleted file mode 100644 index 4f13737c8..000000000 --- a/src/pages/chats-and-chat.pug +++ /dev/null @@ -1,90 +0,0 @@ -extends ../layout/main.pug - -block meta - - const pageName = 'Chats and chat'; - - title= pageName - meta(name='description', content=pageName) - -block content - - .chats-and-chat - aside.sidebar - .sidebar__f - form.search - +field-text({ - mods: 'white, center', - val: '', - attrs: { - name: 'search', - placeholder: 'search', - } - }) - - .sidebar__s - .sidebar__s-inner - .chats - each val in [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15] - article.chat(tabindex="0") - .chat__f - .avatar - img.avatar__img(src="img/avatar-dummy-1.jpg", alt="user name avatar", loading="lazy") - .chat__s - .chat__s-f - h6.name Chat name - time.chat__time 10:45 - .chat__s-s - p.chat__last-message last message last message last message last message last message last message - - .sidebar__t - .user-information - .user-information__f - .user-information__f-f - .avatar - img.avatar__img(src="img/avatar-dummy-1.jpg", alt="user name avatar", loading="lazy") - .user-information__f-s - h6.name User name - - .user-information__s - button.settings(type="button") ... - - .chat-area - .chat-area__f - .chat-area__f-f - .chat-area__info - .chat-area__info-l - .avatar - img.avatar__img(src="img/avatar-dummy-1.jpg", alt="user name avatar", loading="lazy") - .chat-area__info-r - h6.name Chat name - - .chat-area__f-s - button.settings(type="button") ... - - .chat-area__s - .messages-area - .messages-area__f - .messages-area__conversation - each val in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - .messages-area__conversation-item.messages-area__conversation-received - .messages-area__conversation-message - | Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum iste non corrupti molestias. Provident dolorum nostrum quasi id modi cumque doloribus nemo, beatae veritatis rem. Ducimus, amet dicta? Adipisci, corrupti? - .messages-area__conversation-message - | Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum iste non corrupti molestias. Provident dolorum nostrum quasi id modi cumque doloribus nemo, beatae veritatis rem. Ducimus, amet dicta? Adipisci, corrupti? - - .messages-area__conversation-item.messages-area__conversation-sent - .messages-area__conversation-message - | Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum iste non corrupti molestias. Provident dolorum nostrum quasi id modi cumque doloribus nemo, beatae veritatis rem. Ducimus, amet dicta? Adipisci, corrupti? - - .messages-area__s - .messages-area__s-inner - +field-text({ - mods: 'main', - attrs: { - name: 'message', - placeholder: 'Type a message', - } - }) - .messages-area__buttons - button.settings(type="button") ... - button.settings(type="button") ... diff --git a/src/pages/index.ts b/src/pages/index.ts new file mode 100644 index 000000000..e3f957b3f --- /dev/null +++ b/src/pages/index.ts @@ -0,0 +1,17 @@ +import Authorization from "./Authorization/Authorization"; +import Registration from "./Registration/Registration"; +import ErrorPage from "./ErrorPage/ErrorPage"; +import ChatsAndChat from "./ChatsAndChat/ChatsAndChat"; +import UserSettings from "./UserSettings/UserSettings" +import EditingSettings from "./EditingSettings/EditingSettings" +import EditingPassword from "./EditingPassword/EditingPassword"; + +export { + Authorization, + Registration, + ErrorPage, + ChatsAndChat, + UserSettings, + EditingSettings, + EditingPassword, +} diff --git a/src/pages/registration.pug b/src/pages/registration.pug deleted file mode 100644 index 43c765c8b..000000000 --- a/src/pages/registration.pug +++ /dev/null @@ -1,58 +0,0 @@ -extends ../layout/main.pug - -block meta - - const pageName = 'Registration page'; - - title= pageName - meta(name='description', content=pageName) - -block content - - +form('Sign up') - +form-field() - +field-text({ - title: 'First name:', - attrs: { - name: 'first_name', - } - }) - +form-field() - +field-text({ - title: 'Second name:', - attrs: { - name: 'second_name', - } - }) - +form-field() - +field-text({ - title: 'Email:', - attrs: { - name: 'email', - } - }) - +form-field() - +field-text({ - title: 'Phone:', - attrs: { - name: 'phone', - } - }) - +form-field() - +field-text({ - title: 'Login:', - attrs: { - name: 'login', - } - }) - +form-field() - +field-text({ - title: 'Password:', - attrs: { - type: 'password', - name: 'password', - } - }) - +form-field('accent') - +btn('Create profile', 'w-100, big')(type="button") - +form-field().t-right - a.link(href="index.html") Sign in diff --git a/src/pages/registration/registration.tmp.pug b/src/pages/registration/registration.tmp.pug index b1eb73fa0..1c3aa7595 100644 --- a/src/pages/registration/registration.tmp.pug +++ b/src/pages/registration/registration.tmp.pug @@ -1,20 +1 @@ -form.form - h1.form__title Sign in - - .form__field - label.field-text - span.field-text__name Login: - span.field-text__input-wrap - input.field-text__input(type='text', name='login') - - .form__field - label.field-text - span.field-text__name Password: - span.field-text__input-wrap - input.field-text__input(type='password', name='password') - - .form__field.form__field--accent - button.btn.btn--w-100.btn--big(type='button') Sign in - - .form__field.t-right - a.link(href='#') Sign up +.page__content!= registratonForm diff --git a/src/pages/registration/registration.ts b/src/pages/registration/registration.ts index 647480a0c..edd4c0221 100644 --- a/src/pages/registration/registration.ts +++ b/src/pages/registration/registration.ts @@ -1,15 +1,98 @@ -// import { compileClient } from 'pug'; -// // import { Block } from '../../core/index'; +import { Block } from "../../core/index"; +import template from "./Registration.tmp.pug"; +import { + RegistrationForm, + FieldText, + Button, + Link, +} from "../../components/index"; -// const template = 'p #{name} is a #{occupation}'; -// const data = { name: 'John Doe', occupation: 'gardener' }; +const registratonForm = new RegistrationForm({ + withId: true, + title: 'Sign up', + fieldFirstName: new FieldText({ + withId: true, + title: 'First name:', + type: 'text', + name: 'first_name', + helpText: ` + латиница или кириллица, первая буква должна быть заглавной, + без пробелов и без цифр, нет спецсимволов (допустим только дефис) + `, + pattern: '(?=^[A-ZА-Я])[a-zA-Zа-яА-Я-]+$', + }), + fieldSecondName: new FieldText({ + withId: true, + title: 'Second name:', + type: 'text', + name: 'second_name', + helpText: ` + латиница или кириллица, первая буква должна быть заглавной, + без пробелов и без цифр, нет спецсимволов (допустим только дефис) + `, + pattern: '(?=^[A-ZА-Я])[a-zA-Zа-яА-Я-]+$', + }), + fieldEmail: new FieldText({ + withId: true, + title: 'Email:', + type: 'text', + inputmode: 'email', + name: 'email', + helpText: ` + латиница, может включать цифры и спецсимволы вроде дефиса и подчёркивания, + обязательно должна быть «собака» (@) и точка после неё, + но перед точкой обязательно должны быть буквы + `, + pattern: '^[a-zA-Z\\d_-]+@[a-z]+\\.[a-z]{2,3}$', + }), + fieldPhone: new FieldText({ + withId: true, + title: 'Phone:', + type: 'text', + inputmode: 'tel', + name: 'phone', + helpText: ` + от 10 до 15 символов, состоит из цифр, может начинается с плюса. + `, + pattern: '(?=^.{10,15}$)\\+?[0-9]+$', + }), + fieldLogin: new FieldText({ + withId: true, + title: 'Login:', + type: 'text', + name: 'login', + helpText: ` + от 3 до 20 символов, латиница, может содержать цифры, + но не состоять из них, без пробелов, без спецсимволов (допустимы дефис и нижнее подчёркивание) + `, + pattern: '(?=^.{3,20}$)[a-zA-Z_-]+[\\d_-a-zA-Z]*', + }), + fieldPassword: new FieldText({ + withId: true, + title: 'Password:', + type: 'password', + name: 'password', + value: '', + helpText: 'от 8 до 40 символов, обязательно хотя бы одна заглавная буква и цифра', + pattern: '(?=^.{8,40}$)(?=.*[A-Z])(?=.*\\d).*', + }), + sendBtn: new Button({ + className: 'btn btn--w-100 btn--big', + type: 'submit', + text: 'Sign in', + }), + link: new Link({ + href: '/', + text: 'Sign in', + }), +}) -// // export default class RegistrationPage extends Block { -// // render() { -// // return compileClient(template, data); -// // } -// // } +export default class Registration extends Block { + constructor(props?: object) { + super('div', { ...props, registratonForm }); + } -// const res = compileClient(template, data); - -// export { res }; + render() { + return this.compile(template, this.props); + } +} diff --git a/src/pages/sign-in.pug b/src/pages/sign-in.pug deleted file mode 100644 index 9d1ab23de..000000000 --- a/src/pages/sign-in.pug +++ /dev/null @@ -1,30 +0,0 @@ -extends ../layout/main.pug - -block meta - - const pageName = 'Sign in'; - - title= pageName - meta(name='description', content=pageName) - -block content - - +form('Sign in') - +form-field() - +field-text({ - title: 'Login:', - attrs: { - name: 'login', - } - }) - +form-field() - +field-text({ - title: 'Password:', - attrs: { - type: 'password', - name: 'password', - } - }) - +form-field('accent') - +btn('Sign in', 'w-100, big')(type="button") - +form-field().t-right - a.link(href="registration.html") Sign up diff --git a/src/pages/user-editing-settings.pug b/src/pages/user-editing-settings.pug deleted file mode 100644 index e4c6e1848..000000000 --- a/src/pages/user-editing-settings.pug +++ /dev/null @@ -1,41 +0,0 @@ -extends ../layout/main.pug - -block meta - - const pageName = 'User editing settings'; - - title= pageName - meta(name='description', content=pageName) - -block content - - .modal - .modal__inner - button.modal__close(title="Close modal") - .modal__content - section.user-settings - .user-settings__f - .user-settings__avatar - h2.user-settings__name User name - - .user-settings__s - form#userSettingsForm.user-settings__form - ul.user-settings__list - li.user-settings__list-item - label.user-settings__list-item-name(for="display_name") Nickname: - input.user-settings__list-item-value.t-right(id="display_name" type="text" name="display_name" value="user name") - li.user-settings__list-item - label.user-settings__list-item-name(for="first_name") First name: - input.user-settings__list-item-value.t-right(id="first_name" type="text" name="first_name" value="first name") - li.user-settings__list-item - label.user-settings__list-item-name(for="second_name") Second name: - input.user-settings__list-item-value.t-right(id="second_name" type="text" name="second_name" value="second name") - li.user-settings__list-item - label.user-settings__list-item-name(for="email") Email: - input.user-settings__list-item-value.t-right(id="email" type="text" name="email" value="test@test.ru") - li.user-settings__list-item - label.user-settings__list-item-name(for="phone") Phone: - input.user-settings__list-item-value.t-right(id="phone" type="text" name="phone" value="+7 (000) 000-00-00") - input(type="file", name="avatar", hidden) - - .user-settings__t - button.link(type="button") Save settings diff --git a/src/pages/user-settings.pug b/src/pages/user-settings.pug deleted file mode 100644 index 5e86ffffa..000000000 --- a/src/pages/user-settings.pug +++ /dev/null @@ -1,43 +0,0 @@ -extends ../layout/main.pug - -block meta - - const pageName = 'User settings'; - - title= pageName - meta(name='description', content=pageName) - -block content - - .modal - .modal__inner - button.modal__close(title="Close modal") - .modal__content - section.user-settings - .user-settings__f - .user-settings__avatar - h2.user-settings__name User name - - .user-settings__s - ul.user-settings__list - li.user-settings__list-item - span.user-settings__list-item-name Nickname: - span.user-settings__list-item-value user name - li.user-settings__list-item - span.user-settings__list-item-name First name: - span.user-settings__list-item-value first name - li.user-settings__list-item - span.user-settings__list-item-name Second name: - span.user-settings__list-item-value second name - li.user-settings__list-item - span.user-settings__list-item-name Email: - span.user-settings__list-item-value test@test.ru - li.user-settings__list-item - span.user-settings__list-item-name Phone: - span.user-settings__list-item-value +7 (000) 000-00-00 - li.user-settings__list-item - span.user-settings__list-item-name Login: - span.user-settings__list-item-value mylogin - - .user-settings__t - button.link(type="button") Change settings - button.link(type="button") Change password diff --git a/src/registration/index.html b/src/registration/index.html new file mode 100644 index 000000000..ba9f4ce2d --- /dev/null +++ b/src/registration/index.html @@ -0,0 +1,12 @@ + + + + + + Registration page + + +
+ + + diff --git a/src/registration/registration.ts b/src/registration/registration.ts new file mode 100644 index 000000000..a6640ec49 --- /dev/null +++ b/src/registration/registration.ts @@ -0,0 +1,8 @@ +import { render } from "../utils/index"; +import { Registration } from "../pages/index"; + +import "../scss/index.scss"; + +const page = new Registration(); + +render(page); diff --git a/src/scss/index.scss b/src/scss/index.scss new file mode 100644 index 000000000..d0a275cb0 --- /dev/null +++ b/src/scss/index.scss @@ -0,0 +1,3 @@ +@import "./reset.scss"; +@import "./main.scss"; +@import "./additions.scss"; diff --git a/src/scss/main.scss b/src/scss/main.scss index 5d971f5dc..eaebafa48 100644 --- a/src/scss/main.scss +++ b/src/scss/main.scss @@ -35,6 +35,7 @@ --h-indent: 15px; --b-radius: 5px; --on-focus: 0 0 0 2px var(--m-color-1); + --on-error: 0 0 0 2px tomato; --m-color-1: #784dd4; --m-color-1-1: #5e36b2; @@ -199,6 +200,18 @@ body { font-weight: 500; font-size: 15px; + .field-text, + > div:first-child { + width: 100%; + } + + .field-text { + display: flex; + justify-content: space-between; + align-content: center; + flex-wrap: wrap; + } + & + & { margin-top: 15px; } @@ -209,6 +222,10 @@ body { &__list-item-value { color: var(--t-l-grey-1); + + @at-root .field-text--invalid & { + color: tomato; + } } &__t { @@ -398,6 +415,7 @@ body { width: 60px; height: 60px; border-radius: 50%; + background-color: var(--t-l-grey-3); &__img { width: 100%; @@ -519,9 +537,13 @@ body { gap: var(--indent-h); width: 100%; - .field-text { + > div:first-child { width: 75%; } + + .field-text { + width: 100%; + } } &__conversation { @@ -563,3 +585,34 @@ body { gap: 5px var(--indent-h); } } + +/* ----- .form ----- */ + +.form { + $block-name: &; + + margin-left: auto; + margin-right: auto; + padding: 50px; + width: 400px; + max-width: 100%; + border-radius: var(--b-radius); + background-color: var(--bg-whiteSmoke); + + &__title { + margin-bottom: 70px; + font-size: 35px; + font-weight: 500; + text-align: center; + } + + &__field { + & + & { + margin-top: 20px; + } + + & + &--accent { + margin-top: 40px; + } + } +} diff --git a/src/scss/style.scss b/src/scss/style.scss deleted file mode 100644 index 8453c1f8f..000000000 --- a/src/scss/style.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import "./reset.scss"; - -@import "./main.scss"; - -@import "../components/btn/btn.scss"; -@import "../components/field-text/field-text.scss"; -@import "../components/form/form.scss"; - -@import "./additions.scss"; diff --git a/src/user-settings/index.html b/src/user-settings/index.html new file mode 100644 index 000000000..3dc836112 --- /dev/null +++ b/src/user-settings/index.html @@ -0,0 +1,12 @@ + + + + + + User settings page + + +
+ + + diff --git a/src/user-settings/user-settings.ts b/src/user-settings/user-settings.ts new file mode 100644 index 000000000..24f43142a --- /dev/null +++ b/src/user-settings/user-settings.ts @@ -0,0 +1,8 @@ +import { render } from "../utils/index"; +import { UserSettings } from "../pages/index"; + +import "../scss/index.scss"; + +const page = new UserSettings(); + +render(page); diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 000000000..bb2b1416b --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,5 @@ +import render from "./render"; + +export { + render, +} diff --git a/src/utils/render.ts b/src/utils/render.ts new file mode 100644 index 000000000..fbc45d394 --- /dev/null +++ b/src/utils/render.ts @@ -0,0 +1,11 @@ +type Page = { + getContent: Function +} + +export default function render(page: Page) { + document.addEventListener('DOMContentLoaded', () => { + const app = document.getElementById('app'); + + app?.append(page.getContent()); + }) +} diff --git a/tsconfig.json b/tsconfig.json index 9db061ae1..dec7251c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,6 @@ "sourceMap": true, "strictNullChecks": true, }, - "include": ["src"] + "include": ["src", "types/pug.d.ts"], + "exclude": ["node_modules", "build", "static"] } diff --git a/types/pug.d.ts b/types/pug.d.ts new file mode 100644 index 000000000..1485f02a0 --- /dev/null +++ b/types/pug.d.ts @@ -0,0 +1 @@ +declare module "*.pug"; diff --git a/vite.config.ts b/vite.config.ts index 6bf5a05d4..1f6e297fe 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,12 +4,26 @@ import pugPrecompile from './vite-plugin-pug-precompile'; const root = resolve(__dirname, 'src'); +console.log(__dirname); + export default defineConfig({ root, base: '', build: { outDir: resolve(__dirname, 'build'), emptyOutDir: true, + rollupOptions: { + input: { + index: resolve(__dirname, 'src/index.html'), + registration: resolve(__dirname, 'src/registration/index.html'), + page404: resolve(__dirname, 'src/404/index.html'), + page500: resolve(__dirname, 'src/500/index.html'), + chatsAndchat: resolve(__dirname, 'src/chats-and-chat/index.html'), + userSettings: resolve(__dirname, 'src/user-settings/index.html'), + editingSettings: resolve(__dirname, 'src/editing-settings/index.html'), + editingPassword: resolve(__dirname, 'src/editing-password/index.html'), + }, + }, }, publicDir: resolve(__dirname, 'static'), plugins: [pugPrecompile()],