diff --git a/src/const.js b/src/const.js index 128e191..652bbd1 100644 --- a/src/const.js +++ b/src/const.js @@ -11,6 +11,11 @@ export const DateFormat = { export const EventMode = { DEFAULT: 'default', + FORM: 'form', +}; + +export const FormMode = { + CREATE: 'create', EDIT: 'edit', }; @@ -26,6 +31,7 @@ export const EventListMessage = { [FilterType.PAST]: 'There are no past events now', [FilterType.PRESENT]: 'There are no present events now', [FilterType.FUTURE]: 'There are no future events now', + LOADING: 'Loading...' }; export const SortType = { @@ -36,11 +42,6 @@ export const SortType = { OFFER: 'offers', }; -export const FormType = { - CREATE: 'create', - EDIT: 'edit', -}; - export const KeyCode = { ESC: 'Escape', ENTER: 'Enter', @@ -48,9 +49,8 @@ export const KeyCode = { export const tripDefault = { basePrice: 0, - destination: null, isFavorite: false, - offers: [], + offerIds: [], type: 'flight', }; @@ -72,3 +72,25 @@ export const TripTitleQuantity = { MIN: 1, MAX: 3, }; + +export const UserAction = { + UPDATE_EVENT: 'UPDATE_EVENT', + CANCEL_EVENT: 'CANCEL_EVENT', + CREATE_EVENT: 'CREATE_EVENT', + DELETE_EVENT: 'DELETE_EVENT', +}; + +export const UpdateType = { + PATCH: 'PATCH', + MINOR: 'MINOR', + MAJOR: 'MAJOR', + FILTER: 'FILTER', + INIT: 'INIT', +}; + +export const Method = { + GET: 'GET', + PUT: 'PUT', + POST: 'POST', + DELETE: 'DELETE', +}; diff --git a/src/event-api-service.js b/src/event-api-service.js new file mode 100644 index 0000000..8707999 --- /dev/null +++ b/src/event-api-service.js @@ -0,0 +1,74 @@ +import { Method } from './const'; +import ApiService from './framework/api-service'; + +export default class EventApiService extends ApiService { + + async getTripData() { + const data = await Promise.all([ + this._load({ url: 'points' }), + this._load({ url: 'offers' }), + this._load({ url: 'destinations' }) + ]).then((response) => response.map(ApiService.parseResponse)); + + const result = []; + for await (const item of data) { + result.push(item); + } + + return result; + } + + async createEvent(event) { + const response = await this._load({ + url: 'points', + method: Method.POST, + body: JSON.stringify(this.#adaptToServer(event, true)), + headers: new Headers({ 'Content-Type': 'application/json' }), + }); + + const parsedResponse = await ApiService.parseResponse(response); + + return parsedResponse; + } + + async updateEvent(event) { + const response = await this._load({ + url: `points/${event.id}`, + method: Method.PUT, + body: JSON.stringify(this.#adaptToServer(event)), + headers: new Headers({ 'Content-Type': 'application/json' }), + }); + + const parsedResponse = await ApiService.parseResponse(response); + + return parsedResponse; + } + + async deleteEvent(event) { + const response = await this._load({ + url: `points/${event.id}`, + method: Method.DELETE, + }); + + return response; + } + + #adaptToServer = (event, isCreation = false) => { + const adaptedEvent = { + id: event.id, + 'base_price': +event.basePrice, + 'date_from': event.dateFrom, + 'date_to': event.dateTo, + destination: event.destinationId, + 'is_favorite': event.isFavorite, + offers: event.offerIds, + type: event.type, + }; + + if (isCreation) { + delete adaptedEvent.id; + } + + return adaptedEvent; + }; +} diff --git a/src/framework/api-service.js b/src/framework/api-service.js index ef9f928..a010754 100644 --- a/src/framework/api-service.js +++ b/src/framework/api-service.js @@ -30,7 +30,7 @@ export default class ApiService { const response = await fetch( `${this._endPoint}/${url}`, - {method, body, headers}, + { method, body, headers }, ); try { diff --git a/src/framework/view/abstract-view.js b/src/framework/view/abstract-view.js index 4ac8408..dae7164 100644 --- a/src/framework/view/abstract-view.js +++ b/src/framework/view/abstract-view.js @@ -15,7 +15,7 @@ export default class AbstractView { #element = null; /**@type {AbortController} */ - #controller = new AbortController(); + #controller = null; constructor() { if (new.target === AbstractView) { @@ -32,6 +32,10 @@ export default class AbstractView { this.#element = createElement(this.template); } + if (!this.#controller) { + this.#controller = new AbortController(); + } + return this.#element; } @@ -92,6 +96,7 @@ export default class AbstractView { /** Метод для удаления элемента */ removeElement() { this.#controller.abort(); // Метод для удаления обработчиков событий элемента + this.#controller = null; this.#element = null; } diff --git a/src/main.js b/src/main.js index cfd7781..018d7be 100644 --- a/src/main.js +++ b/src/main.js @@ -1,16 +1,22 @@ +import { nanoid } from 'nanoid'; +import EventApiService from './event-api-service'; import TripModel from './model/trip-model'; import BoardPresenter from './presenter/board-presenter'; import HeaderPresenter from './presenter/header-presenter'; +const AUTHORIZATION = `Basic ${nanoid()}`; +const END_POINT = 'https://22.objects.htmlacademy.pro/big-trip'; + const tripInfoContainer = document.querySelector('.trip-main'); const filtersContainer = tripInfoContainer.querySelector('.trip-controls__filters'); const contentContainer = document.querySelector('.trip-events'); -const model = new TripModel(); -model.init(); +const model = new TripModel(new EventApiService(END_POINT, AUTHORIZATION)); const header = new HeaderPresenter(tripInfoContainer, filtersContainer, model); header.init(); const board = new BoardPresenter(contentContainer, tripInfoContainer, model); board.init(); + +model.init(); diff --git a/src/model/data.type.ts b/src/model/data.type.ts index 74b12de..3da79c2 100644 --- a/src/model/data.type.ts +++ b/src/model/data.type.ts @@ -23,7 +23,7 @@ type Offer = { isChecked: boolean; } -type OfferList = [ +type Offers = [ { type: EventType; offers: Offer[]; @@ -32,11 +32,11 @@ type OfferList = [ type RawEvent = { id: string; - basePrice: number; - dateFrom: string; - dateTo: string; + base_price: number; + date_from: string; + date_to: string; destination: Id; - isFavorite: boolean; + is_favorite: boolean; offers: Id[]; type: EventType; }; @@ -46,8 +46,8 @@ type TripEvent = { basePrice: number; dateFrom: string; dateTo: string; - destination: Destination; + destinationId: Id; isFavorite: boolean; - offers: Offer[]; + offerIds: Id[]; type: EventType; }; diff --git a/src/model/trip-model.js b/src/model/trip-model.js index a5f9a39..96055f3 100644 --- a/src/model/trip-model.js +++ b/src/model/trip-model.js @@ -1,16 +1,24 @@ +import Observable from '../framework/observable'; import { createFilters } from '../utils/filter'; import { createSortItems } from '../utils/sort'; import { generateTripData } from '../../mocks/generate-data'; -import { FilterType, tripDefault } from '../const'; +import { FilterType, tripDefault, UpdateType } from '../const'; -export default class TripModel { - #events = []; +export default class TripModel extends Observable { + #eventApiService = null; + #events = new Map(); #destinations = new Map(); #offers = new Map(); #filterType = FilterType.EVERYTHING; + constructor(eventApiService) { + super(); + + this.#eventApiService = eventApiService; + } + get events() { - return this.#events; + return Array.from(this.#events.values()); } get destinations() { @@ -22,17 +30,21 @@ export default class TripModel { } get filters() { - return createFilters(this.#events); + return createFilters(this.events); } get sortItems() { - return createSortItems(this.#events); + return createSortItems(this.events); } get filterType() { return this.#filterType; } + get eventsSize() { + return this.#events.size; + } + getDefaultEvent = () => { const date = new Date().toISOString(); const type = this.#offers.has(tripDefault.type) ? tripDefault.type : this.#offers.keys()[0]; @@ -41,22 +53,98 @@ export default class TripModel { ...tripDefault, dateFrom: date, dateTo: date, - offerIds: this.#offers.get(type), + destinationId: this.#destinations.values().next().value.id, type, }; }; - init() { - const [rawEvents, rawOffers, rawDestinations] = generateTripData(); + async init() { + try { + const [rawEvents, rawOffers, rawDestinations] = await this.#eventApiService.getTripData(); + + this.#events = new Map(rawEvents.map(this.#adaptEventToClient)); + this.#offers = new Map(rawOffers.map(this.#adaptOffersToClient)); + this.#destinations = new Map(rawDestinations.map(this.#adaptDestinationsToClient)); + } catch (err) { - this.#events = rawEvents; + const [rawEvents, rawOffers, rawDestinations] = generateTripData(); - rawOffers.forEach(({ type, offers }) => { - this.#offers.set(type, offers); - }); + rawEvents.forEach((event) => { + this.#events.set(event.id, event); + }); + + rawOffers.forEach(({ type, offers }) => { + this.#offers.set(type, offers); + }); + + rawDestinations.forEach((destination) => { + this.#destinations.set(destination.id, destination); + }); + + } finally { + this._notify(UpdateType.INIT); + } - rawDestinations.forEach((destination) => { - this.#destinations.set(destination.id, destination); - }); } + + #checkEventExistence = (id) => { + if (this.#events.has(id)) { + return; + } + + throw new Error(`Event with id: ${id} doesn't exist`); + }; + + createEvent = async (updateType, event) => { + try { + const response = await this.#eventApiService.createEvent(event); + const [id, newEvent] = this.#adaptEventToClient(response); + this.#events.set(id, newEvent); + this._notify(updateType, newEvent); + } catch { + throw new Error('Can\'t crete event'); + } + }; + + updateEvent = async (updateType, event) => { + try { + this.#checkEventExistence(event.id); + const response = await this.#eventApiService.updateEvent(event); + const [id, newEvent] = this.#adaptEventToClient(response); + this.#events.set(id, newEvent); + this._notify(updateType, newEvent); + } catch { + throw new Error('Can\'t update event'); + } + }; + + deleteEvent = async (updateType, event) => { + try { + this.#checkEventExistence(event.id); + await this.#eventApiService.deleteEvent(event); + this.#events.delete(event.id); + this._notify(updateType, event); + } catch { + throw new Error('Can\'t delete event'); + } + }; + + updateFilterType = (updateType, filterType) => { + this.#filterType = filterType; + this._notify(updateType); + }; + + #adaptEventToClient = (event) => ([event.id, { + id: event.id, + basePrice: event['base_price'], + dateFrom: event['date_from'], + dateTo: event['date_to'], + destinationId: event.destination, + isFavorite: event['is_favorite'], + offerIds: event.offers, + type: event.type, + }]); + + #adaptOffersToClient = ({ type, offers }) => [type, offers]; + #adaptDestinationsToClient = (destination) => [destination.id, destination]; } diff --git a/src/presenter/board-presenter.js b/src/presenter/board-presenter.js index 44a9361..5c2e543 100644 --- a/src/presenter/board-presenter.js +++ b/src/presenter/board-presenter.js @@ -1,25 +1,24 @@ import { sortEvents } from '../utils/sort'; -import { ElementSelectors as E, EventListMessage, EventMode, KeyCode, SortType } from '../const'; +import { ElementSelectors as E, EventListMessage, EventMode, FilterType, FormMode, KeyCode, SortType, UpdateType, UserAction } from '../const'; import SortView from '../view/sort-view'; import EventListView from '../view/event-list-view'; import NoEventsView from '../view/no-events-view'; import NewEventButtonView from '../view/new-event-button-view'; import { filterEvents } from '../utils/filter'; import EventPresenter from './event-presenter'; -import { render } from '../framework/render'; -import { updateItem } from '../utils/common'; +import { remove, render } from '../framework/render'; export default class BoardPresenter { #boardContainer = null; #newEventButtonContainer = null; - #eventListComponent = new EventListView(); + #eventListComponent = null; #newEventButtonComponent = new NewEventButtonView(); + #sortComponent = null; + #noEventsComponent = null; /** @type {TripEvent[]} */ #events = []; - #destinations = null; - #offers = null; /** @type {TripModel} */ #tripModel = null; @@ -27,27 +26,37 @@ export default class BoardPresenter { #activeSortType = SortType.PRICE; #eventPresenters = new Map(); #activeEventId = ''; + #newEventPresenter = null; + + #isLoading = true; constructor(boardContainer, newEventButtonContainer, tripModel) { this.#boardContainer = boardContainer; - this.#tripModel = tripModel; this.#newEventButtonContainer = newEventButtonContainer; + this.#tripModel = tripModel; + this.#tripModel.addObserver(this.#onModelChangeHandler); + } + + get #offers() { + return this.#tripModel.offers; + } + + get #destinations() { + return this.#tripModel.destinations; } init() { - this.#events = filterEvents([...this.#tripModel.events], this.#tripModel.filterType); - this.#destinations = this.#tripModel.destinations; - this.#offers = this.#tripModel.offers; + + this.#newEventButtonComponent.setOnClickHandler(this.#newEventHandler); + render(this.#newEventButtonContainer, this.#newEventButtonComponent); this.#renderBoard(); } #renderBoard = () => { - render(this.#newEventButtonContainer, this.#newEventButtonComponent); - - if (this.#events.length === 0) { - render(this.#boardContainer, new NoEventsView(EventListMessage[this.#tripModel.filterType])); + if ((this.#events.length === 0) || (this.#isLoading)) { + this.#renderNoEvents(); return; } @@ -56,50 +65,143 @@ export default class BoardPresenter { this.#renderEvents(); }; + #renderNoEvents = () => { + const message = this.#isLoading ? EventListMessage.LOADING : EventListMessage[this.#tripModel.filterType]; + this.#noEventsComponent = new NoEventsView(message); + render(this.#boardContainer, this.#noEventsComponent); + }; + #renderEventList = () => { + this.#eventListComponent = new EventListView() + .setEventToggleHandler(this.#eventToggleHandler) + .setEscKeyDownHandler(this.#escKeyHandler); render(this.#boardContainer, this.#eventListComponent); - this.#eventListComponent.setEventToggleHandler(this.#eventToggleHandler); - this.#eventListComponent.setEscKeyDownHandler(this.#escKeyHandler); }; #renderSort = () => { - const sortComponent = new SortView(this.#tripModel.sortItems, this.#activeSortType); - sortComponent.setOnSortClickHandler(this.#sortChangeHandler); + this.#sortComponent = new SortView(this.#tripModel.sortItems, this.#activeSortType).setOnSortClickHandler(this.#sortChangeHandler); + render(this.#boardContainer, this.#sortComponent); + }; + + #sortChangeHandler = (sortType) => { + if ((sortType === this.#activeSortType) || !(Object.values(SortType).includes(sortType))) { + return; + } + + this.#activeSortType = sortType; + this.#updateEventList(); + }; + + #createEventPresenter = (event, mode = FormMode.EDIT) => new EventPresenter(this.#eventListComponent.element, () => this.#events.length) + .init(event, this.#offers, this.#destinations, mode) + .setViewActionHandler(this.#viewActionHandler); - render(this.#boardContainer, sortComponent); + #renderEvents = () => sortEvents(this.#events, this.#activeSortType).forEach((event) => { + const presenter = this.#createEventPresenter(event); + this.#eventPresenters.set(event.id, presenter); + }); + + #newEventHandler = () => { + this.#toggleEventMode(); + this.#activeSortType = SortType.DAY; + this.#tripModel.updateFilterType(UpdateType.MAJOR, FilterType.EVERYTHING); + this.#newEventButtonComponent.disable(); + if (this.#events.length === 0) { + remove(this.#noEventsComponent); + this.#renderEventList(); //!!! не удалять EventList? + } + this.#newEventPresenter = this.#createEventPresenter(this.#tripModel.getDefaultEvent(), FormMode.CREATE); }; - #renderEvents = () => sortEvents(this.#events, this.#activeSortType) - .forEach((event) => { - const presenter = new EventPresenter(this.#eventListComponent.element); - presenter.init(event, this.#offers, this.#destinations); - presenter.setOnFormSubmitHandler(this.#formSubmitHandler); - presenter.setOnDataChangeHandler(this.#eventChangeHandler); + #updateEventList = () => { + this.#destroyEventPresenters(); + this.#renderEvents(); + }; - this.#eventPresenters.set(event.id, presenter); - }); + #updateBoard = () => { + this.#destroyBoard(); + this.#renderBoard(); + }; - #clearEventList = () => { + #destroyEventPresenters = () => { + this.#destroyNewEventPresenter(); this.#eventPresenters.forEach((presenter) => presenter.destroy()); this.#eventPresenters.clear(); + this.#activeEventId = ''; }; - #sortChangeHandler = (sortType) => { - if ((sortType === this.#activeSortType) || !(Object.values(SortType).includes(sortType))) { - return; + #destroyBoard = () => { + remove(this.#sortComponent); + remove(this.#eventListComponent); + remove(this.#noEventsComponent); + this.#destroyEventPresenters(); + }; + + #destroyNewEventPresenter = () => { + if (this.#newEventPresenter) { + this.#newEventButtonComponent.enable(); + this.#newEventPresenter.destroy(); + this.#newEventPresenter = null; } + }; - this.#activeSortType = sortType; - this.#clearEventList(); - this.#renderEvents(); + #viewActionHandler = (actionType, updateType, updatedEvent) => { + + switch (actionType) { + case UserAction.UPDATE_EVENT: + this.#tripModel.updateEvent(updateType, updatedEvent); + break; + + case UserAction.CREATE_EVENT: + this.#tripModel.createEvent(updateType, updatedEvent); + break; + + case UserAction.CANCEL_EVENT: + this.#destroyNewEventPresenter(); + if (this.#events.length === 0) { + this.#renderNoEvents(); + } + break; + + case UserAction.DELETE_EVENT: + this.#tripModel.deleteEvent(updateType, updatedEvent); + break; + } }; - #eventChangeHandler = (updatedEvent) => { - this.#events = updateItem(this.#events, updatedEvent); - this.#eventPresenters.get(updatedEvent.id).init(updatedEvent, this.#offers, this.#destinations); + #onModelChangeHandler = (updateType, payload) => { + + this.#events = filterEvents(this.#tripModel.events, this.#tripModel.filterType); + + switch (updateType) { + case UpdateType.PATCH: + this.#eventPresenters.get(payload.id).init(payload); + break; + case UpdateType.MINOR: + this.#updateEventList(); + break; + + case UpdateType.MAJOR: + this.#updateBoard(); + break; + + case UpdateType.FILTER: + this.#activeSortType = SortType.DAY; + this.#updateBoard(); + break; + + case UpdateType.INIT: + this.#isLoading = false; + remove(this.#noEventsComponent); + this.#newEventButtonComponent.enable(); + this.#renderBoard(); + break; + } }; #toggleEventMode = (newEventId = '') => { + this.#destroyNewEventPresenter(); + const eventId = this.#activeEventId; if (this.#activeEventId) { this.#eventPresenters.get(eventId).toggleEventView(EventMode.DEFAULT); @@ -107,22 +209,17 @@ export default class BoardPresenter { } if (newEventId && (eventId !== newEventId)) { - this.#eventPresenters.get(newEventId).toggleEventView(EventMode.EDIT); + this.#eventPresenters.get(newEventId).toggleEventView(EventMode.FORM); this.#activeEventId = newEventId; } }; #escKeyHandler = (evt) => { - if ((evt.key === KeyCode.ESC) && (this.#activeEventId)) { + if ((evt.key === KeyCode.ESC) && (this.#activeEventId || this.#newEventPresenter)) { this.#toggleEventMode(); } }; - #formSubmitHandler = (updatedEvent) => { - this.#eventChangeHandler(updatedEvent); - this.#toggleEventMode(updatedEvent.id); - }; - #eventToggleHandler = ({ target }) => { if (target.matches(E.ROLL_UP_BUTTON)) { const eventItem = target.closest(E.EVENT_ITEM); diff --git a/src/presenter/event-presenter.js b/src/presenter/event-presenter.js index 8460708..eb0fd89 100644 --- a/src/presenter/event-presenter.js +++ b/src/presenter/event-presenter.js @@ -1,5 +1,5 @@ -import { EventMode } from '../const'; -import { remove, render, replace } from '../framework/render'; +import { EventMode, FormMode, UpdateType, UserAction } from '../const'; +import { remove, render, RenderPosition, replace } from '../framework/render'; import EventView from '../view/event-view'; import FormView from '../view/form-view'; @@ -10,28 +10,41 @@ export default class EventPresenter { /** @type {TripEvent} */ #event = null; - #onEventChangeCallback = null; - #onFormSubmitCallback = null; + /** @type {Offers} */ + #offers = null; + /** @type {Destination} */ + #destinations = null; - constructor(eventContainer) { + #formMode = ''; + + #viewActionCallback = null; + #getEventsQuantity = null; + + constructor(eventContainer, getEventsQuantity) { this.#eventContainer = eventContainer; + this.#getEventsQuantity = getEventsQuantity; } - init(event, offers, destinations) { + init(event = this.#event, offers = this.#offers, destinations = this.#destinations, formMode = FormMode.EDIT) { this.#event = event; + this.#offers = offers; + this.#destinations = destinations; + this.#formMode = formMode; const prevEventComponent = this.#eventComponent; const prevFormComponent = this.#formComponent; - this.#eventComponent = new EventView(event, offers, destinations); - this.#eventComponent.setOnFavoriteClickHandler(this.#favoriteClickHandler); - this.#formComponent = new FormView(event, offers, destinations); - this.#formComponent.setOnFormSubmitHandler(this.#formSubmitHandler); + this.#eventComponent = new EventView(this.#event, this.#offers, this.#destinations).setOnFavoriteClickHandler(this.#favoriteClickHandler); + this.#formComponent = new FormView(this.#event, this.#offers, this.#destinations, formMode) + .setOnFormSubmitHandler(this.#formSubmitHandler) + .setOnFormDeleteHandler(this.#formDeleteHandler); if (prevEventComponent === null || prevFormComponent === null) { - render(this.#eventContainer, this.#eventComponent); - return; + const renderingComponent = (this.#formMode === FormMode.CREATE) ? this.#formComponent : this.#eventComponent; + const position = (this.#formMode === FormMode.CREATE) ? RenderPosition.AFTERBEGIN : RenderPosition.BEFOREEND; + render(this.#eventContainer, renderingComponent, position); + return this; } if (this.#eventContainer.contains(prevEventComponent.element)) { @@ -43,6 +56,8 @@ export default class EventPresenter { remove(prevEventComponent); remove(prevFormComponent); + + return this; } destroy() { @@ -50,28 +65,38 @@ export default class EventPresenter { remove(this.#formComponent); } - setOnDataChangeHandler = (callback) => { - this.#onEventChangeCallback = callback; - }; - - setOnFormSubmitHandler = (callback) => { - this.#onFormSubmitCallback = callback; + setViewActionHandler = (callback) => { + this.#viewActionCallback = callback; + return this; }; toggleEventView = (direction = EventMode.DEFAULT) => { if (direction === EventMode.DEFAULT) { - this.#formComponent.updateElement(this.#event); + this.#formComponent.reset(this.#event); replace(this.#formComponent, this.#eventComponent); } else { replace(this.#eventComponent, this.#formComponent); } + + return this; }; #formSubmitHandler = (updatedEvent) => { - this.#onFormSubmitCallback?.(updatedEvent); + const updateType = (this.#formMode === FormMode.CREATE) ? UpdateType.MAJOR : UpdateType.MINOR; + const action = (this.#formMode === FormMode.CREATE) ? UserAction.CREATE_EVENT : UserAction.UPDATE_EVENT; + this.#viewActionCallback?.(action, updateType, updatedEvent); + }; + + #formDeleteHandler = (updatedEvent) => { + const eventsQuantity = this.#getEventsQuantity() - 1; + const updateType = eventsQuantity ? UpdateType.MINOR : UpdateType.MAJOR; + const action = (this.#formMode === FormMode.CREATE) ? UserAction.CANCEL_EVENT : UserAction.DELETE_EVENT; + this.#viewActionCallback?.(action, updateType, updatedEvent); }; #favoriteClickHandler = () => { - this.#onEventChangeCallback?.({ ...this.#event, isFavorite: !this.#event.isFavorite }); + this.#viewActionCallback?.(UserAction.UPDATE_EVENT, UpdateType.PATCH, + { ...this.#event, isFavorite: !this.#event.isFavorite } + ); }; } diff --git a/src/presenter/header-presenter.js b/src/presenter/header-presenter.js index 0845070..eb76cca 100644 --- a/src/presenter/header-presenter.js +++ b/src/presenter/header-presenter.js @@ -1,18 +1,14 @@ import FilterView from '../view/filter-view'; import TripInfoView from '../view/trip-info-view'; -import { SortType } from '../const'; +import { SortType, UpdateType } from '../const'; import { sortEvents } from '../utils/sort'; -import { render, RenderPosition } from '../framework/render'; +import { remove, render, RenderPosition, replace } from '../framework/render'; export default class HeaderPresenter { #headerContainer = null; #filterContainer = null; - #tripInfo = null; - - #filters = []; - - /** @type {TripEvent[]} */ - #events = []; + #tripInfoComponent = null; + #filtersComponent = null; /** @type {TripModel} */ #tripModel = null; @@ -21,17 +17,59 @@ export default class HeaderPresenter { this.#headerContainer = headerContainer; this.#filterContainer = filtersContainer; this.#tripModel = tripModel; + this.#tripModel.addObserver(this.#onModelChangeHandler); + } + + get #events() { + return sortEvents(this.#tripModel.events, SortType.DAY); } init() { + this.#renderTripInfo(); + this.#renderFilters(); + } - this.#events = sortEvents(this.#tripModel.events, SortType.DAY); + #renderTripInfo = () => { - if (this.#events.length !== 0) { - this.#tripInfo = new TripInfoView(this.#events, this.#tripModel.destinations); - render(this.#headerContainer, this.#tripInfo, RenderPosition.AFTERBEGIN); + if (this.#tripInfoComponent && (this.#tripModel.eventsSize === 0)) { + remove(this.#tripInfoComponent); + this.#tripInfoComponent = null; + return; } - this.#filters = new FilterView(this.#tripModel.filters, this.#tripModel.filterType); - render(this.#filterContainer, this.#filters); - } + + if (this.#tripModel.eventsSize !== 0) { + const prevComponent = this.#tripInfoComponent; + this.#tripInfoComponent = new TripInfoView(this.#events, this.#tripModel.offers, this.#tripModel.destinations); + + if (!prevComponent) { + render(this.#headerContainer, this.#tripInfoComponent, RenderPosition.AFTERBEGIN); + } else { + replace(prevComponent, this.#tripInfoComponent); + remove(prevComponent); + } + } + }; + + #renderFilters = () => { + const prevComponent = this.#filtersComponent; + this.#filtersComponent = new FilterView(this.#tripModel.filters, this.#tripModel.filterType).setOnFilterChangeHandler(this.#onFilterChangeHandler); + + if (!prevComponent) { + render(this.#filterContainer, this.#filtersComponent); + return; + } + + replace(prevComponent, this.#filtersComponent); + remove(prevComponent); + }; + + #onModelChangeHandler = (updateType) => { + if ((updateType !== UpdateType.PATCH) && (updateType !== UpdateType.FILTER)) { + this.init(); + } + }; + + #onFilterChangeHandler = (filterType) => { + this.#tripModel.updateFilterType(UpdateType.FILTER, filterType); + }; } diff --git a/src/utils/common.js b/src/utils/common.js index 9e11d89..c41ea38 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -12,5 +12,3 @@ export const mix = (data = [], length = 0) => { return mixedData; }; - -export const updateItem = (items, update) => items.map((item) => item.id === update.id ? update : item); diff --git a/src/view/event-list-view.js b/src/view/event-list-view.js index f63ddfc..b25f846 100644 --- a/src/view/event-list-view.js +++ b/src/view/event-list-view.js @@ -13,9 +13,11 @@ export default class EventListView extends AbstractView { setEventToggleHandler = (callback) => { this.createEventListener(this.element, 'click', callback); + return this; }; setEscKeyDownHandler = (callback) => { this.createEventListener(document.body, 'keydown', callback); + return this; }; } diff --git a/src/view/event-view.js b/src/view/event-view.js index e987963..961a4e7 100644 --- a/src/view/event-view.js +++ b/src/view/event-view.js @@ -34,7 +34,7 @@ const createEventTemplate = (event, offers, destinations) => { const timeStart = start.format(DateFormat.EVENT_TIME); const timeEnd = end.format(DateFormat.EVENT_TIME); - const eventTitle = `${type} ${destinations.get(destinationId).name}`; + const eventTitle = `${type} ${destinations.get(destinationId)?.name || ''}`; const eventClass = isFavorite ? 'event__favorite-btn--active' : ''; return (` @@ -93,5 +93,6 @@ export default class EventView extends AbstractView { setOnFavoriteClickHandler = (callback) => { this.createEventListener('.event__favorite-btn', 'click', callback); + return this; }; } diff --git a/src/view/filter-view.js b/src/view/filter-view.js index 2204409..4d4dd98 100644 --- a/src/view/filter-view.js +++ b/src/view/filter-view.js @@ -8,8 +8,8 @@ const createFiltersItemTemplate = (filters, activeFilter) => filters.map(({ type return (`