diff --git a/package-lock.json b/package-lock.json index cffce3a..446c2d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@babel/core": "7.21.4", "dayjs": "1.11.13", "flatpickr": "4.6.13", + "he": "1.2.0", "webpack-dev-server": "4.13.3" }, "devDependencies": { @@ -4569,7 +4570,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, "bin": { "he": "bin/he" } diff --git a/package.json b/package.json index 126cf90..7e096c9 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@babel/core": "7.21.4", "dayjs": "1.11.13", "flatpickr": "4.6.13", + "he": "1.2.0", "webpack-dev-server": "4.13.3" } } diff --git a/public/index.html b/public/index.html index 44c26b4..a29324b 100644 --- a/public/index.html +++ b/public/index.html @@ -21,8 +21,6 @@

Filter events

- - diff --git a/src/const.js b/src/const.js index 6f1aade..aaf9e5d 100644 --- a/src/const.js +++ b/src/const.js @@ -19,4 +19,33 @@ const SortType = { OFFER: 'offer' }; -export { TYPES, DATE_FORMAT, TIME_FORMAT, DATE_WITH_TIME_FORMAT, FilterType, SortType }; +const UserAction = { + UPDATE_POINT: 'UPDATE_POINT', + ADD_POINT: 'ADD_POINT', + DELETE_POINT: 'DELETE_POINT', +}; + +const UpdateType = { + PATCH: 'PATCH', + MINOR: 'MINOR', + MAJOR: 'MAJOR', +}; + +const ListEmptyText = { + [FilterType.EVERYTHING]: 'Click New Event to create your first point', + [FilterType.PAST]: 'There are no past events now', + [FilterType.PRESENT]: 'There are no present events now', + [FilterType.FUTURE]: 'There are no future events now', +}; + +const BLANK_POINT = { + type: TYPES[5], + destination: null, + dateFrom: null, + dateTo: null, + basePrice: 0, + offers: [], + isFavorite: false, +}; + +export { TYPES, DATE_FORMAT, TIME_FORMAT, DATE_WITH_TIME_FORMAT, FilterType, SortType, UserAction, UpdateType, ListEmptyText, BLANK_POINT }; diff --git a/src/main.js b/src/main.js index 5bd0ee5..d0bbbb3 100644 --- a/src/main.js +++ b/src/main.js @@ -1,21 +1,46 @@ -import NewFilters from './view/filters-view'; import NewTripInfo from './view/trip-info-view'; import { RenderPosition, render } from './framework/render'; import MainPresenter from './presenter/main-presenter'; import PointModel from './model/point-model'; -import { generateFilter } from './mock/filter-mock'; +import FiltersModel from './model/filters-model'; +import FiltersPresenter from './presenter/filters-presenter'; +import AddNewPointButtonView from './view/add-new-point-button-view'; const mainContainer = document.querySelector('.trip-main'); const filtersContainer = document.querySelector('.trip-controls__filters'); const pointsContainer = document.querySelector('.trip-events'); + const pointModel = new PointModel(); +const filtersModel = new FiltersModel(); + +const addNewPointButton = new AddNewPointButtonView({ + onClick: onNewPointButtonClick, +}); + const mainPresenter = new MainPresenter({ pointsContainer: pointsContainer, pointModel, + filtersModel, + onNewPointCancel: cancelNewPoint, +}); + +const filtersPresenter = new FiltersPresenter({ + filtersContainer: filtersContainer, + pointModel, + filtersModel, }); -const filters = generateFilter(pointModel.points); render(new NewTripInfo(), mainContainer, RenderPosition.AFTERBEGIN); -render(new NewFilters({filters}), filtersContainer); +render(addNewPointButton, mainContainer); + +function onNewPointButtonClick() { + mainPresenter.createPoint(); + addNewPointButton.element.disabled = true; +} + +function cancelNewPoint() { + addNewPointButton.element.disabled = false; +} +filtersPresenter.init(); mainPresenter.init(); diff --git a/src/mock/destinations-mock.js b/src/mock/destinations-mock.js index 1c56a05..c3d1e6e 100644 --- a/src/mock/destinations-mock.js +++ b/src/mock/destinations-mock.js @@ -1,5 +1,5 @@ import { CITIES, PICTURES, DESCRIPTION_TEXT } from './const-mock'; -import { getRandomDescriptionPoint } from '../utils/common-utils'; +import { getRandomDescriptionPoint, getRandomInteger } from '../utils/common-utils'; let destinationId = 0; @@ -10,23 +10,23 @@ const getDestinationsMock = (city) => { description: getRandomDescriptionPoint(DESCRIPTION_TEXT), pictures: [ { - src: PICTURES[0], + src: PICTURES[getRandomInteger(0, 4)], description: `${city} parliament building` }, { - src: PICTURES[1], + src: PICTURES[getRandomInteger(0, 4)], description: `${city} main square` }, { - src: PICTURES[2], + src: PICTURES[getRandomInteger(0, 4)], description: `${city} best view` }, { - src: PICTURES[3], + src: PICTURES[getRandomInteger(0, 4)], description: `${city} landscape` }, { - src: PICTURES[4], + src: PICTURES[getRandomInteger(0, 4)], description: `${city} church` } ], diff --git a/src/mock/point-mock.js b/src/mock/point-mock.js index 66054bc..3f9f944 100644 --- a/src/mock/point-mock.js +++ b/src/mock/point-mock.js @@ -1,12 +1,12 @@ -import { getRandomArrayElement, getRandomInteger, createIdGenerator } from '../utils/common-utils'; +import { getRandomArrayElement, getRandomInteger } from '../utils/common-utils'; import { CITIES, DATES } from './const-mock'; import { TYPES } from '../const'; import { getOffers } from './offers-mock'; +import { nanoid } from 'nanoid'; const POINTS_COUNT = 10; const offersData = getOffers(); -const generateRandomPointId = createIdGenerator(); const createPointMock = () => { const pointDate = getRandomArrayElement(DATES); @@ -32,7 +32,7 @@ const createPointMock = () => { }; const pointMock = { - id: generateRandomPointId(), + id: nanoid(), type: pointType, destination: getRandomInteger(1, CITIES.length), dateFrom: pointDate.dateFrom, diff --git a/src/model/filters-model.js b/src/model/filters-model.js new file mode 100644 index 0000000..f161a06 --- /dev/null +++ b/src/model/filters-model.js @@ -0,0 +1,15 @@ +import Observable from '../framework/observable'; +import { FilterType } from '../const'; + +export default class FiltersModel extends Observable { + #filter = FilterType.EVERYTHING; + + get filter() { + return this.#filter; + } + + setFilter(updateType, filter) { + this.#filter = filter; + this._notify(updateType, filter); + } +} diff --git a/src/model/point-model.js b/src/model/point-model.js index 0634b89..a09b7b9 100644 --- a/src/model/point-model.js +++ b/src/model/point-model.js @@ -1,21 +1,52 @@ import { getPoints } from '../mock/point-mock'; import { getDestinations } from '../mock/destinations-mock'; import { getOffers } from '../mock/offers-mock'; +import Observable from '../framework/observable'; +import { nanoid } from 'nanoid'; -export default class PointModel { +export default class PointModel extends Observable { #points = getPoints(); - #destinations = getDestinations(); - #offers = getOffers(); + #allDestinations = getDestinations(); + #allOffers = getOffers(); get points() { return this.#points; } + updatePoint(updateType, updatedPoint) { + const pointIndex = this.#points.findIndex((point) => point.id === updatedPoint.id); + + this.#points = [ + ...this.#points.slice(0, pointIndex), + updatedPoint, + ...this.#points.slice(pointIndex + 1), + ]; + + this._notify(updateType, updatedPoint); + } + + addPoint(updateType, updatedPoint) { + this.#points = [{id: nanoid(), ...updatedPoint}, ...this.#points]; + + this._notify(updateType, updatedPoint); + } + + deletePoint(updateType, updatedPoint) { + const pointIndex = this.#points.findIndex((point) => point.id === updatedPoint.id); + + this.#points = [ + ...this.#points.slice(0, pointIndex), + ...this.#points.slice(pointIndex + 1), + ]; + + this._notify(updateType, updatedPoint); + } + get destinations() { - return this.#destinations; + return this.#allDestinations; } get offers() { - return this.#offers; + return this.#allOffers; } } diff --git a/src/presenter/filters-presenter.js b/src/presenter/filters-presenter.js new file mode 100644 index 0000000..59c850a --- /dev/null +++ b/src/presenter/filters-presenter.js @@ -0,0 +1,60 @@ +import { render, replace, remove } from '../framework/render'; +import FiltersView from '../view/filters-view'; +import { UpdateType } from '../const'; +import { filter } from '../utils/filter-utils'; + +export default class FiltersPresenter { + #filtersModel = null; + #filtersComponent = null; + #filtersContainer = null; + #pointModel = null; + + constructor({ filtersContainer, pointModel, filtersModel }) { + this.#filtersContainer = filtersContainer; + this.#pointModel = pointModel; + this.#filtersModel = filtersModel; + + this.#pointModel.addObserver(this.#handleModelEvent); + this.#filtersModel.addObserver(this.#handleModelEvent); + } + + get filters() { + const points = this.#pointModel.points; + + return Object.entries(filter).map( + ([filterType, filterPoints]) => ({ + type: filterType, + count: filterPoints(points).length, + }), + ); + } + + init() { + const prevFiltersComponent = this.#filtersComponent; + + this.#filtersComponent = new FiltersView({ + filters: this.filters, + onFiltersChange: this.#handleFiltersChange + }); + + if(prevFiltersComponent === null){ + render(this.#filtersComponent, this.#filtersContainer); + return; + } + + replace(this.#filtersComponent, prevFiltersComponent); + remove(prevFiltersComponent); + } + + #handleModelEvent = () => { + this.init(); + }; + + #handleFiltersChange = (filterType) => { + if (this.#filtersModel.filter === filterType) { + return; + } + + this.#filtersModel.setFilter(UpdateType.MAJOR, filterType); + }; +} diff --git a/src/presenter/main-presenter.js b/src/presenter/main-presenter.js index 981070c..6a80615 100644 --- a/src/presenter/main-presenter.js +++ b/src/presenter/main-presenter.js @@ -1,37 +1,74 @@ import PointListView from '../view/point-list-view'; import SortingView from '../view/sorting-view'; import NoPointsView from '../view/no-points-view'; -import { RenderPosition, render } from '../framework/render'; +import { RenderPosition, remove, render } from '../framework/render'; import PointPresenter from './point-presenter'; -import { updatePoint } from '../utils/common-utils'; -import { SortType } from '../const'; +import { SortType, UpdateType, UserAction, FilterType } from '../const'; import { getWeightForPrice, getWeightForTime } from '../utils/point-utils'; +import { filter } from '../utils/filter-utils'; +import NewPointPresenter from './new-point-presenter'; export default class MainPresenter { #pointsListComponent = new PointListView(); #pointsContainer = null; #pointModel = null; - #points = []; - #destinations = []; - #offers = []; #pointPresenters = new Map(); - #noPoints = new NoPointsView(); + #noPoints = null; + #filtersModel = null; + #newPointPresenter = null; #sorting = null; #currentSortType = SortType.DAY; - #initialPointsLayout = []; + #currentFilterType = FilterType.EVERYTHING; - constructor({ pointsContainer, pointModel }) { + constructor({ pointsContainer, pointModel, filtersModel, onNewPointCancel }) { this.#pointsContainer = pointsContainer; this.#pointModel = pointModel; + this.#filtersModel = filtersModel; + + this.#newPointPresenter = new NewPointPresenter({ + pointsListContainer: this.#pointsListComponent.element, + onPointAdd: this.#handleViewAction, + onDestroy: onNewPointCancel, + }); + + this.#pointModel.addObserver(this.#handleModelEvent); + this.#filtersModel.addObserver(this.#handleModelEvent); } - init() { - this.#points = [...this.#pointModel.points]; - this.#destinations = [...this.#pointModel.destinations]; - this.#offers = [...this.#pointModel.offers]; - this.#initialPointsLayout = [...this.#pointModel.points]; + get filter() { + return this.#filtersModel.filter; + } + + get points() { + this.#currentFilterType = this.filter; + const points = [...this.#pointModel.points]; + const filteredPoints = filter[this.#currentFilterType](points); + + switch (this.#currentSortType) { + case SortType.TIME: + return filteredPoints.sort(getWeightForTime); + case SortType.PRICE: + return filteredPoints.sort(getWeightForPrice); + } + return filteredPoints; + } + get offers() { + return this.#pointModel.offers; + } + + get destinations() { + return this.#pointModel.destinations; + } + + createPoint() { + this.#currentSortType = FilterType.DAY; + this.#filtersModel.setFilter(UpdateType.MAJOR, FilterType.EVERYTHING); + this.#newPointPresenter.init(this.offers, this.destinations); + } + + init() { this.#renderSorting(this.#currentSortType); this.#renderMain(); } @@ -39,10 +76,6 @@ export default class MainPresenter { #renderMain() { render(this.#pointsListComponent, this.#pointsContainer); - if (this.#points.length === 0) { - this.#renderNoPoints(); - } - this.#renderPointsList(); } @@ -55,51 +88,68 @@ export default class MainPresenter { render(this.#sorting, this.#pointsContainer, RenderPosition.AFTERBEGIN); } - #sortPoints = (sortType) => { - switch (sortType) { - case SortType.TIME: - this.#points.sort(getWeightForTime); - break; - case SortType.PRICE: - this.#points.sort(getWeightForPrice); - break; - case SortType.EVENT: - break; - case SortType.OFFER: - break; - case SortType.DAY: - this.#points = [...this.#initialPointsLayout]; - } - this.#currentSortType = sortType; - }; - #handleSortingClick = (sortType) => { if (this.#currentSortType === sortType) { return; } - this.#sortPoints(sortType); + this.#currentSortType = sortType; this.#clearPointsList(); + remove(this.#sorting); + this.#renderSorting(this.#currentSortType); this.#renderPointsList(); - this.#renderSorting(sortType); }; #renderPoint(point) { const pointPresenter = new PointPresenter({ pointsListComponent: this.#pointsListComponent.element, - onPointsChange: this.#handlePointsChange, + onPointsChange: this.#handleModelEvent, onModeChange: this.#handleModeChange, onPointClear: this.#clearPoint, - onEditPointView: this.#resetPointView + onEditPointView: this.#resetPointView, + onModelUpdate: this.#handleViewAction, }); - pointPresenter.init(point, this.#offers, this.#destinations); + pointPresenter.init(point, this.offers, this.destinations); this.#pointPresenters.set(point.id, pointPresenter); } - #handlePointsChange = (updatedPoint) => { - this.#points = updatePoint(this.#points, updatedPoint); - this.#pointPresenters.get(updatedPoint.id).init(updatedPoint, this.#offers, this.#destinations); + // Здесь будем вызывать обновление модели. + // actionType - действие пользователя, нужно чтобы понять, какой метод модели вызвать + // updateType - тип изменений, нужно чтобы понять, что после нужно обновить + // update - обновленные данные + #handleViewAction = (actionType, updateType, update) => { + switch (actionType) { + case UserAction.UPDATE_POINT: + this.#pointModel.updatePoint(updateType, update); + break; + case UserAction.ADD_POINT: + this.#pointModel.addPoint(updateType, update); + break; + case UserAction.DELETE_POINT: + this.#pointModel.deletePoint(updateType, update); + break; + } + }; + + // В зависимости от типа изменений решаем, что делать: + #handleModelEvent = (updateType, updatedPoint) => { + switch (updateType) { + // - обновить часть списка (например, когда поменялись данные поинта при редактировании) + case UpdateType.PATCH: + this.#pointPresenters.get(updatedPoint.id).init(updatedPoint, this.offers, this.destinations); + break; + // - обновить список + case UpdateType.MINOR: + this.#clearPointsList(); + this.#renderPointsList(); + break; + // - обновить всю доску (с очисткой фильтров и сортировки) + case UpdateType.MAJOR: + this.#clearPointsList({ resetFilters: true, resetSorting: true }); + this.#renderPointsList(); + break; + } }; #handleModeChange = () => { @@ -111,22 +161,40 @@ export default class MainPresenter { }; #renderPointsList() { - for (const point of this.#points) { + remove(this.#noPoints); + + if (this.points.length === 0) { + this.#renderNoPoints(); + return; + } + + for (const point of this.points) { this.#renderPoint(point); } } - #clearPointsList() { + #clearPointsList({ resetFilters = false, resetSorting = false } = {}) { this.#pointPresenters.forEach((presenter) => presenter.destroy()); this.#pointPresenters.clear(); + + if (resetFilters) { + this.#currentFilterType = FilterType.EVERYTHING; + } + + if (resetSorting) { + this.#currentSortType = SortType.DAY; + } } #renderNoPoints() { + this.#noPoints = new NoPointsView({ + filter: this.#currentFilterType, + }); + render(this.#noPoints, this.#pointsListComponent.element); } #clearPoint = (point) => { - const targetPresenter = this.#pointPresenters.get(point.id); - targetPresenter.destroy(); + this.#handleViewAction(UserAction.DELETE_POINT, UpdateType.MINOR, point); }; } diff --git a/src/presenter/new-point-presenter.js b/src/presenter/new-point-presenter.js new file mode 100644 index 0000000..a3ad4f2 --- /dev/null +++ b/src/presenter/new-point-presenter.js @@ -0,0 +1,76 @@ +import { BLANK_POINT, UpdateType, UserAction } from '../const'; +import { render, remove, RenderPosition } from '../framework/render'; +import EditPointView from '../view/edit-point-view'; +import {nanoid} from 'nanoid'; + +export default class NewPointPresenter { + #pointsListContainer = null; + #editPointComponent = null; + #handlePointAdd = null; + #handleDestroy = null; + #allOffers = []; + #allDestinations = []; + + constructor({ pointsListContainer, onPointAdd, onDestroy }) { + this.#pointsListContainer = pointsListContainer; + this.#handlePointAdd = onPointAdd; + this.#handleDestroy = onDestroy; + } + + init(offers, destinations) { + this.#allOffers = offers; + this.#allDestinations = destinations; + + if (this.#editPointComponent !== null) { + return; + } + + this.#editPointComponent = new EditPointView({ + point: BLANK_POINT, + offers: this.#allOffers, + destinations: this.#allDestinations, + onFormSaveClick: this.#handleFormSaveClick, + onFormDeleteClick: this.#handleFormDeleteClick, + isNewPoint: true + }); + + render(this.#editPointComponent, this.#pointsListContainer, RenderPosition.AFTERBEGIN); + + document.addEventListener('keydown', this.#escKeyDownHandler); + } + + destroy() { + if (this.#editPointComponent === null) { + return; + } + + this.#handleDestroy(); + + remove(this.#editPointComponent); + + this.#editPointComponent = null; + + document.removeEventListener('keydown', this.#escKeyDownHandler); + } + + #handleFormSaveClick = (point) => { + this.#handlePointAdd( + UserAction.ADD_POINT, + UpdateType.MINOR, + {id: nanoid(), ...point} , + ); + this.destroy(); + }; + + #handleFormDeleteClick = () => { + this.destroy(); + }; + + #escKeyDownHandler = (evt) => { + if (evt.key === 'Escape') { + evt.preventDefault(); + this.destroy(); + document.removeEventListener('keydown', this.#escKeyDownHandler); + } + }; +} diff --git a/src/presenter/point-presenter.js b/src/presenter/point-presenter.js index f8a6f70..3037bef 100644 --- a/src/presenter/point-presenter.js +++ b/src/presenter/point-presenter.js @@ -1,6 +1,7 @@ import { render, replace, remove } from '../framework/render'; import PointItemView from '../view/point-item-view'; import EditPointView from '../view/edit-point-view'; +import { UpdateType, UserAction} from '../const'; const Mode = { DEFAULT: 'DEFAULT', @@ -9,40 +10,42 @@ const Mode = { export default class PointPresenter { #point = null; - #destinations = null; - #offers = null; - #pointsListComponent = null; + #allDestinations = []; + #allOffers = []; #pointComponent = null; #editPointComponent = null; - #handlePointsChange = null; + #pointsListComponent = null; + #handleModelEvent = null; #handleModeChange = null; #clearPoint = null; #resetPointView = null; + #handleModelUpdate = null; #mode = Mode.DEFAULT; - constructor({ pointsListComponent, onPointsChange, onModeChange, onPointClear, onEditPointView }) { + constructor({ pointsListComponent, onPointsChange, onModeChange, onPointClear, onEditPointView, onModelUpdate }) { this.#pointsListComponent = pointsListComponent; - this.#handlePointsChange = onPointsChange; + this.#handleModelEvent = onPointsChange; this.#handleModeChange = onModeChange; this.#clearPoint = onPointClear; this.#resetPointView = onEditPointView; + this.#handleModelUpdate = onModelUpdate; } init(point, offers, destinations) { this.#point = point; - this.#offers = offers; - this.#destinations = destinations; + this.#allOffers = offers; + this.#allDestinations = destinations; const prevPointComponent = this.#pointComponent; const prevEditPointComponent = this.#editPointComponent; this.#pointComponent = new PointItemView({ point: this.#point, - offers: this.#offers, - destinations: this.#destinations, + offers: this.#allOffers, + destinations: this.#allDestinations, onEditClick: () => { this.#replacePointToForm(); }, @@ -50,12 +53,13 @@ export default class PointPresenter { }); this.#editPointComponent = new EditPointView({ - point, - offers, - destinations, + point: this.#point, + offers: this.#allOffers, + destinations: this.#allDestinations, onEditClick: this.#handleFormEditClick, onFormSaveClick: this.#handleFormSaveClick, onFormDeleteClick: this.#handleFormDeleteClick, + isNewPoint: false, }); if (prevPointComponent === null || prevEditPointComponent === null) { @@ -102,17 +106,19 @@ export default class PointPresenter { // обработчики событий #handleFavoriteClick = () => { - this.#handlePointsChange({ ...this.#point, isFavorite: !this.#point.isFavorite }); + this.#handleModelEvent(UpdateType.PATCH, { ...this.#point, isFavorite: !this.#point.isFavorite }); }; #handleFormSaveClick = (point) => { - this.#handlePointsChange(point); + this.#handleModelUpdate(UserAction.UPDATE_POINT, UpdateType.PATCH, point); + this.#replaceFormToPoint(); document.removeEventListener('keydown', this.#escKeyDownHandler); }; #handleFormDeleteClick = (point) => { this.#clearPoint(point); + this.#replaceFormToPoint(); document.removeEventListener('keydown', this.#escKeyDownHandler); }; diff --git a/src/utils/common-utils.js b/src/utils/common-utils.js index adec794..ddcb55e 100644 --- a/src/utils/common-utils.js +++ b/src/utils/common-utils.js @@ -27,15 +27,6 @@ const getRandomIntegerArray = (min, max) => { return randomIntegerArray; }; -const createIdGenerator = () => { - let numberId = 0; - - return () => { - numberId += 1; - return numberId; - }; -}; - function updatePoint(points, update) { return points.map((point) => point.id === update.id ? update : point); } @@ -46,4 +37,4 @@ const getRandomDescriptionPoint = (text) => { return randomDescriptionText; }; -export { capitalize, getRandomArrayElement, getRandomInteger, createIdGenerator, getRandomIntegerArray, updatePoint, getRandomDescriptionPoint }; +export { capitalize, getRandomArrayElement, getRandomInteger, getRandomIntegerArray, updatePoint, getRandomDescriptionPoint }; diff --git a/src/utils/filter-utils.js b/src/utils/filter-utils.js index 3d58a1d..c007873 100644 --- a/src/utils/filter-utils.js +++ b/src/utils/filter-utils.js @@ -34,6 +34,6 @@ const filter = { [FilterType.FUTURE]: (points) => points.filter((point) => isPointFuture(point)), }; -export { filter }; +export { filter, isPointPast , isPointFuture, isPointPresent}; diff --git a/src/utils/point-utils.js b/src/utils/point-utils.js index 23b9d21..bbb783a 100644 --- a/src/utils/point-utils.js +++ b/src/utils/point-utils.js @@ -53,6 +53,7 @@ function getWeightForTime(a, b) { } } +// const getOffersByType = (type, offers) => offers.find((offer) => offer.type === type).offers; const getOffersByType = (type, offers) => offers.find((offer) => offer.type === type).offers; const getDestinationId = (destinationName, destinations) => destinations.find((destinationElement) => destinationElement.name === destinationName).id; diff --git a/src/view/add-new-point-button-view.js b/src/view/add-new-point-button-view.js new file mode 100644 index 0000000..01078eb --- /dev/null +++ b/src/view/add-new-point-button-view.js @@ -0,0 +1,27 @@ +import AbstractView from '../framework/view/abstract-view'; + +function createAddNewPointButtonTemplate() { + return ''; +} + +export default class AddNewPointButtonView extends AbstractView { + #handleClick = null; + + constructor({ onClick }) { + super(); + this.#handleClick = onClick; + + this.element.addEventListener('click', this.#clickHandler); + } + + get template() { + return createAddNewPointButtonTemplate(); + } + + #clickHandler = (evt) => { + evt.preventDefault(); + this.#handleClick(); + }; +} + + diff --git a/src/view/create-point-view.js b/src/view/create-point-view.js deleted file mode 100644 index a17de79..0000000 --- a/src/view/create-point-view.js +++ /dev/null @@ -1,150 +0,0 @@ -import { capitalize } from '../util'; -import { TYPES, CITIES } from '../const'; -import AbstractView from '../framework/view/abstract-view'; - -const DEFAULT_TYPE = 'Flight'; -const DEFAULT_DESTINATION = 'Geneva'; -const DEFAULT_START_TIME = '19/03/19 00:00'; -const DEFAULT_END_TIME = '19/03/19 00:00'; - -const createPointTypeItem = (pointType, pointTypeChecked) => ` -
- - -
`; - -const createDestinationsList = (destination) => - ``; - -function createCreatePointTemplate() { - - const getTypeCheckedAttribute = (pointType) => { - if (pointType === 'flight') { - return 'checked'; - } else { - return ''; - } - }; - - return `
  • -
    -
    -
    - - - -
    -
    - Event type - ${TYPES.map((pointType) => createPointTypeItem(pointType, getTypeCheckedAttribute(pointType))).join('')} -
    -
    -
    - -
    - - - - ${CITIES.map((city) => createDestinationsList(city)).join('')} - -
    - -
    - - - — - - -
    - -
    - - -
    - - - -
    -
    -
    -

    Offers

    - -
    -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    -
    -
    - -
    -

    Destination

    -

    Geneva is a city in Switzerland that lies at the southern tip of expansive Lac Léman (Lake Geneva). Surrounded by the Alps and Jura mountains, the city has views of dramatic Mont Blanc.

    - -
    -
    - Event photo - Event photo - Event photo - Event photo - Event photo -
    -
    -
    -
    -
    -
  • `; -} -export default class CreatePointView extends AbstractView { - get template() { - return createCreatePointTemplate(); - } -} diff --git a/src/view/edit-point-view.js b/src/view/edit-point-view.js index 71dd8d3..853e5dc 100644 --- a/src/view/edit-point-view.js +++ b/src/view/edit-point-view.js @@ -1,8 +1,8 @@ import { capitalize } from '../utils/common-utils'; import { getOffersByType, getDestinationId, humanizePointDate } from '../utils/point-utils'; import { DATE_WITH_TIME_FORMAT, TYPES } from '../const'; -import { CITIES } from '../mock/const-mock'; import AbstractStatefulView from '../framework/view/abstract-stateful-view'; +import he from 'he'; import flatpickr from 'flatpickr'; import 'flatpickr/dist/flatpickr.min.css'; @@ -11,6 +11,8 @@ const createOfferClass = (offerTitle) => { return splittedOfferTitles[splittedOfferTitles.length - 1]; }; +const getDestinationPicture = (picture) => `${picture.description}`; + const createDestinationsList = (destination) => ``; @@ -20,8 +22,8 @@ const createPointTypeItem = (pointType, pointTypeChecked) => ` `; -const getPointOfferItem = (pointOffer, pointOfferChecked) => `
    - +const getPointOfferItem = (pointOffer, pointOfferChecked, offerId) => `
    +
    - - - -
    -
    -

    Offers

    - -
    - ${offersArray.map((pointOffer) => getPointOfferItem(pointOffer, getOfferCheckedAttribute(pointOffer.id))).join('')} -
    -
    - -
    -

    Destination

    -

    ${description}

    -
    + ${getOffersInfo(allOffers, pointOffers) ?? ''} + ${getDestinationInfo(description, pictures) ?? ''}
    `; } export default class EditPointView extends AbstractStatefulView { - #offers = null; - #destinations = null; + #allOffers = []; + #allDestinations = []; #handleEditClick = null; #handleFormSave = null; #handleFormDelete = null; #dateFromPicker = null; #dateToPicker = null; + #isNewPoint = null; _state = {}; - constructor({ point, offers, destinations, onEditClick, onFormSaveClick, onFormDeleteClick }) { + constructor({ point, offers, destinations, onEditClick, onFormSaveClick, onFormDeleteClick, isNewPoint }) { super(); this._setState(EditPointView.parsePointToState(point)); - this.#offers = offers; - this.#destinations = destinations; + this.#allOffers = offers; + this.#allDestinations = destinations; this.#handleEditClick = onEditClick; this.#handleFormSave = onFormSaveClick; this.#handleFormDelete = onFormDeleteClick; + this.#isNewPoint = isNewPoint; this._restoreHandlers(); } get template() { - return createEditPointTemplate(this._state, this.#offers, this.#destinations); + return createEditPointTemplate(this._state, this.#allOffers, this.#allDestinations, this.#isNewPoint); } removeElement() { @@ -167,11 +199,19 @@ export default class EditPointView extends AbstractStatefulView { } _restoreHandlers() { - this.element.querySelector('.event__rollup-btn').addEventListener('click', this.#editClickHandler); + if (!this.#isNewPoint) { + this.element.querySelector('.event__rollup-btn').addEventListener('click', this.#editClickHandler); + } + this.element.querySelector('form').addEventListener('submit', this.#formSaveHandler); this.element.querySelector('form').addEventListener('reset', this.#formDeleteHandler); this.element.querySelector('.event__type-group').addEventListener('change', this.#formTypeChangeHandler); - this.element.querySelector('.event__input--price').addEventListener('input', this.#formPriceInputHandler); + + if (this.element.querySelector('.event__available-offers')) { + this.element.querySelector('.event__available-offers').addEventListener('change', this.#offersChooseHandler); + } + + this.element.querySelector('.event__input--price').addEventListener('change', this.#formPriceInputHandler); this.element.querySelector('.event__input--destination').addEventListener('change', this.#formDestinationChangeHandler); this.#setDateFromPicker(); @@ -199,15 +239,23 @@ export default class EditPointView extends AbstractStatefulView { #formDeleteHandler = (evt) => { evt.preventDefault(); - this.#handleFormDelete(EditPointView.parseStateToPoint(this._state)); + + if (this.#isNewPoint) { + this.#handleFormDelete(); + } else { + this.#handleFormDelete(EditPointView.parseStateToPoint(this._state)); + } }; #formPriceInputHandler = (evt) => { evt.preventDefault(); - - this.updateElement(({ - basePrice: evt.target.value, - })); + if (Number.isFinite(Number(evt.target.value))) { + this.updateElement(({ + basePrice: evt.target.value, + })); + return; + } + evt.target.value = ''; }; #formTypeChangeHandler = (evt) => { @@ -216,16 +264,42 @@ export default class EditPointView extends AbstractStatefulView { this.updateElement(({ type: evt.target.value, - offers: getOffersByType(evt.target.value, this.#offers), + allOffers: getOffersByType(evt.target.value, this.#allOffers), })); }; + #offersChooseHandler = (evt) => { + evt.preventDefault(); + + if (evt.target.tagName !== 'INPUT') { + return; + } + + let updatedOffers = []; + const newOffer = Number(Object.values(evt.target.dataset)); + const isNewOfferInList = this._state.offers.find((offer) => offer === newOffer) > 0; + + if (isNewOfferInList) { + updatedOffers = this._state.offers.filter((offer) => offer !== newOffer); + } else { + updatedOffers = this._state.offers.concat(newOffer); + } + + this.updateElement({ + offers: updatedOffers, + }); + }; + #formDestinationChangeHandler = (evt) => { evt.preventDefault(); - this.updateElement(({ - destination: getDestinationId(evt.target.value, this.#destinations), - })); + this.#allDestinations.forEach((destination) => { + if (evt.target.value === destination.name) { + this.updateElement(({ + destination: getDestinationId(evt.target.value, this.#allDestinations), + })); + } + }); }; #dateFromChangeHandler = ([userDate]) => { @@ -247,6 +321,7 @@ export default class EditPointView extends AbstractStatefulView { enableTime: true, dateFormat: 'd/m/y H:i', 'time_24hr': true, + maxDate: humanizePointDate(this._state.dateTo, DATE_WITH_TIME_FORMAT), defaultDate: humanizePointDate(this._state.dateFrom, DATE_WITH_TIME_FORMAT), onChange: this.#dateFromChangeHandler, } diff --git a/src/view/filters-view.js b/src/view/filters-view.js index bee3846..68d881f 100644 --- a/src/view/filters-view.js +++ b/src/view/filters-view.js @@ -8,9 +8,8 @@ const getFiltersItem = (type, count) => `
    type="radio" name="trip-filter" value="${type}" - ${type === 'everything' ? 'checked' : ''} - ${count === 0 ? 'disabled' : 'checked'} > - + ${type === 'everything' ? 'checked' : ''}> +
    `; function createFiltersTemplate(filters) { @@ -22,13 +21,26 @@ function createFiltersTemplate(filters) { export default class FiltersView extends AbstractView { #filters = []; + #handleFiltersChange = null; - constructor({ filters }) { + constructor({ filters, onFiltersChange }) { super(); this.#filters = filters; + this.#handleFiltersChange = onFiltersChange; + + this.element.addEventListener('click', this.#filtersChangeHandler); } get template() { return createFiltersTemplate(this.#filters); } + + #filtersChangeHandler = (evt) => { + if (evt.target.tagName !== 'LABEL') { + return; + } + + evt.preventDefault(); + this.#handleFiltersChange(evt.target.dataset.filterType); + }; } diff --git a/src/view/no-points-view.js b/src/view/no-points-view.js index 9b42832..7ab748f 100644 --- a/src/view/no-points-view.js +++ b/src/view/no-points-view.js @@ -1,11 +1,21 @@ import AbstractView from '../framework/view/abstract-view'; +import { ListEmptyText } from '../const'; -function createNoPointsTemplate() { - return '

    Click New Event to create your first point

    '; +function createNoPointsTemplate(filterType) { + const listEmptyText = ListEmptyText[filterType]; + + return `

    ${listEmptyText}

    `; } export default class NoPointsView extends AbstractView { + #filter = null; + + constructor({ filter }) { + super(); + this.#filter = filter; + } + get template() { - return createNoPointsTemplate(); + return createNoPointsTemplate(this.#filter); } } diff --git a/src/view/point-item-view.js b/src/view/point-item-view.js index 059568d..3161e4f 100644 --- a/src/view/point-item-view.js +++ b/src/view/point-item-view.js @@ -16,8 +16,11 @@ const getOffers = (type, offersList) => { function createPointItemTemplate(point, offers, destinations) { const { type, destination, dateFrom, dateTo, basePrice, isFavorite } = point; + let modifiedDestination = ''; - const modifiedDestination = destinations.find((destinationElement) => destinationElement.id === destination).name; + if (destination !== null) { + modifiedDestination = destinations.find((destinationElement) => destinationElement.id === destination).name; + } const favoriteClassName = isFavorite ? 'event__favorite-btn event__favorite-btn--active' : 'event__favorite-btn'; @@ -58,16 +61,16 @@ function createPointItemTemplate(point, offers, destinations) { export default class PointItemView extends AbstractView { #point = null; - #offers = null; - #destinations = null; + #allOffers = []; + #allDestinations = []; #handleEditClick = null; #handleFavoriteClick = null; constructor({ point, offers, destinations, onEditClick, onFavoriteClick }) { super(); this.#point = point; - this.#offers = offers; - this.#destinations = destinations; + this.#allOffers = offers; + this.#allDestinations = destinations; this.#handleEditClick = onEditClick; this.#handleFavoriteClick = onFavoriteClick; @@ -76,7 +79,7 @@ export default class PointItemView extends AbstractView { } get template() { - return createPointItemTemplate(this.#point, this.#offers, this.#destinations); + return createPointItemTemplate(this.#point, this.#allOffers, this.#allDestinations); } #editClickHandler = (evt) => { diff --git a/src/view/sorting-view.js b/src/view/sorting-view.js index 1d6e66f..9dc9ef7 100644 --- a/src/view/sorting-view.js +++ b/src/view/sorting-view.js @@ -4,9 +4,6 @@ import { SortType } from '../const'; const getSortingItems = (sorting, currentSortType) => - // console.log(`sorting ${ sorting}`); - // console.log(`current ${ currentSortType}`); - // console.log(sorting === currentSortType ? 'checked' : ''); `