diff --git a/src/const.js b/src/const.js index 2020d90..9a58917 100644 --- a/src/const.js +++ b/src/const.js @@ -3,11 +3,11 @@ const WAYPOINTS_COUNT = 3; const PRICES = [100, 150, 200]; const DATES = [ - {from: '2019-05-24T23:55:43.845Z', to: '2019-05-24T09:13:25.845Z'}, - {from: '2020-09-05T13:34:15.845Z', to: '2020-09-05T22:05:34.845Z'}, - {from: '2021-03-12T10:21:12.845Z', to: '2021-03-12T12:18:24.845Z'}, + {from: '2024-01-24T23:55:43.845Z', to: '2024-03-24T09:13:25.845Z'}, + {from: '2024-04-05T13:34:15.845Z', to: '2024-06-05T22:05:34.845Z'}, + {from: '2025-03-12T10:21:12.845Z', to: '2025-03-12T12:18:24.845Z'}, {from: '2022-01-31T19:54:08.845Z', to: '2022-01-31T21:45:19.845Z'}, - {from: '2023-06-17T16:15:14.845Z', to: '2023-06-19T02:06:08.845Z'} + {from: '2025-06-17T16:15:14.845Z', to: '2025-06-19T02:06:08.845Z'} ]; const DESTINATIONS = [ @@ -91,6 +91,18 @@ const EDITING_FORM = { offers: [ '1', '2' ], }; +const ACTIONS = { + UPDATE_POINT: 'update', + ADD_POINT: 'add', + DELETE_POINT: 'delete', +}; + +const UPDATE_TYPE = { + MINOR: 'MINOR', + MAJOR: 'MAJOR', +}; + + const DATE_FORMAT_EDIT = 'DD/MM/YY hh:mm'; const DATE_FORMAT_DAY = 'MMM DD'; const DATE_FORMAT_HOURS = 'hh:mm'; @@ -116,4 +128,4 @@ export { DESTINATIONS }; export { OFFERS }; export { DATE_FORMAT_EDIT, DATE_FORMAT_DAY, DATE_FORMAT_HOURS }; export { EDITING_FORM }; -export { FILTER_TYPE, SORTING_TYPES }; +export { FILTER_TYPE, SORTING_TYPES, ACTIONS, UPDATE_TYPE }; diff --git a/src/main.js b/src/main.js index 194b2a0..0c3f0b9 100644 --- a/src/main.js +++ b/src/main.js @@ -1,10 +1,12 @@ -import { render, RenderPosition } from './framework/render.js'; -import FilterView from './view/filter-view.js'; +import {render, RenderPosition} from './framework/render.js'; import InfoView from './view/info-view.js'; import EventPresenter from './presenter/event-presenter.js'; import WaypointsModel from './model/waypoint-model.js'; -import { getMockFilters } from './mock/filter.js'; -import { getMockSorts } from './mock/sort.js'; +import {getMockFilters} from './mock/filter.js'; +import {getMockSorts} from './mock/sort.js'; +import FilterPresenter from './presenter/filter-presenter'; +import FilterModel from './model/filter-model'; +import NewPointButtonView from './view/new-point-button-view'; const mainContainer = document.querySelector('.trip-main'); const filterContainer = document.querySelector('.trip-controls__filters'); @@ -13,10 +15,42 @@ const eventContainer = document.querySelector('.trip-events'); const mockFilters = getMockFilters(); const mockSorts = getMockSorts(); + const waypointsModel = new WaypointsModel(); -const eventPresenter = new EventPresenter({eventContainer, waypointsModel, sorts: mockSorts}); +const filterModel = new FilterModel(); + +const filterPresenter = new FilterPresenter({ + filterContainer, + filterModel, + filters: mockFilters +}); + +const newPointButtonComponent = new NewPointButtonView({ + onClick: handleNewPointButtonClick +}); + +function handleNewPointFormClose() { + newPointButtonComponent.element.disabled = false; +} + +const eventPresenter = new EventPresenter({ + eventContainer, + waypointsModel, + filterModel, + sorts: mockSorts, + filters: mockFilters, + onNewPointDestroy: handleNewPointFormClose +}); + + +function handleNewPointButtonClick() { + eventPresenter.createWaypoint(); + newPointButtonComponent.element.disabled = true; +} + render(new InfoView(), mainContainer, RenderPosition.AFTERBEGIN); -render(new FilterView({filters: mockFilters}), filterContainer); +render(newPointButtonComponent, mainContainer, RenderPosition.BEFOREEND); +filterPresenter.init(); eventPresenter.init(); diff --git a/src/mock/waypoint.js b/src/mock/waypoint.js index 006a405..9a640fb 100644 --- a/src/mock/waypoint.js +++ b/src/mock/waypoint.js @@ -1,5 +1,5 @@ import {BOOL, DATES, OFFERS, PRICES, TYPES} from '../const.js'; -import {getRandomArrayElement, getRandomArrayElements, getRandomInt} from '../utils.js'; +import {getId, getRandomArrayElement, getRandomArrayElements, getRandomInt} from '../utils.js'; function getMockWaypoint() { const type = getRandomArrayElement(TYPES); @@ -25,7 +25,7 @@ const mockWaypoints = [ function getRandomWaypoint() { return { ...getRandomArrayElement(mockWaypoints), - id: Date.now().toString(36) + Math.random().toString(36).slice(2) + id: getId() }; } diff --git a/src/model/filter-model.js b/src/model/filter-model.js new file mode 100644 index 0000000..3dc8fb2 --- /dev/null +++ b/src/model/filter-model.js @@ -0,0 +1,16 @@ +import {FILTER_TYPE, UPDATE_TYPE} from '../const'; +import Observable from '../framework/observable.js'; + + +export default class FilterModel extends Observable { + #filter = FILTER_TYPE.EVERYTHING; + + get filter() { + return this.#filter; + } + + setFilter(filter) { + this.#filter = filter; + this._notify(UPDATE_TYPE.MAJOR, filter); + } +} diff --git a/src/model/waypoint-model.js b/src/model/waypoint-model.js index 7056b11..509553d 100644 --- a/src/model/waypoint-model.js +++ b/src/model/waypoint-model.js @@ -1,10 +1,62 @@ -import { getRandomWaypoint } from '../mock/waypoint.js'; -import { WAYPOINTS_COUNT } from '../const.js'; +import {getRandomWaypoint} from '../mock/waypoint.js'; +import {UPDATE_TYPE, WAYPOINTS_COUNT} from '../const.js'; +import Observable from '../framework/observable'; -export default class WaypointsModel { +export default class WaypointsModel extends Observable { waypoints = Array.from({length: WAYPOINTS_COUNT}, getRandomWaypoint); getWaypoints() { return this.waypoints; } + + getWaypoint(id) { + return this.waypoints.find((waypoint) => waypoint.id === id); + } + + setWaypoints(waypoints) { + this.waypoints = waypoints; + } + + setWaypoint(waypoint, id) { + this.waypoints = [...this.waypoints.filter((other) => other.id !== id), waypoint]; + } + + addPoint(updateType, update) { + this.waypoints = [ + update, + ...this.waypoints, + ]; + this._notify(updateType, update); + } + + updatePoint(updateType, update) { + const index = this.waypoints.findIndex((point) => point.id === update.id); + + if (index === -1) { + throw new Error('The point doesn\'t exist!'); + } + + this.waypoints = [ + ...this.waypoints.slice(0, index), + update, + ...this.waypoints.slice(index + 1), + ]; + + this._notify(updateType, update); + } + + deletePoint(updateType, update) { + const index = this.waypoints.findIndex((point) => point.id === update.id); + + if (index === -1) { + throw new Error('The point doesn\'t exist!'); + } + + this.waypoints = [ + ...this.waypoints.slice(0, index), + ...this.waypoints.slice(index + 1), + ]; + + this._notify(UPDATE_TYPE.MINOR, update); + } } diff --git a/src/presenter/event-presenter.js b/src/presenter/event-presenter.js index 817e7b2..e9fb80c 100644 --- a/src/presenter/event-presenter.js +++ b/src/presenter/event-presenter.js @@ -1,44 +1,97 @@ -import {render} from '../framework/render.js'; +import {remove, render, RenderPosition} from '../framework/render.js'; import SortView from '../view/sort-view.js'; import EventListView from '../view/event-list-view.js'; import WaypointPresenter from './waypoint-presenter'; +import {ACTIONS as USER_ACTION, SORTING_TYPES, UPDATE_TYPE} from '../const'; +import NewWaypointPresenter from './new-waypoint-presenter'; export default class EventPresenter { #eventListContainer = new EventListView(); #waypointPresenters = []; #eventContainer; #sorts; + #filters; #currentSortType; + #emptyComponent; + #filterModel; + #newPointPresenter; + #onNewPointDestroy; - constructor({eventContainer, waypointsModel, sorts}) { + constructor({eventContainer, waypointsModel, filterModel, sorts, filters, onNewPointDestroy}) { this.#eventContainer = eventContainer; this.#sorts = sorts; - this.waypoints = waypointsModel.getWaypoints(); + this.#filters = filters; + this.waypointsModel = waypointsModel; + this.#filterModel = filterModel; + this.#onNewPointDestroy = onNewPointDestroy; this.#currentSortType = sorts[0].name; this.#sortWaypoints(sorts[0].name); } init() { - render(new SortView({sorts: this.#sorts, onChange: this.#handleSortTypeChange}), this.#eventContainer); + render( + new SortView({sorts: this.#sorts, currentSort: this.#currentSortType, onChange: this.#handleSortTypeChange}), + this.#eventContainer, + RenderPosition.BEFOREEND + ); + this.renderWaypoints(); + this.#filterModel.addObserver(this.#handleFilterTypeChange.bind(this)); + this.waypointsModel.addObserver(this.#handleModelEvent); + } + + createWaypoint() { + this.#newPointPresenter = new NewWaypointPresenter({ + pointListContainer: this.#eventListContainer.element, + onDataChange: this.#handleWaypointChange, + onDestroy: this.#onNewPointDestroy, + closeAllEditForms: () => this.#closeAllEditForms(), + }); + + this.#newPointPresenter.init(); + } + + renderWaypoints() { render(this.#eventListContainer, this.#eventContainer); - this.waypoints.forEach((waypoint) => this.#renderWaypoint(waypoint)); + const filteredPoints = this.#getFilteredWaypoints(this.waypointsModel.getWaypoints()); + filteredPoints.forEach((waypoint) => this.#renderWaypoint(waypoint)); + } + + reset() { + this.#waypointPresenters.forEach((waypointPresenter) => waypointPresenter.destroy()); + this.#waypointPresenters = []; + + if (this.#emptyComponent) { + remove(this.#emptyComponent); + } } #closeAllEditForms() { + if (this.#newPointPresenter) { + this.#newPointPresenter.destroy(); + } + this.#waypointPresenters.forEach((waypoint) => waypoint.closeForm()); } - #handleWaypointChange = (updatedWaypoint) => { - this.waypoints = this.waypoints.map((waypoint) => waypoint.id === updatedWaypoint.id ? updatedWaypoint : waypoint); - this.#waypointPresenters - .find((waypointPresenter) => waypointPresenter.id === updatedWaypoint.id) - .init(updatedWaypoint); + #handleWaypointChange = (action, type, waypoint) => { + switch (action) { + case USER_ACTION.ADD_POINT: + this.#sortWaypoints(SORTING_TYPES.DAY); + this.waypointsModel.addPoint(type, waypoint); + break; + case USER_ACTION.DELETE_POINT: + this.waypointsModel.deletePoint(type, waypoint); + break; + case USER_ACTION.UPDATE_POINT: + this.waypointsModel.updatePoint(type, waypoint); + break; + } }; #renderWaypoint(waypoint) { const waypointPresenter = new WaypointPresenter({ waypoint, - containerElement: this.#eventContainer, + containerElement: this.#eventListContainer.element, closeAllEditForms: () => this.#closeAllEditForms(), onChange: this.#handleWaypointChange }); @@ -52,16 +105,40 @@ export default class EventPresenter { } this.#sortWaypoints(sortType); this.#deleteWaypoints(); - this.waypoints.forEach((waypoint) => this.#renderWaypoint(waypoint)); + this.waypointsModel.getWaypoints().forEach((waypoint) => this.#renderWaypoint(waypoint)); }; #sortWaypoints(sortType) { - this.#sorts.find((sort) => sort.name === sortType).getPoints(this.waypoints); + this.#sorts.find((sort) => sort.name === sortType).getPoints(this.waypointsModel.getWaypoints()); this.#currentSortType = sortType; } + #getFilteredWaypoints(waypoints) { + return this.#filters.find((filter) => filter.name === this.#filterModel.filter).getPoints(waypoints); + } + #deleteWaypoints() { this.#waypointPresenters.forEach((waypoint) => waypoint.destroy()); this.#waypointPresenters = []; } + + #handleFilterTypeChange() { + this.reset(); + this.#currentSortType = SORTING_TYPES.DAY; + this.renderWaypoints(); + } + + #handleModelEvent = (updateType) => { + switch (updateType) { + case UPDATE_TYPE.MINOR: + this.reset(); + this.renderWaypoints(); + break; + case UPDATE_TYPE.MAJOR: + this.reset(); + this.renderWaypoints(); + this.#currentSortType = SORTING_TYPES.DAY; + break; + } + }; } diff --git a/src/presenter/filter-presenter.js b/src/presenter/filter-presenter.js new file mode 100644 index 0000000..d68d54e --- /dev/null +++ b/src/presenter/filter-presenter.js @@ -0,0 +1,45 @@ +import {remove, render, replace} from '../framework/render'; +import FilterView from '../view/filter-view'; + +export default class FilterPresenter { + #filterContainer; + #filters; + #filterComponent = null; + #filterModel; + + constructor({filterContainer, filterModel, filters}) { + this.#filterContainer = filterContainer; + this.#filters = filters; + this.#filterModel = filterModel; + + this.#filterModel.addObserver(this.init.bind(this)); + } + + init() { + const prevFilterComponent = this.#filterComponent; + + this.#filterComponent = new FilterView({ + filters: this.#filters, + type: this.#filterModel.filter, + onChange: this.#handleTypeChange + }); + + if (prevFilterComponent === null) { + render(this.#filterComponent, this.#filterContainer); + return; + } + + replace(this.#filterComponent, prevFilterComponent); + remove(prevFilterComponent); + + render(this.#filterComponent, this.#filterContainer); + } + + #handleTypeChange = (type) => { + if (this.#filterModel.filter === type) { + return; + } + + this.#filterModel.setFilter(type); + }; +} diff --git a/src/presenter/new-waypoint-presenter.js b/src/presenter/new-waypoint-presenter.js new file mode 100644 index 0000000..a116c01 --- /dev/null +++ b/src/presenter/new-waypoint-presenter.js @@ -0,0 +1,73 @@ +import {remove, render, RenderPosition} from '../framework/render.js'; +import {ACTIONS as USER_ACTION, UPDATE_TYPE} from '../const'; +import EditingFormView from '../view/editing-form-view'; + +export default class NewWaypointPresenter { + #pointListContainer; + #newWaypointComponent = null; + #handleDataChange; + #handleDestroy; + #closeAllEditForms; + + constructor({pointListContainer, onDataChange, onDestroy, closeAllEditForms}) { + this.#handleDataChange = onDataChange; + this.#handleDestroy = onDestroy; + this.#pointListContainer = pointListContainer; + this.#closeAllEditForms = closeAllEditForms; + } + + init() { + if (this.#newWaypointComponent !== null) { + return; + } + + this.#closeAllEditForms(); + + this.#newWaypointComponent = new EditingFormView({ + formType: 'create', + waypoint: {type: 'flight', destination: '', price: '0', offers: []}, + onFormSubmit: (newWaypoint) => this.#handleSaveClick(newWaypoint), + onClose: () => this.#handleCancelClick(), + onDelete: this.#handleCancelClick + }); + + render(this.#newWaypointComponent, this.#pointListContainer, RenderPosition.AFTERBEGIN); + document.addEventListener('keydown', this.#escKeyDownHandler); + } + + destroy() { + if (this.#newWaypointComponent === null) { + return; + } + + this.#handleDestroy(); + + remove(this.#newWaypointComponent); + this.#newWaypointComponent = null; + + document.removeEventListener('keydown', this.#escKeyDownHandler); + } + + #handleSaveClick = (point) => { + this.#handleDataChange( + USER_ACTION.ADD_POINT, + UPDATE_TYPE.MAJOR, + { + id: Date.now().toString(36) + Math.random().toString(36).slice(2), + ...point + }, + ); + this.destroy(); + }; + + #handleCancelClick = () => { + this.destroy(); + }; + + #escKeyDownHandler = (evt) => { + if (evt.key === 'Esc' || evt.key === 'Escape') { + evt.preventDefault(); + this.destroy(); + } + }; +} diff --git a/src/presenter/waypoint-presenter.js b/src/presenter/waypoint-presenter.js index f561834..ca38f33 100644 --- a/src/presenter/waypoint-presenter.js +++ b/src/presenter/waypoint-presenter.js @@ -1,6 +1,8 @@ import {remove, render, replace} from '../framework/render.js'; import EditingFormView from '../view/editing-form-view.js'; import WaypointView from '../view/waypoint-view.js'; +import {ACTIONS as USER_ACTION, UPDATE_TYPE} from '../const'; +import {getId} from '../utils'; export default class WaypointPresenter { #waypoint; @@ -38,6 +40,7 @@ export default class WaypointPresenter { waypoint: waypoint, onFormSubmit: (newWaypoint) => this.#handleSaveClick(newWaypoint), onClose: () => this.closeForm(), + onDelete: this.#handleDeleteClick }); if (!prevWaypointComponent || !prevEditComponent) { @@ -80,11 +83,22 @@ export default class WaypointPresenter { }; #handleFavoriteClick = () => { - this.#onChange({...this.#waypoint, isFavorite: !this.#waypoint.isFavorite}); + this.#onChange( + USER_ACTION.UPDATE_POINT, + UPDATE_TYPE.MINOR, + {...this.#waypoint, isFavorite: !this.#waypoint.isFavorite} + ); }; #handleSaveClick = (waypoint) => { - this.#onChange({...waypoint}); + this.#onChange( + USER_ACTION.UPDATE_POINT, + UPDATE_TYPE.MINOR, + { + id: getId(), + ...waypoint + } + ); this.closeForm(); }; @@ -92,4 +106,12 @@ export default class WaypointPresenter { remove(this.#waypointComponent); remove(this.#editComponent); } + + #handleDeleteClick = (point) => { + this.#onChange( + USER_ACTION.DELETE_POINT, + UPDATE_TYPE.MINOR, + point, + ); + }; } diff --git a/src/utils.js b/src/utils.js index a77442c..e3f991c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -64,11 +64,15 @@ function stringToDate(str, format) { return new Date(year, month, day, hour, minute, second); } +function getId() { + return Date.now().toString(36) + Math.random().toString(36).slice(2); +} + const filters = { [FILTER_TYPE.EVERYTHING]: (points) => points.filter((point) => point), - [FILTER_TYPE.FUTURE]: (points) => points.filter((point) => point), - [FILTER_TYPE.PRESENT]: (points) => points.filter((point) => point), - [FILTER_TYPE.PAST]: (points) => points.filter((point) => point), + [FILTER_TYPE.FUTURE]: (points) => points.filter((point) => new Date(point.dateFrom) > new Date()), + [FILTER_TYPE.PRESENT]: (points) => points.filter((point) => new Date(point.dateFrom) <= new Date() && new Date() <= new Date(point.dateTo)), + [FILTER_TYPE.PAST]: (points) => points.filter((point) => new Date(point.dateTo) < new Date()), }; const sorts = { @@ -87,6 +91,7 @@ export { getRandomInt, countDuration, formatDuration, - stringToDate + stringToDate, + getId }; export {filters, sorts}; diff --git a/src/view/editing-form-view.js b/src/view/editing-form-view.js index 825d2ca..183f9cc 100644 --- a/src/view/editing-form-view.js +++ b/src/view/editing-form-view.js @@ -19,7 +19,7 @@ function getEventOffer({id, title, price, isChecked}) { function getEventOffers(checkedOffers, type) { let result = ''; for (const offer of OFFERS.find((findOffer) => findOffer.type === type).offers) { - result += getEventOffer({...offer, isChecked: checkedOffers.includes(offer.id)}); + result += getEventOffer({...offer, isChecked: checkedOffers?.includes(offer.id)}); } return result; } @@ -49,8 +49,10 @@ function getDestinations() { return result; } -function createEditingFormTemplate({type, destination, offers, price, dateFrom, dateTo}) { +function createEditingFormTemplate({formType, type, destination, offers, price, dateFrom, dateTo}) { const destinationObject = DESTINATIONS.find((dest) => dest.id === destination); + const waypointType = type || 'flight'; + const offersList = getEventOffers(offers, waypointType); return `
${destinationObject.description}
+${destinationObject?.description || ''}
@@ -46,7 +46,7 @@ function createWaypointTemplate({type, destination, offers, price, dateFrom, dat