From 5f0a9a0ae8460cdaa923d82031488499aed09902 Mon Sep 17 00:00:00 2001 From: Ilya Andriyanov Date: Wed, 6 Nov 2024 05:16:47 +0300 Subject: [PATCH 01/11] feature: vacancy-page --- .../ButtonContainer/ButtonContainer.js | 18 +++ .../ButtonContainer/ButtonContainerView.js | 52 ++++++++ .../vacancy-article__button-container.hbs | 13 ++ .../VacancyArticle/VacancyArticle.js | 38 ++++++ .../VacancyArticleController.js | 19 +++ .../VacancyArticle/VacancyArticleModel.js | 24 ++++ .../VacancyArticle/VacancyArticleView.js | 41 ++++++ .../VacancyArticle/vacancy-article.hbs | 16 +++ src/Pages/VacancyPage/VacancyPage.js | 61 +++++++++ .../VacancyPage/VacancyPageController.js | 11 ++ src/Pages/VacancyPage/VacancyPageModel.js | 3 + src/Pages/VacancyPage/VacancyPageView.js | 20 +++ src/Pages/VacancyPage/vacancy-page.hbs | 12 ++ src/css/index.css | 11 ++ src/css/vacancies.css | 11 -- src/css/vacancy.css | 121 ++++++++++++++++++ src/index.js | 2 + src/modules/Api/Api.js | 17 +++ src/modules/Events/Events.js | 4 + src/modules/Handlebars/Handlebars.js | 1 + src/modules/ObjectUtils/Zip.js | 31 +++++ src/modules/UrlUtils/UrlUtils.js | 1 + src/modules/models/Vacancy.js | 17 +++ 23 files changed, 533 insertions(+), 11 deletions(-) create mode 100644 src/Components/VacancyArticle/ButtonContainer/ButtonContainer.js create mode 100644 src/Components/VacancyArticle/ButtonContainer/ButtonContainerView.js create mode 100644 src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs create mode 100644 src/Components/VacancyArticle/VacancyArticle.js create mode 100644 src/Components/VacancyArticle/VacancyArticleController.js create mode 100644 src/Components/VacancyArticle/VacancyArticleModel.js create mode 100644 src/Components/VacancyArticle/VacancyArticleView.js create mode 100644 src/Components/VacancyArticle/vacancy-article.hbs create mode 100644 src/Pages/VacancyPage/VacancyPage.js create mode 100644 src/Pages/VacancyPage/VacancyPageController.js create mode 100644 src/Pages/VacancyPage/VacancyPageModel.js create mode 100644 src/Pages/VacancyPage/VacancyPageView.js create mode 100644 src/Pages/VacancyPage/vacancy-page.hbs create mode 100644 src/css/vacancy.css create mode 100644 src/modules/ObjectUtils/Zip.js create mode 100644 src/modules/models/Vacancy.js diff --git a/src/Components/VacancyArticle/ButtonContainer/ButtonContainer.js b/src/Components/VacancyArticle/ButtonContainer/ButtonContainer.js new file mode 100644 index 0000000..0fbedad --- /dev/null +++ b/src/Components/VacancyArticle/ButtonContainer/ButtonContainer.js @@ -0,0 +1,18 @@ +import { + Component, + ComponentController, + ComponentModel, +} from '../../../modules/Components/Component.js'; +import { ButtonContainerView } from './ButtonContainerView.js'; + +export class ButtonContainer extends Component { + constructor({ isOwner, isApplicant, ownerId, vacancyId, existingElement }) { + super({ + modelClass: ComponentModel, + viewClass: ButtonContainerView, + controllerClass: ComponentController, + viewParams: { isOwner, isApplicant, ownerId, vacancyId }, + existingElement, + }); + } +} diff --git a/src/Components/VacancyArticle/ButtonContainer/ButtonContainerView.js b/src/Components/VacancyArticle/ButtonContainer/ButtonContainerView.js new file mode 100644 index 0000000..c610848 --- /dev/null +++ b/src/Components/VacancyArticle/ButtonContainer/ButtonContainerView.js @@ -0,0 +1,52 @@ +import { VACANCY_APPLY, VACANCY_DELETE, VACANCY_EDIT } from '../../../modules/Events/Events.js'; +import { addEventListeners } from '../../../modules/Events/EventUtils.js'; +import { ComponentView } from '/src/modules/Components/Component.js'; +import eventBus from '/src/modules/Events/EventBus.js'; + +export class ButtonContainerView extends ComponentView { + #applyButton; + #editButton; + #deleteButton; + #vacancyId; + constructor({ isOwner, isApplicant, ownerId, vacancyId }, existingElement) { + super({ + renderParams: { isOwner, isApplicant, ownerId }, + existingElement, + templateName: 'vacancy-article__button-container.hbs', + }); + this.#vacancyId = vacancyId; + if (isApplicant) { + this.#applyButton = this._html.querySelector('.vacancy-article__apply-button'); + this._eventListeners.push({ + event: 'click', + object: this.#applyButton, + listener: (ev) => { + ev.preventDefault(); + eventBus.emit(VACANCY_APPLY, { vacancyId: this.#vacancyId }); + }, + }); + } else if (isOwner) { + this.#editButton = this._html.querySelector('.vacancy-article__edit-button'); + this.#deleteButton = this._html.querySelector('.vacancy-article__delete-button'); + this._eventListeners.push( + { + event: 'click', + object: this.#editButton, + listener: function (ev) { + ev.preventDefault(); + eventBus.emit(VACANCY_EDIT, { vacancyId: this.#vacancyId }); + }.bind(this), + }, + { + event: 'click', + object: this.#deleteButton, + listener: function (ev) { + ev.preventDefault(); + eventBus.emit(VACANCY_DELETE, { vacancyId: this.#vacancyId }); + }.bind(this), + }, + ); + } + addEventListeners(this._eventListeners); + } +} diff --git a/src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs b/src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs new file mode 100644 index 0000000..c34e92e --- /dev/null +++ b/src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs @@ -0,0 +1,13 @@ +
+{{#if isApplicant}} + + Другие вакансии +{{else}} + {{#if isOwner}} + + + {{/if}} +{{/if}} +
+
\ No newline at end of file diff --git a/src/Components/VacancyArticle/VacancyArticle.js b/src/Components/VacancyArticle/VacancyArticle.js new file mode 100644 index 0000000..64ac9bd --- /dev/null +++ b/src/Components/VacancyArticle/VacancyArticle.js @@ -0,0 +1,38 @@ +import { Component } from '../../modules/Components/Component.js'; +import { VacancyArticleView } from './VacancyArticleView.js'; +import { VacancyArticleModel } from './VacancyArticleModel.js'; +import { VacancyArticleController } from './VacancyArticleController.js'; +import { ButtonContainer } from './ButtonContainer/ButtonContainer.js'; +import USER_TYPE from '../../modules/UserSession/UserType.js'; + +export class VacancyArticle extends Component { + constructor({ elementClass, vacancyId, userId, userType }) { + super({ + modelClass: VacancyArticleModel, + modelParams: { vacancyId }, + viewClass: VacancyArticleView, + controllerClass: VacancyArticleController, + viewParams: { elementClass }, + }); + this._userId = userId; + this._userType = userType; + this._vacancyId = vacancyId; + } + + async makeButtons() { + const modelData = await this._controller.fetchData(); + this._buttonContainer = new ButtonContainer({ + isOwner: modelData.employerId === this._userId && this._userType === USER_TYPE.EMPLOYER, + isApplicant: this._userType === USER_TYPE.APPLICANT, + ownerId: modelData.employerId, + vacancyId: this._vacancyId, + }); + this._children.push(this._buttonContainer); + this._controller.addButtonContainer(this._buttonContainer); + this._controller.renderData(); + } + + async getEmployerId() { + return this._model.getEmployerId(); + } +} diff --git a/src/Components/VacancyArticle/VacancyArticleController.js b/src/Components/VacancyArticle/VacancyArticleController.js new file mode 100644 index 0000000..f033607 --- /dev/null +++ b/src/Components/VacancyArticle/VacancyArticleController.js @@ -0,0 +1,19 @@ +import { ComponentController } from '../../modules/Components/Component.js'; + +export class VacancyArticleController extends ComponentController { + constructor(model, view, component) { + super(model, view, component); + } + + async fetchData() { + return this._model.getVacancyData(); + } + + async renderData() { + return this._view.renderData(await this._model.getVacancyData()); + } + + addButtonContainer(container) { + this._view.addButtonContainer(container.render()); + } +} diff --git a/src/Components/VacancyArticle/VacancyArticleModel.js b/src/Components/VacancyArticle/VacancyArticleModel.js new file mode 100644 index 0000000..22e8733 --- /dev/null +++ b/src/Components/VacancyArticle/VacancyArticleModel.js @@ -0,0 +1,24 @@ +import { ComponentModel } from '../../modules/Components/Component.js'; +import { Api } from '../../modules/Api/Api.js'; +import { Vacancy } from '../../modules/models/Vacancy.js'; + +export class VacancyArticleModel extends ComponentModel { + #vacancyData; + #vacancyId; + + constructor({ vacancyId }) { + super(); + this.#vacancyId = vacancyId; + this.#vacancyData = Api.getVacancyById({ id: this.#vacancyId }).then( + (data) => new Vacancy(data), + ); + } + + async getVacancyData() { + return this.#vacancyData; + } + + async getEmployerId() { + return this.#vacancyData.employerId; + } +} diff --git a/src/Components/VacancyArticle/VacancyArticleView.js b/src/Components/VacancyArticle/VacancyArticleView.js new file mode 100644 index 0000000..201aedf --- /dev/null +++ b/src/Components/VacancyArticle/VacancyArticleView.js @@ -0,0 +1,41 @@ +import { ComponentView } from '../../modules/Components/Component.js'; + +export class VacancyArticleView extends ComponentView { + constructor({ elementClass, isOwner, isApplicant }, existingElement) { + super({ + renderParams: { + elementClass, + isOwner, + isApplicant, + }, + templateName: 'vacancy-article.hbs', + existingElement, + }); + this._avatar = this._html.querySelector('.vacancy-article__company-picture'); + this._position = this._html.querySelector('.vacancy-summary__header'); + this._companyName = this._html.querySelector('.vacancy-summary__company-name'); + this._salary = this._html.querySelector('.vacancy-summary__salary'); + this._workType = this._html.querySelector('.vacancy-summary__work-type'); + this._description = this._html.querySelector('.vacancy-article__description'); + } + + renderData({ avatar, position, companyName, salary, workType, description, updatedAt }) { + this._avatar.href = avatar; + this._position.innerText = position; + this._companyName.innerText = companyName; + this._salary.innerText = salary ? `${salary} руб.` : 'Не указана'; + this._workType.innerText = workType; + this._description.innerText = description; + this._updatedAt.innerText = `последнее обновление: ${updatedAt.toLocaleDateString('ru-RU', { + weekday: 'short', + day: 'numeric', + month: 'numeric', + year: 'numeric', + })}`; + } + + addButtonContainer(containerRender) { + this._html.appendChild(containerRender); + this._updatedAt = this._html.querySelector('.vacancy-article__created-at'); + } +} diff --git a/src/Components/VacancyArticle/vacancy-article.hbs b/src/Components/VacancyArticle/vacancy-article.hbs new file mode 100644 index 0000000..141f47b --- /dev/null +++ b/src/Components/VacancyArticle/vacancy-article.hbs @@ -0,0 +1,16 @@ +
+
+ +
+

+
+
+ + +
+
+ +
+
+
+
\ No newline at end of file diff --git a/src/Pages/VacancyPage/VacancyPage.js b/src/Pages/VacancyPage/VacancyPage.js new file mode 100644 index 0000000..6340658 --- /dev/null +++ b/src/Pages/VacancyPage/VacancyPage.js @@ -0,0 +1,61 @@ +import { Header } from '../../Components/Header/Header.js'; +import { ProfileMinicard } from '../../Components/ProfileMinicard/ProfileMinicard.js'; +import state from '../../modules/AppState/AppState.js'; +import { Page } from '../../modules/Page/Page.js'; +import { NotFoundError } from '../../modules/Router/Router.js'; +import USER_TYPE from '../../modules/UserSession/UserType.js'; +import { VacancyPageController } from './VacancyPageController.js'; +import { VacancyPageModel } from './VacancyPageModel.js'; +import { VacancyPageView } from './VacancyPageView.js'; +import { VacancyArticle } from '../../Components/VacancyArticle/VacancyArticle.js'; + +export class VacancyPage extends Page { + #vacancyId; + #userType; + #userId; + #employerId; + + VACANCY_ID_PARAM = 'id'; + + constructor({ url }) { + super({ + url, + modelClass: VacancyPageModel, + viewClass: VacancyPageView, + controllerClass: VacancyPageController, + viewParams: Header.getViewParams(), + }); + this.#vacancyId = +url.searchParams.get(this.VACANCY_ID_PARAM); + if (!this.#vacancyId) { + throw new NotFoundError(); + } + this.#userType = state.userSession.userType; + this.#userId = state.userSession.userId; + } + + postRenderInit() { + this._header = new Header({ + existingElement: this._view.header, + }); + this._children.push(this._header); + this._vacancyArticle = new VacancyArticle({ + elementClass: '.vacancy-page__vacancy-article', + userId: this.#userId, + vacancyId: this.#vacancyId, + userType: this.#userType, + }); + this._vacancyArticle.makeButtons().then(async () => { + this.#employerId = await this._vacancyArticle.getEmployerId(); + this._controller.addVacancyArticle(this._vacancyArticle); + this._children.push(this._vacancyArticle); + if (this.#userType !== USER_TYPE.EMPLOYER) { + this._profileMinicard = new ProfileMinicard({ + userId: this.#employerId, + userType: USER_TYPE.EMPLOYER, + existingElement: this._view.profileMinicard, + }); + this._children.push(this._profileMinicard); + } + }); + } +} diff --git a/src/Pages/VacancyPage/VacancyPageController.js b/src/Pages/VacancyPage/VacancyPageController.js new file mode 100644 index 0000000..1443919 --- /dev/null +++ b/src/Pages/VacancyPage/VacancyPageController.js @@ -0,0 +1,11 @@ +import { PageController } from '../../modules/Page/Page.js'; + +export class VacancyPageController extends PageController { + constructor(model, view, component) { + super(model, view, component); + } + + addVacancyArticle(vacancyArticle) { + this._view.addVacancyArticle(vacancyArticle.render()); + } +} diff --git a/src/Pages/VacancyPage/VacancyPageModel.js b/src/Pages/VacancyPage/VacancyPageModel.js new file mode 100644 index 0000000..a8cbe8a --- /dev/null +++ b/src/Pages/VacancyPage/VacancyPageModel.js @@ -0,0 +1,3 @@ +import { PageModel } from '../../modules/Page/Page.js'; + +export class VacancyPageModel extends PageModel {} diff --git a/src/Pages/VacancyPage/VacancyPageView.js b/src/Pages/VacancyPage/VacancyPageView.js new file mode 100644 index 0000000..c38cc35 --- /dev/null +++ b/src/Pages/VacancyPage/VacancyPageView.js @@ -0,0 +1,20 @@ +import { PageView } from '../../modules/Page/Page.js'; + +export class VacancyPageView extends PageView { + constructor(renderParams) { + super({ + templateName: 'vacancy-page.hbs', + renderParams: renderParams, + }); + this.header = this._html.querySelector('.header'); + if (renderParams.isApplicant) { + this.profileMinicard = this._html.querySelector('.vacancy-page__profile-minicard'); + } + this.vacancyContainer = this._html.querySelector('.vacancy-page__vacancy-container'); + } + + addVacancyArticle(render) { + this.vacancyContainer.innerHTML = ''; + this.vacancyContainer.appendChild(render); + } +} diff --git a/src/Pages/VacancyPage/vacancy-page.hbs b/src/Pages/VacancyPage/vacancy-page.hbs new file mode 100644 index 0000000..7627019 --- /dev/null +++ b/src/Pages/VacancyPage/vacancy-page.hbs @@ -0,0 +1,12 @@ +
+ {{> header}} +
+
+ {{#if isApplicant}} + {{> profile-minicard elementClass="vacancy-page__profile-minicard"}} + {{/if}} +
+
+
+
+
\ No newline at end of file diff --git a/src/css/index.css b/src/css/index.css index 282e973..b13c4ba 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -6,6 +6,7 @@ @import url(registration.css); @import url(forms.css); @import url(profile.css); +@import url(vacancy.css); :root { --grey-very-dark: #1b1b1b; @@ -247,3 +248,13 @@ body { .hidden { display: none !important; } + +.pill { + text-align: center; +} + +.pill_main { + border-radius: 14px; + padding: 4px 12px; + background-color: var(--color-background-700); +} diff --git a/src/css/vacancies.css b/src/css/vacancies.css index 6196293..93a2536 100644 --- a/src/css/vacancies.css +++ b/src/css/vacancies.css @@ -117,14 +117,3 @@ background-color: var(--color-background-800); color: white; } - -/* vacancy-summary component */ - -.vacancy-summary { - display: flex; - flex-direction: column; -} -.vacancy-summary__header { - margin-top: 0px; - margin-bottom: 8px; -} diff --git a/src/css/vacancy.css b/src/css/vacancy.css new file mode 100644 index 0000000..111df51 --- /dev/null +++ b/src/css/vacancy.css @@ -0,0 +1,121 @@ +.vacancy-page { + display: grid; + align-items: start; + margin: auto; + width: 1280px; + padding-top: 32px; + grid-template-columns: 1fr 3fr; +} + +.vacancy-page__profile-minicard { + background-color: var(--color-background-900); + border-radius: 25px; + padding: 16px 36px; + color: var(--color-main-100); +} + +.vacancy-page__vacancy-container { + margin-left: 32px; + border-radius: 25px; + background-color: var(--color-background-900); +} + +/* Vacancy article */ + +.vacancy-article { + display: flex; + flex-direction: column; + align-items: start; +} + +.vacancy-article__header-container { + display: flex; + flex-direction: row; + justify-content: start; + align-items: center; + padding: 32px; +} + +.vacancy-article__company-picture { + width: 128px; + height: 128px; +} + +.vacancy-article__header { + display: flex; + flex-direction: column; + justify-content: start; + margin-left: 20px; +} + +.vacancy-article__description { + padding: 32px 32px 0px 32px; + font-size: var(--text-size-5); + line-height: 1.5; + color: var(--colo-main-100); +} + +.vacancy-article__footer { + margin-top: 10px; +} + +.vacancy-article__apply-button { + margin-right: 10px; + box-shadow: none; + padding: 4px 12px; +} + +.vacancy-article__created-at { + color: var(--color-main-100); + opacity: 0.7; + text-decoration: none; + text-transform: lowercase; +} + +.vacancy-article_theme-dark { + background-color: var(--color-background-800); + color: var(--color-main-100); +} + +.vacancy-article__divider { + display: inline-block; + background-color: var(--color-background-100); + height: 1px; + width: 100%; +} + +.vacancy-article__button-container { + display: flex; + align-items: baseline; + gap: 8px; + padding: 32px 32px; + justify-content: start; +} + +.vacancy-article__apply-button { + padding: 10px 20px; +} + +/* vacancy-summary component */ + +.vacancy-summary { + display: flex; + flex-direction: column; + color: var(--color-main-100); +} + +.vacancy-summary__header { + font-size: var(--text-size-10); + margin-top: 0px; + margin-bottom: 8px; +} + +.vacancy-summary__company-name { + font-size: var(--text-size-5); + margin-bottom: 8px; +} + +.vacancy-summary__salary { + font-size: var(--text-size-4); + margin-right: 20px; +} diff --git a/src/index.js b/src/index.js index 366777f..7fb32d0 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ import { LoginPage } from './Pages/LoginPage/LoginPage.js'; import { RegistrationPage } from './Pages/RegistrationPage/RegistrationPage.js'; import { VacanciesPage } from './Pages/VacanciesPage/VacanciesPage.js'; import { ProfilePage } from './Pages/ProfilePage/ProfilePage.js'; +import { VacancyPage } from './Pages/VacancyPage/VacancyPage.js'; handlebarsInit(); @@ -13,6 +14,7 @@ router.addRoute('/login', LoginPage); router.addRoute('/registration', RegistrationPage); router.addRoute('/me', ProfilePage); router.addRoute('/profile', ProfilePage); +router.addRoute('/vacancy', VacancyPage); appState.userSession.checkAuthorization().finally(() => { router.start(); diff --git a/src/modules/Api/Api.js b/src/modules/Api/Api.js index 103d6a1..170bec7 100644 --- a/src/modules/Api/Api.js +++ b/src/modules/Api/Api.js @@ -192,6 +192,23 @@ export class Api { return response.ok; }; + static getVacancyById = async ({ id }) => { + console.log(`getVacancyById: ${id}`); + return { + id: 3, + employer: 1, + salary: 10000, + companyName: 'ООО Рога и Копыта', + position: 'Инженер', + location: 'Москва', + description: 'Небольшой коллектив ищет близкого по духу инженера для работы', + workType: 'Разовая', + avatar: '', + createdAt: '2024-10-10', + updatedAt: '2024-10-10', + }; + }; + static vacanciesFeed = async ({ offset, num }) => { return fetchCorsJson( backendApi.get('vacancies') + diff --git a/src/modules/Events/Events.js b/src/modules/Events/Events.js index ecd29b7..a0f006f 100644 --- a/src/modules/Events/Events.js +++ b/src/modules/Events/Events.js @@ -21,3 +21,7 @@ export const RESET_FORM = 'reset form'; export const USER_UPDATED = 'user updated'; export const MINICARD_DELETE = 'minicard delete'; + +export const VACANCY_APPLY = 'vacancy apply'; +export const VACANCY_EDIT = 'vacancy edit'; +export const VACANCY_DELETE = 'vacancy delete'; diff --git a/src/modules/Handlebars/Handlebars.js b/src/modules/Handlebars/Handlebars.js index a1cee01..3930099 100644 --- a/src/modules/Handlebars/Handlebars.js +++ b/src/modules/Handlebars/Handlebars.js @@ -31,4 +31,5 @@ export const handlebarsInit = () => { 'employer-profile-form', Handlebars.templates['employer-profile-form.hbs'], ); + Handlebars.registerPartial('vacancy-article', Handlebars.templates['vacancy-article.hbs']); }; diff --git a/src/modules/ObjectUtils/Zip.js b/src/modules/ObjectUtils/Zip.js new file mode 100644 index 0000000..235d531 --- /dev/null +++ b/src/modules/ObjectUtils/Zip.js @@ -0,0 +1,31 @@ +/** + * Zip multiple objects into one object containing all their attributes. If + * some of the objects have same attribute, value of the first object with + * such an attribute will be taken. + * Note that subobjects are not deepcopied. + * @param {...Object} objects - The objects to be zipped + * @throws {TypeError} All of the arguments must be objects + */ +export const zip = (...objects) => { + if (!objects.every(isObject)) { + throw new TypeError('All of the arguments must be objects'); + } + + return objects.reduce((zippedObject, currentObject) => { + Object.entries(currentObject).forEach(([key, value]) => { + if (!Object.prototype.hasOwnProperty.call(zippedObject, key)) { + zippedObject[key] = value; + } + }); + return zippedObject; + }, {}); +}; + +/** + * Check if input is object. + * Array is considered to be an object too. + * @param {*} potentialObject - a value to be checked + */ +const isObject = (potentialObject) => { + return Object.prototype.toString.call(potentialObject) === '[object Object]'; +}; diff --git a/src/modules/UrlUtils/UrlUtils.js b/src/modules/UrlUtils/UrlUtils.js index f437767..9027e5d 100644 --- a/src/modules/UrlUtils/UrlUtils.js +++ b/src/modules/UrlUtils/UrlUtils.js @@ -5,6 +5,7 @@ const urls = { register: '/registration', vacancies: '/', myProfile: '/me', + profile: '/profile', }; const knownUrls = new Map(Object.entries(urls)); diff --git a/src/modules/models/Vacancy.js b/src/modules/models/Vacancy.js new file mode 100644 index 0000000..7575c6a --- /dev/null +++ b/src/modules/models/Vacancy.js @@ -0,0 +1,17 @@ +import { resolveStatic } from '../UrlUtils/UrlUtils.js'; + +export class Vacancy { + constructor(backendResponse) { + this.id = backendResponse.id; + this.employerId = backendResponse.employer; + this.companyName = backendResponse.companyName; + this.salary = backendResponse.salary; + this.position = backendResponse.position; + this.location = backendResponse.location; + this.description = backendResponse.description; + this.workType = backendResponse.workType; + this.avatar = backendResponse.avatar || resolveStatic('img/company-icon.svg'); + this.createdAt = new Date(backendResponse.createdAt); + this.updatedAt = new Date(backendResponse.updatedAt); + } +} From eed209cdd71f4938144bfe9aba592bfc31520633 Mon Sep 17 00:00:00 2001 From: Ilya Andriyanov Date: Wed, 6 Nov 2024 10:47:27 +0300 Subject: [PATCH 02/11] feature: working vacancy page --- src/Components/AppliersList/AppliersList.js | 21 +++++++++++ .../AppliersList/AppliersListController.js | 12 +++++++ .../AppliersList/AppliersListModel.js | 26 ++++++++++++++ .../AppliersList/AppliersListView.js | 27 ++++++++++++++ src/Components/AppliersList/appliers-list.hbs | 6 ++++ .../VacancyArticle/VacancyArticleModel.js | 3 +- src/Pages/VacancyPage/VacancyPage.js | 9 +++++ src/Pages/VacancyPage/VacancyPageView.js | 6 ++++ src/css/vacancy.css | 36 ++++++++++++++++++- src/modules/Api/Api.js | 20 +++++++++-- src/server/server.mjs | 2 +- 11 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 src/Components/AppliersList/AppliersList.js create mode 100644 src/Components/AppliersList/AppliersListController.js create mode 100644 src/Components/AppliersList/AppliersListModel.js create mode 100644 src/Components/AppliersList/AppliersListView.js create mode 100644 src/Components/AppliersList/appliers-list.hbs diff --git a/src/Components/AppliersList/AppliersList.js b/src/Components/AppliersList/AppliersList.js new file mode 100644 index 0000000..d8d24a1 --- /dev/null +++ b/src/Components/AppliersList/AppliersList.js @@ -0,0 +1,21 @@ +import { Component } from '../../modules/Components/Component.js'; +import { AppliersListModel } from './AppliersListModel.js'; +import { AppliersListController } from './AppliersListController.js'; +import { AppliersListView } from './AppliersListView.js'; +import { ListMixin } from '../Lists/List/ListMixin.js'; + +export class AppliersList extends Component { + constructor({ vacancyId, elementClass, existingElement }) { + super({ + modelClass: AppliersListModel, + controllerClass: AppliersListController, + viewClass: AppliersListView, + viewParams: { elementClass }, + modelParams: { vacancyId }, + existingElement, + }); + this._controller.fillList(); + } +} + +Object.assign(AppliersList.prototype, ListMixin); diff --git a/src/Components/AppliersList/AppliersListController.js b/src/Components/AppliersList/AppliersListController.js new file mode 100644 index 0000000..54f970a --- /dev/null +++ b/src/Components/AppliersList/AppliersListController.js @@ -0,0 +1,12 @@ +import { ComponentController } from '../../modules/Components/Component.js'; + +export class AppliersListController extends ComponentController { + constructor(model, view, component) { + super(model, view, component); + } + + async fillList() { + const appliers = await this._model.getItems(); + appliers.forEach((applier) => this._view.addListItem(applier)); + } +} diff --git a/src/Components/AppliersList/AppliersListModel.js b/src/Components/AppliersList/AppliersListModel.js new file mode 100644 index 0000000..29724a3 --- /dev/null +++ b/src/Components/AppliersList/AppliersListModel.js @@ -0,0 +1,26 @@ +import { ComponentModel } from '../../modules/Components/Component.js'; +import { Api } from '../../modules/Api/Api.js'; +import { Applicant } from '../../modules/models/Applicant.js'; + +export class AppliersListModel extends ComponentModel { + #vacancyId; + constructor({ vacancyId }) { + super(); + this.#vacancyId = vacancyId; + } + + async getItems() { + const peopleJson = await Api.getAppliersByVacancyId({ id: this.#vacancyId }); + const applicantObjects = peopleJson.reduce((applicantObjects, applicantJsonItem) => { + try { + const applicant = new Applicant(applicantJsonItem); + applicant.name = `${applicant.firstName} ${applicant.secondName}`; + applicantObjects.push(applicant); + return applicantObjects; + } catch { + return applicantObjects; + } + }, []); + return applicantObjects; + } +} diff --git a/src/Components/AppliersList/AppliersListView.js b/src/Components/AppliersList/AppliersListView.js new file mode 100644 index 0000000..be34993 --- /dev/null +++ b/src/Components/AppliersList/AppliersListView.js @@ -0,0 +1,27 @@ +import { ComponentView } from '../../modules/Components/Component.js'; +import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; +import USER_TYPE from '../../modules/UserSession/UserType.js'; + +export class AppliersListView extends ComponentView { + #listItems; + constructor(renderParams, existingElement) { + super({ + renderParams, + existingElement, + templateName: 'appliers-list.hbs', + }); + this.list = this._html.querySelector('.appliers-list__list'); + this.#listItems = []; + } + + addListItem({ id, name }) { + const listItem = document.createElement('li'); + const anchorElement = document.createElement('a'); + anchorElement.href = `${resolveUrl('profile')}?id=${id}&userType=${USER_TYPE.APPLICANT}`; + anchorElement.innerText = name; + anchorElement.classList.add('appliers-list__list-item'); + listItem.appendChild(anchorElement); + this.list.appendChild(listItem); + this.#listItems.push(listItem); + } +} diff --git a/src/Components/AppliersList/appliers-list.hbs b/src/Components/AppliersList/appliers-list.hbs new file mode 100644 index 0000000..55911bb --- /dev/null +++ b/src/Components/AppliersList/appliers-list.hbs @@ -0,0 +1,6 @@ +
+

Откликнулись

+
+
    +
+
\ No newline at end of file diff --git a/src/Components/VacancyArticle/VacancyArticleModel.js b/src/Components/VacancyArticle/VacancyArticleModel.js index 22e8733..85a0dd3 100644 --- a/src/Components/VacancyArticle/VacancyArticleModel.js +++ b/src/Components/VacancyArticle/VacancyArticleModel.js @@ -19,6 +19,7 @@ export class VacancyArticleModel extends ComponentModel { } async getEmployerId() { - return this.#vacancyData.employerId; + const vacancyData = await this.#vacancyData; + return vacancyData.employerId; } } diff --git a/src/Pages/VacancyPage/VacancyPage.js b/src/Pages/VacancyPage/VacancyPage.js index 6340658..fff18fc 100644 --- a/src/Pages/VacancyPage/VacancyPage.js +++ b/src/Pages/VacancyPage/VacancyPage.js @@ -8,6 +8,7 @@ import { VacancyPageController } from './VacancyPageController.js'; import { VacancyPageModel } from './VacancyPageModel.js'; import { VacancyPageView } from './VacancyPageView.js'; import { VacancyArticle } from '../../Components/VacancyArticle/VacancyArticle.js'; +import { AppliersList } from '../../Components/AppliersList/AppliersList.js'; export class VacancyPage extends Page { #vacancyId; @@ -56,6 +57,14 @@ export class VacancyPage extends Page { }); this._children.push(this._profileMinicard); } + if (this.#userType === USER_TYPE.EMPLOYER && this.#userId === this.#employerId) { + this._appliersList = new AppliersList({ + elementClass: 'vacancy-page__appliers-list', + vacancyId: this.#vacancyId, + }); + this._children.push(this._appliersList); + this._view.addAppliersList(this._appliersList.render()); + } }); } } diff --git a/src/Pages/VacancyPage/VacancyPageView.js b/src/Pages/VacancyPage/VacancyPageView.js index c38cc35..801d88b 100644 --- a/src/Pages/VacancyPage/VacancyPageView.js +++ b/src/Pages/VacancyPage/VacancyPageView.js @@ -11,10 +11,16 @@ export class VacancyPageView extends PageView { this.profileMinicard = this._html.querySelector('.vacancy-page__profile-minicard'); } this.vacancyContainer = this._html.querySelector('.vacancy-page__vacancy-container'); + this.sideColumn = this._html.querySelector('.vacancy-page__left-column'); } addVacancyArticle(render) { this.vacancyContainer.innerHTML = ''; this.vacancyContainer.appendChild(render); } + + addAppliersList(render) { + this.sideColumn.appendChild(render); + render.classList.remove('hidden'); + } } diff --git a/src/css/vacancy.css b/src/css/vacancy.css index 111df51..09606e9 100644 --- a/src/css/vacancy.css +++ b/src/css/vacancy.css @@ -52,7 +52,7 @@ padding: 32px 32px 0px 32px; font-size: var(--text-size-5); line-height: 1.5; - color: var(--colo-main-100); + color: var(--color-main-100); } .vacancy-article__footer { @@ -80,6 +80,7 @@ .vacancy-article__divider { display: inline-block; background-color: var(--color-background-100); + opacity: 0.3; height: 1px; width: 100%; } @@ -119,3 +120,36 @@ font-size: var(--text-size-4); margin-right: 20px; } + +.vacancy-page__appliers-list { + border-radius: 25px; + padding: 24px 0px; + background-color: var(--color-background-900); + color: var(--color-main-100); + font-size: var(--text-size-5); +} + +/* Appliers list */ + +.appliers-list__header { + margin: 0; + padding: 0 24px; + font-size: var(--text-size-9); +} + +.appliers-list__divider { + margin: 12px 0px; + width: 100%; + height: 1px; + opacity: 0.3; + background-color: var(--color-main-100); +} + +.appliers-list__list { + margin: 0; + line-height: 1.5; +} + +.appliers-list__list-item { + color: var(--color-main-200); +} diff --git a/src/modules/Api/Api.js b/src/modules/Api/Api.js index 170bec7..a6c0345 100644 --- a/src/modules/Api/Api.js +++ b/src/modules/Api/Api.js @@ -1,4 +1,4 @@ -const backendPrefix = 'http://127.0.0.1:8080/api/v1/'; +const backendPrefix = 'http://192.168.88.82:8080/api/v1/'; const backendApi = new Map( Object.entries({ authenticated: backendPrefix + 'authorized', @@ -196,7 +196,7 @@ export class Api { console.log(`getVacancyById: ${id}`); return { id: 3, - employer: 1, + employer: 2, salary: 10000, companyName: 'ООО Рога и Копыта', position: 'Инженер', @@ -209,6 +209,22 @@ export class Api { }; }; + static getAppliersByVacancyId = async ({ id }) => { + console.log(`GetAppliersByVacancyId: ${id}`); + return [ + { + id: 2, + firstName: 'Илья', + lastName: 'Андриянов', + }, + { + id: 1, + firstName: 'Иван', + lastName: 'Иванов', + }, + ]; + }; + static vacanciesFeed = async ({ offset, num }) => { return fetchCorsJson( backendApi.get('vacancies') + diff --git a/src/server/server.mjs b/src/server/server.mjs index e35d5d8..657d934 100644 --- a/src/server/server.mjs +++ b/src/server/server.mjs @@ -1,7 +1,7 @@ import { createServer } from 'node:http'; import { readFile } from 'node:fs'; -const hostname = '127.0.0.1'; +const hostname = '0.0.0.0'; const port = '8000'; const routingTemplates = { From 1639253ce2c923bfc177ec445306a4575be45a3f Mon Sep 17 00:00:00 2001 From: Ilya Andriyanov Date: Thu, 7 Nov 2024 22:55:37 +0300 Subject: [PATCH 03/11] feature: fully working vacancies --- .../AppliersList/AppliersListModel.js | 2 +- .../AppliersList/AppliersListView.js | 7 + .../EmployerProfileForm.js | 72 ---------- .../EmployerProfileFormController.js | 43 ------ .../EmployerProfileFormModel.js | 23 ---- .../EmployerProfileFormView.js | 31 ----- .../employer-company-form.hbs | 5 - .../FormInputs/CurrencyInput/CurrencyInput.js | 16 +++ .../CurrencyInput/CurrencyInputModel.js | 9 ++ .../LiteralInput/LiteralInputModel.js | 2 +- .../ValidatedInput/validated-input.hbs | 1 + .../EmployerVacancyListModel.js | 28 +++- src/Components/Lists/List/ListController.js | 9 +- src/Components/Minicard/MinicardView.js | 4 +- src/Components/Minicard/minicard.hbs | 2 +- .../VacancyArticleController.js | 28 ++++ .../VacancyArticle/VacancyArticleModel.js | 17 +++ src/Components/VacancyCard/VacancyCard.js | 4 +- src/Components/VacancyCard/VacancyCardView.js | 7 +- src/Components/VacancyCard/vacancy-card.hbs | 12 +- src/Components/VacancyForm/VacancyForm.js | 60 ++++++++ .../VacancyForm/VacancyFormController.js | 68 +++++++++ .../VacancyForm/VacancyFormModel.js | 54 ++++++++ src/Components/VacancyForm/VacancyFormView.js | 45 ++++++ src/Components/VacancyForm/vacancy-form.hbs | 19 +++ .../VacanciesPage/VacanciesPageController.js | 2 +- src/Pages/VacanciesPage/VacanciesPageModel.js | 23 ++-- src/Pages/VacancyEditPage/VacancyEditPage.js | 59 ++++++++ .../VacancyEditPageController.js | 11 ++ .../VacancyEditPage/VacancyEditPageModel.js | 3 + .../VacancyEditPage/VacancyEditPageView.js | 18 +++ .../VacancyEditPage/vacancy-page-edit.hbs | 8 ++ src/Pages/VacancyPage/VacancyPage.js | 52 +++---- src/Pages/VacancyPage/VacancyPageView.js | 3 +- src/Pages/VacancyPage/vacancy-page.hbs | 4 +- src/css/index.css | 1 + src/css/registration.css | 1 + src/css/vacancies.css | 4 +- src/css/vacancy.css | 60 ++++++++ src/index.html | 8 +- src/index.js | 26 +++- src/modules/Api/Api.js | 130 +++++++++++------- src/modules/Events/Events.js | 3 + src/modules/Handlebars/Handlebars.js | 1 + src/modules/Router/Router.js | 8 +- src/modules/UrlUtils/UrlUtils.js | 3 + src/modules/models/Vacancy.js | 2 +- src/server/server.mjs | 3 +- 48 files changed, 690 insertions(+), 311 deletions(-) delete mode 100644 src/Components/EmployerCompanyForm/EmployerProfileForm.js delete mode 100644 src/Components/EmployerCompanyForm/EmployerProfileFormController.js delete mode 100644 src/Components/EmployerCompanyForm/EmployerProfileFormModel.js delete mode 100644 src/Components/EmployerCompanyForm/EmployerProfileFormView.js delete mode 100644 src/Components/EmployerCompanyForm/employer-company-form.hbs create mode 100644 src/Components/FormInputs/CurrencyInput/CurrencyInput.js create mode 100644 src/Components/FormInputs/CurrencyInput/CurrencyInputModel.js create mode 100644 src/Components/VacancyForm/VacancyForm.js create mode 100644 src/Components/VacancyForm/VacancyFormController.js create mode 100644 src/Components/VacancyForm/VacancyFormModel.js create mode 100644 src/Components/VacancyForm/VacancyFormView.js create mode 100644 src/Components/VacancyForm/vacancy-form.hbs create mode 100644 src/Pages/VacancyEditPage/VacancyEditPage.js create mode 100644 src/Pages/VacancyEditPage/VacancyEditPageController.js create mode 100644 src/Pages/VacancyEditPage/VacancyEditPageModel.js create mode 100644 src/Pages/VacancyEditPage/VacancyEditPageView.js create mode 100644 src/Pages/VacancyEditPage/vacancy-page-edit.hbs diff --git a/src/Components/AppliersList/AppliersListModel.js b/src/Components/AppliersList/AppliersListModel.js index 29724a3..410d28a 100644 --- a/src/Components/AppliersList/AppliersListModel.js +++ b/src/Components/AppliersList/AppliersListModel.js @@ -10,7 +10,7 @@ export class AppliersListModel extends ComponentModel { } async getItems() { - const peopleJson = await Api.getAppliersByVacancyId({ id: this.#vacancyId }); + const peopleJson = (await Api.getAppliersByVacancyId({ id: this.#vacancyId })).subscribers; const applicantObjects = peopleJson.reduce((applicantObjects, applicantJsonItem) => { try { const applicant = new Applicant(applicantJsonItem); diff --git a/src/Components/AppliersList/AppliersListView.js b/src/Components/AppliersList/AppliersListView.js index be34993..e176f53 100644 --- a/src/Components/AppliersList/AppliersListView.js +++ b/src/Components/AppliersList/AppliersListView.js @@ -4,6 +4,7 @@ import USER_TYPE from '../../modules/UserSession/UserType.js'; export class AppliersListView extends ComponentView { #listItems; + #isEmpty; constructor(renderParams, existingElement) { super({ renderParams, @@ -12,9 +13,15 @@ export class AppliersListView extends ComponentView { }); this.list = this._html.querySelector('.appliers-list__list'); this.#listItems = []; + this.#isEmpty = true; + this.list.innerText = 'Пока никто не откликнулся'; } addListItem({ id, name }) { + if (this.#isEmpty) { + this.#isEmpty = false; + this.list.innerHTML = ''; + } const listItem = document.createElement('li'); const anchorElement = document.createElement('a'); anchorElement.href = `${resolveUrl('profile')}?id=${id}&userType=${USER_TYPE.APPLICANT}`; diff --git a/src/Components/EmployerCompanyForm/EmployerProfileForm.js b/src/Components/EmployerCompanyForm/EmployerProfileForm.js deleted file mode 100644 index 2d8767d..0000000 --- a/src/Components/EmployerCompanyForm/EmployerProfileForm.js +++ /dev/null @@ -1,72 +0,0 @@ -import { Component } from '../../modules/Components/Component.js'; -import { EmployerProfileFormView } from './EmployerProfileFormView.js'; -import { EmployerProfileFormModel } from './EmployerProfileFormModel.js'; -import { EmployerProfileFormController } from './EmployerProfileFormController.js'; -import { LiteralInput } from '/src/Components/FormInputs/LiteralInput/LiteralInput.js'; -import { CityInput } from '/src/Components/FormInputs/CityInput/CityInput.js'; -import { ValidatedTextArea } from '../FormInputs/ValidatedTextArea/ValidatedTextArea.js'; - -export class EmployerProfileForm extends Component { - constructor({ userId, elementClass, existingElement }) { - super({ - modelClass: EmployerProfileFormModel, - viewClass: EmployerProfileFormView, - controllerClass: EmployerProfileFormController, - modelParams: { userId }, - existingElement, - viewParams: { elementClass }, - }); - this._firstNameField = new LiteralInput({ - existingElement: this._view.firstNameField, - selfValidate: true, - }); - this._secondNameField = new LiteralInput({ - existingElement: this._view.secondNameField, - selfValidate: true, - }); - this._cityField = new CityInput({ - existingElement: this._view.cityField, - selfValidate: true, - }); - this._contactsField = new ValidatedTextArea({ - existingElement: this._view.contactsField, - selfValidate: true, - }); - this._children.push( - this._firstNameField, - this._secondNameField, - this._cityField, - this._contactsField, - ); - - this.reset(); - } - - get view() { - return this._view; - } - - enable() { - [this._firstNameField, this._secondNameField, this._cityField, this._contactsField].forEach( - (field) => { - field.controller.enable(); - }, - ); - } - - disable() { - [this._firstNameField, this._secondNameField, this._cityField, this._contactsField].forEach( - (field) => { - field.controller.disable(); - }, - ); - } - - reset() { - return this._controller.reset(); - } - - submit() { - return this._controller.submit(); - } -} diff --git a/src/Components/EmployerCompanyForm/EmployerProfileFormController.js b/src/Components/EmployerCompanyForm/EmployerProfileFormController.js deleted file mode 100644 index 3375312..0000000 --- a/src/Components/EmployerCompanyForm/EmployerProfileFormController.js +++ /dev/null @@ -1,43 +0,0 @@ -import { ComponentController } from '../../modules/Components/Component.js'; -import eventBus from '../../modules/Events/EventBus.js'; -import { USER_UPDATED } from '../../modules/Events/Events.js'; - -export class EmployerProfileFormController extends ComponentController { - constructor(model, view, controller) { - super(model, view, controller); - } - - _validate() { - return [ - this._component._firstNameField.controller.validateInput({ - callerView: this._component._firstNameField._view, - }), - - this._component._secondNameField.controller.validateInput({ - callerView: this._component._secondNameField._view, - }), - - this._component._cityField.controller.validateInput({ - callerView: this._component._cityField._view, - }), - - this._component._contactsField.controller.validateInput({ - callerView: this._component._contactsField._view, - }), - ].every((val) => val); - } - - submit() { - if (!this._validate() || !this._model.submit(this._view.getData())) { - return false; - } - eventBus.emit(USER_UPDATED); - return true; - } - - async reset() { - const oldData = await this._model.lastValidData; - this._view.renderData(oldData); - return true; - } -} diff --git a/src/Components/EmployerCompanyForm/EmployerProfileFormModel.js b/src/Components/EmployerCompanyForm/EmployerProfileFormModel.js deleted file mode 100644 index 09df162..0000000 --- a/src/Components/EmployerCompanyForm/EmployerProfileFormModel.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Api } from '../../modules/Api/Api.js'; -import { ComponentModel } from '../../modules/Components/Component.js'; - -export class EmployerProfileFormModel extends ComponentModel { - #lastValidData; - - constructor({ userId }) { - super(); - this.#lastValidData = Api.getApplicantById({ id: userId }); - } - - get lastValidData() { - return this.#lastValidData; - } - - async submit(formData) { - if (Api.updateEmployerProfile(formData)) { - this.#lastValidData = formData; - return true; - } - return false; - } -} diff --git a/src/Components/EmployerCompanyForm/EmployerProfileFormView.js b/src/Components/EmployerCompanyForm/EmployerProfileFormView.js deleted file mode 100644 index 0193739..0000000 --- a/src/Components/EmployerCompanyForm/EmployerProfileFormView.js +++ /dev/null @@ -1,31 +0,0 @@ -import { ComponentView } from '../../modules/Components/Component.js'; -import { getFormData } from '../../modules/FormUtils/FormUtils.js'; - -export class EmployerProfileFormView extends ComponentView { - constructor({ elementClass }, existingElement) { - super({ - renderParams: { elementClass }, - existingElement, - templateName: 'employer-profile-form.hbs', - }); - this.firstNameField = this._html.querySelector('.employer-profile-form__first-name'); - this.secondNameField = this._html.querySelector('.employer-profile-form__second-name'); - this.cityField = this._html.querySelector('.employer-profile-form__city'); - this.contactsField = this._html.querySelector('.employer-profile-form__contacts'); - } - - getData() { - return getFormData(this._html); - } - - getId() { - return 'employer-profile-form'; - } - - renderData({ firstName, secondName, city, contacts }) { - this.firstNameField.querySelector('.validated-input__input').value = firstName; - this.secondNameField.querySelector('.validated-input__input').value = secondName; - this.cityField.querySelector('.validated-input__input').value = city; - this.contactsField.querySelector('.validated-textarea__textarea').value = contacts; - } -} diff --git a/src/Components/EmployerCompanyForm/employer-company-form.hbs b/src/Components/EmployerCompanyForm/employer-company-form.hbs deleted file mode 100644 index 13bffdc..0000000 --- a/src/Components/EmployerCompanyForm/employer-company-form.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
- {{> validated-input formName="employer-company-form" elementName="first-name" inputName="firstName" inputCaption="Имя" inputType="text"}} - {{> validated-input formName="employer-company-form" elementName="city" inputName="city" inputCaption="Город" inputType="text"}} - {{> validated-textarea formName="employer-company-form" elementName="contacts" inputName="contacts" inputCaption="Контакты"}} -
\ No newline at end of file diff --git a/src/Components/FormInputs/CurrencyInput/CurrencyInput.js b/src/Components/FormInputs/CurrencyInput/CurrencyInput.js new file mode 100644 index 0000000..832ae20 --- /dev/null +++ b/src/Components/FormInputs/CurrencyInput/CurrencyInput.js @@ -0,0 +1,16 @@ +import { Component } from '../../../modules/Components/Component.js'; +import { ValidatedInputController } from '../ValidatedInput/ValidatedInputController.js'; +import { ValidatedInputView } from '../ValidatedInput/ValidatedInputView.js'; +import { CurrencyInputModel } from './CurrencyInputModel.js'; + +export class CurrencyInput extends Component { + constructor({ existingElement, selfValidate = false }) { + super({ + modelClass: CurrencyInputModel, + controllerClass: ValidatedInputController, + viewClass: ValidatedInputView, + existingElement, + controllerParams: { selfValidate }, + }); + } +} diff --git a/src/Components/FormInputs/CurrencyInput/CurrencyInputModel.js b/src/Components/FormInputs/CurrencyInput/CurrencyInputModel.js new file mode 100644 index 0000000..c244df4 --- /dev/null +++ b/src/Components/FormInputs/CurrencyInput/CurrencyInputModel.js @@ -0,0 +1,9 @@ +import { ValidatedInputModel } from '/src/Components/FormInputs/ValidatedInput/ValidatedInputModel.js'; + +export class CurrencyInputModel extends ValidatedInputModel { + validate(numberStr) { + numberStr = numberStr.trim(); + const matches = numberStr.match(/^[0-9]+$/); + return matches ? '' : 'Тут нужно ввести целое число'; + } +} diff --git a/src/Components/FormInputs/LiteralInput/LiteralInputModel.js b/src/Components/FormInputs/LiteralInput/LiteralInputModel.js index 5b89a06..b9b68b0 100644 --- a/src/Components/FormInputs/LiteralInput/LiteralInputModel.js +++ b/src/Components/FormInputs/LiteralInput/LiteralInputModel.js @@ -2,7 +2,7 @@ import { ComponentModel } from '../../../modules/Components/Component.js'; export class LiteralInputModel extends ComponentModel { validate(text) { - if (!text.match(/^[a-zA-zа-яА-я ]*$/)) { + if (!text.match(/^[a-zA-zа-яА-я\- ]*$/)) { return 'Здесь нужно вводить только буквы'; } return ''; diff --git a/src/Components/FormInputs/ValidatedInput/validated-input.hbs b/src/Components/FormInputs/ValidatedInput/validated-input.hbs index d28d2fd..5ca3741 100644 --- a/src/Components/FormInputs/ValidatedInput/validated-input.hbs +++ b/src/Components/FormInputs/ValidatedInput/validated-input.hbs @@ -8,6 +8,7 @@ class='validated-input__input text_roboto-regular' id='{{formName}}__{{elementName}}' name='{{inputName}}' + placeholder='{{inputPlaceholder}}' /> \ No newline at end of file diff --git a/src/Components/Lists/EmployerVacancyList/EmployerVacancyListModel.js b/src/Components/Lists/EmployerVacancyList/EmployerVacancyListModel.js index 46fb9e3..2713692 100644 --- a/src/Components/Lists/EmployerVacancyList/EmployerVacancyListModel.js +++ b/src/Components/Lists/EmployerVacancyList/EmployerVacancyListModel.js @@ -2,28 +2,36 @@ import { ComponentModel } from '../../../modules/Components/Component.js'; import { Api } from '../../../modules/Api/Api.js'; import { Minicard } from '../../Minicard/Minicard.js'; import { resolveUrl } from '../../../modules/UrlUtils/UrlUtils.js'; +import { Vacancy } from '../../../modules/models/Vacancy.js'; +import { VacancyPage } from '../../../Pages/VacancyPage/VacancyPage.js'; export class EmployerVacancyListModel extends ComponentModel { #userId; #isOwner; + #items; constructor({ userId, isListOwner }) { super(); this.#userId = userId; this.#isOwner = isListOwner; + this.#items = []; } async getItems() { const vacanciesJson = await Api.getEmployerVacancies({ id: this.#userId }); const vacanciesObjects = vacanciesJson.reduce((vacanciesObjects, vacancyJson) => { try { - const { id, position } = vacancyJson; + const vacancy = new Vacancy(vacancyJson); + this.#items.push(vacancy); + const urlSearchQuery = {}; + urlSearchQuery[`${VacancyPage.VACANCY_ID_PARAM}`] = vacancy.id; vacanciesObjects.push( new Minicard({ renderParams: { elementClass: 'employer-vacancy-list__minicard', - title: position, + title: vacancy.position, isCardOwner: this.#isOwner, - editButtonUrl: resolveUrl(`/vacancy/edit/${id}`), + goToLink: resolveUrl('vacancy', urlSearchQuery), + editButtonUrl: resolveUrl('editVacancy', urlSearchQuery), }, }), ); @@ -34,4 +42,18 @@ export class EmployerVacancyListModel extends ComponentModel { }, []); return vacanciesObjects; } + + async removeChild(vacancyArrId) { + if (vacancyArrId >= this.#items.length || vacancyArrId < 0) { + return false; + } + const vacancy = this.#items[vacancyArrId]; + try { + await Api.deleteVacancyById({ id: vacancy.id }); + return true; + } catch (err) { + console.log(err); + return false; + } + } } diff --git a/src/Components/Lists/List/ListController.js b/src/Components/Lists/List/ListController.js index 8bcf16f..7ec102f 100644 --- a/src/Components/Lists/List/ListController.js +++ b/src/Components/Lists/List/ListController.js @@ -20,11 +20,14 @@ export class ListController extends ComponentController { }); } - removeMinicard({ caller }) { + async removeMinicard({ caller }) { const minicardIndex = this._view.findChildIndex(caller.render()); if (minicardIndex >= 0) { - this._component.unbindMinicard(minicardIndex); - this._view.removeChild(minicardIndex); + const removed = await this._model.removeChild(minicardIndex); + if (removed) { + this._component.unbindMinicard(minicardIndex); + this._view.removeChild(minicardIndex); + } } } } diff --git a/src/Components/Minicard/MinicardView.js b/src/Components/Minicard/MinicardView.js index c03dbed..bc2f90e 100644 --- a/src/Components/Minicard/MinicardView.js +++ b/src/Components/Minicard/MinicardView.js @@ -5,9 +5,9 @@ import { addEventListeners } from '../../modules/Events/EventUtils.js'; export class MinicardView extends ComponentView { #deleteButton; - constructor({ elementClass, isCardOwner, editButtonUrl, title }, existingElement) { + constructor({ elementClass, isCardOwner, editButtonUrl, title, goToLink }, existingElement) { super({ - renderParams: { elementClass, isCardOwner, editButtonUrl, title }, + renderParams: { elementClass, isCardOwner, editButtonUrl, title, goToLink }, existingElement, templateName: 'minicard.hbs', }); diff --git a/src/Components/Minicard/minicard.hbs b/src/Components/Minicard/minicard.hbs index 7e00ce8..0c08010 100644 --- a/src/Components/Minicard/minicard.hbs +++ b/src/Components/Minicard/minicard.hbs @@ -1,5 +1,5 @@
- {{title}} + {{title}} {{#if isCardOwner}}
Редактировать diff --git a/src/Components/VacancyArticle/VacancyArticleController.js b/src/Components/VacancyArticle/VacancyArticleController.js index f033607..ba90729 100644 --- a/src/Components/VacancyArticle/VacancyArticleController.js +++ b/src/Components/VacancyArticle/VacancyArticleController.js @@ -1,8 +1,22 @@ import { ComponentController } from '../../modules/Components/Component.js'; +import { GO_TO, REDIRECT_TO, VACANCY_DELETE, VACANCY_EDIT } from '../../modules/Events/Events.js'; +import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; +import eventBus from '../../modules/Events/EventBus.js'; +import { VacancyEditPage } from '../../Pages/VacancyEditPage/VacancyEditPage.js'; export class VacancyArticleController extends ComponentController { constructor(model, view, component) { super(model, view, component); + this.setHandlers([ + { + event: VACANCY_DELETE, + handler: this.vacancyDelete.bind(this), + }, + { + event: VACANCY_EDIT, + handler: this.vacancyEdit.bind(this), + }, + ]); } async fetchData() { @@ -16,4 +30,18 @@ export class VacancyArticleController extends ComponentController { addButtonContainer(container) { this._view.addButtonContainer(container.render()); } + + async vacancyDelete() { + const deleted = await this._model.vacancyDelete(); + if (deleted) { + eventBus.emit(REDIRECT_TO, { redirectUrl: resolveUrl('myProfile') }); + } + } + + async vacancyEdit() { + const query = {}; + const vacancy = await this._model.getVacancyData(); + query[VacancyEditPage.VACANCY_ID_PARAM] = vacancy.id; + eventBus.emit(GO_TO, { redirectUrl: resolveUrl('editVacancy', query) }); + } } diff --git a/src/Components/VacancyArticle/VacancyArticleModel.js b/src/Components/VacancyArticle/VacancyArticleModel.js index 85a0dd3..190c1db 100644 --- a/src/Components/VacancyArticle/VacancyArticleModel.js +++ b/src/Components/VacancyArticle/VacancyArticleModel.js @@ -1,6 +1,7 @@ import { ComponentModel } from '../../modules/Components/Component.js'; import { Api } from '../../modules/Api/Api.js'; import { Vacancy } from '../../modules/models/Vacancy.js'; +import { NotFoundError } from '../../modules/Router/Router.js'; export class VacancyArticleModel extends ComponentModel { #vacancyData; @@ -11,6 +12,9 @@ export class VacancyArticleModel extends ComponentModel { this.#vacancyId = vacancyId; this.#vacancyData = Api.getVacancyById({ id: this.#vacancyId }).then( (data) => new Vacancy(data), + () => { + throw new NotFoundError('vacancy not found'); + }, ); } @@ -22,4 +26,17 @@ export class VacancyArticleModel extends ComponentModel { const vacancyData = await this.#vacancyData; return vacancyData.employerId; } + + async vacancyDelete() { + if (!this.#vacancyId) { + return false; + } + try { + await Api.deleteVacancyById({ id: this.#vacancyId }); + return true; + } catch (err) { + console.log(err); + return false; + } + } } diff --git a/src/Components/VacancyCard/VacancyCard.js b/src/Components/VacancyCard/VacancyCard.js index de51e15..0c2e21a 100644 --- a/src/Components/VacancyCard/VacancyCard.js +++ b/src/Components/VacancyCard/VacancyCard.js @@ -6,12 +6,12 @@ import { import { VacancyCardView } from './VacancyCardView.js'; export class VacancyCard extends Component { - constructor({ employer, vacancy, existingElement }) { + constructor({ vacancyObj, existingElement }) { super({ modelClass: ComponentModel, controllerClass: ComponentController, viewClass: VacancyCardView, - viewParams: { employer, vacancy }, + viewParams: { vacancyObj }, existingElement, }); } diff --git a/src/Components/VacancyCard/VacancyCardView.js b/src/Components/VacancyCard/VacancyCardView.js index 7c12eab..18dadc2 100644 --- a/src/Components/VacancyCard/VacancyCardView.js +++ b/src/Components/VacancyCard/VacancyCardView.js @@ -1,12 +1,9 @@ import { ComponentView } from '../../modules/Components/Component.js'; export class VacancyCardView extends ComponentView { - constructor({ employer, vacancy }, existingElement) { + constructor({ vacancyObj }, existingElement) { super({ - renderParams: { - employer, - vacancy, - }, + renderParams: vacancyObj, templateName: 'vacancy-card.hbs', existingElement, }); diff --git a/src/Components/VacancyCard/vacancy-card.hbs b/src/Components/VacancyCard/vacancy-card.hbs index 1ce90c9..9aefc8e 100644 --- a/src/Components/VacancyCard/vacancy-card.hbs +++ b/src/Components/VacancyCard/vacancy-card.hbs @@ -1,20 +1,20 @@
- +
-

{{vacancy.position}}

-
{{employer.name}}, {{employer.city}}
-
Зарплата: {{vacancy.salary}}
+

{{position}}

+
{{companyName}}, {{location}}
+
Зарплата: {{salary}}
- {{vacancy.description}} + {{description}}
\ No newline at end of file diff --git a/src/Components/VacancyForm/VacancyForm.js b/src/Components/VacancyForm/VacancyForm.js new file mode 100644 index 0000000..e057c21 --- /dev/null +++ b/src/Components/VacancyForm/VacancyForm.js @@ -0,0 +1,60 @@ +import { Component } from '../../modules/Components/Component.js'; +import { LiteralInput } from '/src/Components/FormInputs/LiteralInput/LiteralInput.js'; +import { CityInput } from '/src/Components/FormInputs/CityInput/CityInput.js'; +import { CurrencyInput } from '../../Components/FormInputs/CurrencyInput/CurrencyInput.js'; +import { ValidatedTextArea } from '../FormInputs/ValidatedTextArea/ValidatedTextArea.js'; +import { VacancyFormController } from './VacancyFormController.js'; +import { VacancyFormModel } from './VacancyFormModel.js'; +import { VacancyFormView } from './VacancyFormView.js'; + +export class VacancyForm extends Component { + #isNew; + constructor({ vacancyId = null, elementClass, existingElement }) { + super({ + modelClass: VacancyFormModel, + viewClass: VacancyFormView, + controllerClass: VacancyFormController, + modelParams: { vacancyId }, + existingElement, + viewParams: { elementClass, isNew: !vacancyId }, + }); + this.#isNew = !vacancyId; + this._positionField = new LiteralInput({ + existingElement: this._view.positionField, + selfValidate: true, + }); + this._workTypeField = new LiteralInput({ + existingElement: this._view.workTypeField, + selfValidate: true, + }); + this._salaryField = new CurrencyInput({ + existingElement: this._view.salaryField, + selfValidate: true, + }); + this._locationField = new CityInput({ + existingElement: this._view.locationField, + selfValidate: true, + }); + this._descriptionField = new ValidatedTextArea({ + existingElement: this._view.descriptionField, + selfValidate: true, + }); + this._children.push( + this._positionField, + this._workTypeField, + this._locationField, + this._descriptionField, + ); + if (!this.#isNew) { + this.reset(); + } + } + + get view() { + return this._view; + } + + reset() { + return this._controller.reset(); + } +} diff --git a/src/Components/VacancyForm/VacancyFormController.js b/src/Components/VacancyForm/VacancyFormController.js new file mode 100644 index 0000000..8e0e918 --- /dev/null +++ b/src/Components/VacancyForm/VacancyFormController.js @@ -0,0 +1,68 @@ +import { ComponentController } from '../../modules/Components/Component.js'; +import { REDIRECT_TO, SUBMIT_FORM } from '../../modules/Events/Events.js'; +import { Vacancy } from '../../modules/models/Vacancy.js'; +import { VacancyPage } from '../../Pages/VacancyPage/VacancyPage.js'; +import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; +import eventBus from '../../modules/Events/EventBus.js'; + +export class VacancyFormController extends ComponentController { + constructor(model, view, controller) { + super(model, view, controller); + this.setHandlers([ + { + event: SUBMIT_FORM, + handler: this.submit.bind(this), + }, + ]); + } + + _validate() { + const errorMessage = this._model.validate(this._view.getData()); + if (errorMessage) { + return false; + } + return [ + this._component._positionField.controller.validateInput({ + callerView: this._component._positionField._view, + }), + + this._component._salaryField.controller.validateInput({ + callerView: this._component._salaryField._view, + }), + + this._component._workTypeField.controller.validateInput({ + callerView: this._component._workTypeField._view, + }), + + this._component._locationField.controller.validateInput({ + callerView: this._component._locationField._view, + }), + + this._component._descriptionField.controller.validateInput({ + callerView: this._component._descriptionField._view, + }), + ].every((val) => val); + } + + async submit({ caller }) { + if (!Object.is(caller, this._view)) { + return; + } + if (!this._validate()) { + return; + } + const vacancy = await this._model.submit(new Vacancy(this._view.getData())); + if (!vacancy) { + return; + } + const query = {}; + query[VacancyPage.VACANCY_ID_PARAM] = vacancy.id; + eventBus.emit(REDIRECT_TO, { redirectUrl: resolveUrl('vacancy', query) }); + } + + async reset() { + const oldData = await this._model.getLastValidData(); + this._view.renderData(oldData); + return true; + } +} diff --git a/src/Components/VacancyForm/VacancyFormModel.js b/src/Components/VacancyForm/VacancyFormModel.js new file mode 100644 index 0000000..19c6800 --- /dev/null +++ b/src/Components/VacancyForm/VacancyFormModel.js @@ -0,0 +1,54 @@ +import { Api } from '../../modules/Api/Api.js'; +import { ComponentModel } from '../../modules/Components/Component.js'; +import { Vacancy } from '../../modules/models/Vacancy.js'; +import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; +import { zip } from '../../modules/ObjectUtils/Zip.js'; +import eventBus from '../../modules/Events/EventBus.js'; +import { REDIRECT_TO } from '../../modules/Events/Events.js'; + +export class VacancyFormModel extends ComponentModel { + #lastValidData; + #vacancyId; + #isNew; + + constructor({ vacancyId = null }) { + super(); + this.#vacancyId = vacancyId; + this.#isNew = !this.#vacancyId; + this.#lastValidData = this.#vacancyId + ? Api.getVacancyById({ id: this.#vacancyId }).then( + (vacancy) => new Vacancy(vacancy), + () => { + eventBus.emit(REDIRECT_TO, { redirectUrl: resolveUrl('createVacancy') }); + }, + ) + : null; + } + + async getLastValidData() { + return this.#lastValidData; + } + + async submit(formData) { + const vacancy = this.#isNew + ? await Api.createVacancy(formData) + : await Api.updateVacancyById(zip({ id: this.#vacancyId }, formData)); + if (vacancy) { + this.#lastValidData = formData; + return vacancy; + } + return null; + } + + validate(formData) { + const hasEmptyFields = Object.entries(formData).some(([fieldKey, fieldValue]) => { + if (fieldKey === 'salary') { + return false; + } + return !fieldValue.trim(); + }); + if (hasEmptyFields) { + return 'Заполните пустые поля'; + } + } +} diff --git a/src/Components/VacancyForm/VacancyFormView.js b/src/Components/VacancyForm/VacancyFormView.js new file mode 100644 index 0000000..4c0c736 --- /dev/null +++ b/src/Components/VacancyForm/VacancyFormView.js @@ -0,0 +1,45 @@ +import { ComponentView } from '../../modules/Components/Component.js'; +import eventBus from '../../modules/Events/EventBus.js'; +import { SUBMIT_FORM } from '../../modules/Events/Events.js'; +import { addEventListeners } from '../../modules/Events/EventUtils.js'; +import { getFormData } from '../../modules/FormUtils/FormUtils.js'; + +export class VacancyFormView extends ComponentView { + constructor({ elementClass, isNew }, existingElement) { + super({ + renderParams: { elementClass, isNew }, + existingElement, + templateName: 'vacancy-form.hbs', + }); + this.positionField = this._html.querySelector('.vacancy-form__position'); + this.workTypeField = this._html.querySelector('.vacancy-form__work-type'); + this.locationField = this._html.querySelector('.vacancy-form__location'); + this.salaryField = this._html.querySelector('.vacancy-form__salary'); + this.descriptionField = this._html.querySelector('.vacancy-form__description'); + this._eventListeners.push({ + event: 'submit', + object: this._html, + listener: function (ev) { + ev.preventDefault(); + eventBus.emit(SUBMIT_FORM, { caller: this }); + }.bind(this), + }); + addEventListeners(this._eventListeners); + } + + getData() { + return getFormData(this._html); + } + + getId() { + return 'vacancy-form'; + } + + renderData({ position, workType, salary, location, description }) { + this.positionField.querySelector('.validated-input__input').value = position; + this.workTypeField.querySelector('.validated-input__input').value = workType; + this.locationField.querySelector('.validated-input__input').value = location; + this.salaryField.querySelector('.validated-input__input').value = salary; + this.descriptionField.querySelector('.validated-textarea__textarea').value = description; + } +} diff --git a/src/Components/VacancyForm/vacancy-form.hbs b/src/Components/VacancyForm/vacancy-form.hbs new file mode 100644 index 0000000..0897dc1 --- /dev/null +++ b/src/Components/VacancyForm/vacancy-form.hbs @@ -0,0 +1,19 @@ +
+ {{> validated-input formName="vacancy-form" elementName="position" inputName="position" inputCaption="Должность" inputType="text"}} + {{> validated-input formName="vacancy-form" elementName="salary" inputName="salary" inputCaption="Заработная плата (в руб.)" inputType="numeric"}} + {{> validated-input formName="vacancy-form" elementName="work-type" inputName="workType" inputCaption="Вид работы (к примеру разовая или постоянная)" inputType="text"}} + {{> validated-input formName="vacancy-form" elementName="location" inputName="location" inputCaption="Город, где нужно будет работать" inputType="text"}} + {{> validated-textarea formName="vacancy-form" elementName="description" inputName="description" inputCaption="Описание вакансии"}} +
+ {{#if isNew}} + + {{else}} + + {{/if}} + В профиль +
+
\ No newline at end of file diff --git a/src/Pages/VacanciesPage/VacanciesPageController.js b/src/Pages/VacanciesPage/VacanciesPageController.js index 8598523..d5dc413 100644 --- a/src/Pages/VacanciesPage/VacanciesPageController.js +++ b/src/Pages/VacanciesPage/VacanciesPageController.js @@ -17,7 +17,7 @@ export class VacanciesPageController extends PageController { } async fetchVacancies() { - const vacancies = await this._model.getVacancies().catch(() => []); + const vacancies = await this._model.getVacancies(); this._component.bindVacancies(vacancies); vacancies.forEach((vacancy) => { this._view.addVacancy(vacancy.view.render()); diff --git a/src/Pages/VacanciesPage/VacanciesPageModel.js b/src/Pages/VacanciesPage/VacanciesPageModel.js index b78f92b..f98db5d 100644 --- a/src/Pages/VacanciesPage/VacanciesPageModel.js +++ b/src/Pages/VacanciesPage/VacanciesPageModel.js @@ -4,6 +4,7 @@ import USER_TYPE from '/src/modules/UserSession/UserType.js'; import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; import { AlertWindow } from '../../Components/AlertWindow/AlertWindow.js'; import { VacancyCard } from '/src/Components/VacancyCard/VacancyCard.js'; +import { Vacancy } from '../../modules/models/Vacancy.js'; import { Api } from '../../modules/Api/Api.js'; export class VacanciesPageModel extends PageModel { @@ -24,7 +25,7 @@ export class VacanciesPageModel extends PageModel { viewParams: { elementClass: 'ruler__alert-window', text: 'Попробуйте добавить свою вакансию!', - buttonUrl: '/', + buttonUrl: resolveUrl('createVacancy'), buttonText: 'Добавить вакансию', }, }), @@ -60,21 +61,17 @@ export class VacanciesPageModel extends PageModel { offset: this.#vacanciesLoaded, num: this.#VACANCIES_AMOUNT, }); - const vacanciesObjects = vacanciesJson.reduce((vacanciesObjects, vacancyJson) => { + const vacanciesCards = vacanciesJson.reduce((vacanciesCards, vacancyJson) => { try { - const { createdAt, description, employer, location, logo, position, salary } = vacancyJson; - vacanciesObjects.push( - new VacancyCard({ - employer: { logo, city: location, name: employer }, - vacancy: { createdAt, description, position, salary }, - }), - ); + const vacancy = new Vacancy(vacancyJson); + vacanciesCards.push(new VacancyCard({ vacancyObj: vacancy })); this.#vacanciesLoaded++; - return vacanciesObjects; - } catch { - return vacanciesObjects; + return vacanciesCards; + } catch (err) { + console.log(err); + return vacanciesCards; } }, []); - return vacanciesObjects; + return vacanciesCards; } } diff --git a/src/Pages/VacancyEditPage/VacancyEditPage.js b/src/Pages/VacancyEditPage/VacancyEditPage.js new file mode 100644 index 0000000..e526e6e --- /dev/null +++ b/src/Pages/VacancyEditPage/VacancyEditPage.js @@ -0,0 +1,59 @@ +import { Header } from '../../Components/Header/Header.js'; +import state from '../../modules/AppState/AppState.js'; +import { Page } from '../../modules/Page/Page.js'; +import { ForbiddenPage, NotFoundError } from '../../modules/Router/Router.js'; +import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; +import { VacancyEditPageController } from './VacancyEditPageController.js'; +import { VacancyEditPageModel } from './VacancyEditPageModel.js'; +import { VacancyEditPageView } from './VacancyEditPageView.js'; +import { VacancyForm } from '../../Components/VacancyForm/VacancyForm.js'; +import USER_TYPE from '../../modules/UserSession/UserType.js'; +import { zip } from '../../modules/ObjectUtils/Zip.js'; + +export class VacancyEditPage extends Page { + #vacancyId; + + static VACANCY_ID_PARAM = 'id'; + + constructor({ url }) { + if (state.userSession.userType !== USER_TYPE.EMPLOYER) { + throw new ForbiddenPage(resolveUrl('vacancies')); + } + let vacancyId; + switch (url.pathname) { + case resolveUrl('editVacancy').pathname: { + vacancyId = +url.searchParams.get(VacancyEditPage.VACANCY_ID_PARAM); + if (!vacancyId) { + throw new NotFoundError(); + } + break; + } + case resolveUrl('createVacancy').pathname: { + vacancyId = null; + break; + } + } + super({ + url, + modelClass: VacancyEditPageModel, + viewClass: VacancyEditPageView, + controllerClass: VacancyEditPageController, + viewParams: zip(Header.getViewParams(), { isNew: !vacancyId }), + }); + this.#vacancyId = vacancyId; + } + + postRenderInit() { + this._header = new Header({ + existingElement: this._view.header, + }); + this._children.push(this._header); + this._vacancyForm = new VacancyForm({ + userId: state.userSession.userId, + vacancyId: this.#vacancyId, + elementClass: 'vacancy-edit-page__vacancy-form', + }); + this._children.push(this._vacancyForm); + this._controller.addVacancyForm(this._vacancyForm); + } +} diff --git a/src/Pages/VacancyEditPage/VacancyEditPageController.js b/src/Pages/VacancyEditPage/VacancyEditPageController.js new file mode 100644 index 0000000..c0ac327 --- /dev/null +++ b/src/Pages/VacancyEditPage/VacancyEditPageController.js @@ -0,0 +1,11 @@ +import { PageController } from '../../modules/Page/Page.js'; + +export class VacancyEditPageController extends PageController { + constructor(model, view, component) { + super(model, view, component); + } + + addVacancyForm(vacancyForm) { + this._view.addVacancyForm(vacancyForm.render()); + } +} diff --git a/src/Pages/VacancyEditPage/VacancyEditPageModel.js b/src/Pages/VacancyEditPage/VacancyEditPageModel.js new file mode 100644 index 0000000..3b99fc1 --- /dev/null +++ b/src/Pages/VacancyEditPage/VacancyEditPageModel.js @@ -0,0 +1,3 @@ +import { PageModel } from '../../modules/Page/Page.js'; + +export class VacancyEditPageModel extends PageModel {} diff --git a/src/Pages/VacancyEditPage/VacancyEditPageView.js b/src/Pages/VacancyEditPage/VacancyEditPageView.js new file mode 100644 index 0000000..eb4bdde --- /dev/null +++ b/src/Pages/VacancyEditPage/VacancyEditPageView.js @@ -0,0 +1,18 @@ +import { PageView } from '../../modules/Page/Page.js'; + +export class VacancyEditPageView extends PageView { + constructor(renderParams) { + renderParams.isEmployer = !renderParams.isApplicant && renderParams.isAuthorized; + super({ + templateName: 'vacancy-page-edit.hbs', + renderParams: renderParams, + }); + this.header = this._html.querySelector('.header'); + this.formBox = this._html.querySelector('.vacancy-page-edit__form-container'); + } + + addVacancyForm(formRender) { + this.vacancyForm = formRender; + this.formBox.appendChild(formRender); + } +} diff --git a/src/Pages/VacancyEditPage/vacancy-page-edit.hbs b/src/Pages/VacancyEditPage/vacancy-page-edit.hbs new file mode 100644 index 0000000..e984ca9 --- /dev/null +++ b/src/Pages/VacancyEditPage/vacancy-page-edit.hbs @@ -0,0 +1,8 @@ +
+ {{> header}} +
+
+

{{#if isNew}}Создание{{else}}Редактирование{{/if}} вакансии

+
+
+
\ No newline at end of file diff --git a/src/Pages/VacancyPage/VacancyPage.js b/src/Pages/VacancyPage/VacancyPage.js index fff18fc..26c2b69 100644 --- a/src/Pages/VacancyPage/VacancyPage.js +++ b/src/Pages/VacancyPage/VacancyPage.js @@ -9,6 +9,7 @@ import { VacancyPageModel } from './VacancyPageModel.js'; import { VacancyPageView } from './VacancyPageView.js'; import { VacancyArticle } from '../../Components/VacancyArticle/VacancyArticle.js'; import { AppliersList } from '../../Components/AppliersList/AppliersList.js'; +import { zip } from '../../modules/ObjectUtils/Zip.js'; export class VacancyPage extends Page { #vacancyId; @@ -16,7 +17,7 @@ export class VacancyPage extends Page { #userId; #employerId; - VACANCY_ID_PARAM = 'id'; + static VACANCY_ID_PARAM = 'id'; constructor({ url }) { super({ @@ -24,9 +25,9 @@ export class VacancyPage extends Page { modelClass: VacancyPageModel, viewClass: VacancyPageView, controllerClass: VacancyPageController, - viewParams: Header.getViewParams(), + viewParams: zip(Header.getViewParams(), { isAuthorized: state.userSession.isLoggedIn }), }); - this.#vacancyId = +url.searchParams.get(this.VACANCY_ID_PARAM); + this.#vacancyId = +url.searchParams.get(VacancyPage.VACANCY_ID_PARAM); if (!this.#vacancyId) { throw new NotFoundError(); } @@ -34,7 +35,7 @@ export class VacancyPage extends Page { this.#userId = state.userSession.userId; } - postRenderInit() { + async postRenderInit() { this._header = new Header({ existingElement: this._view.header, }); @@ -45,26 +46,27 @@ export class VacancyPage extends Page { vacancyId: this.#vacancyId, userType: this.#userType, }); - this._vacancyArticle.makeButtons().then(async () => { - this.#employerId = await this._vacancyArticle.getEmployerId(); - this._controller.addVacancyArticle(this._vacancyArticle); - this._children.push(this._vacancyArticle); - if (this.#userType !== USER_TYPE.EMPLOYER) { - this._profileMinicard = new ProfileMinicard({ - userId: this.#employerId, - userType: USER_TYPE.EMPLOYER, - existingElement: this._view.profileMinicard, - }); - this._children.push(this._profileMinicard); - } - if (this.#userType === USER_TYPE.EMPLOYER && this.#userId === this.#employerId) { - this._appliersList = new AppliersList({ - elementClass: 'vacancy-page__appliers-list', - vacancyId: this.#vacancyId, - }); - this._children.push(this._appliersList); - this._view.addAppliersList(this._appliersList.render()); - } - }); + await this._vacancyArticle.makeButtons(); + this.#employerId = await this._vacancyArticle.getEmployerId(); + this._controller.addVacancyArticle(this._vacancyArticle); + this._children.push(this._vacancyArticle); + + if (this.#userType !== USER_TYPE.EMPLOYER) { + this._profileMinicard = new ProfileMinicard({ + userId: this.#employerId, + userType: USER_TYPE.EMPLOYER, + existingElement: this._view.profileMinicard, + }); + this._children.push(this._profileMinicard); + } + + if (this.#userType === USER_TYPE.EMPLOYER && this.#userId === this.#employerId) { + this._appliersList = new AppliersList({ + elementClass: 'vacancy-page__appliers-list', + vacancyId: this.#vacancyId, + }); + this._children.push(this._appliersList); + this._view.addAppliersList(this._appliersList.render()); + } } } diff --git a/src/Pages/VacancyPage/VacancyPageView.js b/src/Pages/VacancyPage/VacancyPageView.js index 801d88b..341aaad 100644 --- a/src/Pages/VacancyPage/VacancyPageView.js +++ b/src/Pages/VacancyPage/VacancyPageView.js @@ -2,12 +2,13 @@ import { PageView } from '../../modules/Page/Page.js'; export class VacancyPageView extends PageView { constructor(renderParams) { + renderParams.isEmployer = !renderParams.isApplicant && renderParams.isAuthorized; super({ templateName: 'vacancy-page.hbs', renderParams: renderParams, }); this.header = this._html.querySelector('.header'); - if (renderParams.isApplicant) { + if (!renderParams.isEmployer) { this.profileMinicard = this._html.querySelector('.vacancy-page__profile-minicard'); } this.vacancyContainer = this._html.querySelector('.vacancy-page__vacancy-container'); diff --git a/src/Pages/VacancyPage/vacancy-page.hbs b/src/Pages/VacancyPage/vacancy-page.hbs index 7627019..989d96f 100644 --- a/src/Pages/VacancyPage/vacancy-page.hbs +++ b/src/Pages/VacancyPage/vacancy-page.hbs @@ -2,9 +2,9 @@ {{> header}}
- {{#if isApplicant}} + {{#unless isEmployer}} {{> profile-minicard elementClass="vacancy-page__profile-minicard"}} - {{/if}} + {{/unless}}
diff --git a/src/css/index.css b/src/css/index.css index b13c4ba..a0d7302 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -223,6 +223,7 @@ body { .minicard__title { font-size: var(--text-size-7); color: var(--color-main-100); + text-decoration: none; } .minicard__button-container { diff --git a/src/css/registration.css b/src/css/registration.css index 8805cea..e066ff9 100755 --- a/src/css/registration.css +++ b/src/css/registration.css @@ -25,6 +25,7 @@ .registration-container__header { margin-top: 0px; text-align: center; + margin: 10px; color: white; } diff --git a/src/css/vacancies.css b/src/css/vacancies.css index 93a2536..d2d6056 100644 --- a/src/css/vacancies.css +++ b/src/css/vacancies.css @@ -15,14 +15,14 @@ } .vacancies-page__side-column { - border-radius: 10px; + border-radius: 25px; padding: 12px; width: 20%; } .vacancies-page__content { width: 70%; - border-radius: 10px; + border-radius: 25px; padding: 20px; } diff --git a/src/css/vacancy.css b/src/css/vacancy.css index 09606e9..b398e10 100644 --- a/src/css/vacancy.css +++ b/src/css/vacancy.css @@ -107,6 +107,7 @@ .vacancy-summary__header { font-size: var(--text-size-10); + text-decoration: none; margin-top: 0px; margin-bottom: 8px; } @@ -153,3 +154,62 @@ .appliers-list__list-item { color: var(--color-main-200); } + +/* Vacancy Edit Page */ + +.vacancy-page-edit { + display: flex; + flex-direction: column; + justify-content: start; + align-items: center; + width: 100vw; + height: 100vh; +} + +.vacancy-page-edit__form-container { + margin-top: 32px; + padding: 20px; + border-radius: 14px; + width: 100%; + max-width: 600px; + box-sizing: border-box; + background-color: var(--color-background-900); +} + +.vacancy-page-edit__header { + font-size: var(--text-size-8); + color: var(--color-main-100); + text-align: center; +} + +/* Vacancy Form */ + +.vacancy-form__position { + margin-bottom: 10px; +} + +.vacancy-form__salary { + margin-bottom: 10px; +} + +.vacancy-form__work-type { + margin-bottom: 10px; +} + +.vacancy-form__location { + margin-bottom: 10px; +} + +.vacancy-form__description { + margin-bottom: 20px; +} + +.vacancy-form__description-textarea { + resize: vertical; +} + +.vacancy-form__button-container { + display: flex; + align-items: baseline; + gap: 20px; +} diff --git a/src/index.html b/src/index.html index 223edb6..2789216 100644 --- a/src/index.html +++ b/src/index.html @@ -3,13 +3,13 @@ μArt - +
- - - + + + diff --git a/src/index.js b/src/index.js index 7fb32d0..f2c8828 100644 --- a/src/index.js +++ b/src/index.js @@ -1,20 +1,34 @@ import { handlebarsInit } from '/src/modules/Handlebars/Handlebars.js'; import router from '/src/modules/Router/Router.js'; +import eventBus from './modules/Events/EventBus.js'; import appState from './modules/AppState/AppState.js'; import { LoginPage } from './Pages/LoginPage/LoginPage.js'; import { RegistrationPage } from './Pages/RegistrationPage/RegistrationPage.js'; import { VacanciesPage } from './Pages/VacanciesPage/VacanciesPage.js'; import { ProfilePage } from './Pages/ProfilePage/ProfilePage.js'; import { VacancyPage } from './Pages/VacancyPage/VacancyPage.js'; +import { VacancyEditPage } from './Pages/VacancyEditPage/VacancyEditPage.js'; +import { resolveUrl } from './modules/UrlUtils/UrlUtils.js'; +import { REDIRECT_TO, GO_TO } from './modules/Events/Events.js'; handlebarsInit(); -router.addRoute('/', VacanciesPage); -router.addRoute('/login', LoginPage); -router.addRoute('/registration', RegistrationPage); -router.addRoute('/me', ProfilePage); -router.addRoute('/profile', ProfilePage); -router.addRoute('/vacancy', VacancyPage); +router.addRoute(resolveUrl('vacancies').pathname, VacanciesPage); +router.addRoute(resolveUrl('login').pathname, LoginPage); +router.addRoute(resolveUrl('register').pathname, RegistrationPage); +router.addRoute(resolveUrl('myProfile').pathname, ProfilePage); +router.addRoute(resolveUrl('profile').pathname, ProfilePage); +router.addRoute(resolveUrl('vacancy').pathname, VacancyPage); +router.addRoute(resolveUrl('createVacancy').pathname, VacancyEditPage); +router.addRoute(resolveUrl('editVacancy').pathname, VacancyEditPage); + +eventBus.on(REDIRECT_TO, ({ redirectUrl }) => { + router.navigate(redirectUrl, true, true); +}); + +eventBus.on(GO_TO, ({ redirectUrl }) => { + router.navigate(redirectUrl, false, true); +}); appState.userSession.checkAuthorization().finally(() => { router.start(); diff --git a/src/modules/Api/Api.js b/src/modules/Api/Api.js index a6c0345..1ab631a 100644 --- a/src/modules/Api/Api.js +++ b/src/modules/Api/Api.js @@ -1,4 +1,4 @@ -const backendPrefix = 'http://192.168.88.82:8080/api/v1/'; +const backendPrefix = 'http://127.0.0.1:8081/api/v1/'; const backendApi = new Map( Object.entries({ authenticated: backendPrefix + 'authorized', @@ -12,12 +12,21 @@ const backendApi = new Map( applicantPortfolio: backendPrefix + 'applicant/portfolio/', applicantCv: backendPrefix + 'applicant/cv/', employerVacancies: backendPrefix + 'employer/vacancies/', + vacancy: backendPrefix + 'vacancy/', + vacancySubscribers: backendPrefix + 'vacancy/subscribers/', }), ); export class UnmarshallError extends Error {} export class ResponseError extends Error {} +export const HTTP_METHOD = { + GET: 'get', + POST: 'post', + PUT: 'put', + DELETE: 'delete', +}; + export const HTTP_STATUSCODE = { OK: 200, BAD_REQUEST: 400, @@ -40,13 +49,16 @@ const unpackStandardApiCall = async (response) => { } throw new ResponseError(responseBody.error); } - if (!responseBody.body) { + if (!responseBody.body && responseBody.body !== null) { throw new UnmarshallError('Expected body in json, but not found'); } return responseBody.body; }; -const fetchCorsJson = (url, { method = 'GET', credentials = 'same-origin', body = undefined }) => { +const fetchCorsJson = ( + url, + { method = HTTP_METHOD.GET, credentials = 'same-origin', body = undefined }, +) => { return fetch(url, { method, headers: { @@ -61,7 +73,7 @@ const fetchCorsJson = (url, { method = 'GET', credentials = 'same-origin', body export class Api { static isAuthenticated = async () => { const response = await fetchCorsJson(backendApi.get('authenticated'), { - method: 'POST', + method: HTTP_METHOD.POST, credentials: 'include', }); return unpackStandardApiCall(response); @@ -69,7 +81,7 @@ export class Api { static login = async ({ userType, email, password }) => { const response = await fetchCorsJson(backendApi.get('login'), { - method: 'POST', + method: HTTP_METHOD.POST, body: JSON.stringify({ userType, email, @@ -82,7 +94,7 @@ export class Api { static registerApplicant = async ({ firstName, secondName, birthDate, email, password }) => { const response = await fetchCorsJson(backendApi.get('registerApplicant'), { - method: 'POST', + method: HTTP_METHOD.POST, credentials: 'include', body: JSON.stringify({ firstName, @@ -106,7 +118,7 @@ export class Api { password, }) => { const response = await fetchCorsJson(backendApi.get('registerEmployer'), { - method: 'POST', + method: HTTP_METHOD.POST, credentials: 'include', body: JSON.stringify({ firstName: firstName, @@ -124,35 +136,35 @@ export class Api { static getApplicantById = async ({ id }) => { const response = await fetchCorsJson(backendApi.get('applicantProfile') + id, { - method: 'GET', + method: HTTP_METHOD.GET, }); return await unpackStandardApiCall(response); }; static getEmployerById = async ({ id }) => { const response = await fetchCorsJson(backendApi.get('employerProfile') + id, { - method: 'GET', + method: HTTP_METHOD.GET, }); return unpackStandardApiCall(response); }; static async getEmployerVacancies({ id }) { const response = await fetchCorsJson(backendApi.get('employerVacancies') + id, { - method: 'GET', + method: HTTP_METHOD.GET, }); return unpackStandardApiCall(response); } static async getApplicantPortfolios({ id }) { const response = await fetchCorsJson(backendApi.get('applicantPortfolio') + id, { - method: 'GET', + method: HTTP_METHOD.GET, }); return unpackStandardApiCall(response); } static async getApplicantCvs({ id }) { const response = await fetchCorsJson(backendApi.get('applicantCv') + id, { - method: 'GET', + method: HTTP_METHOD.GET, }); return unpackStandardApiCall(response); } @@ -176,7 +188,7 @@ export class Api { }; const response = await fetchCorsJson(backendApi.get('applicantProfile') + id, { credentials: 'include', - method: 'PUT', + method: HTTP_METHOD.PUT, body: JSON.stringify(inputData), }); return response.ok; @@ -186,65 +198,77 @@ export class Api { const inputData = { firstName, lastName: secondName, city, contacts }; const response = await fetchCorsJson(backendApi.get('employerProfile') + id, { credentials: 'include', - method: 'PUT', + method: HTTP_METHOD.PUT, body: JSON.stringify(inputData), }); return response.ok; }; static getVacancyById = async ({ id }) => { - console.log(`getVacancyById: ${id}`); - return { - id: 3, - employer: 2, - salary: 10000, - companyName: 'ООО Рога и Копыта', - position: 'Инженер', - location: 'Москва', - description: 'Небольшой коллектив ищет близкого по духу инженера для работы', - workType: 'Разовая', - avatar: '', - createdAt: '2024-10-10', - updatedAt: '2024-10-10', - }; + const response = await fetchCorsJson(backendApi.get('vacancy') + id, { + method: HTTP_METHOD.GET, + }); + return unpackStandardApiCall(response); + }; + + static deleteVacancyById = async ({ id }) => { + const response = await fetchCorsJson(backendApi.get('vacancy') + id, { + method: HTTP_METHOD.DELETE, + credentials: 'include', + }); + return unpackStandardApiCall(response); + }; + + static createVacancy = async ({ salary, position, location, description, workType }) => { + const response = await fetchCorsJson(backendApi.get('vacancy'), { + method: HTTP_METHOD.POST, + body: JSON.stringify({ + salary, + position, + location, + description, + workType, + }), + credentials: 'include', + }); + return unpackStandardApiCall(response); + }; + + static updateVacancyById = async ({ id, salary, position, location, description, workType }) => { + const response = await fetchCorsJson(backendApi.get('vacancy') + id, { + method: HTTP_METHOD.PUT, + body: JSON.stringify({ + salary, + position, + location, + description, + workType, + }), + credentials: 'include', + }); + return unpackStandardApiCall(response); }; static getAppliersByVacancyId = async ({ id }) => { - console.log(`GetAppliersByVacancyId: ${id}`); - return [ - { - id: 2, - firstName: 'Илья', - lastName: 'Андриянов', - }, - { - id: 1, - firstName: 'Иван', - lastName: 'Иванов', - }, - ]; + const response = await fetchCorsJson(backendApi.get('vacancySubscribers') + id, { + method: HTTP_METHOD.GET, + credentials: 'include', + }); + return unpackStandardApiCall(response); }; static vacanciesFeed = async ({ offset, num }) => { - return fetchCorsJson( + const response = await fetchCorsJson( backendApi.get('vacancies') + new URLSearchParams({ offset: offset, num: num, }), { - method: 'GET', + method: HTTP_METHOD.GET, }, - ) - .then((response) => { - return response.json(); - }) - .then((body) => { - return body.status == '200' && body.vacancies instanceof Array - ? body.vacancies - : Promise.reject('failed to unpack data'); - }) - .catch(() => 'failed to receive valid data'); + ); + return unpackStandardApiCall(response); }; static logout = async () => { diff --git a/src/modules/Events/Events.js b/src/modules/Events/Events.js index a0f006f..e147773 100644 --- a/src/modules/Events/Events.js +++ b/src/modules/Events/Events.js @@ -25,3 +25,6 @@ export const MINICARD_DELETE = 'minicard delete'; export const VACANCY_APPLY = 'vacancy apply'; export const VACANCY_EDIT = 'vacancy edit'; export const VACANCY_DELETE = 'vacancy delete'; + +export const REDIRECT_TO = 'redirect to'; +export const GO_TO = 'go to'; diff --git a/src/modules/Handlebars/Handlebars.js b/src/modules/Handlebars/Handlebars.js index 3930099..325245a 100644 --- a/src/modules/Handlebars/Handlebars.js +++ b/src/modules/Handlebars/Handlebars.js @@ -32,4 +32,5 @@ export const handlebarsInit = () => { Handlebars.templates['employer-profile-form.hbs'], ); Handlebars.registerPartial('vacancy-article', Handlebars.templates['vacancy-article.hbs']); + Handlebars.registerPartial('vacancy-form', Handlebars.templates['vacancy-form.hbs']); }; diff --git a/src/modules/Router/Router.js b/src/modules/Router/Router.js index 32b3d8b..5f1c8ea 100644 --- a/src/modules/Router/Router.js +++ b/src/modules/Router/Router.js @@ -73,7 +73,7 @@ export class Router { * @param {boolean} modifyHistory - If true, browser history will be modified * @throws {TypeError} Invalid argument types */ - navigate(url, redirection = false, modifyHistory = true) { + async navigate(url, redirection = false, modifyHistory = true) { if (typeof redirection !== 'boolean') { throw TypeError('redirection must be a boolean'); } @@ -96,7 +96,7 @@ export class Router { const newPage = this.#routes.has(url.pathname) ? this.#routes.get(url.pathname) : NotFoundPage; - this._replacePage(newPage, url); + await this._replacePage(newPage, url); } catch (err) { if (err instanceof ForbiddenPage) { this.navigate(err.redirectUrl, true, true); @@ -110,7 +110,7 @@ export class Router { } } - _replacePage(newPageClass, newPageUrl) { + async _replacePage(newPageClass, newPageUrl) { if (this.#currentPage) { this.#currentPage.cleanup(); } @@ -118,7 +118,7 @@ export class Router { const app = document.getElementById(APP_ID); app.innerHTML = ''; app.appendChild(this.#currentPage.render()); - this.#currentPage.postRenderInit(); + await this.#currentPage.postRenderInit(); } /** diff --git a/src/modules/UrlUtils/UrlUtils.js b/src/modules/UrlUtils/UrlUtils.js index 9027e5d..d705cc3 100644 --- a/src/modules/UrlUtils/UrlUtils.js +++ b/src/modules/UrlUtils/UrlUtils.js @@ -6,6 +6,9 @@ const urls = { vacancies: '/', myProfile: '/me', profile: '/profile', + vacancy: '/vacancy', + createVacancy: '/vacancy/new', + editVacancy: '/vacancy/edit', }; const knownUrls = new Map(Object.entries(urls)); diff --git a/src/modules/models/Vacancy.js b/src/modules/models/Vacancy.js index 7575c6a..f0cf750 100644 --- a/src/modules/models/Vacancy.js +++ b/src/modules/models/Vacancy.js @@ -5,7 +5,7 @@ export class Vacancy { this.id = backendResponse.id; this.employerId = backendResponse.employer; this.companyName = backendResponse.companyName; - this.salary = backendResponse.salary; + this.salary = +backendResponse.salary; this.position = backendResponse.position; this.location = backendResponse.location; this.description = backendResponse.description; diff --git a/src/server/server.mjs b/src/server/server.mjs index 657d934..ade6224 100644 --- a/src/server/server.mjs +++ b/src/server/server.mjs @@ -6,7 +6,8 @@ const port = '8000'; const routingTemplates = { '.js$': '.', - '(.css|.html|.png|.svg)$': 'src/', + '(.css|.html|.png|.svg)$': './src/', + 'index.css$': './src', '.ttf$': '.', }; From f9fe3979e13708afdaf416a32dc6cc6cde850cdb Mon Sep 17 00:00:00 2001 From: Ilya Andriyanov Date: Fri, 8 Nov 2024 01:46:55 +0300 Subject: [PATCH 04/11] feature: cv article --- .../ButtonContainer/ButtonContainer.js | 18 +++ .../ButtonContainer/ButtonContainerView.js | 41 ++++++ .../cv-article__button-container.hbs | 12 ++ src/Components/CvArticle/CvArticle.js | 38 ++++++ .../CvArticle/CvArticleController.js | 47 +++++++ src/Components/CvArticle/CvArticleModel.js | 42 ++++++ src/Components/CvArticle/CvArticleView.js | 45 +++++++ src/Components/CvArticle/cv-article.hbs | 14 ++ src/Components/Header/header.hbs | 4 +- .../ApplicantCvList/ApplicantCvListModel.js | 14 +- src/Components/Minicard/minicard.hbs | 2 +- src/Pages/CvPage/CvPage.js | 61 +++++++++ src/Pages/CvPage/CvPageController.js | 11 ++ src/Pages/CvPage/CvPageModel.js | 3 + src/Pages/CvPage/CvPageView.js | 20 +++ src/Pages/CvPage/cv-page.hbs | 10 ++ src/Pages/ProfilePage/ProfilePage.js | 19 ++- src/css/cv.css | 121 ++++++++++++++++++ src/css/index.css | 1 + src/index.js | 2 + src/modules/Api/Api.js | 16 +++ src/modules/Events/Events.js | 3 + src/modules/UrlUtils/UrlUtils.js | 1 + src/modules/models/Cv.js | 16 +++ 24 files changed, 545 insertions(+), 16 deletions(-) create mode 100644 src/Components/CvArticle/ButtonContainer/ButtonContainer.js create mode 100644 src/Components/CvArticle/ButtonContainer/ButtonContainerView.js create mode 100644 src/Components/CvArticle/ButtonContainer/cv-article__button-container.hbs create mode 100644 src/Components/CvArticle/CvArticle.js create mode 100644 src/Components/CvArticle/CvArticleController.js create mode 100644 src/Components/CvArticle/CvArticleModel.js create mode 100644 src/Components/CvArticle/CvArticleView.js create mode 100644 src/Components/CvArticle/cv-article.hbs create mode 100644 src/Pages/CvPage/CvPage.js create mode 100644 src/Pages/CvPage/CvPageController.js create mode 100644 src/Pages/CvPage/CvPageModel.js create mode 100644 src/Pages/CvPage/CvPageView.js create mode 100644 src/Pages/CvPage/cv-page.hbs create mode 100644 src/css/cv.css create mode 100644 src/modules/models/Cv.js diff --git a/src/Components/CvArticle/ButtonContainer/ButtonContainer.js b/src/Components/CvArticle/ButtonContainer/ButtonContainer.js new file mode 100644 index 0000000..4239d80 --- /dev/null +++ b/src/Components/CvArticle/ButtonContainer/ButtonContainer.js @@ -0,0 +1,18 @@ +import { + Component, + ComponentController, + ComponentModel, +} from '../../../modules/Components/Component.js'; +import { ButtonContainerView } from './ButtonContainerView.js'; + +export class ButtonContainer extends Component { + constructor({ isOwner, isEmployer, ownerId, cvId, existingElement }) { + super({ + modelClass: ComponentModel, + viewClass: ButtonContainerView, + controllerClass: ComponentController, + viewParams: { isOwner, isEmployer, ownerId, cvId }, + existingElement, + }); + } +} diff --git a/src/Components/CvArticle/ButtonContainer/ButtonContainerView.js b/src/Components/CvArticle/ButtonContainer/ButtonContainerView.js new file mode 100644 index 0000000..9e9263c --- /dev/null +++ b/src/Components/CvArticle/ButtonContainer/ButtonContainerView.js @@ -0,0 +1,41 @@ +import { CV_DELETE, CV_EDIT } from '../../../modules/Events/Events.js'; +import { addEventListeners } from '../../../modules/Events/EventUtils.js'; +import { ComponentView } from '/src/modules/Components/Component.js'; +import eventBus from '/src/modules/Events/EventBus.js'; + +export class ButtonContainerView extends ComponentView { + #editButton; + #deleteButton; + #cvId; + constructor({ isOwner, isEmployer, ownerId, cvId }, existingElement) { + super({ + renderParams: { isOwner, isEmployer, ownerId }, + existingElement, + templateName: 'cv-article__button-container.hbs', + }); + this.#cvId = cvId; + if (isOwner) { + this.#editButton = this._html.querySelector('.cv-article__edit-button'); + this.#deleteButton = this._html.querySelector('.cv-article__delete-button'); + this._eventListeners.push( + { + event: 'click', + object: this.#editButton, + listener: function (ev) { + ev.preventDefault(); + eventBus.emit(CV_EDIT, { vacancyId: this.#cvId }); + }.bind(this), + }, + { + event: 'click', + object: this.#deleteButton, + listener: function (ev) { + ev.preventDefault(); + eventBus.emit(CV_DELETE, { vacancyId: this.#cvId }); + }.bind(this), + }, + ); + } + addEventListeners(this._eventListeners); + } +} diff --git a/src/Components/CvArticle/ButtonContainer/cv-article__button-container.hbs b/src/Components/CvArticle/ButtonContainer/cv-article__button-container.hbs new file mode 100644 index 0000000..34eb9b7 --- /dev/null +++ b/src/Components/CvArticle/ButtonContainer/cv-article__button-container.hbs @@ -0,0 +1,12 @@ +
+{{#if isEmployer}} + Другие резюме +{{else}} + {{#if isOwner}} + + + {{/if}} +{{/if}} +
+
\ No newline at end of file diff --git a/src/Components/CvArticle/CvArticle.js b/src/Components/CvArticle/CvArticle.js new file mode 100644 index 0000000..a8d6b18 --- /dev/null +++ b/src/Components/CvArticle/CvArticle.js @@ -0,0 +1,38 @@ +import { Component } from '../../modules/Components/Component.js'; +import { CvArticleController } from './CvArticleController.js'; +import { CvArticleModel } from './CvArticleModel.js'; +import { CvArticleView } from './CvArticleView.js'; +import { ButtonContainer } from './ButtonContainer/ButtonContainer.js'; +import USER_TYPE from '../../modules/UserSession/UserType.js'; + +export class CvArticle extends Component { + constructor({ elementClass, cvId, userId, userType }) { + super({ + modelClass: CvArticleModel, + modelParams: { cvId }, + viewClass: CvArticleView, + controllerClass: CvArticleController, + viewParams: { elementClass }, + }); + this._userId = userId; + this._userType = userType; + this._cvId = cvId; + } + + async makeButtons() { + const modelData = await this._controller.fetchData(); + this._buttonContainer = new ButtonContainer({ + isOwner: modelData.applicantId === this._userId && this._userType === USER_TYPE.APPLICANT, + isEmployer: this._userType === USER_TYPE.EMPLOYER, + ownerId: modelData.applicantId, + cvId: this._cvId, + }); + this._children.push(this._buttonContainer); + this._controller.addButtonContainer(this._buttonContainer); + this._controller.renderData(); + } + + async getApplicantId() { + return this._model.getApplicantId(); + } +} diff --git a/src/Components/CvArticle/CvArticleController.js b/src/Components/CvArticle/CvArticleController.js new file mode 100644 index 0000000..2554ede --- /dev/null +++ b/src/Components/CvArticle/CvArticleController.js @@ -0,0 +1,47 @@ +import { ComponentController } from '../../modules/Components/Component.js'; +import { GO_TO, REDIRECT_TO, CV_DELETE, CV_EDIT } from '../../modules/Events/Events.js'; +import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; +import eventBus from '../../modules/Events/EventBus.js'; + +export class CvArticleController extends ComponentController { + constructor(model, view, component) { + super(model, view, component); + this.setHandlers([ + { + event: CV_DELETE, + handler: this.cvDelete.bind(this), + }, + { + event: CV_EDIT, + handler: this.cvEdit.bind(this), + }, + ]); + } + + async fetchData() { + return this._model.getCvData(); + } + + async renderData() { + return this._view.renderData(await this._model.getCvData()); + } + + addButtonContainer(container) { + this._view.addButtonContainer(container.render()); + } + + async cvDelete() { + const deleted = await this._model.cvDelete(); + if (deleted) { + eventBus.emit(REDIRECT_TO, { redirectUrl: resolveUrl('myProfile') }); + } + } + + async cvEdit() { + const query = {}; + // const vacancy = await this._model.getCvData(); + // query[CvArticlePage.CV_ID] = vacancy.id; + eventBus.emit(GO_TO, { redirectUrl: resolveUrl('editCv', query) }); + throw Error('Not implemented'); + } +} diff --git a/src/Components/CvArticle/CvArticleModel.js b/src/Components/CvArticle/CvArticleModel.js new file mode 100644 index 0000000..d49a5ac --- /dev/null +++ b/src/Components/CvArticle/CvArticleModel.js @@ -0,0 +1,42 @@ +import { ComponentModel } from '../../modules/Components/Component.js'; +import { Api } from '../../modules/Api/Api.js'; +import { NotFoundError } from '../../modules/Router/Router.js'; +import { Cv } from '../../modules/models/Cv.js'; + +export class CvArticleModel extends ComponentModel { + #cvData; + #cvId; + + constructor({ cvId }) { + super(); + this.#cvId = cvId; + this.#cvData = Api.getCvById({ id: this.#cvId }).then( + (data) => new Cv(data), + () => { + throw new NotFoundError('cv not found'); + }, + ); + } + + async getCvData() { + return this.#cvData; + } + + async getApplicantId() { + const vacancyData = await this.#cvData; + return vacancyData.applicantId; + } + + async cvDelete() { + if (!this.#cvId) { + return false; + } + try { + await Api.deleteCvById({ id: this.#cvId }); + return true; + } catch (err) { + console.log(err); + return false; + } + } +} diff --git a/src/Components/CvArticle/CvArticleView.js b/src/Components/CvArticle/CvArticleView.js new file mode 100644 index 0000000..96b8429 --- /dev/null +++ b/src/Components/CvArticle/CvArticleView.js @@ -0,0 +1,45 @@ +import { ComponentView } from '../../modules/Components/Component.js'; + +export class CvArticleView extends ComponentView { + constructor({ elementClass, isOwner, isEmployer }, existingElement) { + super({ + renderParams: { + elementClass, + isOwner, + isEmployer, + }, + templateName: 'cv-article.hbs', + existingElement, + }); + this._avatar = this._html.querySelector('.cv-article__cv-picture'); + this._position = this._html.querySelector('.cv-article__header-text'); + this._jobSearchStatus = this._html.querySelector('.cv-article__job-search-status'); + this._workingExperience = this._html.querySelector('.cv-article__working-experience'); + this._description = this._html.querySelector('.cv-article__description'); + } + + renderData({ + positionRu, + positionEn, + description, + jobSearchStatus, + workingExperience, + updatedAt, + }) { + this._position.innerText = `${positionRu} / ${positionEn}`; + this._jobSearchStatus.innerText = jobSearchStatus; + this._description.innerText = description || 'Не указано'; + this._workingExperience.innerText = workingExperience || 'Не указан'; + this._updatedAt.innerText = `последнее обновление: ${updatedAt.toLocaleDateString('ru-RU', { + weekday: 'short', + day: 'numeric', + month: 'numeric', + year: 'numeric', + })}`; + } + + addButtonContainer(containerRender) { + this._html.appendChild(containerRender); + this._updatedAt = this._html.querySelector('.cv-article__created-at'); + } +} diff --git a/src/Components/CvArticle/cv-article.hbs b/src/Components/CvArticle/cv-article.hbs new file mode 100644 index 0000000..ae78c15 --- /dev/null +++ b/src/Components/CvArticle/cv-article.hbs @@ -0,0 +1,14 @@ +
+
+
+

+
+
+
+
+

Описание и достижения

+
+
+

Опыт работы

+
+
\ No newline at end of file diff --git a/src/Components/Header/header.hbs b/src/Components/Header/header.hbs index 3d4662a..a99b857 100644 --- a/src/Components/Header/header.hbs +++ b/src/Components/Header/header.hbs @@ -21,10 +21,10 @@ Мои резюме - + {{!-- Портфолио - + --}} {{else}} diff --git a/src/Components/Lists/ApplicantCvList/ApplicantCvListModel.js b/src/Components/Lists/ApplicantCvList/ApplicantCvListModel.js index 03067fe..3d9db9d 100644 --- a/src/Components/Lists/ApplicantCvList/ApplicantCvListModel.js +++ b/src/Components/Lists/ApplicantCvList/ApplicantCvListModel.js @@ -2,28 +2,36 @@ import { ComponentModel } from '../../../modules/Components/Component.js'; import { Api } from '../../../modules/Api/Api.js'; import { Minicard } from '../../Minicard/Minicard.js'; import { resolveUrl } from '../../../modules/UrlUtils/UrlUtils.js'; +import { Cv } from '../../../modules/models/Cv.js'; +import { CvPage } from '../../../Pages/CvPage/CvPage.js'; export class ApplicantCvListModel extends ComponentModel { #userId; #isOwner; + #items; constructor({ userId, isListOwner }) { super(); this.#userId = userId; this.#isOwner = isListOwner; + this.#items = []; } async getItems() { const cvsJson = await Api.getApplicantCvs({ id: this.#userId }); const cvsObjects = cvsJson.reduce((cvsObjects, cvJsonItem) => { try { - const { id, positionRu } = cvJsonItem; + const cv = new Cv(cvJsonItem); + this.#items.push(cv); + const urlSearchQuery = {}; + urlSearchQuery[`${CvPage.CV_ID_PARAM}`] = cv.id; cvsObjects.push( new Minicard({ renderParams: { elementClass: 'applicant-cv-list__minicard', - title: positionRu, + title: cv.positionRu, isCardOwner: this.#isOwner, - editButtonUrl: resolveUrl(`/cv/edit/${id}`), + goToLink: resolveUrl('cv', urlSearchQuery), + editButtonUrl: resolveUrl('editCv', urlSearchQuery), }, }), ); diff --git a/src/Components/Minicard/minicard.hbs b/src/Components/Minicard/minicard.hbs index 0c08010..e89a518 100644 --- a/src/Components/Minicard/minicard.hbs +++ b/src/Components/Minicard/minicard.hbs @@ -2,7 +2,7 @@ {{title}} {{#if isCardOwner}} {{/if}} diff --git a/src/Pages/CvPage/CvPage.js b/src/Pages/CvPage/CvPage.js new file mode 100644 index 0000000..4d598cf --- /dev/null +++ b/src/Pages/CvPage/CvPage.js @@ -0,0 +1,61 @@ +import { Header } from '../../Components/Header/Header.js'; +import { ProfileMinicard } from '../../Components/ProfileMinicard/ProfileMinicard.js'; +import state from '../../modules/AppState/AppState.js'; +import { Page } from '../../modules/Page/Page.js'; +import { NotFoundError } from '../../modules/Router/Router.js'; +import USER_TYPE from '../../modules/UserSession/UserType.js'; +import { CvPageController } from './CvPageController.js'; +import { CvPageModel } from './CvPageModel.js'; +import { CvPageView } from './CvPageView.js'; +import { CvArticle } from '../../Components/CvArticle/CvArticle.js'; +import { zip } from '../../modules/ObjectUtils/Zip.js'; + +export class CvPage extends Page { + #cvId; + #userType; + #userId; + #applicantId; + + static CV_ID_PARAM = 'id'; + + constructor({ url }) { + super({ + url, + modelClass: CvPageModel, + viewClass: CvPageView, + controllerClass: CvPageController, + viewParams: zip(Header.getViewParams(), { isAuthorized: state.userSession.isLoggedIn }), + }); + this.#cvId = +url.searchParams.get(CvPage.CV_ID_PARAM); + if (!this.#cvId) { + throw new NotFoundError(); + } + this.#userType = state.userSession.userType; + this.#userId = state.userSession.userId; + } + + async postRenderInit() { + this._header = new Header({ + existingElement: this._view.header, + }); + this._children.push(this._header); + + this._cvArticle = new CvArticle({ + elementClass: '.vacancy-page__vacancy-article', + userId: this.#userId, + cvId: this.#cvId, + userType: this.#userType, + }); + await this._cvArticle.makeButtons(); + this.#applicantId = await this._cvArticle.getApplicantId(); + this._controller.addCvArticle(this._cvArticle); + this._children.push(this._cvArticle); + + this._profileMinicard = new ProfileMinicard({ + userId: this.#applicantId, + userType: USER_TYPE.APPLICANT, + existingElement: this._view.profileMinicard, + }); + this._children.push(this._profileMinicard); + } +} diff --git a/src/Pages/CvPage/CvPageController.js b/src/Pages/CvPage/CvPageController.js new file mode 100644 index 0000000..f0d96da --- /dev/null +++ b/src/Pages/CvPage/CvPageController.js @@ -0,0 +1,11 @@ +import { PageController } from '../../modules/Page/Page.js'; + +export class CvPageController extends PageController { + constructor(model, view, component) { + super(model, view, component); + } + + addCvArticle(cvArticle) { + this._view.addCvArticle(cvArticle.render()); + } +} diff --git a/src/Pages/CvPage/CvPageModel.js b/src/Pages/CvPage/CvPageModel.js new file mode 100644 index 0000000..48a831f --- /dev/null +++ b/src/Pages/CvPage/CvPageModel.js @@ -0,0 +1,3 @@ +import { PageModel } from '../../modules/Page/Page.js'; + +export const CvPageModel = PageModel; diff --git a/src/Pages/CvPage/CvPageView.js b/src/Pages/CvPage/CvPageView.js new file mode 100644 index 0000000..e784ed0 --- /dev/null +++ b/src/Pages/CvPage/CvPageView.js @@ -0,0 +1,20 @@ +import { PageView } from '../../modules/Page/Page.js'; + +export class CvPageView extends PageView { + constructor(renderParams) { + renderParams.isEmployer = !renderParams.isApplicant && renderParams.isAuthorized; + super({ + templateName: 'cv-page.hbs', + renderParams: renderParams, + }); + this.header = this._html.querySelector('.header'); + this.profileMinicard = this._html.querySelector('.cv-page__profile-minicard'); + this.cvContainer = this._html.querySelector('.cv-page__cv-container'); + this.sideColumn = this._html.querySelector('.cv-page__left-column'); + } + + addCvArticle(render) { + this.cvContainer.innerHTML = ''; + this.cvContainer.appendChild(render); + } +} diff --git a/src/Pages/CvPage/cv-page.hbs b/src/Pages/CvPage/cv-page.hbs new file mode 100644 index 0000000..1452db3 --- /dev/null +++ b/src/Pages/CvPage/cv-page.hbs @@ -0,0 +1,10 @@ +
+ {{> header}} +
+
+ {{> profile-minicard elementClass="cv-page__profile-minicard"}} +
+
+
+
+
\ No newline at end of file diff --git a/src/Pages/ProfilePage/ProfilePage.js b/src/Pages/ProfilePage/ProfilePage.js index a07bb45..b553c2e 100644 --- a/src/Pages/ProfilePage/ProfilePage.js +++ b/src/Pages/ProfilePage/ProfilePage.js @@ -13,7 +13,6 @@ import { NotFoundError } from '../../modules/Router/Router.js'; import USER_TYPE from '../../modules/UserSession/UserType.js'; import { ProfileMinicard } from '../../Components/ProfileMinicard/ProfileMinicard.js'; import { EmployerVacancyList } from '../../Components/Lists/EmployerVacancyList/EmployerVacancyList.js'; -import { ApplicantPortfolioList } from '../../Components/Lists/ApplicantPortfolioList/ApplicantPortfolioList.js'; import { ApplicantCvList } from '../../Components/Lists/ApplicantCvList/ApplicantCvList.js'; export const PROFILE_PAGE_PARAMS = { @@ -103,15 +102,15 @@ export class ProfilePage extends Page { elementClass: 'profile-page__personal-data', }), }, - { - frameName: PROFILE_STARTING_FRAMES.PORTFOLIOS, - frameCaption: 'Портфолио', - frameComponent: new ApplicantPortfolioList({ - userId: this.#userId, - isListOwner: this.#isProfileOwner, - elementClass: 'profile-page__portfolio-list', - }), - }, + // { + // frameName: PROFILE_STARTING_FRAMES.PORTFOLIOS, + // frameCaption: 'Портфолио', + // frameComponent: new ApplicantPortfolioList({ + // userId: this.#userId, + // isListOwner: this.#isProfileOwner, + // elementClass: 'profile-page__portfolio-list', + // }), + // }, { frameName: PROFILE_STARTING_FRAMES.CVS, frameCaption: 'Резюме', diff --git a/src/css/cv.css b/src/css/cv.css new file mode 100644 index 0000000..551e56c --- /dev/null +++ b/src/css/cv.css @@ -0,0 +1,121 @@ +.cv-page { + display: grid; + align-items: start; + margin: auto; + width: 1280px; + padding-top: 32px; + grid-template-columns: 1fr 3fr; +} + +.cv-page__profile-minicard { + background-color: var(--color-background-900); + border-radius: 25px; + padding: 16px 36px; + color: var(--color-main-100); +} + +.cv-page__cv-container { + margin-left: 32px; + border-radius: 25px; + background-color: var(--color-background-900); +} + +/* Vacancy article */ + +.cv-article { + display: flex; + flex-direction: column; + align-items: start; +} + +.cv-article__header-container { + display: flex; + flex-direction: row; + justify-content: start; + align-items: center; + padding: 32px; +} + +.cv-article__cv-picture { + width: 128px; + height: 128px; +} + +.cv-article__header { + display: flex; + flex-direction: column; + justify-content: start; + color: var(--color-main-100); +} + +.cv-article__description { + padding: 0 32px; + font-size: var(--text-size-5); + line-height: 1.5; + color: var(--color-main-100); + margin-bottom: 32px; +} + +.cv-article__working-experience { + padding: 0px 32px; + font-size: var(--text-size-5); + line-height: 1.5; + color: var(--color-main-100); +} + +.cv-article__another-button { + margin-right: 10px; + box-shadow: none; + padding: 10px 20px; +} + +.cv-article__created-at { + color: var(--color-main-100); + opacity: 0.7; + text-decoration: none; + text-transform: lowercase; +} + +.cv-article_theme-dark { + background-color: var(--color-background-800); + color: var(--color-main-100); +} + +.cv-article__divider { + display: inline-block; + background-color: var(--color-background-100); + opacity: 0.3; + height: 1px; + width: 100%; +} + +.cv-article__button-container { + display: flex; + align-items: baseline; + gap: 8px; + padding: 32px 32px; + justify-content: start; +} + +.cv-article__header-text { + font-size: var(--text-size-10); + text-decoration: none; + margin-top: 0px; + margin-bottom: 8px; +} + +.cv-article__job-search-status { + box-sizing: border-box; +} + +.cv-article__description-header { + font-size: var(--text-size-9); + color: var(--color-main-100); + padding: 0 32px; +} + +.cv-article__working-experience-header { + font-size: var(--text-size-9); + color: var(--color-main-100); + padding: 0 32px; +} diff --git a/src/css/index.css b/src/css/index.css index a0d7302..b78e6de 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -7,6 +7,7 @@ @import url(forms.css); @import url(profile.css); @import url(vacancy.css); +@import url(cv.css); :root { --grey-very-dark: #1b1b1b; diff --git a/src/index.js b/src/index.js index f2c8828..db55dc3 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ import { VacancyPage } from './Pages/VacancyPage/VacancyPage.js'; import { VacancyEditPage } from './Pages/VacancyEditPage/VacancyEditPage.js'; import { resolveUrl } from './modules/UrlUtils/UrlUtils.js'; import { REDIRECT_TO, GO_TO } from './modules/Events/Events.js'; +import { CvPage } from './Pages/CvPage/CvPage.js'; handlebarsInit(); @@ -21,6 +22,7 @@ router.addRoute(resolveUrl('profile').pathname, ProfilePage); router.addRoute(resolveUrl('vacancy').pathname, VacancyPage); router.addRoute(resolveUrl('createVacancy').pathname, VacancyEditPage); router.addRoute(resolveUrl('editVacancy').pathname, VacancyEditPage); +router.addRoute(resolveUrl('cv').pathname, CvPage); eventBus.on(REDIRECT_TO, ({ redirectUrl }) => { router.navigate(redirectUrl, true, true); diff --git a/src/modules/Api/Api.js b/src/modules/Api/Api.js index 1ab631a..c61e347 100644 --- a/src/modules/Api/Api.js +++ b/src/modules/Api/Api.js @@ -14,6 +14,7 @@ const backendApi = new Map( employerVacancies: backendPrefix + 'employer/vacancies/', vacancy: backendPrefix + 'vacancy/', vacancySubscribers: backendPrefix + 'vacancy/subscribers/', + cv: backendPrefix + 'cv/', }), ); @@ -271,6 +272,21 @@ export class Api { return unpackStandardApiCall(response); }; + static getCvById = async ({ id }) => { + const response = await fetchCorsJson(backendApi.get('cv') + id, { + method: HTTP_METHOD.GET, + }); + return unpackStandardApiCall(response); + }; + + static deleteCvById = async ({ id }) => { + const response = await fetchCorsJson(backendApi.get('cv') + id, { + method: HTTP_METHOD.DELETE, + credentials: 'include', + }); + return unpackStandardApiCall(response); + }; + static logout = async () => { const response = await fetchCorsJson(backendApi.get('logout'), { method: 'POST', diff --git a/src/modules/Events/Events.js b/src/modules/Events/Events.js index e147773..4ddc7c9 100644 --- a/src/modules/Events/Events.js +++ b/src/modules/Events/Events.js @@ -26,5 +26,8 @@ export const VACANCY_APPLY = 'vacancy apply'; export const VACANCY_EDIT = 'vacancy edit'; export const VACANCY_DELETE = 'vacancy delete'; +export const CV_EDIT = 'cv edit'; +export const CV_DELETE = 'cv delete'; + export const REDIRECT_TO = 'redirect to'; export const GO_TO = 'go to'; diff --git a/src/modules/UrlUtils/UrlUtils.js b/src/modules/UrlUtils/UrlUtils.js index d705cc3..6c021e3 100644 --- a/src/modules/UrlUtils/UrlUtils.js +++ b/src/modules/UrlUtils/UrlUtils.js @@ -9,6 +9,7 @@ const urls = { vacancy: '/vacancy', createVacancy: '/vacancy/new', editVacancy: '/vacancy/edit', + cv: '/cv', }; const knownUrls = new Map(Object.entries(urls)); diff --git a/src/modules/models/Cv.js b/src/modules/models/Cv.js new file mode 100644 index 0000000..2e1c915 --- /dev/null +++ b/src/modules/models/Cv.js @@ -0,0 +1,16 @@ +import { resolveStatic } from '../UrlUtils/UrlUtils.js'; + +export class Cv { + constructor(backendResponse) { + this.id = backendResponse.id; + this.applicantId = backendResponse.applicant; + this.positionRu = backendResponse.positionRu; + this.positionEn = backendResponse.positionEn; + this.description = backendResponse.description; + this.jobSearchStatus = backendResponse.jobSearchStatus; + this.workingExperience = backendResponse.workingExperience; + this.avatar = backendResponse.avatar || resolveStatic('img/company-icon.svg'); + this.createdAt = new Date(backendResponse.createdAt); + this.updatedAt = new Date(backendResponse.updatedAt); + } +} From 2bfed4ac2f9a3d9ea6442953eaba42d1f6b2ab05 Mon Sep 17 00:00:00 2001 From: Ilya Andriyanov Date: Fri, 8 Nov 2024 04:24:49 +0300 Subject: [PATCH 05/11] feature: fully working cvs --- .../ApplicantProfileFormModel.js | 4 +- .../CvArticle/CvArticleController.js | 5 +- src/Components/CvArticle/CvArticleView.js | 2 +- src/Components/CvForm/CvForm.js | 59 ++++++++++++++++ src/Components/CvForm/CvFormController.js | 64 +++++++++++++++++ src/Components/CvForm/CvFormModel.js | 53 ++++++++++++++ src/Components/CvForm/CvFormView.js | 47 +++++++++++++ src/Components/CvForm/cv-form.hbs | 19 +++++ .../EmployerProfileFormModel.js | 1 - .../ApplicantCvList/ApplicantCvListModel.js | 14 ++++ .../ButtonContainer/ButtonContainer.js | 4 +- .../ButtonContainer/ButtonContainerView.js | 41 +++++++++-- .../vacancy-article__button-container.hbs | 3 +- .../VacancyArticle/VacancyArticle.js | 13 +++- .../VacancyArticleController.js | 29 +++++++- .../VacancyArticle/VacancyArticleModel.js | 30 ++++++++ .../VacancyArticle/VacancyArticleView.js | 13 +++- src/Components/VacancyCard/VacancyCard.js | 2 +- src/Components/VacancyCard/VacancyCardView.js | 15 +++- src/Components/VacancyCard/vacancy-card.hbs | 6 +- src/Components/VacancyForm/VacancyForm.js | 2 +- src/Components/VacancyForm/VacancyFormView.js | 4 +- src/Components/VacancyForm/vacancy-form.hbs | 2 +- src/Pages/CvEditPage/CvEditPage.js | 60 ++++++++++++++++ src/Pages/CvEditPage/CvEditPageController.js | 11 +++ src/Pages/CvEditPage/CvEditPageModel.js | 3 + src/Pages/CvEditPage/CvEditPageView.js | 17 +++++ src/Pages/CvEditPage/cv-page-edit.hbs | 8 +++ src/css/cv.css | 63 +++++++++++++++++ src/css/vacancy.css | 12 ++-- src/index.js | 3 + src/modules/Api/Api.js | 70 +++++++++++++++++++ src/modules/Events/Events.js | 1 + src/modules/UrlUtils/UrlUtils.js | 2 + 34 files changed, 648 insertions(+), 34 deletions(-) create mode 100644 src/Components/CvForm/CvForm.js create mode 100644 src/Components/CvForm/CvFormController.js create mode 100644 src/Components/CvForm/CvFormModel.js create mode 100644 src/Components/CvForm/CvFormView.js create mode 100644 src/Components/CvForm/cv-form.hbs create mode 100644 src/Pages/CvEditPage/CvEditPage.js create mode 100644 src/Pages/CvEditPage/CvEditPageController.js create mode 100644 src/Pages/CvEditPage/CvEditPageModel.js create mode 100644 src/Pages/CvEditPage/CvEditPageView.js create mode 100644 src/Pages/CvEditPage/cv-page-edit.hbs diff --git a/src/Components/ApplicantProfileForm/ApplicantProfileFormModel.js b/src/Components/ApplicantProfileForm/ApplicantProfileFormModel.js index e787aeb..b357045 100644 --- a/src/Components/ApplicantProfileForm/ApplicantProfileFormModel.js +++ b/src/Components/ApplicantProfileForm/ApplicantProfileFormModel.js @@ -24,7 +24,9 @@ export class ApplicantProfileFormModel extends ComponentModel { formData.birthDate = new Date(formData.birthDate); formData.id = this.#userId; if (await Api.updateApplicantProfile(formData)) { - this.#lastValidData = new Applicant(formData); + const app = new Applicant(formData); + app.birthDate = app.birthDate.toISOString().split('T')[0]; + this.#lastValidData = app; return true; } return false; diff --git a/src/Components/CvArticle/CvArticleController.js b/src/Components/CvArticle/CvArticleController.js index 2554ede..655056a 100644 --- a/src/Components/CvArticle/CvArticleController.js +++ b/src/Components/CvArticle/CvArticleController.js @@ -1,6 +1,7 @@ import { ComponentController } from '../../modules/Components/Component.js'; import { GO_TO, REDIRECT_TO, CV_DELETE, CV_EDIT } from '../../modules/Events/Events.js'; import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; +import { CvPage } from '../../Pages/CvPage/CvPage.js'; import eventBus from '../../modules/Events/EventBus.js'; export class CvArticleController extends ComponentController { @@ -39,8 +40,8 @@ export class CvArticleController extends ComponentController { async cvEdit() { const query = {}; - // const vacancy = await this._model.getCvData(); - // query[CvArticlePage.CV_ID] = vacancy.id; + const cv = await this._model.getCvData(); + query[CvPage.CV_ID] = cv.id; eventBus.emit(GO_TO, { redirectUrl: resolveUrl('editCv', query) }); throw Error('Not implemented'); } diff --git a/src/Components/CvArticle/CvArticleView.js b/src/Components/CvArticle/CvArticleView.js index 96b8429..2d0cf4e 100644 --- a/src/Components/CvArticle/CvArticleView.js +++ b/src/Components/CvArticle/CvArticleView.js @@ -26,7 +26,7 @@ export class CvArticleView extends ComponentView { workingExperience, updatedAt, }) { - this._position.innerText = `${positionRu} / ${positionEn}`; + this._position.innerText = positionEn ? `${positionRu} / ${positionEn}` : positionRu; this._jobSearchStatus.innerText = jobSearchStatus; this._description.innerText = description || 'Не указано'; this._workingExperience.innerText = workingExperience || 'Не указан'; diff --git a/src/Components/CvForm/CvForm.js b/src/Components/CvForm/CvForm.js new file mode 100644 index 0000000..ae7053c --- /dev/null +++ b/src/Components/CvForm/CvForm.js @@ -0,0 +1,59 @@ +import { Component } from '../../modules/Components/Component.js'; +import { LiteralInput } from '/src/Components/FormInputs/LiteralInput/LiteralInput.js'; +import { ValidatedTextArea } from '../FormInputs/ValidatedTextArea/ValidatedTextArea.js'; +import { CvFormModel } from './CvFormModel.js'; +import { CvFormView } from './CvFormView.js'; +import { CvFormController } from './CvFormController.js'; + +export class CvForm extends Component { + #isNew; + constructor({ cvId = null, elementClass, existingElement }) { + super({ + modelClass: CvFormModel, + viewClass: CvFormView, + controllerClass: CvFormController, + modelParams: { cvId }, + existingElement, + viewParams: { elementClass, isNew: !cvId }, + }); + this.#isNew = !cvId; + this._positionRuField = new LiteralInput({ + existingElement: this._view.positionRuField, + selfValidate: true, + }); + this._positionEnField = new LiteralInput({ + existingElement: this._view.positionEnField, + selfValidate: true, + }); + this._jobSearchStatusField = new LiteralInput({ + existingElement: this._view.jobSearchStatusField, + selfValidate: true, + }); + this._descriptionField = new ValidatedTextArea({ + existingElement: this._view.descriptionField, + selfValidate: true, + }); + this._workingExperienceField = new ValidatedTextArea({ + existingElement: this._view.workingExperienceField, + selfValidate: true, + }); + this._children.push( + this._positionEnField, + this._positionRuField, + this._jobSearchStatusField, + this._descriptionField, + this._workingExperienceField, + ); + if (!this.#isNew) { + this.reset(); + } + } + + get view() { + return this._view; + } + + reset() { + return this._controller.reset(); + } +} diff --git a/src/Components/CvForm/CvFormController.js b/src/Components/CvForm/CvFormController.js new file mode 100644 index 0000000..01a8fbc --- /dev/null +++ b/src/Components/CvForm/CvFormController.js @@ -0,0 +1,64 @@ +import { ComponentController } from '../../modules/Components/Component.js'; +import { REDIRECT_TO, SUBMIT_FORM } from '../../modules/Events/Events.js'; +import { CvPage } from '../../Pages/CvPage/CvPage.js'; +import { Cv } from '../../modules/models/Cv.js'; +import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; +import eventBus from '../../modules/Events/EventBus.js'; + +export class CvFormController extends ComponentController { + constructor(model, view, controller) { + super(model, view, controller); + this.setHandlers([ + { + event: SUBMIT_FORM, + handler: this.submit.bind(this), + }, + ]); + } + + _validate() { + const errorMessage = this._model.validate(this._view.getData()); + if (errorMessage) { + return false; + } + return [ + this._component._positionRuField.controller.validateInput({ + callerView: this._component._positionRuField._view, + }), + this._component._positionEnField.controller.validateInput({ + callerView: this._component._positionEnField._view, + }), + this._component._jobSearchStatusField.controller.validateInput({ + callerView: this._component._jobSearchStatusField._view, + }), + this._component._workingExperienceField.controller.validateInput({ + callerView: this._component._workingExperienceField._view, + }), + this._component._descriptionField.controller.validateInput({ + callerView: this._component._descriptionField._view, + }), + ].every((val) => val); + } + + async submit({ caller }) { + if (!Object.is(caller, this._view)) { + return; + } + if (!this._validate()) { + return; + } + const cv = await this._model.submit(new Cv(this._view.getData())); + if (!cv) { + return; + } + const query = {}; + query[CvPage.CV_ID_PARAM] = cv.id; + eventBus.emit(REDIRECT_TO, { redirectUrl: resolveUrl('cv', query) }); + } + + async reset() { + const oldData = await this._model.getLastValidData(); + this._view.renderData(oldData); + return true; + } +} diff --git a/src/Components/CvForm/CvFormModel.js b/src/Components/CvForm/CvFormModel.js new file mode 100644 index 0000000..205ce0f --- /dev/null +++ b/src/Components/CvForm/CvFormModel.js @@ -0,0 +1,53 @@ +import { Api } from '../../modules/Api/Api.js'; +import { ComponentModel } from '../../modules/Components/Component.js'; +import { Cv } from '../../modules/models/Cv.js'; +import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; +import { zip } from '../../modules/ObjectUtils/Zip.js'; +import eventBus from '../../modules/Events/EventBus.js'; +import { REDIRECT_TO } from '../../modules/Events/Events.js'; + +export class CvFormModel extends ComponentModel { + #lastValidData; + #cvId; + #isNew; + + constructor({ cvId = null }) { + super(); + this.#cvId = cvId; + this.#isNew = !this.#cvId; + this.#lastValidData = this.#cvId + ? Api.getCvById({ id: this.#cvId }).then( + (cv) => new Cv(cv), + () => { + eventBus.emit(REDIRECT_TO, { redirectUrl: resolveUrl('createCv') }); + }, + ) + : null; + } + + async getLastValidData() { + return this.#lastValidData; + } + + async submit(formData) { + const cv = this.#isNew + ? await Api.createCv(formData) + : await Api.updateCvById(zip({ id: this.#cvId }, formData)); + if (cv) { + this.#lastValidData = formData; + return cv; + } + return null; + } + + validate(formData) { + const hasEmptyRequiredFields = [formData.positionRu, formData.jobSearchStatus].some( + (fieldValue) => { + return !fieldValue.trim(); + }, + ); + if (hasEmptyRequiredFields) { + return 'Заполните обязательные поля'; + } + } +} diff --git a/src/Components/CvForm/CvFormView.js b/src/Components/CvForm/CvFormView.js new file mode 100644 index 0000000..1802fe6 --- /dev/null +++ b/src/Components/CvForm/CvFormView.js @@ -0,0 +1,47 @@ +import { ComponentView } from '../../modules/Components/Component.js'; +import eventBus from '../../modules/Events/EventBus.js'; +import { SUBMIT_FORM } from '../../modules/Events/Events.js'; +import { addEventListeners } from '../../modules/Events/EventUtils.js'; +import { getFormData } from '../../modules/FormUtils/FormUtils.js'; + +export class CvFormView extends ComponentView { + constructor({ elementClass, isNew }, existingElement) { + super({ + renderParams: { elementClass, isNew }, + existingElement, + templateName: 'cv-form.hbs', + }); + this.positionRuField = this._html.querySelector('.cv-form__position-ru'); + this.positionEnField = this._html.querySelector('.cv-form__position-en'); + this.jobSearchStatusField = this._html.querySelector('.cv-form__job-search-status'); + this.descriptionField = this._html.querySelector('.cv-form__description'); + this.workingExperienceField = this._html.querySelector('.cv-form__working-experience'); + + this._eventListeners.push({ + event: 'submit', + object: this._html, + listener: function (ev) { + ev.preventDefault(); + eventBus.emit(SUBMIT_FORM, { caller: this }); + }.bind(this), + }); + addEventListeners(this._eventListeners); + } + + getData() { + return getFormData(this._html); + } + + getId() { + return 'cv-form'; + } + + renderData({ positionRu, positionEn, jobSearchStatus, description, workingExperience }) { + this.positionRuField.querySelector('.validated-input__input').value = positionRu; + this.positionEnField.querySelector('.validated-input__input').value = positionEn; + this.jobSearchStatusField.querySelector('.validated-input__input').value = jobSearchStatus; + this.descriptionField.querySelector('.validated-textarea__textarea').value = description; + this.workingExperienceField.querySelector('.validated-textarea__textarea').value = + workingExperience; + } +} diff --git a/src/Components/CvForm/cv-form.hbs b/src/Components/CvForm/cv-form.hbs new file mode 100644 index 0000000..f8c1273 --- /dev/null +++ b/src/Components/CvForm/cv-form.hbs @@ -0,0 +1,19 @@ +
+ {{> validated-input formName="cv-form" elementName="position-ru" inputName="positionRu" inputCaption="Должность" inputType="text"}} + {{> validated-input formName="cv-form" elementName="position-en" inputName="positionEn" inputCaption="Перевод должности на английский" inputType="text"}} + {{> validated-input formName="cv-form" elementName="job-search-status" inputName="jobSearchStatus" inputCaption="Статус поиска работы" inputType="text"}} + {{> validated-textarea formName="cv-form" elementName="description" inputName="description" inputCaption="Описание и достижения"}} + {{> validated-textarea formName="cv-form" elementName="working-experience" inputName="workingExperience" inputCaption="Опыт работы"}} +
+ {{#if isNew}} + + {{else}} + + {{/if}} + В профиль +
+
\ No newline at end of file diff --git a/src/Components/EmployerProfileForm/EmployerProfileFormModel.js b/src/Components/EmployerProfileForm/EmployerProfileFormModel.js index 0b025e2..3c6aa0e 100644 --- a/src/Components/EmployerProfileForm/EmployerProfileFormModel.js +++ b/src/Components/EmployerProfileForm/EmployerProfileFormModel.js @@ -22,7 +22,6 @@ export class EmployerProfileFormModel extends ComponentModel { formData.birthDate = new Date(formData.birthDate); formData.id = this.#userId; if (await Api.updateEmployerProfile(formData)) { - this.#lastValidData = new Employer(formData); return true; } return false; diff --git a/src/Components/Lists/ApplicantCvList/ApplicantCvListModel.js b/src/Components/Lists/ApplicantCvList/ApplicantCvListModel.js index 3d9db9d..195fb32 100644 --- a/src/Components/Lists/ApplicantCvList/ApplicantCvListModel.js +++ b/src/Components/Lists/ApplicantCvList/ApplicantCvListModel.js @@ -42,4 +42,18 @@ export class ApplicantCvListModel extends ComponentModel { }, []); return cvsObjects; } + + async removeChild(cvArrId) { + if (cvArrId >= this.#items.length || cvArrId < 0) { + return false; + } + const cv = this.#items[cvArrId]; + try { + await Api.deleteCvById({ id: cv.id }); + return true; + } catch (err) { + console.log(err); + return false; + } + } } diff --git a/src/Components/VacancyArticle/ButtonContainer/ButtonContainer.js b/src/Components/VacancyArticle/ButtonContainer/ButtonContainer.js index 0fbedad..670f4f9 100644 --- a/src/Components/VacancyArticle/ButtonContainer/ButtonContainer.js +++ b/src/Components/VacancyArticle/ButtonContainer/ButtonContainer.js @@ -6,12 +6,12 @@ import { import { ButtonContainerView } from './ButtonContainerView.js'; export class ButtonContainer extends Component { - constructor({ isOwner, isApplicant, ownerId, vacancyId, existingElement }) { + constructor({ isOwner, isApplied, isApplicant, ownerId, vacancyId, existingElement }) { super({ modelClass: ComponentModel, viewClass: ButtonContainerView, controllerClass: ComponentController, - viewParams: { isOwner, isApplicant, ownerId, vacancyId }, + viewParams: { isOwner, isApplicant, isApplied, ownerId, vacancyId }, existingElement, }); } diff --git a/src/Components/VacancyArticle/ButtonContainer/ButtonContainerView.js b/src/Components/VacancyArticle/ButtonContainer/ButtonContainerView.js index c610848..7a1840a 100644 --- a/src/Components/VacancyArticle/ButtonContainer/ButtonContainerView.js +++ b/src/Components/VacancyArticle/ButtonContainer/ButtonContainerView.js @@ -1,4 +1,9 @@ -import { VACANCY_APPLY, VACANCY_DELETE, VACANCY_EDIT } from '../../../modules/Events/Events.js'; +import { + VACANCY_APPLY, + VACANCY_DELETE, + VACANCY_EDIT, + VACANCY_RESET_APPLY, +} from '../../../modules/Events/Events.js'; import { addEventListeners } from '../../../modules/Events/EventUtils.js'; import { ComponentView } from '/src/modules/Components/Component.js'; import eventBus from '/src/modules/Events/EventBus.js'; @@ -8,23 +13,38 @@ export class ButtonContainerView extends ComponentView { #editButton; #deleteButton; #vacancyId; - constructor({ isOwner, isApplicant, ownerId, vacancyId }, existingElement) { + #isApplied; + #resetApplyButton; + #activeApplyButton; + constructor({ isOwner, isApplicant, ownerId, vacancyId, isApplied }, existingElement) { super({ renderParams: { isOwner, isApplicant, ownerId }, existingElement, templateName: 'vacancy-article__button-container.hbs', }); + this.#isApplied = isApplied; this.#vacancyId = vacancyId; if (isApplicant) { this.#applyButton = this._html.querySelector('.vacancy-article__apply-button'); this._eventListeners.push({ event: 'click', object: this.#applyButton, - listener: (ev) => { + listener: function (ev) { ev.preventDefault(); - eventBus.emit(VACANCY_APPLY, { vacancyId: this.#vacancyId }); - }, + eventBus.emit(VACANCY_APPLY, { caller: this, vacancyId: this.#vacancyId }); + }.bind(this), + }); + this.#resetApplyButton = this._html.querySelector('.vacancy-article__reset-apply-button'); + this._eventListeners.push({ + event: 'click', + object: this.#resetApplyButton, + listener: function (ev) { + ev.preventDefault(); + eventBus.emit(VACANCY_RESET_APPLY, { caller: this, vacancyId: this.#vacancyId }); + }.bind(this), }); + this.#activeApplyButton = this.#isApplied ? this.#resetApplyButton : this.#applyButton; + this.#activeApplyButton.classList.remove('hidden'); } else if (isOwner) { this.#editButton = this._html.querySelector('.vacancy-article__edit-button'); this.#deleteButton = this._html.querySelector('.vacancy-article__delete-button'); @@ -49,4 +69,15 @@ export class ButtonContainerView extends ComponentView { } addEventListeners(this._eventListeners); } + + toggleApplyButton() { + this.#activeApplyButton.classList.add('hidden'); + if (Object.is(this.#activeApplyButton, this.#applyButton)) { + this.#activeApplyButton = this.#resetApplyButton; + this.#resetApplyButton.classList.remove('hidden'); + } else { + this.#activeApplyButton = this.#applyButton; + this.#applyButton.classList.remove('hidden'); + } + } } diff --git a/src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs b/src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs index c34e92e..6f085d1 100644 --- a/src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs +++ b/src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs @@ -1,6 +1,7 @@
{{#if isApplicant}} - + + Другие вакансии {{else}} {{#if isOwner}} diff --git a/src/Components/VacancyArticle/VacancyArticle.js b/src/Components/VacancyArticle/VacancyArticle.js index 64ac9bd..e06c544 100644 --- a/src/Components/VacancyArticle/VacancyArticle.js +++ b/src/Components/VacancyArticle/VacancyArticle.js @@ -4,6 +4,7 @@ import { VacancyArticleModel } from './VacancyArticleModel.js'; import { VacancyArticleController } from './VacancyArticleController.js'; import { ButtonContainer } from './ButtonContainer/ButtonContainer.js'; import USER_TYPE from '../../modules/UserSession/UserType.js'; +import { Api } from '../../modules/Api/Api.js'; export class VacancyArticle extends Component { constructor({ elementClass, vacancyId, userId, userType }) { @@ -20,9 +21,19 @@ export class VacancyArticle extends Component { } async makeButtons() { - const modelData = await this._controller.fetchData(); + let modelData = {}; + let appliedStatus = {}; + if (this._userType === USER_TYPE.APPLICANT) { + [modelData, appliedStatus] = await Promise.all([ + this._controller.fetchData(), + Api.getVacancyApplyStatusById({ id: this._vacancyId }), + ]); + } else { + modelData = await this._controller.fetchData(); + } this._buttonContainer = new ButtonContainer({ isOwner: modelData.employerId === this._userId && this._userType === USER_TYPE.EMPLOYER, + isApplied: appliedStatus.isSubscribed, isApplicant: this._userType === USER_TYPE.APPLICANT, ownerId: modelData.employerId, vacancyId: this._vacancyId, diff --git a/src/Components/VacancyArticle/VacancyArticleController.js b/src/Components/VacancyArticle/VacancyArticleController.js index ba90729..b4afba2 100644 --- a/src/Components/VacancyArticle/VacancyArticleController.js +++ b/src/Components/VacancyArticle/VacancyArticleController.js @@ -1,5 +1,12 @@ import { ComponentController } from '../../modules/Components/Component.js'; -import { GO_TO, REDIRECT_TO, VACANCY_DELETE, VACANCY_EDIT } from '../../modules/Events/Events.js'; +import { + GO_TO, + REDIRECT_TO, + VACANCY_APPLY, + VACANCY_DELETE, + VACANCY_EDIT, + VACANCY_RESET_APPLY, +} from '../../modules/Events/Events.js'; import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; import eventBus from '../../modules/Events/EventBus.js'; import { VacancyEditPage } from '../../Pages/VacancyEditPage/VacancyEditPage.js'; @@ -16,6 +23,14 @@ export class VacancyArticleController extends ComponentController { event: VACANCY_EDIT, handler: this.vacancyEdit.bind(this), }, + { + event: VACANCY_APPLY, + handler: this.vacancyApply.bind(this), + }, + { + event: VACANCY_RESET_APPLY, + handler: this.vacancyResetApply.bind(this), + }, ]); } @@ -44,4 +59,16 @@ export class VacancyArticleController extends ComponentController { query[VacancyEditPage.VACANCY_ID_PARAM] = vacancy.id; eventBus.emit(GO_TO, { redirectUrl: resolveUrl('editVacancy', query) }); } + + async vacancyApply({ caller }) { + if (this._model.vacancyApply()) { + caller.toggleApplyButton(); + } + } + + async vacancyResetApply({ caller }) { + if (this._model.vacancyResetApply()) { + caller.toggleApplyButton(); + } + } } diff --git a/src/Components/VacancyArticle/VacancyArticleModel.js b/src/Components/VacancyArticle/VacancyArticleModel.js index 190c1db..e18c456 100644 --- a/src/Components/VacancyArticle/VacancyArticleModel.js +++ b/src/Components/VacancyArticle/VacancyArticleModel.js @@ -2,10 +2,13 @@ import { ComponentModel } from '../../modules/Components/Component.js'; import { Api } from '../../modules/Api/Api.js'; import { Vacancy } from '../../modules/models/Vacancy.js'; import { NotFoundError } from '../../modules/Router/Router.js'; +import state from '../../modules/AppState/AppState.js'; +import USER_TYPE from '../../modules/UserSession/UserType.js'; export class VacancyArticleModel extends ComponentModel { #vacancyData; #vacancyId; + #userType; constructor({ vacancyId }) { super(); @@ -16,6 +19,7 @@ export class VacancyArticleModel extends ComponentModel { throw new NotFoundError('vacancy not found'); }, ); + this.#userType = state.userSession.userType; } async getVacancyData() { @@ -39,4 +43,30 @@ export class VacancyArticleModel extends ComponentModel { return false; } } + + async vacancyApply() { + if (!this.#vacancyId || this.#userType !== USER_TYPE.APPLICANT) { + return false; + } + try { + await Api.vacancyApply({ id: this.#vacancyId }); + return true; + } catch (err) { + console.log(err); + return false; + } + } + + async vacancyResetApply() { + if (!this.#vacancyId || this.#userType !== USER_TYPE.APPLICANT) { + return false; + } + try { + await Api.vacancyResetApply({ id: this.#vacancyId }); + return true; + } catch (err) { + console.log(err); + return false; + } + } } diff --git a/src/Components/VacancyArticle/VacancyArticleView.js b/src/Components/VacancyArticle/VacancyArticleView.js index 201aedf..5ea4e87 100644 --- a/src/Components/VacancyArticle/VacancyArticleView.js +++ b/src/Components/VacancyArticle/VacancyArticleView.js @@ -19,10 +19,19 @@ export class VacancyArticleView extends ComponentView { this._description = this._html.querySelector('.vacancy-article__description'); } - renderData({ avatar, position, companyName, salary, workType, description, updatedAt }) { + renderData({ + avatar, + position, + companyName, + salary, + workType, + description, + location, + updatedAt, + }) { this._avatar.href = avatar; this._position.innerText = position; - this._companyName.innerText = companyName; + this._companyName.innerText = `${companyName}, ${location}`; this._salary.innerText = salary ? `${salary} руб.` : 'Не указана'; this._workType.innerText = workType; this._description.innerText = description; diff --git a/src/Components/VacancyCard/VacancyCard.js b/src/Components/VacancyCard/VacancyCard.js index 0c2e21a..d505af6 100644 --- a/src/Components/VacancyCard/VacancyCard.js +++ b/src/Components/VacancyCard/VacancyCard.js @@ -11,7 +11,7 @@ export class VacancyCard extends Component { modelClass: ComponentModel, controllerClass: ComponentController, viewClass: VacancyCardView, - viewParams: { vacancyObj }, + viewParams: vacancyObj, existingElement, }); } diff --git a/src/Components/VacancyCard/VacancyCardView.js b/src/Components/VacancyCard/VacancyCardView.js index 18dadc2..8d0dc2f 100644 --- a/src/Components/VacancyCard/VacancyCardView.js +++ b/src/Components/VacancyCard/VacancyCardView.js @@ -1,9 +1,20 @@ import { ComponentView } from '../../modules/Components/Component.js'; export class VacancyCardView extends ComponentView { - constructor({ vacancyObj }, existingElement) { + constructor( + { avatar, id, position, companyName, location, salary, description, updatedAt }, + existingElement, + ) { + const renderParams = { avatar, id, position, companyName, location, salary, description }; + renderParams.updatedAt = updatedAt.toLocaleDateString('ru-RU', { + weekday: 'short', + day: 'numeric', + month: 'numeric', + year: 'numeric', + }); + super({ - renderParams: vacancyObj, + renderParams, templateName: 'vacancy-card.hbs', existingElement, }); diff --git a/src/Components/VacancyCard/vacancy-card.hbs b/src/Components/VacancyCard/vacancy-card.hbs index 9aefc8e..4a629ab 100644 --- a/src/Components/VacancyCard/vacancy-card.hbs +++ b/src/Components/VacancyCard/vacancy-card.hbs @@ -6,15 +6,11 @@
{{companyName}}, {{location}}
Зарплата: {{salary}}
-
{{description}}
\ No newline at end of file diff --git a/src/Components/VacancyForm/VacancyForm.js b/src/Components/VacancyForm/VacancyForm.js index e057c21..09454d1 100644 --- a/src/Components/VacancyForm/VacancyForm.js +++ b/src/Components/VacancyForm/VacancyForm.js @@ -16,7 +16,7 @@ export class VacancyForm extends Component { controllerClass: VacancyFormController, modelParams: { vacancyId }, existingElement, - viewParams: { elementClass, isNew: !vacancyId }, + viewParams: { elementClass, isNew: !vacancyId, vacancyId }, }); this.#isNew = !vacancyId; this._positionField = new LiteralInput({ diff --git a/src/Components/VacancyForm/VacancyFormView.js b/src/Components/VacancyForm/VacancyFormView.js index 4c0c736..afa446d 100644 --- a/src/Components/VacancyForm/VacancyFormView.js +++ b/src/Components/VacancyForm/VacancyFormView.js @@ -5,9 +5,9 @@ import { addEventListeners } from '../../modules/Events/EventUtils.js'; import { getFormData } from '../../modules/FormUtils/FormUtils.js'; export class VacancyFormView extends ComponentView { - constructor({ elementClass, isNew }, existingElement) { + constructor({ elementClass, isNew, vacancyId }, existingElement) { super({ - renderParams: { elementClass, isNew }, + renderParams: { elementClass, isNew, vacancyId }, existingElement, templateName: 'vacancy-form.hbs', }); diff --git a/src/Components/VacancyForm/vacancy-form.hbs b/src/Components/VacancyForm/vacancy-form.hbs index 0897dc1..10e6a4f 100644 --- a/src/Components/VacancyForm/vacancy-form.hbs +++ b/src/Components/VacancyForm/vacancy-form.hbs @@ -14,6 +14,6 @@ Сохранить {{/if}} - В профиль + В профиль
\ No newline at end of file diff --git a/src/Pages/CvEditPage/CvEditPage.js b/src/Pages/CvEditPage/CvEditPage.js new file mode 100644 index 0000000..b802f77 --- /dev/null +++ b/src/Pages/CvEditPage/CvEditPage.js @@ -0,0 +1,60 @@ +import { Header } from '../../Components/Header/Header.js'; +import state from '../../modules/AppState/AppState.js'; +import { Page } from '../../modules/Page/Page.js'; +import { ForbiddenPage, NotFoundError } from '../../modules/Router/Router.js'; +import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; +import { CvEditPageController } from './CvEditPageController.js'; +import { CvEditPageModel } from './CvEditPageModel.js'; +import { CvEditPageView } from './CvEditPageView.js'; +import { CvForm } from '../../Components/CvForm/CvForm.js'; +import USER_TYPE from '../../modules/UserSession/UserType.js'; +import { zip } from '../../modules/ObjectUtils/Zip.js'; + +export class CvEditPage extends Page { + #cvId; + + static CV_ID_PARAM = 'id'; + + constructor({ url }) { + if (state.userSession.userType !== USER_TYPE.APPLICANT) { + throw new ForbiddenPage(resolveUrl('vacancies')); + } + let cvId; + switch (url.pathname) { + case resolveUrl('editCv').pathname: { + cvId = +url.searchParams.get(CvEditPage.CV_ID_PARAM); + if (!cvId) { + throw new NotFoundError(); + } + break; + } + case resolveUrl('createCv').pathname: { + cvId = null; + break; + } + } + super({ + url, + modelClass: CvEditPageModel, + viewClass: CvEditPageView, + controllerClass: CvEditPageController, + viewParams: zip(Header.getViewParams(), { isNew: !cvId }), + }); + this.#cvId = cvId; + } + + postRenderInit() { + this._header = new Header({ + existingElement: this._view.header, + }); + this._children.push(this._header); + + this._cvForm = new CvForm({ + userId: state.userSession.userId, + cvId: this.#cvId, + elementClass: 'cv-edit-page__cv-form', + }); + this._children.push(this._cvForm); + this._controller.addCvForm(this._cvForm); + } +} diff --git a/src/Pages/CvEditPage/CvEditPageController.js b/src/Pages/CvEditPage/CvEditPageController.js new file mode 100644 index 0000000..e15eb3f --- /dev/null +++ b/src/Pages/CvEditPage/CvEditPageController.js @@ -0,0 +1,11 @@ +import { PageController } from '../../modules/Page/Page.js'; + +export class CvEditPageController extends PageController { + constructor(model, view, component) { + super(model, view, component); + } + + addCvForm(cvForm) { + this._view.addCvForm(cvForm.render()); + } +} diff --git a/src/Pages/CvEditPage/CvEditPageModel.js b/src/Pages/CvEditPage/CvEditPageModel.js new file mode 100644 index 0000000..c994c89 --- /dev/null +++ b/src/Pages/CvEditPage/CvEditPageModel.js @@ -0,0 +1,3 @@ +import { PageModel } from '../../modules/Page/Page.js'; + +export const CvEditPageModel = PageModel; diff --git a/src/Pages/CvEditPage/CvEditPageView.js b/src/Pages/CvEditPage/CvEditPageView.js new file mode 100644 index 0000000..262192f --- /dev/null +++ b/src/Pages/CvEditPage/CvEditPageView.js @@ -0,0 +1,17 @@ +import { PageView } from '../../modules/Page/Page.js'; + +export class CvEditPageView extends PageView { + constructor(renderParams) { + super({ + templateName: 'cv-page-edit.hbs', + renderParams: renderParams, + }); + this.header = this._html.querySelector('.header'); + this.formBox = this._html.querySelector('.cv-page-edit__form-container'); + } + + addCvForm(formRender) { + this.cvForm = formRender; + this.formBox.appendChild(formRender); + } +} diff --git a/src/Pages/CvEditPage/cv-page-edit.hbs b/src/Pages/CvEditPage/cv-page-edit.hbs new file mode 100644 index 0000000..4008baa --- /dev/null +++ b/src/Pages/CvEditPage/cv-page-edit.hbs @@ -0,0 +1,8 @@ +
+ {{> header}} +
+
+

{{#if isNew}}Создание{{else}}Редактирование{{/if}} резюме

+
+
+
\ No newline at end of file diff --git a/src/css/cv.css b/src/css/cv.css index 551e56c..0f280b2 100644 --- a/src/css/cv.css +++ b/src/css/cv.css @@ -119,3 +119,66 @@ color: var(--color-main-100); padding: 0 32px; } + +/* Cv Edit Page */ + +.cv-page-edit { + display: flex; + flex-direction: column; + justify-content: start; + align-items: center; + width: 100vw; + height: 100vh; +} + +.cv-page-edit__form-container { + margin-top: 32px; + padding: 20px; + border-radius: 14px; + width: 100%; + max-width: 600px; + box-sizing: border-box; + background-color: var(--color-background-900); +} + +.cv-page-edit__header { + font-size: var(--text-size-8); + color: var(--color-main-100); + text-align: center; +} + +/* Vacancy Form */ + +.cv-form__position-ru { + margin-bottom: 10px; +} + +.cv-form__position-en { + margin-bottom: 10px; +} + +.cv-form__job-search-status { + margin-bottom: 10px; +} + +.cv-form__working-experience { + margin-bottom: 20px; +} + +.cv-form__description { + margin-bottom: 10px; +} + +.cv-form__description-textarea { + resize: vertical; +} + +.cv-form__working-experience-textarea { + resize: vertical; +} + +.cv-form__button-container { + display: flex; + align-items: baseline; + gap: 20px; +} diff --git a/src/css/vacancy.css b/src/css/vacancy.css index b398e10..c3bf97a 100644 --- a/src/css/vacancy.css +++ b/src/css/vacancy.css @@ -62,7 +62,13 @@ .vacancy-article__apply-button { margin-right: 10px; box-shadow: none; - padding: 4px 12px; + padding: 10px 20px; +} + +.vacancy-article__reset-apply-button { + margin-right: 10px; + box-shadow: none; + padding: 10px 20px; } .vacancy-article__created-at { @@ -93,10 +99,6 @@ justify-content: start; } -.vacancy-article__apply-button { - padding: 10px 20px; -} - /* vacancy-summary component */ .vacancy-summary { diff --git a/src/index.js b/src/index.js index db55dc3..a480114 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,7 @@ import { VacancyEditPage } from './Pages/VacancyEditPage/VacancyEditPage.js'; import { resolveUrl } from './modules/UrlUtils/UrlUtils.js'; import { REDIRECT_TO, GO_TO } from './modules/Events/Events.js'; import { CvPage } from './Pages/CvPage/CvPage.js'; +import { CvEditPage } from './Pages/CvEditPage/CvEditPage.js'; handlebarsInit(); @@ -23,6 +24,8 @@ router.addRoute(resolveUrl('vacancy').pathname, VacancyPage); router.addRoute(resolveUrl('createVacancy').pathname, VacancyEditPage); router.addRoute(resolveUrl('editVacancy').pathname, VacancyEditPage); router.addRoute(resolveUrl('cv').pathname, CvPage); +router.addRoute(resolveUrl('createCv').pathname, CvEditPage); +router.addRoute(resolveUrl('editCv').pathname, CvEditPage); eventBus.on(REDIRECT_TO, ({ redirectUrl }) => { router.navigate(redirectUrl, true, true); diff --git a/src/modules/Api/Api.js b/src/modules/Api/Api.js index c61e347..968eda9 100644 --- a/src/modules/Api/Api.js +++ b/src/modules/Api/Api.js @@ -15,6 +15,7 @@ const backendApi = new Map( vacancy: backendPrefix + 'vacancy/', vacancySubscribers: backendPrefix + 'vacancy/subscribers/', cv: backendPrefix + 'cv/', + vacancyApply: backendPrefix + 'vacancy/subscription/', }), ); @@ -287,6 +288,49 @@ export class Api { return unpackStandardApiCall(response); }; + static createCv = async ({ + positionRu, + positionEn, + workingExperience, + jobSearchStatus, + description, + }) => { + const response = await fetchCorsJson(backendApi.get('cv'), { + method: HTTP_METHOD.POST, + body: JSON.stringify({ + positionRu, + positionEn, + workingExperience, + jobSearchStatus, + description, + }), + credentials: 'include', + }); + return unpackStandardApiCall(response); + }; + + static updateCvById = async ({ + id, + positionRu, + positionEn, + workingExperience, + jobSearchStatus, + description, + }) => { + const response = await fetchCorsJson(backendApi.get('cv') + id, { + method: HTTP_METHOD.PUT, + body: JSON.stringify({ + positionRu, + positionEn, + workingExperience, + jobSearchStatus, + description, + }), + credentials: 'include', + }); + return unpackStandardApiCall(response); + }; + static logout = async () => { const response = await fetchCorsJson(backendApi.get('logout'), { method: 'POST', @@ -295,4 +339,30 @@ export class Api { }); return unpackStandardApiCall(response); }; + + static getVacancyApplyStatusById = async ({ id }) => { + const response = await fetchCorsJson(backendApi.get('vacancyApply') + id, { + method: HTTP_METHOD.GET, + credentials: 'include', + }); + return unpackStandardApiCall(response); + }; + + static vacancyApply = async ({ id }) => { + const response = await fetchCorsJson(backendApi.get('vacancyApply') + id, { + method: HTTP_METHOD.POST, + body: {}, + credentials: 'include', + }); + return unpackStandardApiCall(response); + }; + + static vacancyResetApply = async ({ id }) => { + const response = await fetchCorsJson(backendApi.get('vacancyApply') + id, { + method: HTTP_METHOD.DELETE, + body: {}, + credentials: 'include', + }); + return unpackStandardApiCall(response); + }; } diff --git a/src/modules/Events/Events.js b/src/modules/Events/Events.js index 4ddc7c9..988628e 100644 --- a/src/modules/Events/Events.js +++ b/src/modules/Events/Events.js @@ -23,6 +23,7 @@ export const USER_UPDATED = 'user updated'; export const MINICARD_DELETE = 'minicard delete'; export const VACANCY_APPLY = 'vacancy apply'; +export const VACANCY_RESET_APPLY = 'vacancy reset apply'; export const VACANCY_EDIT = 'vacancy edit'; export const VACANCY_DELETE = 'vacancy delete'; diff --git a/src/modules/UrlUtils/UrlUtils.js b/src/modules/UrlUtils/UrlUtils.js index 6c021e3..3375159 100644 --- a/src/modules/UrlUtils/UrlUtils.js +++ b/src/modules/UrlUtils/UrlUtils.js @@ -10,6 +10,8 @@ const urls = { createVacancy: '/vacancy/new', editVacancy: '/vacancy/edit', cv: '/cv', + createCv: '/cv/new', + editCv: '/cv/edit', }; const knownUrls = new Map(Object.entries(urls)); From 6dca94af23ddc4b521982f4700bc51cfc855edcf Mon Sep 17 00:00:00 2001 From: Ilya Andriyanov Date: Fri, 8 Nov 2024 12:03:23 +0300 Subject: [PATCH 06/11] feature: notification service --- .../ApplicantProfileFormController.js | 7 +- .../ApplicantProfileFormModel.js | 6 +- .../ApplicantRegistrationFormController.js | 12 ++- .../ApplicantRegistrationFormModel.js | 13 +++- .../ApplicantRegistrationFormView.js | 13 ++-- .../applicant-registration-form.hbs | 1 - .../AppliersList/AppliersListModel.js | 29 ++++--- .../CrudFormBox/CrudFormBoxController.js | 5 +- src/Components/CvArticle/CvArticleModel.js | 3 +- src/Components/CvForm/CvFormController.js | 32 ++++++-- src/Components/CvForm/CvFormModel.js | 17 +++-- .../EmployerProfileFormController.js | 7 +- .../EmployerProfileFormModel.js | 8 +- .../EmployerRegistrationFormController.js | 1 - .../EmployerRegistrationFormModel.js | 13 +++- .../EmployerRegistrationFormView.js | 12 +-- .../employer-registration-form.hbs | 1 - .../Lists/ApplicantCvList/ApplicantCvList.js | 3 +- .../EmployerVacancyList.js | 3 +- src/Components/Lists/List/ListController.js | 35 ++++++--- src/Components/Lists/List/list.hbs | 4 + .../LoginForm/LoginFormController.js | 8 +- src/Components/LoginForm/LoginFormModel.js | 30 ++++---- src/Components/LoginForm/LoginFormView.js | 15 ++-- src/Components/LoginForm/login-form.hbs | 1 - .../NotificationBox/NotificationBox.js | 21 +++++ .../NotificationBoxController.js | 35 +++++++++ .../NotificationBox/NotificationBoxModel.js | 3 + .../NotificationBox/NotificationBoxView.js | 76 +++++++++++++++++++ .../ProfileMinicardController.js | 7 +- .../VacancyArticle/VacancyArticleModel.js | 7 +- src/Components/VacancyCard/vacancy-card.hbs | 3 +- src/Components/VacancyForm/VacancyForm.js | 3 +- .../VacancyForm/VacancyFormController.js | 29 +++++-- src/Pages/VacanciesPage/VacanciesPageModel.js | 37 +++++---- src/css/index.css | 57 +++++++++----- src/css/vacancies.css | 31 ++++++-- src/index.html | 1 + src/index.js | 6 ++ src/modules/Api/Api.js | 5 +- src/modules/Api/Errors.js | 21 +++++ src/modules/Events/EventBus.js | 1 - src/modules/Events/Events.js | 3 + src/modules/Page/Page.js | 4 +- 44 files changed, 482 insertions(+), 147 deletions(-) create mode 100644 src/Components/NotificationBox/NotificationBox.js create mode 100644 src/Components/NotificationBox/NotificationBoxController.js create mode 100644 src/Components/NotificationBox/NotificationBoxModel.js create mode 100644 src/Components/NotificationBox/NotificationBoxView.js create mode 100644 src/modules/Api/Errors.js diff --git a/src/Components/ApplicantProfileForm/ApplicantProfileFormController.js b/src/Components/ApplicantProfileForm/ApplicantProfileFormController.js index 7fb5598..967219a 100644 --- a/src/Components/ApplicantProfileForm/ApplicantProfileFormController.js +++ b/src/Components/ApplicantProfileForm/ApplicantProfileFormController.js @@ -1,6 +1,7 @@ import { ComponentController } from '../../modules/Components/Component.js'; import eventBus from '../../modules/Events/EventBus.js'; -import { USER_UPDATED } from '../../modules/Events/Events.js'; +import { NOTIFICATION_OK, USER_UPDATED } from '../../modules/Events/Events.js'; +import { NOTIFICATION_TIMEOUT } from '../NotificationBox/NotificationBox.js'; export class ApplicantProfileFormController extends ComponentController { constructor(model, view, controller) { @@ -40,6 +41,10 @@ export class ApplicantProfileFormController extends ComponentController { return false; } eventBus.emit(USER_UPDATED); + eventBus.emit(NOTIFICATION_OK, { + message: 'Успешно сохранено', + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); return true; } diff --git a/src/Components/ApplicantProfileForm/ApplicantProfileFormModel.js b/src/Components/ApplicantProfileForm/ApplicantProfileFormModel.js index b357045..e00096b 100644 --- a/src/Components/ApplicantProfileForm/ApplicantProfileFormModel.js +++ b/src/Components/ApplicantProfileForm/ApplicantProfileFormModel.js @@ -1,6 +1,7 @@ import { ComponentModel } from '../../modules/Components/Component.js'; import { Api } from '../../modules/Api/Api.js'; import { Applicant } from '../../modules/models/Applicant.js'; +import { catchStandardResponseError } from '../../modules/Api/Errors.js'; export class ApplicantProfileFormModel extends ComponentModel { #lastValidData; @@ -23,11 +24,14 @@ export class ApplicantProfileFormModel extends ComponentModel { async submit(formData) { formData.birthDate = new Date(formData.birthDate); formData.id = this.#userId; - if (await Api.updateApplicantProfile(formData)) { + try { + await Api.updateApplicantProfile(formData); const app = new Applicant(formData); app.birthDate = app.birthDate.toISOString().split('T')[0]; this.#lastValidData = app; return true; + } catch (err) { + catchStandardResponseError(err); } return false; } diff --git a/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormController.js b/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormController.js index 6f31206..fdc73d8 100644 --- a/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormController.js +++ b/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormController.js @@ -2,6 +2,9 @@ import { ComponentController } from '../../modules/Components/Component.js'; import { REGISTER_APPLICANT } from '../../modules/Events/Events.js'; import router from '/src/modules/Router/Router.js'; import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; +import eventBus from '../../modules/Events/EventBus.js'; +import { NOTIFICATION_OK } from '../../modules/Events/Events.js'; +import { NOTIFICATION_TIMEOUT } from '../NotificationBox/NotificationBox.js'; export class ApplicantRegistrationFormController extends ComponentController { constructor(model, view, controller) { @@ -20,14 +23,19 @@ export class ApplicantRegistrationFormController extends ComponentController { } this._model .register(formData) - .then(() => router.navigate(new URL(resolveUrl('vacancies')), true, true)) + .then(() => { + eventBus.emit(NOTIFICATION_OK, { + message: 'Успешно сохранено', + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); + router.navigate(new URL(resolveUrl('vacancies')), true, true); + }) .catch((errorMsg) => { this._view.declineValidation(errorMsg); }); } _validate(formData) { - this._view.hideError(); const formValidationError = this._model.validate(formData); if (formValidationError) { this._view.declineValidation(formValidationError); diff --git a/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormModel.js b/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormModel.js index b26ad0d..08c262c 100644 --- a/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormModel.js +++ b/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormModel.js @@ -1,6 +1,8 @@ import state from '../../modules/AppState/AppState.js'; import { ComponentModel } from '../../modules/Components/Component.js'; import USER_TYPE from '../../modules/UserSession/UserType.js'; +import { USER_ALREADY_EXISTS_ERROR } from '../../modules/Api/Errors.js'; +import { TransportError, ResponseError } from '../../modules/Api/Api.js'; export class ApplicantRegistrationFormModel extends ComponentModel { validate(formData) { @@ -18,6 +20,15 @@ export class ApplicantRegistrationFormModel extends ComponentModel { } async register(formData) { - return state.userSession.register(USER_TYPE.APPLICANT, formData); + return state.userSession.register(USER_TYPE.APPLICANT, formData).catch((err) => { + if (err.toString() === USER_ALREADY_EXISTS_ERROR) { + return Promise.reject('Соискатель с таким адресом электронной почты уже зарегистрирован'); + } + if (err instanceof TransportError) { + return Promise.reject('Произошла сетевая ошибка, повторите позднее'); + } + if (err instanceof ResponseError) + return Promise.reject('Произошла непредвиденная ошибка, повторите позднее'); + }); } } diff --git a/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormView.js b/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormView.js index a09928d..83f7e9b 100644 --- a/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormView.js +++ b/src/Components/ApplicantRegistrationForm/ApplicantRegistrationFormView.js @@ -1,8 +1,10 @@ import { ComponentView } from '../../modules/Components/Component.js'; +import { NOTIFICATION_ERROR } from '../../modules/Events/Events.js'; import { addEventListeners } from '../../modules/Events/EventUtils.js'; import { getFormData } from '../../modules/FormUtils/FormUtils.js'; import eventBus from '/src/modules/Events/EventBus.js'; import { REGISTER_APPLICANT } from '/src/modules/Events/Events.js'; +import { NOTIFICATION_TIMEOUT } from '../NotificationBox/NotificationBox.js'; export class ApplicantRegistrationFormView extends ComponentView { constructor({ elementClass }, existingElement) { @@ -28,19 +30,16 @@ export class ApplicantRegistrationFormView extends ComponentView { this.repeatPasswordField = this._html.querySelector( '.applicant-registration-form__repeat-password', ); - this.error = this._html.querySelector('.applicant-registration-form__error'); } getData() { return getFormData(this._html); } - hideError() { - this.error.hidden = true; - } - declineValidation(errorMessage) { - this.error.innerText = errorMessage; - this.error.hidden = false; + eventBus.emit(NOTIFICATION_ERROR, { + message: errorMessage, + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); } } diff --git a/src/Components/ApplicantRegistrationForm/applicant-registration-form.hbs b/src/Components/ApplicantRegistrationForm/applicant-registration-form.hbs index 34ea4b1..fcb996b 100644 --- a/src/Components/ApplicantRegistrationForm/applicant-registration-form.hbs +++ b/src/Components/ApplicantRegistrationForm/applicant-registration-form.hbs @@ -1,5 +1,4 @@
- {{> validated-input formName="applicant-registration-form" elementName="first-name" inputName="firstName" inputCaption="Имя" inputType="text"}} {{> validated-input formName="applicant-registration-form" elementName="second-name" inputName="secondName" inputCaption="Фамилия" inputType="text"}} {{> validated-input formName="applicant-registration-form" elementName="birthdate" inputName="birthDate" inputCaption="Дата Рождения" inputType="date"}} diff --git a/src/Components/AppliersList/AppliersListModel.js b/src/Components/AppliersList/AppliersListModel.js index 410d28a..f6c5bb9 100644 --- a/src/Components/AppliersList/AppliersListModel.js +++ b/src/Components/AppliersList/AppliersListModel.js @@ -1,6 +1,7 @@ import { ComponentModel } from '../../modules/Components/Component.js'; import { Api } from '../../modules/Api/Api.js'; import { Applicant } from '../../modules/models/Applicant.js'; +import { catchStandardResponseError } from '../../modules/Api/Errors.js'; export class AppliersListModel extends ComponentModel { #vacancyId; @@ -10,17 +11,21 @@ export class AppliersListModel extends ComponentModel { } async getItems() { - const peopleJson = (await Api.getAppliersByVacancyId({ id: this.#vacancyId })).subscribers; - const applicantObjects = peopleJson.reduce((applicantObjects, applicantJsonItem) => { - try { - const applicant = new Applicant(applicantJsonItem); - applicant.name = `${applicant.firstName} ${applicant.secondName}`; - applicantObjects.push(applicant); - return applicantObjects; - } catch { - return applicantObjects; - } - }, []); - return applicantObjects; + try { + const peopleJson = (await Api.getAppliersByVacancyId({ id: this.#vacancyId })).subscribers; + const applicantObjects = peopleJson.reduce((applicantObjects, applicantJsonItem) => { + try { + const applicant = new Applicant(applicantJsonItem); + applicant.name = `${applicant.firstName} ${applicant.secondName}`; + applicantObjects.push(applicant); + return applicantObjects; + } catch { + return applicantObjects; + } + }, []); + return applicantObjects; + } catch (err) { + catchStandardResponseError(err); + } } } diff --git a/src/Components/CrudFormBox/CrudFormBoxController.js b/src/Components/CrudFormBox/CrudFormBoxController.js index 275e4e7..a4584d5 100644 --- a/src/Components/CrudFormBox/CrudFormBoxController.js +++ b/src/Components/CrudFormBox/CrudFormBoxController.js @@ -39,11 +39,12 @@ export class CrudFormBoxController extends ComponentController { this._component.disableForm(); } - submitForm({ caller }) { + async submitForm({ caller }) { if (!Object.is(this._view.formView, caller)) { return; } - if (!this._component.form.submit()) { + const submitResult = await this._component.form.submit(); + if (!submitResult) { return; } this._view.readState(); diff --git a/src/Components/CvArticle/CvArticleModel.js b/src/Components/CvArticle/CvArticleModel.js index d49a5ac..63863a9 100644 --- a/src/Components/CvArticle/CvArticleModel.js +++ b/src/Components/CvArticle/CvArticleModel.js @@ -2,6 +2,7 @@ import { ComponentModel } from '../../modules/Components/Component.js'; import { Api } from '../../modules/Api/Api.js'; import { NotFoundError } from '../../modules/Router/Router.js'; import { Cv } from '../../modules/models/Cv.js'; +import { catchStandardResponseError } from '../../modules/Api/Errors.js'; export class CvArticleModel extends ComponentModel { #cvData; @@ -35,7 +36,7 @@ export class CvArticleModel extends ComponentModel { await Api.deleteCvById({ id: this.#cvId }); return true; } catch (err) { - console.log(err); + catchStandardResponseError(err); return false; } } diff --git a/src/Components/CvForm/CvFormController.js b/src/Components/CvForm/CvFormController.js index 01a8fbc..36ab204 100644 --- a/src/Components/CvForm/CvFormController.js +++ b/src/Components/CvForm/CvFormController.js @@ -1,9 +1,16 @@ import { ComponentController } from '../../modules/Components/Component.js'; -import { REDIRECT_TO, SUBMIT_FORM } from '../../modules/Events/Events.js'; +import { + NOTIFICATION_ERROR, + NOTIFICATION_OK, + REDIRECT_TO, + SUBMIT_FORM, +} from '../../modules/Events/Events.js'; import { CvPage } from '../../Pages/CvPage/CvPage.js'; import { Cv } from '../../modules/models/Cv.js'; import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; import eventBus from '../../modules/Events/EventBus.js'; +import { NOTIFICATION_TIMEOUT } from '../NotificationBox/NotificationBox.js'; +import { catchStandardResponseError } from '../../modules/Api/Errors.js'; export class CvFormController extends ComponentController { constructor(model, view, controller) { @@ -19,6 +26,10 @@ export class CvFormController extends ComponentController { _validate() { const errorMessage = this._model.validate(this._view.getData()); if (errorMessage) { + eventBus.emit(NOTIFICATION_ERROR, { + message: errorMessage, + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); return false; } return [ @@ -47,13 +58,22 @@ export class CvFormController extends ComponentController { if (!this._validate()) { return; } - const cv = await this._model.submit(new Cv(this._view.getData())); - if (!cv) { + try { + const cv = await this._model.submit(new Cv(this._view.getData())); + if (!cv) { + return; + } + const query = {}; + query[CvPage.CV_ID_PARAM] = cv.id; + eventBus.emit(NOTIFICATION_OK, { + message: 'Операция проведена успешно', + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); + eventBus.emit(REDIRECT_TO, { redirectUrl: resolveUrl('cv', query) }); + } catch (err) { + catchStandardResponseError(err); return; } - const query = {}; - query[CvPage.CV_ID_PARAM] = cv.id; - eventBus.emit(REDIRECT_TO, { redirectUrl: resolveUrl('cv', query) }); } async reset() { diff --git a/src/Components/CvForm/CvFormModel.js b/src/Components/CvForm/CvFormModel.js index 205ce0f..4e0e6b4 100644 --- a/src/Components/CvForm/CvFormModel.js +++ b/src/Components/CvForm/CvFormModel.js @@ -5,6 +5,7 @@ import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; import { zip } from '../../modules/ObjectUtils/Zip.js'; import eventBus from '../../modules/Events/EventBus.js'; import { REDIRECT_TO } from '../../modules/Events/Events.js'; +import { catchStandardResponseError } from '../../modules/Api/Errors.js'; export class CvFormModel extends ComponentModel { #lastValidData; @@ -30,12 +31,16 @@ export class CvFormModel extends ComponentModel { } async submit(formData) { - const cv = this.#isNew - ? await Api.createCv(formData) - : await Api.updateCvById(zip({ id: this.#cvId }, formData)); - if (cv) { - this.#lastValidData = formData; - return cv; + try { + const cv = this.#isNew + ? await Api.createCv(formData) + : await Api.updateCvById(zip({ id: this.#cvId }, formData)); + if (cv) { + this.#lastValidData = formData; + return cv; + } + } catch (err) { + catchStandardResponseError(err); } return null; } diff --git a/src/Components/EmployerProfileForm/EmployerProfileFormController.js b/src/Components/EmployerProfileForm/EmployerProfileFormController.js index 7c7a1a1..4598626 100644 --- a/src/Components/EmployerProfileForm/EmployerProfileFormController.js +++ b/src/Components/EmployerProfileForm/EmployerProfileFormController.js @@ -1,6 +1,7 @@ import { ComponentController } from '../../modules/Components/Component.js'; import eventBus from '../../modules/Events/EventBus.js'; -import { USER_UPDATED } from '../../modules/Events/Events.js'; +import { NOTIFICATION_OK, USER_UPDATED } from '../../modules/Events/Events.js'; +import { NOTIFICATION_TIMEOUT } from '../NotificationBox/NotificationBox.js'; export class EmployerProfileFormController extends ComponentController { constructor(model, view, controller) { @@ -32,6 +33,10 @@ export class EmployerProfileFormController extends ComponentController { return false; } eventBus.emit(USER_UPDATED); + eventBus.emit(NOTIFICATION_OK, { + message: 'Успешно сохранено', + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); return true; } diff --git a/src/Components/EmployerProfileForm/EmployerProfileFormModel.js b/src/Components/EmployerProfileForm/EmployerProfileFormModel.js index 3c6aa0e..fdbb93e 100644 --- a/src/Components/EmployerProfileForm/EmployerProfileFormModel.js +++ b/src/Components/EmployerProfileForm/EmployerProfileFormModel.js @@ -1,4 +1,5 @@ import { Api } from '../../modules/Api/Api.js'; +import { catchStandardResponseError } from '../../modules/Api/Errors.js'; import { ComponentModel } from '../../modules/Components/Component.js'; import { Employer } from '../../modules/models/Employer.js'; @@ -19,10 +20,13 @@ export class EmployerProfileFormModel extends ComponentModel { } async submit(formData) { - formData.birthDate = new Date(formData.birthDate); formData.id = this.#userId; - if (await Api.updateEmployerProfile(formData)) { + try { + const response = await Api.updateEmployerProfile(formData); + this.#lastValidData = new Employer(response); return true; + } catch (err) { + catchStandardResponseError(err); } return false; } diff --git a/src/Components/EmployerRegistrationForm/EmployerRegistrationFormController.js b/src/Components/EmployerRegistrationForm/EmployerRegistrationFormController.js index 7987227..29c938a 100644 --- a/src/Components/EmployerRegistrationForm/EmployerRegistrationFormController.js +++ b/src/Components/EmployerRegistrationForm/EmployerRegistrationFormController.js @@ -27,7 +27,6 @@ export class EmployerRegistrationFormController extends ComponentController { } _validate(formData) { - this._view.hideError(); const formValidationError = this._model.validate(formData); if (formValidationError) { this._view.declineValidation(formValidationError); diff --git a/src/Components/EmployerRegistrationForm/EmployerRegistrationFormModel.js b/src/Components/EmployerRegistrationForm/EmployerRegistrationFormModel.js index 535dd60..8ce5810 100644 --- a/src/Components/EmployerRegistrationForm/EmployerRegistrationFormModel.js +++ b/src/Components/EmployerRegistrationForm/EmployerRegistrationFormModel.js @@ -1,6 +1,8 @@ import { ComponentModel } from '../../modules/Components/Component.js'; import state from '../../modules/AppState/AppState.js'; import USER_TYPE from '../../modules/UserSession/UserType.js'; +import { USER_ALREADY_EXISTS_ERROR } from '../../modules/Api/Errors.js'; +import { ResponseError, TransportError } from '../../modules/Api/Api.js'; export class EmployerRegistrationFormModel extends ComponentModel { validate(formData) { @@ -18,6 +20,15 @@ export class EmployerRegistrationFormModel extends ComponentModel { } async register(formData) { - return state.userSession.register(USER_TYPE.EMPLOYER, formData); + return state.userSession.register(USER_TYPE.EMPLOYER, formData).catch((err) => { + if (err.toString() === USER_ALREADY_EXISTS_ERROR) { + return Promise.reject('Работодатель с таким адресом электронной почты уже зарегистрирован'); + } + if (err instanceof TransportError) { + return Promise.reject('Произошла сетевая ошибка, повторите позднее'); + } + if (err instanceof ResponseError) + return Promise.reject('Произошла непредвиденная ошибка, повторите позднее'); + }); } } diff --git a/src/Components/EmployerRegistrationForm/EmployerRegistrationFormView.js b/src/Components/EmployerRegistrationForm/EmployerRegistrationFormView.js index 6a383ad..e3d4962 100644 --- a/src/Components/EmployerRegistrationForm/EmployerRegistrationFormView.js +++ b/src/Components/EmployerRegistrationForm/EmployerRegistrationFormView.js @@ -1,6 +1,8 @@ import { ComponentView } from '../../modules/Components/Component.js'; +import { NOTIFICATION_ERROR } from '../../modules/Events/Events.js'; import { addEventListeners } from '../../modules/Events/EventUtils.js'; import { getFormData } from '../../modules/FormUtils/FormUtils.js'; +import { NOTIFICATION_TIMEOUT } from '../NotificationBox/NotificationBox.js'; import eventBus from '/src/modules/Events/EventBus.js'; import { REGISTER_EMPLOYER } from '/src/modules/Events/Events.js'; @@ -40,12 +42,10 @@ export class EmployerRegistrationFormView extends ComponentView { return getFormData(this._html); } - hideError() { - this.error.hidden = true; - } - declineValidation(errorMessage) { - this.error.innerText = errorMessage; - this.error.hidden = false; + eventBus.emit(NOTIFICATION_ERROR, { + message: errorMessage, + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); } } diff --git a/src/Components/EmployerRegistrationForm/employer-registration-form.hbs b/src/Components/EmployerRegistrationForm/employer-registration-form.hbs index f04b5c1..c32152f 100644 --- a/src/Components/EmployerRegistrationForm/employer-registration-form.hbs +++ b/src/Components/EmployerRegistrationForm/employer-registration-form.hbs @@ -1,5 +1,4 @@ - {{> validated-input formName="employer-registration-form" elementName="first-name" inputName="firstName" inputCaption="Имя" inputType="text"}} {{> validated-input formName="employer-registration-form" elementName="second-name" inputName="secondName" inputCaption="Фамилия" inputType="text"}} {{> validated-input formName="employer-registration-form" elementName="position" inputName="position" inputCaption="Должность" inputType="text"}} diff --git a/src/Components/Lists/ApplicantCvList/ApplicantCvList.js b/src/Components/Lists/ApplicantCvList/ApplicantCvList.js index 6e49b1d..c4dbd0b 100644 --- a/src/Components/Lists/ApplicantCvList/ApplicantCvList.js +++ b/src/Components/Lists/ApplicantCvList/ApplicantCvList.js @@ -3,6 +3,7 @@ import { ApplicantCvListModel } from './ApplicantCvListModel.js'; import { ApplicantCvListController } from './ApplicantCvListController.js'; import { ApplicantCvListView } from './ApplicantCvListView.js'; import { ListMixin } from '../List/ListMixin.js'; +import { resolveUrl } from '../../../modules/UrlUtils/UrlUtils.js'; export class ApplicantCvList extends Component { constructor({ userId, isListOwner, elementClass, existingElement }) { @@ -10,7 +11,7 @@ export class ApplicantCvList extends Component { modelClass: ApplicantCvListModel, controllerClass: ApplicantCvListController, viewClass: ApplicantCvListView, - viewParams: { elementClass }, + viewParams: { elementClass, isOwner: isListOwner, addHref: resolveUrl('createCv') }, modelParams: { userId, isListOwner }, existingElement, }); diff --git a/src/Components/Lists/EmployerVacancyList/EmployerVacancyList.js b/src/Components/Lists/EmployerVacancyList/EmployerVacancyList.js index 26099ca..7105b35 100644 --- a/src/Components/Lists/EmployerVacancyList/EmployerVacancyList.js +++ b/src/Components/Lists/EmployerVacancyList/EmployerVacancyList.js @@ -3,6 +3,7 @@ import { EmployerVacancyListModel } from './EmployerVacancyListModel.js'; import { EmployerVacancyListController } from './EmployerVacancyListController.js'; import { EmployerVacancyListView } from './EmployerVacancyListView.js'; import { ListMixin } from '../List/ListMixin.js'; +import { resolveUrl } from '../../../modules/UrlUtils/UrlUtils.js'; export class EmployerVacancyList extends Component { constructor({ userId, isListOwner, elementClass, existingElement }) { @@ -10,7 +11,7 @@ export class EmployerVacancyList extends Component { modelClass: EmployerVacancyListModel, controllerClass: EmployerVacancyListController, viewClass: EmployerVacancyListView, - viewParams: { elementClass }, + viewParams: { elementClass, isOwner: isListOwner, addHref: resolveUrl('createVacancy') }, modelParams: { userId, isListOwner }, existingElement, }); diff --git a/src/Components/Lists/List/ListController.js b/src/Components/Lists/List/ListController.js index 7ec102f..683b15a 100644 --- a/src/Components/Lists/List/ListController.js +++ b/src/Components/Lists/List/ListController.js @@ -1,5 +1,8 @@ +import { catchStandardResponseError } from '../../../modules/Api/Errors.js'; import { ComponentController } from '../../../modules/Components/Component.js'; -import { MINICARD_DELETE } from '../../../modules/Events/Events.js'; +import eventBus from '../../../modules/Events/EventBus.js'; +import { MINICARD_DELETE, NOTIFICATION_OK } from '../../../modules/Events/Events.js'; +import { NOTIFICATION_TIMEOUT } from '../../NotificationBox/NotificationBox.js'; export class ListController extends ComponentController { constructor(model, view, component) { @@ -13,20 +16,32 @@ export class ListController extends ComponentController { } async loadList() { - const items = await this._model.getItems(); - items.forEach((item) => { - this._view.addChild(item.render()); - this._component.bindMinicard(item); - }); + try { + const items = await this._model.getItems(); + items.forEach((item) => { + this._view.addChild(item.render()); + this._component.bindMinicard(item); + }); + } catch (err) { + catchStandardResponseError(err); + } } async removeMinicard({ caller }) { const minicardIndex = this._view.findChildIndex(caller.render()); if (minicardIndex >= 0) { - const removed = await this._model.removeChild(minicardIndex); - if (removed) { - this._component.unbindMinicard(minicardIndex); - this._view.removeChild(minicardIndex); + try { + const removed = await this._model.removeChild(minicardIndex); + if (removed) { + this._component.unbindMinicard(minicardIndex); + this._view.removeChild(minicardIndex); + eventBus.emit(NOTIFICATION_OK, { + message: 'Успешно удалено', + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); + } + } catch (err) { + catchStandardResponseError(err); } } } diff --git a/src/Components/Lists/List/list.hbs b/src/Components/Lists/List/list.hbs index edf07ba..ec7fda9 100644 --- a/src/Components/Lists/List/list.hbs +++ b/src/Components/Lists/List/list.hbs @@ -1,2 +1,6 @@
+ {{#if isOwner}} + {{/if}}
\ No newline at end of file diff --git a/src/Components/LoginForm/LoginFormController.js b/src/Components/LoginForm/LoginFormController.js index e135585..575343a 100644 --- a/src/Components/LoginForm/LoginFormController.js +++ b/src/Components/LoginForm/LoginFormController.js @@ -2,6 +2,8 @@ import { ComponentController } from '../../modules/Components/Component.js'; import { USER_WANTS_LOGIN } from '../../modules/Events/Events.js'; import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; import router from '/src/modules/Router/Router.js'; +import { NOTIFICATION_OK } from '../../modules/Events/Events.js'; +import eventBus from '../../modules/Events/EventBus.js'; export class LoginFormController extends ComponentController { constructor(model, view, component) { @@ -15,7 +17,6 @@ export class LoginFormController extends ComponentController { } _validate(formData) { - this._view.hideError(); const formValidationError = this._model.validate(formData); if (formValidationError) { this._view.declineValidation(formValidationError); @@ -42,7 +43,10 @@ export class LoginFormController extends ComponentController { } this._model .login(formData) - .then(() => router.navigate(new URL(resolveUrl('vacancies')), true, true)) + .then(() => { + eventBus.emit(NOTIFICATION_OK, { message: 'Вы успешно вошли', timeout: 3000 }); + router.navigate(new URL(resolveUrl('vacancies')), true, true); + }) .catch((errorMsg) => { this._view.declineValidation(errorMsg); }); diff --git a/src/Components/LoginForm/LoginFormModel.js b/src/Components/LoginForm/LoginFormModel.js index f9dad6b..f097163 100644 --- a/src/Components/LoginForm/LoginFormModel.js +++ b/src/Components/LoginForm/LoginFormModel.js @@ -1,5 +1,8 @@ import { ComponentModel } from '../../modules/Components/Component.js'; import state from '/src/modules/AppState/AppState.js'; +import { TransportError, ResponseError } from '../../modules/Api/Api.js'; + +const WRONG_AUTH_ERROR = 'wrong login or password'; export class LoginFormModel extends ComponentModel { validate(formData) { @@ -13,20 +16,17 @@ export class LoginFormModel extends ComponentModel { } async login(formData) { - return state.userSession.login(formData); - // await this._state.userSession - // .login({ - // userType: data.get('user-type'), - // login: data.get('email'), - // password: data.get('password'), - // }) - // .catch((status) => { - // if (status === 401) { - // this.error('Неверный email или пароль'); - // return Promise.resolve(); - // } - // return Promise.reject(status); - // }) - // .catch(() => this.error('Произошла непредвиденная ошибка, повторите позднее')); + return state.userSession.login(formData).catch((err) => { + if (err.toString() === WRONG_AUTH_ERROR) { + return Promise.reject('Неверный email или пароль'); + } + if (err instanceof TransportError) { + return Promise.reject('Произошла сетевая ошибка, повторите позднее'); + } + if (err instanceof ResponseError) { + return Promise.reject('Произошла непредвиденная ошибка, повторите позднее'); + } + return Promise.reject(err); + }); } } diff --git a/src/Components/LoginForm/LoginFormView.js b/src/Components/LoginForm/LoginFormView.js index 7a9a58f..49f9d48 100644 --- a/src/Components/LoginForm/LoginFormView.js +++ b/src/Components/LoginForm/LoginFormView.js @@ -1,11 +1,11 @@ import { ComponentView } from '../../modules/Components/Component.js'; -import { USER_WANTS_LOGIN } from '../../modules/Events/Events.js'; +import { NOTIFICATION_ERROR, USER_WANTS_LOGIN } from '../../modules/Events/Events.js'; import { addEventListeners } from '../../modules/Events/EventUtils.js'; +import { NOTIFICATION_TIMEOUT } from '../NotificationBox/NotificationBox.js'; import eventBus from '/src/modules/Events/EventBus.js'; export class LoginFormView extends ComponentView { #emailField; - #error; #passField; #userTypeRadioGroup; constructor({ elementClass }, existingElement) { @@ -26,7 +26,6 @@ export class LoginFormView extends ComponentView { this.#userTypeRadioGroup = this._html.querySelector('.login-form__user-type-radiogroup'); this.#emailField = this._html.querySelector('.login-form__email'); this.#passField = this._html.querySelector('.login-form__password'); - this.#error = this._html.querySelector('.login-form__error'); } get userTypeRadioGroup() { @@ -41,13 +40,11 @@ export class LoginFormView extends ComponentView { return this.#passField; } - hideError() { - this.#error.hidden = true; - } - declineValidation(errorMessage) { - this.#error.innerText = errorMessage; - this.#error.hidden = false; + eventBus.emit(NOTIFICATION_ERROR, { + message: errorMessage, + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); } getData() { diff --git a/src/Components/LoginForm/login-form.hbs b/src/Components/LoginForm/login-form.hbs index 81b1d5c..e31b5ac 100644 --- a/src/Components/LoginForm/login-form.hbs +++ b/src/Components/LoginForm/login-form.hbs @@ -1,5 +1,4 @@ - {{> user-type-radiogroup elementClass="login-form__user-type-radiogroup"}} {{> validated-input formName="login-form" elementName="email" inputName="email" inputCaption="Электронная почта" inputType="email"}} {{> validated-input formName="login-form" elementName="password" inputName="password" inputCaption="Пароль" inputType="password"}} diff --git a/src/Components/NotificationBox/NotificationBox.js b/src/Components/NotificationBox/NotificationBox.js new file mode 100644 index 0000000..9b14e37 --- /dev/null +++ b/src/Components/NotificationBox/NotificationBox.js @@ -0,0 +1,21 @@ +import { NotificationBoxController } from './NotificationBoxController.js'; +import { NotificationBoxModel } from './NotificationBoxModel.js'; +import { NotificationBoxView } from './NotificationBoxView.js'; +import { Component } from '../../modules/Components/Component.js'; + +export const NOTIFICATION_TIMEOUT = { + LONG: 3000, + MEDIUM: 2000, + SHORT: 1000, +}; + +export class NotificationBox extends Component { + constructor({ existingElement }) { + super({ + modelClass: NotificationBoxModel, + controllerClass: NotificationBoxController, + viewClass: NotificationBoxView, + existingElement, + }); + } +} diff --git a/src/Components/NotificationBox/NotificationBoxController.js b/src/Components/NotificationBox/NotificationBoxController.js new file mode 100644 index 0000000..40abf48 --- /dev/null +++ b/src/Components/NotificationBox/NotificationBoxController.js @@ -0,0 +1,35 @@ +import { ComponentController } from '../../modules/Components/Component.js'; +import { NOTIFICATION_STYLE } from './NotificationBoxView.js'; +import { NOTIFICATION_ERROR, NOTIFICATION_OK } from '../../modules/Events/Events.js'; + +export class NotificationBoxController extends ComponentController { + constructor(model, view, component) { + super(model, view, component); + this.setHandlers([ + { + event: NOTIFICATION_ERROR, + handler: this.errorNotification.bind(this), + }, + { + event: NOTIFICATION_OK, + handler: this.okNotification.bind(this), + }, + ]); + } + + errorNotification({ message, timeout }) { + this._view.addNotification({ + notificationText: message, + timeoutMs: timeout, + style: NOTIFICATION_STYLE.ERROR, + }); + } + + okNotification({ message, timeout }) { + this._view.addNotification({ + notificationText: message, + timeoutMs: timeout, + style: NOTIFICATION_STYLE.OK, + }); + } +} diff --git a/src/Components/NotificationBox/NotificationBoxModel.js b/src/Components/NotificationBox/NotificationBoxModel.js new file mode 100644 index 0000000..892fc87 --- /dev/null +++ b/src/Components/NotificationBox/NotificationBoxModel.js @@ -0,0 +1,3 @@ +import { ComponentModel } from '../../modules/Components/Component.js'; + +export const NotificationBoxModel = ComponentModel; diff --git a/src/Components/NotificationBox/NotificationBoxView.js b/src/Components/NotificationBox/NotificationBoxView.js new file mode 100644 index 0000000..2068901 --- /dev/null +++ b/src/Components/NotificationBox/NotificationBoxView.js @@ -0,0 +1,76 @@ +import { ComponentView } from '../../modules/Components/Component.js'; + +export const NOTIFICATION_STYLE = { + ERROR: 'error', + OK: 'ok', +}; + +const STYLE_TO_CLASS = {}; +STYLE_TO_CLASS[NOTIFICATION_STYLE.ERROR] = 'notification_error'; +STYLE_TO_CLASS[NOTIFICATION_STYLE.OK] = 'notification_ok'; + +export class NotificationBoxView extends ComponentView { + #NOTIFICATION_CAP = 5; + #NOTIFICATION_CHECK_TIMEOUT = 500; + #notificationQueue = []; + #notificationsNum = 0; + #intervalDescriptor; + constructor(renderParams, existingElement) { + super({ renderParams, templateName: null, existingElement }); + this._notifications; + this.notificationCallback = this.processNotifications.bind(this); + } + + addNotification({ notificationText, timeoutMs, style }) { + const newNotification = this.renderNotification({ notificationText, style }); + this.#notificationQueue.push({ notificationHTML: newNotification, timeout: timeoutMs }); + this.processNotifications(); + } + + processNotifications() { + if (this.#NOTIFICATION_CAP > this.#notificationsNum) { + if (this.#intervalDescriptor) { + clearInterval(this.#intervalDescriptor); + this.#intervalDescriptor = null; + } + if (this.#notificationQueue.length === 0) { + return; + } + const { notificationHTML, timeout } = this.#notificationQueue.shift(); + this.#notificationsNum++; + this._html.appendChild(notificationHTML); + setTimeout( + function () { + notificationHTML.outerHTML = ''; + this.#notificationsNum--; + }.bind(this), + timeout, + ); + } else { + if (!this.#intervalDescriptor) { + this.#intervalDescriptor = setInterval( + this.notificationCallback, + this.#NOTIFICATION_CHECK_TIMEOUT, + ); + } + } + } + + renderNotification({ notificationText, style }) { + const newNotification = document.createElement('div'); + newNotification.classList.add('notification'); + newNotification.classList.add('notification-box__notification'); + newNotification.innerText = notificationText; + switch (style) { + case NOTIFICATION_STYLE.OK: { + newNotification.classList.add(STYLE_TO_CLASS[NOTIFICATION_STYLE.OK]); + break; + } + case NOTIFICATION_STYLE.ERROR: { + newNotification.classList.add(STYLE_TO_CLASS[NOTIFICATION_STYLE.ERROR]); + break; + } + } + return newNotification; + } +} diff --git a/src/Components/ProfileMinicard/ProfileMinicardController.js b/src/Components/ProfileMinicard/ProfileMinicardController.js index 3d3c808..be3290e 100644 --- a/src/Components/ProfileMinicard/ProfileMinicardController.js +++ b/src/Components/ProfileMinicard/ProfileMinicardController.js @@ -1,3 +1,4 @@ +import { catchStandardResponseError } from '../../modules/Api/Errors.js'; import { ComponentController } from '../../modules/Components/Component.js'; import { USER_UPDATED } from '../../modules/Events/Events.js'; @@ -13,6 +14,10 @@ export class ProfileMinicardController extends ComponentController { } async updateInfo() { - this._view.renderData(await this._model.getData()); + try { + this._view.renderData(await this._model.getData()); + } catch (err) { + catchStandardResponseError(err); + } } } diff --git a/src/Components/VacancyArticle/VacancyArticleModel.js b/src/Components/VacancyArticle/VacancyArticleModel.js index e18c456..b0b2527 100644 --- a/src/Components/VacancyArticle/VacancyArticleModel.js +++ b/src/Components/VacancyArticle/VacancyArticleModel.js @@ -4,6 +4,7 @@ import { Vacancy } from '../../modules/models/Vacancy.js'; import { NotFoundError } from '../../modules/Router/Router.js'; import state from '../../modules/AppState/AppState.js'; import USER_TYPE from '../../modules/UserSession/UserType.js'; +import { catchStandardResponseError } from '../../modules/Api/Errors.js'; export class VacancyArticleModel extends ComponentModel { #vacancyData; @@ -39,7 +40,7 @@ export class VacancyArticleModel extends ComponentModel { await Api.deleteVacancyById({ id: this.#vacancyId }); return true; } catch (err) { - console.log(err); + catchStandardResponseError(err); return false; } } @@ -52,7 +53,7 @@ export class VacancyArticleModel extends ComponentModel { await Api.vacancyApply({ id: this.#vacancyId }); return true; } catch (err) { - console.log(err); + catchStandardResponseError(err); return false; } } @@ -65,7 +66,7 @@ export class VacancyArticleModel extends ComponentModel { await Api.vacancyResetApply({ id: this.#vacancyId }); return true; } catch (err) { - console.log(err); + catchStandardResponseError(err); return false; } } diff --git a/src/Components/VacancyCard/vacancy-card.hbs b/src/Components/VacancyCard/vacancy-card.hbs index 4a629ab..a1a938f 100644 --- a/src/Components/VacancyCard/vacancy-card.hbs +++ b/src/Components/VacancyCard/vacancy-card.hbs @@ -2,7 +2,7 @@
-

{{position}}

+

{{position}}

{{companyName}}, {{location}}
Зарплата: {{salary}}
@@ -11,6 +11,7 @@ {{description}}
\ No newline at end of file diff --git a/src/Components/VacancyForm/VacancyForm.js b/src/Components/VacancyForm/VacancyForm.js index 09454d1..1a3a4bb 100644 --- a/src/Components/VacancyForm/VacancyForm.js +++ b/src/Components/VacancyForm/VacancyForm.js @@ -2,6 +2,7 @@ import { Component } from '../../modules/Components/Component.js'; import { LiteralInput } from '/src/Components/FormInputs/LiteralInput/LiteralInput.js'; import { CityInput } from '/src/Components/FormInputs/CityInput/CityInput.js'; import { CurrencyInput } from '../../Components/FormInputs/CurrencyInput/CurrencyInput.js'; +import { TextInput } from '../FormInputs/TextInput/TextInput.js'; import { ValidatedTextArea } from '../FormInputs/ValidatedTextArea/ValidatedTextArea.js'; import { VacancyFormController } from './VacancyFormController.js'; import { VacancyFormModel } from './VacancyFormModel.js'; @@ -19,7 +20,7 @@ export class VacancyForm extends Component { viewParams: { elementClass, isNew: !vacancyId, vacancyId }, }); this.#isNew = !vacancyId; - this._positionField = new LiteralInput({ + this._positionField = new TextInput({ existingElement: this._view.positionField, selfValidate: true, }); diff --git a/src/Components/VacancyForm/VacancyFormController.js b/src/Components/VacancyForm/VacancyFormController.js index 8e0e918..fd73527 100644 --- a/src/Components/VacancyForm/VacancyFormController.js +++ b/src/Components/VacancyForm/VacancyFormController.js @@ -1,9 +1,12 @@ import { ComponentController } from '../../modules/Components/Component.js'; -import { REDIRECT_TO, SUBMIT_FORM } from '../../modules/Events/Events.js'; +import { NOTIFICATION_OK, REDIRECT_TO, SUBMIT_FORM } from '../../modules/Events/Events.js'; import { Vacancy } from '../../modules/models/Vacancy.js'; import { VacancyPage } from '../../Pages/VacancyPage/VacancyPage.js'; import { resolveUrl } from '../../modules/UrlUtils/UrlUtils.js'; import eventBus from '../../modules/Events/EventBus.js'; +import { NOTIFICATION_ERROR } from '../../modules/Events/Events.js'; +import { NOTIFICATION_TIMEOUT } from '../NotificationBox/NotificationBox.js'; +import { catchStandardResponseError } from '../../modules/Api/Errors.js'; export class VacancyFormController extends ComponentController { constructor(model, view, controller) { @@ -19,6 +22,10 @@ export class VacancyFormController extends ComponentController { _validate() { const errorMessage = this._model.validate(this._view.getData()); if (errorMessage) { + eventBus.emit(NOTIFICATION_ERROR, { + message: errorMessage, + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); return false; } return [ @@ -51,13 +58,21 @@ export class VacancyFormController extends ComponentController { if (!this._validate()) { return; } - const vacancy = await this._model.submit(new Vacancy(this._view.getData())); - if (!vacancy) { - return; + try { + const vacancy = await this._model.submit(new Vacancy(this._view.getData())); + if (!vacancy) { + return; + } + const query = {}; + query[VacancyPage.VACANCY_ID_PARAM] = vacancy.id; + eventBus.emit(NOTIFICATION_OK, { + message: 'Операция проведена успешно', + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); + eventBus.emit(REDIRECT_TO, { redirectUrl: resolveUrl('vacancy', query) }); + } catch (err) { + catchStandardResponseError(err); } - const query = {}; - query[VacancyPage.VACANCY_ID_PARAM] = vacancy.id; - eventBus.emit(REDIRECT_TO, { redirectUrl: resolveUrl('vacancy', query) }); } async reset() { diff --git a/src/Pages/VacanciesPage/VacanciesPageModel.js b/src/Pages/VacanciesPage/VacanciesPageModel.js index f98db5d..30a067c 100644 --- a/src/Pages/VacanciesPage/VacanciesPageModel.js +++ b/src/Pages/VacanciesPage/VacanciesPageModel.js @@ -6,6 +6,7 @@ import { AlertWindow } from '../../Components/AlertWindow/AlertWindow.js'; import { VacancyCard } from '/src/Components/VacancyCard/VacancyCard.js'; import { Vacancy } from '../../modules/models/Vacancy.js'; import { Api } from '../../modules/Api/Api.js'; +import { catchStandardResponseError } from '../../modules/Api/Errors.js'; export class VacanciesPageModel extends PageModel { #vacanciesLoaded; @@ -57,21 +58,25 @@ export class VacanciesPageModel extends PageModel { } async getVacancies() { - let vacanciesJson = await Api.vacanciesFeed({ - offset: this.#vacanciesLoaded, - num: this.#VACANCIES_AMOUNT, - }); - const vacanciesCards = vacanciesJson.reduce((vacanciesCards, vacancyJson) => { - try { - const vacancy = new Vacancy(vacancyJson); - vacanciesCards.push(new VacancyCard({ vacancyObj: vacancy })); - this.#vacanciesLoaded++; - return vacanciesCards; - } catch (err) { - console.log(err); - return vacanciesCards; - } - }, []); - return vacanciesCards; + try { + let vacanciesJson = await Api.vacanciesFeed({ + offset: this.#vacanciesLoaded, + num: this.#VACANCIES_AMOUNT, + }); + const vacanciesCards = vacanciesJson.reduce((vacanciesCards, vacancyJson) => { + try { + const vacancy = new Vacancy(vacancyJson); + vacanciesCards.push(new VacancyCard({ vacancyObj: vacancy })); + this.#vacanciesLoaded++; + return vacanciesCards; + } catch { + return vacanciesCards; + } + }, []); + return vacanciesCards; + } catch (err) { + catchStandardResponseError(err); + return []; + } } } diff --git a/src/css/index.css b/src/css/index.css index b78e6de..952bc2d 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -130,7 +130,7 @@ body { } .ruler__alert-window { - border-radius: 10px; + border-radius: 14px; padding: 12px; } @@ -143,28 +143,13 @@ body { .alert-window_theme-dark { background-color: var(--color-background-800); - color: white; + color: var(--color-main-100); } .alert-window__button { margin-top: 10px; } -.notification { - position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%); - background-color: #ff4c4c; - color: white; - padding: 10px 20px; - border-radius: 5px; - display: none; - z-index: 1000; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - font-size: 14px; -} - /* Frame series component */ .frame-series__frame-selector { @@ -247,6 +232,11 @@ body { gap: 30px; } +.list__add-button-container { + display: flex; + justify-content: end; +} + .hidden { display: none !important; } @@ -260,3 +250,36 @@ body { padding: 4px 12px; background-color: var(--color-background-700); } + +/* Notification-box */ + +.notification-box { + display: flex; + font-family: 'Roboto', sans-serif; + flex-direction: column-reverse; + gap: 20px; + position: absolute; + left: 50%; + bottom: 20px; + width: 400px; + transform: translate(-50%); + z-index: 1000; +} + +.notification_error { + background-color: var(--color-background-700); + color: var(--color-secondary-support-400); + font-size: var(--text-size-5); + border-radius: 14px; + padding: 10px 20px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.notification_ok { + background-color: var(--color-background-700); + color: var(--color-main-500); + font-size: var(--text-size-5); + border-radius: 14px; + padding: 10px 20px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} diff --git a/src/css/vacancies.css b/src/css/vacancies.css index d2d6056..9e30200 100644 --- a/src/css/vacancies.css +++ b/src/css/vacancies.css @@ -11,7 +11,7 @@ background-color: var( --color-background-1000 ); /* for some reason variable for body resolved only here */ - color: white; + color: var(--color-main-100); } .vacancies-page__side-column { @@ -44,7 +44,7 @@ .content-body_theme-dark { background-color: var(--color-background-900); - color: white; + color: var(--color-main-100); } /* Vacancy-container component */ @@ -58,7 +58,7 @@ width: 90%; margin-bottom: 30px; padding: 20px; - border-radius: 5px; + border-radius: 14px; } /* vacancy-card component */ @@ -75,8 +75,10 @@ } .vacancy-card__company-picture { - width: 100px; - height: 100px; + width: 96px; + height: 96px; + border-radius: 100%; + border: solid 1px; } .vacancy-card__header { @@ -86,6 +88,11 @@ margin-left: 20px; } +.vacancy-card__header-link { + text-decoration: none; + color: var(--color-main-100); +} + .vacancy-card__bookmark-icon { margin-left: auto; width: 25px; @@ -94,10 +101,20 @@ .vacancy-card__description { margin: 10px 0; + font-size: var(--text-size-5); + text-overflow: ellipsis; + overflow: hidden; + display: -webkit-box; + line-clamp: 3; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; } .vacancy-card__footer { - margin-top: 10px; + display: flex; + flex-direction: row; + gap: 20px; + align-items: baseline; } .vacancy-card__apply-button { @@ -115,5 +132,5 @@ .vacancy-card_theme-dark { background-color: var(--color-background-800); - color: white; + color: var(--color-main-100); } diff --git a/src/index.html b/src/index.html index 2789216..6c099f4 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,7 @@
+
diff --git a/src/index.js b/src/index.js index a480114..83a02ac 100644 --- a/src/index.js +++ b/src/index.js @@ -12,9 +12,15 @@ import { resolveUrl } from './modules/UrlUtils/UrlUtils.js'; import { REDIRECT_TO, GO_TO } from './modules/Events/Events.js'; import { CvPage } from './Pages/CvPage/CvPage.js'; import { CvEditPage } from './Pages/CvEditPage/CvEditPage.js'; +import { NotificationBox } from './Components/NotificationBox/NotificationBox.js'; handlebarsInit(); +// eslint-disable-next-line no-unused-vars +const notificationBox = new NotificationBox({ + existingElement: document.querySelector('.notification-box'), +}); + router.addRoute(resolveUrl('vacancies').pathname, VacanciesPage); router.addRoute(resolveUrl('login').pathname, LoginPage); router.addRoute(resolveUrl('register').pathname, RegistrationPage); diff --git a/src/modules/Api/Api.js b/src/modules/Api/Api.js index 968eda9..74b230d 100644 --- a/src/modules/Api/Api.js +++ b/src/modules/Api/Api.js @@ -1,4 +1,4 @@ -const backendPrefix = 'http://127.0.0.1:8081/api/v1/'; +const backendPrefix = 'http://192.168.88.82:8080/api/v1/'; const backendApi = new Map( Object.entries({ authenticated: backendPrefix + 'authorized', @@ -21,6 +21,7 @@ const backendApi = new Map( export class UnmarshallError extends Error {} export class ResponseError extends Error {} +export class TransportError extends Error {} export const HTTP_METHOD = { GET: 'get', @@ -70,6 +71,8 @@ const fetchCorsJson = ( mode: 'cors', credentials, body, + }).catch((response) => { + return Promise.reject(new TransportError(response.statusCode)); }); }; export class Api { diff --git a/src/modules/Api/Errors.js b/src/modules/Api/Errors.js new file mode 100644 index 0000000..e7bb6f7 --- /dev/null +++ b/src/modules/Api/Errors.js @@ -0,0 +1,21 @@ +import { NOTIFICATION_TIMEOUT } from '../../Components/NotificationBox/NotificationBox.js'; +import eventBus from '../Events/EventBus.js'; +import { NOTIFICATION_ERROR } from '../Events/Events.js'; +import { ResponseError, TransportError } from './Api.js'; + +export const catchStandardResponseError = (error) => { + if (error instanceof ResponseError) { + eventBus.emit(NOTIFICATION_ERROR, { + message: 'Произошла непредвиденная ошибка, повторите позднее', + timeout: NOTIFICATION_TIMEOUT.MEDIUM, + }); + } + if (error instanceof TransportError) { + eventBus.emit(NOTIFICATION_ERROR, { + message: 'Произошла сетевая ошибка, повторите позднее', + timeout: NOTIFICATION_TIMEOUT.LONG, + }); + } +}; + +export const USER_ALREADY_EXISTS_ERROR = 'user already exists'; diff --git a/src/modules/Events/EventBus.js b/src/modules/Events/EventBus.js index cc4ae39..85f2e7b 100644 --- a/src/modules/Events/EventBus.js +++ b/src/modules/Events/EventBus.js @@ -18,7 +18,6 @@ class EventBus { } emit(eventName, eventData) { - console.log(`got event: ${eventName}`); if (this.#listeners.has(eventName)) { const callbacks = this.#listeners.get(eventName); callbacks.forEach((callback) => { diff --git a/src/modules/Events/Events.js b/src/modules/Events/Events.js index 988628e..769bea4 100644 --- a/src/modules/Events/Events.js +++ b/src/modules/Events/Events.js @@ -32,3 +32,6 @@ export const CV_DELETE = 'cv delete'; export const REDIRECT_TO = 'redirect to'; export const GO_TO = 'go to'; + +export const NOTIFICATION_ERROR = 'notification error'; +export const NOTIFICATION_OK = 'notification ok'; diff --git a/src/modules/Page/Page.js b/src/modules/Page/Page.js index 2988341..042d9a0 100644 --- a/src/modules/Page/Page.js +++ b/src/modules/Page/Page.js @@ -4,8 +4,9 @@ import { ComponentModel, ComponentView, } from '../Components/Component.js'; -import { USER_WANTS_LOGOUT } from '../Events/Events.js'; +import { NOTIFICATION_OK, USER_WANTS_LOGOUT } from '../Events/Events.js'; import state from '/src/modules/AppState/AppState.js'; +import eventBus from '../Events/EventBus.js'; /** Base class representing browser page */ export class Page extends Component { @@ -56,5 +57,6 @@ export class PageController extends ComponentController { _userLogout() { state.userSession.logout(); + eventBus.emit(NOTIFICATION_OK, { message: 'Вы успешно вышли', timeout: 2000 }); } } From 7c9cac76be88bb1d8a61eeacd345aaa829fdd62b Mon Sep 17 00:00:00 2001 From: Ilya Andriyanov Date: Fri, 8 Nov 2024 12:54:17 +0300 Subject: [PATCH 07/11] fix: bad link in cv edit --- src/Components/CvArticle/CvArticleController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/CvArticle/CvArticleController.js b/src/Components/CvArticle/CvArticleController.js index 655056a..3a92979 100644 --- a/src/Components/CvArticle/CvArticleController.js +++ b/src/Components/CvArticle/CvArticleController.js @@ -41,7 +41,7 @@ export class CvArticleController extends ComponentController { async cvEdit() { const query = {}; const cv = await this._model.getCvData(); - query[CvPage.CV_ID] = cv.id; + query[CvPage.CV_ID_PARAM] = cv.id; eventBus.emit(GO_TO, { redirectUrl: resolveUrl('editCv', query) }); throw Error('Not implemented'); } From 8a08a1cdf1b9cde7f9c69ddd6be995bd579b6dce Mon Sep 17 00:00:00 2001 From: Ilya Andriyanov Date: Fri, 8 Nov 2024 12:54:56 +0300 Subject: [PATCH 08/11] fix: bad password text --- src/Components/FormInputs/PasswordInput/PasswordInputModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/FormInputs/PasswordInput/PasswordInputModel.js b/src/Components/FormInputs/PasswordInput/PasswordInputModel.js index 703eccc..e09b7c4 100644 --- a/src/Components/FormInputs/PasswordInput/PasswordInputModel.js +++ b/src/Components/FormInputs/PasswordInput/PasswordInputModel.js @@ -5,7 +5,7 @@ export class PasswordInputModel extends ValidatedInputModel { validate(password) { if (password.length < this.#MIN_PASSWORD_LEN) { - return 'Придумайте пароль длиной хотя бы 8 символов'; + return 'Введите пароль длиной хотя бы 8 символов'; } return ''; } From f219f8a92d77d4db74290b43289568e158941c64 Mon Sep 17 00:00:00 2001 From: Ilya Andriyanov Date: Fri, 8 Nov 2024 13:45:31 +0300 Subject: [PATCH 09/11] fix: go to profile from vacancy edit --- src/Components/VacancyForm/vacancy-form.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/VacancyForm/vacancy-form.hbs b/src/Components/VacancyForm/vacancy-form.hbs index 10e6a4f..0897dc1 100644 --- a/src/Components/VacancyForm/vacancy-form.hbs +++ b/src/Components/VacancyForm/vacancy-form.hbs @@ -14,6 +14,6 @@ Сохранить {{/if}} - В профиль + В профиль
\ No newline at end of file From ab0941a002312489ae672c78462bfe466c7bf4fd Mon Sep 17 00:00:00 2001 From: Ilya Andriyanov Date: Fri, 8 Nov 2024 13:54:42 +0300 Subject: [PATCH 10/11] fix: last name disappears --- src/modules/models/Applicant.js | 2 +- src/modules/models/Employer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/models/Applicant.js b/src/modules/models/Applicant.js index 31d4354..b493ad8 100644 --- a/src/modules/models/Applicant.js +++ b/src/modules/models/Applicant.js @@ -3,7 +3,7 @@ export class Applicant { constructor(backendResponse) { this.id = backendResponse.id; this.firstName = backendResponse.firstName; - this.secondName = backendResponse.lastName; + this.secondName = backendResponse.lastName || backendResponse.secondName; this.city = backendResponse.city; this.birthDate = new Date(backendResponse.birthDate); this.avatar = backendResponse.pathToProfileAvatar || resolveStatic('img/user-icon-80.svg'); diff --git a/src/modules/models/Employer.js b/src/modules/models/Employer.js index 1b081ff..d399045 100644 --- a/src/modules/models/Employer.js +++ b/src/modules/models/Employer.js @@ -3,7 +3,7 @@ export class Employer { constructor(backendResponse) { this.id = backendResponse.id; this.firstName = backendResponse.firstName; - this.secondName = backendResponse.lastName; + this.secondName = backendResponse.lastName || backendResponse.secondName; this.city = backendResponse.city; this.birthDate = new Date(backendResponse.birthDate); this.avatar = backendResponse.pathToProfileAvatar || resolveStatic('img/user-icon-80.svg'); From 4efaf913b33eabfdcef6b6ec48d5a5d67be14f5a Mon Sep 17 00:00:00 2001 From: Ilya Andriyanov Date: Fri, 8 Nov 2024 15:20:31 +0300 Subject: [PATCH 11/11] feature: favicon.ico --- .../cv-article__button-container.hbs | 2 +- .../vacancy-article__button-container.hbs | 2 +- src/index.html | 2 +- src/public/img/company-icon.svg | 5 +++++ src/public/img/favicon.ico | Bin 0 -> 619 bytes src/server/server.mjs | 1 + 6 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/public/img/company-icon.svg create mode 100644 src/public/img/favicon.ico diff --git a/src/Components/CvArticle/ButtonContainer/cv-article__button-container.hbs b/src/Components/CvArticle/ButtonContainer/cv-article__button-container.hbs index 34eb9b7..5d675a1 100644 --- a/src/Components/CvArticle/ButtonContainer/cv-article__button-container.hbs +++ b/src/Components/CvArticle/ButtonContainer/cv-article__button-container.hbs @@ -1,6 +1,6 @@
{{#if isEmployer}} - Другие резюме + Все резюме {{else}} {{#if isOwner}} diff --git a/src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs b/src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs index 6f085d1..8bf8fbd 100644 --- a/src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs +++ b/src/Components/VacancyArticle/ButtonContainer/vacancy-article__button-container.hbs @@ -2,7 +2,7 @@ {{#if isApplicant}} - Другие вакансии + Все вакансии {{else}} {{#if isOwner}} diff --git a/src/index.html b/src/index.html index 6c099f4..b77f723 100644 --- a/src/index.html +++ b/src/index.html @@ -2,7 +2,7 @@ - μArt + Art diff --git a/src/public/img/company-icon.svg b/src/public/img/company-icon.svg new file mode 100644 index 0000000..79e0797 --- /dev/null +++ b/src/public/img/company-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/public/img/favicon.ico b/src/public/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5cf1ed9028cf2720f25dfafcf84fae2d746e1263 GIT binary patch literal 619 zcmV-x0+juUP)00001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0su)wK~!i%?V9aT z!!QhnQ&!!5RXt*)K#2e>r{ua_TST z{t+;QphpXtLeNq`y?=mHfZVqs`ym8ppn3wFO-nFnW}y)OfF38Y36FrN;LUM3Fg62k zeAWmIL8O3`-YaJ?pDlDW2Yf470|Bv9$SgqW-nxFzaPEM;SjfEtZY>~u3APDX%|Cv6 zv5=ch1+0-kgMe`Rh7gc}h7$m6XCWbh<}Uy@Y$RYk`<4E%wLF4_D54^cQr&v{#Rvcx z3Sa$4tu+cDsa8E<7Q#TY@M8@M5Fi=l6bVqn?}kQP)Bbj=5HWY!HS879@e7blzkoVr z)dF*L0onXgPvsg=lB&=(y?Mh}z(kdWNP{#Wf(oGIIJj&rD#JGxYUcp;N(0I=9r_5p z$tC`nmnlTq2NwLpH?7%v6$<1z zkwy>bDT(ZXjD-+m6)xHVRe$#Af%{!X_rf8dH9HqD48O>pY$MSmWGVmv002ovPDHLk FV1oP){%-&P literal 0 HcmV?d00001 diff --git a/src/server/server.mjs b/src/server/server.mjs index ade6224..56735b6 100644 --- a/src/server/server.mjs +++ b/src/server/server.mjs @@ -9,6 +9,7 @@ const routingTemplates = { '(.css|.html|.png|.svg)$': './src/', 'index.css$': './src', '.ttf$': '.', + 'favicon.ico': './src/public/img', }; const customContentTypes = {