From de27a20a12078f8d8bee72f8cdeddbd1d626ec33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=A2=D0=BE=D0=BF=D0=BE=D1=80=D0=BA=D0=BE=D0=B2=D0=B0?= Date: Tue, 28 May 2024 08:46:40 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/const.js | 68 +--------- src/main.js | 13 +- src/model/destinations-model.js | 2 +- src/model/filter-model.js | 2 +- src/model/points-model.js | 2 +- src/presenter/filter-presenter.js | 10 +- src/presenter/new-point-button-presenter.js | 6 +- src/presenter/new-point-presenter.js | 14 +- src/presenter/point-presenter.js | 32 +++-- src/presenter/trip-info-presenter.js | 59 ++++++++ src/presenter/trip-presenter.js | 29 ++-- src/service/points-api-service.js | 2 +- src/utils.js | 143 -------------------- src/utils/common.js | 5 + src/utils/filter.js | 2 +- src/utils/point.js | 106 ++++++++++++++- src/utils/sort.js | 2 +- src/utils/trip-info.js | 40 ++++++ src/view/edit-point-view.js | 129 ++++++++++-------- src/view/empty-list-view.js | 16 --- src/view/filter-view.js | 20 ++- src/view/loading-view.js | 33 ++++- src/view/message-view.js | 14 +- src/view/new-point-button-view.js | 14 +- src/view/point-list-view.js | 8 +- src/view/point-view.js | 28 ++-- src/view/sort-view.js | 18 +-- src/view/trip-info-view.js | 38 ++++-- 28 files changed, 452 insertions(+), 403 deletions(-) create mode 100644 src/presenter/trip-info-presenter.js delete mode 100644 src/utils.js create mode 100644 src/utils/common.js create mode 100644 src/utils/trip-info.js delete mode 100644 src/view/empty-list-view.js diff --git a/src/const.js b/src/const.js index 3f8c252..8206723 100644 --- a/src/const.js +++ b/src/const.js @@ -1,54 +1,3 @@ -const OFFER_COUNT = 5; - -const POINT_COUNT = 5; - -const CITIES = [ - 'Chamonix', - 'Geneva', - 'Amsterdam', - 'Helsinki', - 'Oslo', - 'Kopenhagen', - 'Den Haag', - 'Rotterdam', - 'Saint Petersburg', - 'Moscow', - 'Sochi', - 'Tokyo', -]; - -const DESTINATION_COUNT = CITIES.length; - -const OFFERS = [ - 'Order Uber', - 'Add luggage', - 'Switch to comfort', - 'Rent a car', - 'Add breakfast', - 'Book tickets', - 'Lunch in city', - 'Upgrade to a business class' -]; - -const DESCRIPTION = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras aliquet varius magna, non porta ligula feugiat eget. Fusce tristique felis at fermentum pharetra. Aliquam id orci ut lectus varius viverra.'; - -const Price = { - MIN: 1, - MAX: 1000 -}; - -const TYPES = [ - 'taxi', - 'bus', - 'train', - 'ship', - 'drive', - 'flight', - 'check-in', - 'sightseeing', - 'restaurant' -]; - const DEFAULT_TYPE = 'flight'; const POINT_EMPTY = { @@ -66,7 +15,7 @@ const FilterType = { FUTURE: 'future', PRESENT: 'present', PAST: 'past' -} +}; const Mode = { DEFAULT: 'default', @@ -119,15 +68,9 @@ const TimeLimit = { UPPER_LIMIT: 1000, }; +const DESTINATION_ITEMS_LENGTH = 3; + export { - OFFER_COUNT, - DESTINATION_COUNT, - POINT_COUNT, - CITIES, - OFFERS, - DESCRIPTION, - Price, - TYPES, DEFAULT_TYPE, POINT_EMPTY, FilterType, @@ -138,5 +81,6 @@ export { UpdateType, EditType, Method, - TimeLimit -} + TimeLimit, + DESTINATION_ITEMS_LENGTH +}; diff --git a/src/main.js b/src/main.js index 4305374..5557004 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,4 @@ -import TripInfoView from './view/trip-info-view.js'; +import TripInfoPresenter from './presenter/trip-info-presenter.js'; import TripPresenter from './presenter/trip-presenter.js'; import FilterPresenter from './presenter/filter-presenter.js'; import NewPointButtonPresenter from './presenter/new-point-button-presenter.js'; @@ -7,7 +7,6 @@ import DestinationsModel from './model/destinations-model.js'; import OffersModel from './model/offers-model.js'; import PointsModel from './model/points-model.js'; import FilterModel from './model/filter-model.js'; -import { render, RenderPosition } from './framework/render.js'; const AUTHORIZATION = 'Basic YWxpbmE6b3N0aW4xNzg='; const END_POINT = 'https://21.objects.htmlacademy.pro/big-trip'; @@ -29,6 +28,13 @@ const pointsModel = new PointsModel({ }); const filterModel = new FilterModel(); +const tripInfoPresenter = new TripInfoPresenter({ + container: tripInfoElement, + pointsModel, + destinationsModel, + offersModel +}); + const newPointButtonPresenter = new NewPointButtonPresenter({ container: tripInfoElement }); @@ -48,7 +54,7 @@ const tripPresenter = new TripPresenter({ newPointButtonPresenter: newPointButtonPresenter }); -render(new TripInfoView(), tripInfoElement, RenderPosition.AFTERBEGIN); +tripInfoPresenter.init(); newPointButtonPresenter.init({ onButtonClick: tripPresenter.newPointButtonClickHandler @@ -56,4 +62,5 @@ newPointButtonPresenter.init({ filterPresenter.init(); tripPresenter.init(); + pointsModel.init(); diff --git a/src/model/destinations-model.js b/src/model/destinations-model.js index c58dda8..a1aebd3 100644 --- a/src/model/destinations-model.js +++ b/src/model/destinations-model.js @@ -11,7 +11,7 @@ export default class DestinationsModel extends Observable { async init() { this.#destinations = await this.#service.getDestinations(); - return this.#destinations + return this.#destinations; } get() { diff --git a/src/model/filter-model.js b/src/model/filter-model.js index 3152aca..0975279 100644 --- a/src/model/filter-model.js +++ b/src/model/filter-model.js @@ -10,6 +10,6 @@ export default class FilterModel extends Observable { set(updateType, update) { this.#filter = update; - this._notify(updateType, update) + this._notify(updateType, update); } } diff --git a/src/model/points-model.js b/src/model/points-model.js index 3df5efc..c0b449a 100644 --- a/src/model/points-model.js +++ b/src/model/points-model.js @@ -1,6 +1,6 @@ import Observable from '../framework/observable.js'; -import { updateItem } from '../utils.js'; +import { updateItem } from '../utils/common.js'; import { UpdateType } from '../const.js'; import { adaptToClient, adaptToServer } from '../utils/point.js'; diff --git a/src/presenter/filter-presenter.js b/src/presenter/filter-presenter.js index 2358338..7e0e2a8 100644 --- a/src/presenter/filter-presenter.js +++ b/src/presenter/filter-presenter.js @@ -1,7 +1,7 @@ -import FilterView from "../view/filter-view.js"; -import { render, replace, remove } from "../framework/render.js"; -import { UpdateType } from "../const.js"; -import { filter } from "../utils/filter.js"; +import FilterView from '../view/filter-view.js'; +import { render, replace, remove } from '../framework/render.js'; +import { UpdateType } from '../const.js'; +import { filter } from '../utils/filter.js'; export default class FilterPresenter { #container = null; @@ -44,7 +44,7 @@ export default class FilterPresenter { onItemChange: this.#filterTypeChangeHandler }); - if (prevFilterComponent === null) { + if (!prevFilterComponent) { render(this.#filterComponent, this.#container); return; } diff --git a/src/presenter/new-point-button-presenter.js b/src/presenter/new-point-button-presenter.js index de1a90d..0c857d9 100644 --- a/src/presenter/new-point-button-presenter.js +++ b/src/presenter/new-point-button-presenter.js @@ -26,6 +26,10 @@ export default class NewPointButtonPresenter { } #buttonClickHandler = () => { - this.#handleButtonClick(); + const disabledResetButton = document.querySelector('.event__reset-btn[disabled]'); + + if (!disabledResetButton) { + this.#handleButtonClick(); + } }; } diff --git a/src/presenter/new-point-presenter.js b/src/presenter/new-point-presenter.js index 96ff1ef..4813502 100644 --- a/src/presenter/new-point-presenter.js +++ b/src/presenter/new-point-presenter.js @@ -1,7 +1,7 @@ -import EditPointView from "../view/edit-point-view.js"; +import EditPointView from '../view/edit-point-view.js'; import { remove, render, RenderPosition } from '../framework/render.js'; -import { UserAction, UpdateType, EditType } from "../const.js"; +import { UserAction, UpdateType, EditType } from '../const.js'; export default class NewPointPresenter { #container = null; @@ -20,10 +20,10 @@ export default class NewPointPresenter { this.#offersModel = offersModel; this.#handleDataChange = onDataChange; this.#handleDestroy = onDestroy; - }; + } init() { - if (this.#pointNewComponent !== null) { + if (this.#pointNewComponent) { return; } @@ -40,7 +40,7 @@ export default class NewPointPresenter { } destroy = ({ isCanceled = true } = {}) => { - if (this.#pointNewComponent === null) { + if (!this.#pointNewComponent) { return; } @@ -56,7 +56,7 @@ export default class NewPointPresenter { isDisabled: true, isSaving: true, }); - } + }; setAborting = () => { const resetFormState = () => { @@ -68,7 +68,7 @@ export default class NewPointPresenter { }; this.#pointNewComponent.shake(resetFormState); - } + }; #formSubmitHandler = (point) => { this.#handleDataChange( diff --git a/src/presenter/point-presenter.js b/src/presenter/point-presenter.js index 061ea50..c876f8a 100644 --- a/src/presenter/point-presenter.js +++ b/src/presenter/point-presenter.js @@ -2,7 +2,7 @@ import PointView from '../view/point-view.js'; import EditPointView from '../view/edit-point-view.js'; import { remove, render, replace } from '../framework/render.js'; import { Mode, UserAction, UpdateType } from '../const.js'; -import { isBigDifference } from '../utils.js'; +import { isBigDifference } from '../utils/point.js'; export default class PointPresenter { #container = null; @@ -47,7 +47,7 @@ export default class PointPresenter { onSubmitClick: this.#formSubmitHandler, onResetClick: this.#resetButtonClickHandler, onDeleteClick: this.#deleteButtonClickHandler - }) + }); if (!prevPointComponent || !prevEditPointComponent) { render(this.#pointComponent, this.#container); @@ -114,10 +114,12 @@ export default class PointPresenter { }; #replacePointToForm = () => { - replace(this.#editPointComponent, this.#pointComponent); - document.addEventListener('keydown', this.#escKeyDownHandler); - this.#handleModeChange(); - this.#mode = Mode.EDITING; + if (!this.#editPointComponent._state.isSaving) { + replace(this.#editPointComponent, this.#pointComponent); + document.addEventListener('keydown', this.#escKeyDownHandler); + this.#handleModeChange(); + this.#mode = Mode.EDITING; + } }; #replaceFormToPoint = () => { @@ -135,7 +137,7 @@ export default class PointPresenter { }; #escKeyDownHandler = (evt) => { - if (evt.key === 'Escape') { + if (evt.key === 'Escape' && !this.#editPointComponent._state.isDisabled) { evt.preventDefault(); this.#editPointComponent.reset(this.#point); this.#replaceFormToPoint(); @@ -157,20 +159,24 @@ export default class PointPresenter { ); }; - #formSubmitHandler = (updatedPoint) => { + #formSubmitHandler = async (updatedPoint) => { const isMinor = isBigDifference(updatedPoint, this.#point); - this.#handleDataChange( + await this.#handleDataChange( UserAction.UPDATE_POINT, isMinor ? UpdateType.MINOR : UpdateType.PATCH, updatedPoint ); - this.#replaceFormToPoint(); + if (!this.#editPointComponent._state.isDisabled) { + this.#replaceFormToPoint(); + } }; #resetButtonClickHandler = () => { - this.#editPointComponent.reset(this.#point); - this.#replaceFormToPoint(); - } + if (!this.#editPointComponent._state.isDisabled || this.#editPointComponent._state.isSavingCompleted) { + this.#editPointComponent.reset(this.#point); + this.#replaceFormToPoint(); + } + }; } diff --git a/src/presenter/trip-info-presenter.js b/src/presenter/trip-info-presenter.js new file mode 100644 index 0000000..2d448eb --- /dev/null +++ b/src/presenter/trip-info-presenter.js @@ -0,0 +1,59 @@ +import TripInfoView from '../view/trip-info-view.js'; +import { render, replace, remove, RenderPosition } from '../framework/render.js'; + +export default class TripInfoPresenter { + #container = null; + #tripInfoComponent = null; + #pointsModel = null; + #destinationsModel = null; + #offersModel = null; + + constructor({ container, pointsModel, destinationsModel, offersModel }) { + this.#container = container; + this.#pointsModel = pointsModel; + this.#destinationsModel = destinationsModel; + this.#offersModel = offersModel; + } + + get points() { + return this.#pointsModel.get(); + } + + get destinations() { + return this.#destinationsModel.get(); + } + + get offers() { + return this.#offersModel.get(); + } + + init() { + this.#renderTripInfo(); + this.#pointsModel.addObserver(this.#modelEventHandler); + } + + #renderTripInfo = () => { + const prevTripInfoComponent = this.#tripInfoComponent; + const destinations = this.destinations; + const offers = this.offers; + const points = this.points; + + this.#tripInfoComponent = new TripInfoView({ + destinations, + offers, + points + }); + + if (!prevTripInfoComponent) { + render(this.#tripInfoComponent, this.#container, RenderPosition.AFTERBEGIN); + return; + } + + replace(this.#tripInfoComponent, prevTripInfoComponent); + remove(prevTripInfoComponent); + }; + + #modelEventHandler = () => { + this.#renderTripInfo(); + }; +} diff --git a/src/presenter/trip-presenter.js b/src/presenter/trip-presenter.js index ec60eb8..5c354f2 100644 --- a/src/presenter/trip-presenter.js +++ b/src/presenter/trip-presenter.js @@ -1,5 +1,5 @@ import SortView from '../view/sort-view.js'; -import TripView from '../view/point-list-view.js'; +import PointListView from '../view/point-list-view.js'; import MessageView from '../view/message-view.js'; import LoadingView from '../view/loading-view.js'; import PointPresenter from './point-presenter.js'; @@ -13,10 +13,10 @@ import UiBlocker from '../framework/ui-blocker/ui-blocker.js'; export default class TripPresenter { #tripContainer = null; - #pointListComponent = new TripView(); + #pointListComponent = new PointListView(); #sortComponent = null; #messageComponent = null; - #loadingComponent = new LoadingView(); + #loadingComponent = null; #uiBlocker = new UiBlocker({ lowerLimit: TimeLimit.LOWER_LIMIT, upperLimit: TimeLimit.UPPER_LIMIT @@ -87,7 +87,7 @@ export default class TripPresenter { pointPresenter.init(point); this.#pointPresenters.set(point.id, pointPresenter); - } + }; #renderPoints = () => { this.points.forEach((point) => { @@ -95,7 +95,8 @@ export default class TripPresenter { }); }; - #renderLoading = () => { + #renderLoading = ({ isLoading, isLoadingError }) => { + this.#loadingComponent = new LoadingView({ isLoading, isLoadingError }); render(this.#loadingComponent, this.#tripContainer, RenderPosition.AFTERBEGIN); }; @@ -103,7 +104,7 @@ export default class TripPresenter { this.#pointPresenters.forEach((presenter) => presenter.destroy()); this.#pointPresenters.clear(); this.#newPointPresenter.destroy(); - } + }; #renderSort = () => { const prevSortComponent = this.#sortComponent; @@ -138,11 +139,13 @@ export default class TripPresenter { #renderPointContainer = () => { render(this.#pointListComponent, this.#tripContainer); - } + }; #renderBoard = () => { + const isLoading = this.#isLoading; + const isLoadingError = this.#isLoadingError; if (this.#isLoading) { - this.#renderLoading(); + this.#renderLoading({ isLoading, isLoadingError }); this.#newPointButtonPresenter.disableButton(); return; } @@ -153,6 +156,7 @@ export default class TripPresenter { this.#clearBoard({ resetSortType: true }); remove(this.#sortComponent); this.#sortComponent = null; + this.#renderLoading({ isLoading, isLoadingError }); return; } @@ -176,7 +180,7 @@ export default class TripPresenter { if (resetSortType) { this.#currentSortType = SortType.DAY; } - } + }; #viewActionHandler = async (actionType, updateType, update) => { this.#uiBlocker.block(); @@ -224,6 +228,7 @@ export default class TripPresenter { break; case UpdateType.INIT: if (data.isError) { + this.#isLoading = false; this.#isLoadingError = true; this.#renderBoard(); break; @@ -241,14 +246,14 @@ export default class TripPresenter { this.#newPointPresenter.destroy(); }; - #newPointDestroyHandler = () => { + #newPointDestroyHandler = ({ isCanceled }) => { this.#isCreating = false; this.#newPointButtonPresenter.enableButton(); - if (this.points.length === 0 && isCanceled) { + if (!this.points.length && isCanceled) { this.#clearBoard(); this.#renderBoard(); } - } + }; #sortTypeChangeHandler = (sortType) => { this.#currentSortType = sortType; diff --git a/src/service/points-api-service.js b/src/service/points-api-service.js index 3d0e8c2..42c5f89 100644 --- a/src/service/points-api-service.js +++ b/src/service/points-api-service.js @@ -33,7 +33,7 @@ export default class PointsApiService extends ApiService { async addPoint(point) { const response = await this._load({ - url: `points`, + url: 'points', method: Method.POST, body: JSON.stringify(point), headers: new Headers({ 'Content-Type': 'application/json' }) diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index e674817..0000000 --- a/src/utils.js +++ /dev/null @@ -1,143 +0,0 @@ -import dayjs from 'dayjs'; -import duration from 'dayjs/plugin/duration'; -import relativeTime from 'dayjs/plugin/relativeTime'; - -dayjs.extend(duration); -dayjs.extend(relativeTime); - -const TimePeriods = { - MSEC_IN_SEC: 1000, - SEC_IN_MIN: 60, - MIN_IN_HOUR: 60, - HOUR_IN_DAY: 24 -} - -const MSEC_IN_HOUR = TimePeriods.MIN_IN_HOUR * TimePeriods.SEC_IN_MIN * TimePeriods.MSEC_IN_SEC; -const MSEC_IN_DAY = TimePeriods.HOUR_IN_DAY * MSEC_IN_HOUR; - -const Duration = { - HOUR: 5, - DAY: 5, - MIN: 59 -}; - -let date = dayjs().subtract(getRandomInteger(0, Duration.DAY), 'day').toDate(); - -function getDate({ next }) { - const minsGap = getRandomInteger(0, Duration.MIN); - const hoursGap = getRandomInteger(1, Duration.HOUR); - const daysGap = getRandomInteger(0, Duration.DAY); - - if (next) { - date = dayjs(date) - .add(minsGap, 'minute') - .add(hoursGap, 'hour') - .add(daysGap, 'day') - .toDate(); - } - - return date; -} - -function getRandomInteger(a = 0, b = 1) { - const lower = Math.ceil(Math.min(a, b)); - const upper = Math.floor(Math.max(a, b)); - - return Math.floor(lower + Math.random() * (upper - lower + 1)); -} - -function getRandomValue(items) { - return items[getRandomInteger(0, items.length - 1)]; -} - -function formatStringToDateTime(date) { - return dayjs(date).format('DD/MM/YY HH:mm'); -} - -function formatStringToShortDate(date) { - return dayjs(date).format('MMM DD'); -} - -function formatStringToTime(date) { - return dayjs(date).format('HH:mm'); -} - -function capitalize(string) { - return `${string[0].toUpperCase()}${string.slice(1)}`; -} - -function getPointDuration(dateFrom, dateTo) { - const timeDiff = dayjs(dateTo).diff(dayjs(dateFrom)); - - let pointDuration = 0; - - switch (true) { - case (timeDiff >= MSEC_IN_DAY): - pointDuration = dayjs.duration(timeDiff).format('DD[D] HH[H] mm[M]'); - break; - case (timeDiff >= MSEC_IN_HOUR): - pointDuration = dayjs.duration(timeDiff).format('HH[H] mm[M]'); - break; - case (timeDiff < MSEC_IN_HOUR): - pointDuration = dayjs.duration(timeDiff).format('mm[M]'); - break; - } - - return pointDuration; -} - -function isPointFuture(point) { - return dayjs().isBefore(point.dateFrom); -} - -function isPointPresent(point) { - return (dayjs().isAfter(point.dateFrom) && dayjs().isBefore(point.dateTo)); -} - -function isPointPast(point) { - return dayjs().isAfter(point.dateTo); -} - -function updateItem(items, update) { - return items.map((item) => item.id === update.id ? update : item); -} - -function getPointsDateDifference(pointA, pointB) { - return new Date(pointA.dateFrom) - new Date(pointB.dateFrom); -} - -function getPointsPriceDifference(pointA, pointB) { - return pointB.basePrice - pointA.basePrice; -} - -function getPointsDurationDifference(pointA, pointB) { - const durationA = new Date(pointA.dateTo) - new Date(pointA.dateFrom); - const durationB = new Date(pointB.dateTo) - new Date(pointB.dateFrom); - - return durationB - durationA; -} - -function isBigDifference(pointA, pointB) { - return pointA.dateFrom !== pointB.dateFrom - || pointA.basePrice !== pointB.basePrice - || getPointDuration(pointA.dateFrom, pointA.dateTo) !== getPointDuration(pointB.dateFrom, pointB.dateTo); -} - -export { - getDate, - getRandomInteger, - getRandomValue, - formatStringToDateTime, - formatStringToShortDate, - formatStringToTime, - capitalize, - getPointDuration, - isPointFuture, - isPointPast, - isPointPresent, - updateItem, - getPointsDateDifference, - getPointsPriceDifference, - getPointsDurationDifference, - isBigDifference -} diff --git a/src/utils/common.js b/src/utils/common.js new file mode 100644 index 0000000..d8e5012 --- /dev/null +++ b/src/utils/common.js @@ -0,0 +1,5 @@ +function updateItem(items, update) { + return items.map((item) => item.id === update.id ? update : item); +} + +export { updateItem }; diff --git a/src/utils/filter.js b/src/utils/filter.js index 1db606a..9b8bdb8 100644 --- a/src/utils/filter.js +++ b/src/utils/filter.js @@ -1,5 +1,5 @@ import { FilterType } from '../const.js'; -import { isPointFuture, isPointPresent, isPointPast } from '../utils.js'; +import { isPointFuture, isPointPresent, isPointPast } from '../utils/point.js'; const filter = { [FilterType.EVERYTHING]: (points) => [...points], diff --git a/src/utils/point.js b/src/utils/point.js index 790d928..9958575 100644 --- a/src/utils/point.js +++ b/src/utils/point.js @@ -1,3 +1,93 @@ +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import relativeTime from 'dayjs/plugin/relativeTime'; + +dayjs.extend(duration); +dayjs.extend(relativeTime); + +const TimePeriods = { + MSEC_IN_SEC: 1000, + SEC_IN_MIN: 60, + MIN_IN_HOUR: 60, + HOUR_IN_DAY: 24 +}; + +const MSEC_IN_HOUR = TimePeriods.MIN_IN_HOUR * TimePeriods.SEC_IN_MIN * TimePeriods.MSEC_IN_SEC; +const MSEC_IN_DAY = TimePeriods.HOUR_IN_DAY * MSEC_IN_HOUR; + +function formatStringToDateTime(date) { + return dayjs(date).format('DD/MM/YY HH:mm'); +} + +function formatStringToShortDate(date) { + return dayjs(date).format('MMM DD'); +} + +function formatStringToTime(date) { + return dayjs(date).format('HH:mm'); +} + +function capitalize(string) { + return `${string[0].toUpperCase()}${string.slice(1)}`; +} + +function getPointDuration(dateFrom, dateTo) { + const timeDiff = dayjs(dateTo).diff(dayjs(dateFrom)); + const totalDays = dayjs.duration(timeDiff).days() + (dayjs.duration(timeDiff).years() * 365); + const totalHours = dayjs.duration(timeDiff).hours(); + const totalMinutes = dayjs.duration(timeDiff).minutes(); + + let pointDuration = 0; + + switch (true) { + case (timeDiff >= MSEC_IN_DAY): + pointDuration = dayjs.duration({ days: totalDays, hours: totalHours, minutes: totalMinutes }).format('DD[D] HH[H] mm[M]'); + break; + case (timeDiff >= MSEC_IN_HOUR): + pointDuration = dayjs.duration(timeDiff).format('HH[H] mm[M]'); + break; + case (timeDiff < MSEC_IN_HOUR): + pointDuration = dayjs.duration(timeDiff).format('mm[M]'); + break; + } + + return pointDuration; +} + +function isPointFuture(point) { + return dayjs().isBefore(point.dateFrom); +} + +function isPointPresent(point) { + return (dayjs().isAfter(point.dateFrom) && dayjs().isBefore(point.dateTo)); +} + +function isPointPast(point) { + return dayjs().isAfter(point.dateTo); +} + + +function getPointsDateDifference(pointA, pointB) { + return new Date(pointA.dateFrom) - new Date(pointB.dateFrom); +} + +function getPointsPriceDifference(pointA, pointB) { + return pointB.basePrice - pointA.basePrice; +} + +function getPointsDurationDifference(pointA, pointB) { + const durationA = new Date(pointA.dateTo) - new Date(pointA.dateFrom); + const durationB = new Date(pointB.dateTo) - new Date(pointB.dateFrom); + + return durationB - durationA; +} + +function isBigDifference(pointA, pointB) { + return pointA.dateFrom !== pointB.dateFrom + || pointA.basePrice !== pointB.basePrice + || getPointDuration(pointA.dateFrom, pointA.dateTo) !== getPointDuration(pointB.dateFrom, pointB.dateTo); +} + function adaptToClient(point) { const adaptedPoint = { ...point, @@ -22,7 +112,7 @@ function adaptToServer(point) { ['date_from']: new Date(point.dateFrom).toISOString(), ['date_to']: new Date(point.dateTo).toISOString(), ['is_favorite']: point.isFavorite - } + }; delete adaptedPoint.basePrice; delete adaptedPoint.dateFrom; @@ -33,6 +123,18 @@ function adaptToServer(point) { } export { + formatStringToDateTime, + formatStringToShortDate, + formatStringToTime, + capitalize, + getPointDuration, + isPointFuture, + isPointPast, + isPointPresent, + getPointsDateDifference, + getPointsPriceDifference, + getPointsDurationDifference, + isBigDifference, adaptToClient, adaptToServer -} +}; diff --git a/src/utils/sort.js b/src/utils/sort.js index 1fd114b..eb032f5 100644 --- a/src/utils/sort.js +++ b/src/utils/sort.js @@ -1,5 +1,5 @@ import { SortType } from '../const.js'; -import { getPointsDateDifference, getPointsDurationDifference, getPointsPriceDifference } from '../utils.js'; +import { getPointsDateDifference, getPointsDurationDifference, getPointsPriceDifference } from '../utils/point.js'; const sort = { [SortType.DAY]: (points) => points.sort(getPointsDateDifference), diff --git a/src/utils/trip-info.js b/src/utils/trip-info.js new file mode 100644 index 0000000..33b763e --- /dev/null +++ b/src/utils/trip-info.js @@ -0,0 +1,40 @@ +import dayjs from 'dayjs'; +import { DESTINATION_ITEMS_LENGTH, SortType } from '../const.js'; +import { sort } from './sort.js'; + +function getTripTitle(points = [], destinations = []) { + const destinationNames = sort[SortType.DAY]([...points]) + .map((point) => destinations.find((destination) => destination.id === point.destination).name); + + return destinationNames.length <= DESTINATION_ITEMS_LENGTH + ? destinationNames.join(' — ') + : `${destinationNames.at(0)} — ... — ${destinationNames.at(-1)}`; +} + +function getTripDuration(points = []) { + const sortedPoints = sort[SortType.DAY]([...points]); + + return (sortedPoints.length > 0) + ? `${dayjs(sortedPoints.at(0).dateFrom).format('DD MMM')} — ${dayjs(sortedPoints.at(-1).dateTo).format('DD MMM')}` + : ''; +} + +function getOffersCost(offerIds = [], offers = []) { + return offerIds.reduce( + (result, id) => result + (offers.find((offer) => offer.id === id)?.price ?? 0), + 0 + ); +} + +function getTripCost(points = [], offers = []) { + return points.reduce( + (result, point) => result + point.basePrice + getOffersCost(point.offers, offers.find((offer) => point.type === offer.type)?.offers), + 0 + ); +} + +export { + getTripTitle, + getTripDuration, + getTripCost +}; diff --git a/src/view/edit-point-view.js b/src/view/edit-point-view.js index 2ac095a..6aefd4e 100644 --- a/src/view/edit-point-view.js +++ b/src/view/edit-point-view.js @@ -1,6 +1,6 @@ import AbstractStatefulView from '../framework/view/abstract-stateful-view.js'; -import { POINT_EMPTY, TYPES, CITIES, EditType } from "../const.js"; -import { formatStringToDateTime } from '../utils.js'; +import { POINT_EMPTY, EditType } from '../const.js'; +import { formatStringToDateTime } from '../utils/point.js'; import 'flatpickr/dist/flatpickr.min.css'; import flatpickr from 'flatpickr'; import he from 'he'; @@ -11,63 +11,56 @@ const ButtonLabel = { DELETE_IN_PROGRESS: 'Deleting...', SAVE_DEFAULT: 'Save', SAVE_IN_PROGRESS: 'Saving...' -} +}; const createSaveButtonTemplate = ({ isSaving, isDisabled }) => { const label = isSaving ? ButtonLabel.SAVE_IN_PROGRESS : ButtonLabel.SAVE_DEFAULT; return ``; -} +}; const createResetButtonTemplate = ({ pointType, isDeleting, isDisabled }) => { let label; if (pointType === EditType.CREATING) { label = ButtonLabel.CANCEL_DEFAULT; + return ``; } else { label = isDeleting ? ButtonLabel.DELETE_IN_PROGRESS : ButtonLabel.DELETE_DEFAULT; + return ``; } - return ``; -} +}; -const createRollupButtonTemplate = (isDisabled) => { - return `` -} +const createRollupButtonTemplate = (isDisabled) => ``; -const createPointEditControlsTemplate = ({ pointType, isSaving, isDeleting, isDisabled }) => { - return ` +const createPointEditControlsTemplate = ({ pointType, isSaving, isDeleting, isDisabled }) => ` ${createSaveButtonTemplate({ isSaving, isDisabled })} ${createResetButtonTemplate({ pointType, isDeleting, isDisabled })} ${(pointType === EditType.EDITING) ? createRollupButtonTemplate(isDisabled) : ''} `; -} -const createPointCitiesOptionsTemplate = (isDisabled) => { - return ( - ` - ${CITIES.map((city) => ``).join('')} +const createPointCitiesOptionsTemplate = ({ pointDestination, isDisabled }) => ( + ` + ${pointDestination.map(({ name }) => ``).join('')} ` - ); -} +); -const createPointPhotosTemplate = (pointDestination) => { - return ( +const createPointPhotosTemplate = (pointDestination) => ( + (pointDestination.pictures && pointDestination.pictures.length) ? `
- ${pointDestination.pictures.map((picture) => - `${picture.description}`).join('')} -
` - ); -} - -const createPointTypesTemplate = ({ currentType, isDisabled }) => { - return TYPES.map((type) => - `
+ ${pointDestination.pictures.map((picture) => + `${picture.description}` + ).join('')} +
` + : '' +); +const createPointTypesTemplate = ({ pointOffers, currentType, isDisabled }) => pointOffers.map(({ type }) => + `
`).join(''); -} const createPointOffersTemplate = ({ offersId, currentOffers }) => { - const offerItems = currentOffers.map(offer => { + const offerItems = currentOffers.map((offer) => { const isChecked = offersId.includes(offer.id) ? 'checked' : ''; return ( `
@@ -82,7 +75,7 @@ const createPointOffersTemplate = ({ offersId, currentOffers }) => { }).join(''); return `
${offerItems}
`; -} +}; const createEditPointTemplate = ({ state, pointDestination, pointOffers, pointType }) => { const { point, isDisabled, isSaving, isDeleting } = state; @@ -104,7 +97,7 @@ const createEditPointTemplate = ({ state, pointDestination, pointOffers, pointTy
Event type - ${createPointTypesTemplate({ type, isDisabled })} + ${createPointTypesTemplate({ pointOffers, type, isDisabled })}
@@ -113,7 +106,7 @@ const createEditPointTemplate = ({ state, pointDestination, pointOffers, pointTy ${type} - ${createPointCitiesOptionsTemplate(isDisabled)} + ${createPointCitiesOptionsTemplate({ pointDestination, isDisabled })}
@@ -132,22 +125,20 @@ const createEditPointTemplate = ({ state, pointDestination, pointOffers, pointTy ${createPointEditControlsTemplate({ pointType, isDisabled, isSaving, isDeleting })}
- ${(currentOffers.length !== 0) ? `
+ ${(currentOffers.length) ? `

Offers

${createPointOffersTemplate({ offersId: point.offers, currentOffers })}
` : ''} - ${(currentDestination) ? `
-

Destination

-

${currentDestination.description}

-
- ${createPointPhotosTemplate(currentDestination)}` : ''} -
-
+ ${(currentDestination) ? `${(currentDestination.description.length || currentDestination.pictures.length) ? `
+

Destination

+

${currentDestination.description}

+ ${createPointPhotosTemplate(currentDestination)}` : ''} +
` : ''}
` ); -} +}; export default class EditPointView extends AbstractStatefulView { #pointDestination = null; @@ -168,7 +159,7 @@ export default class EditPointView extends AbstractStatefulView { this.#handleDeleteClick = onDeleteClick; this.#pointType = pointType; - this._setState(EditPointView.parsePointToState({ point })) + this._setState(EditPointView.parsePointToState({ point })); this._restoreHandlers(); } @@ -202,11 +193,11 @@ export default class EditPointView extends AbstractStatefulView { if (this.#pointType === EditType.EDITING) { this.element.querySelector('.event__rollup-btn').addEventListener('click', this.#rollupButtonClickHandler); - this.element.querySelector('.event__reset-btn').addEventListener('click', this.#deleteButtonClickHandler) + this.element.querySelector('.event__reset-btn').addEventListener('click', this.#deleteButtonClickHandler); } if (this.#pointType === EditType.CREATING) { - this.element.querySelector('.event__reset-btn').addEventListener('click', this.#resetButtonClickHandler) + this.element.querySelector('.event__reset-btn').addEventListener('click', this.#resetButtonClickHandler); } this.element.querySelector('form').addEventListener('submit', this.#formSubmitHandler); @@ -220,27 +211,35 @@ export default class EditPointView extends AbstractStatefulView { this.element.querySelector('.event__input--price').addEventListener('change', this.#priceChangeHandler); this.#setDatepickers(); - } + }; - #formSubmitHandler = (evt) => { + #formSubmitHandler = async (evt) => { evt.preventDefault(); - this.#handleSubmitClick(EditPointView.parseStateToPoint(this._state)); - } + await this.#handleSubmitClick(EditPointView.parseStateToPoint(this._state)); + this._setState({ + isSavingCompleted: true + }); + }; #rollupButtonClickHandler = (evt) => { evt.preventDefault(); this.#handleResetClick(); - } + }; #resetButtonClickHandler = (evt) => { + const disabledSavingButton = document.querySelector('.event__save-btn[disabled]'); + evt.preventDefault(); - this.#handleResetClick(); - } + + if (!disabledSavingButton) { + this.#handleResetClick(); + } + }; #deleteButtonClickHandler = (evt) => { evt.preventDefault(); this.#handleDeleteClick(EditPointView.parseStateToPoint(this._state)); - } + }; #typeChangeHandler = (evt) => { this.updateElement({ @@ -284,6 +283,12 @@ export default class EditPointView extends AbstractStatefulView { }); }; + #dateFromOpenHandler = () => { + const today = new Date(); + this.#datepickerFrom.setDate(today); + }; + + #dateFromCloseHandler = ([userDate]) => { this._setState({ point: { @@ -293,7 +298,7 @@ export default class EditPointView extends AbstractStatefulView { }); this.#datepickerTo.set('minDate', this._state.point.dateFrom); - } + }; #dateToCloseHandler = ([userDate]) => { this._setState({ @@ -304,13 +309,14 @@ export default class EditPointView extends AbstractStatefulView { }); this.#datepickerFrom.set('maxDate', this._state.point.dateTo); - } + }; #setDatepickers = () => { const [dateFromElement, dateToElement] = this.element.querySelectorAll('.event__input--time'); const commonConfig = { dateFormat: 'd/m/y H:i', enableTime: true, + allowInput: true, locale: { firstDayOfWeek: 1, }, @@ -323,6 +329,9 @@ export default class EditPointView extends AbstractStatefulView { ...commonConfig, defaultDate: this._state.point.dateFrom, onClose: this.#dateFromCloseHandler, + onOpen: () => { + this.#dateFromOpenHandler(); + }, maxDate: this._state.point.dateTo, }, ); @@ -335,19 +344,21 @@ export default class EditPointView extends AbstractStatefulView { onClose: this.#dateToCloseHandler, maxDate: this._state.point.dateFrom, }, - ) + ); }; static parsePointToState = ({ point, isDisabled = false, isSaving = false, - isDeleting = false + isDeleting = false, + isSavingCompleted = false }) => ({ point, isDisabled, isSaving, - isDeleting + isDeleting, + isSavingCompleted }); static parseStateToPoint = (state) => state.point; diff --git a/src/view/empty-list-view.js b/src/view/empty-list-view.js deleted file mode 100644 index 14ee290..0000000 --- a/src/view/empty-list-view.js +++ /dev/null @@ -1,16 +0,0 @@ -import AbstractView from '../framework/view/abstract-view.js'; - -const createEmptyListViewTemplate = () => { - return ( - `
-

Trip events

-

Click New Event to create your first point

-
` - ); -}; - -export default class EmptyListView extends AbstractView { - get template() { - return createEmptyListViewTemplate(); - } -} diff --git a/src/view/filter-view.js b/src/view/filter-view.js index 3c37819..682fbf6 100644 --- a/src/view/filter-view.js +++ b/src/view/filter-view.js @@ -1,29 +1,25 @@ import AbstractView from '../framework/view/abstract-view.js'; -import { capitalize } from '../utils.js'; +import { capitalize } from '../utils/point.js'; const createFilterItemsTemplate = ({ filters }) => { - const filterItems = filters.map(filter => { - return ( - `
+ const filterItems = filters.map((filter) => ( + `
` - ) - }).join(''); + )).join(''); return filterItems; -} +}; -const createFilterTemplate = ({ filters }) => { - return ( - `
+const createFilterTemplate = ({ filters }) => ( + ` ${createFilterItemsTemplate({ filters })}
` - ); -} +); export default class FilterView extends AbstractView { #filters = null; diff --git a/src/view/loading-view.js b/src/view/loading-view.js index e39b131..ca878ec 100644 --- a/src/view/loading-view.js +++ b/src/view/loading-view.js @@ -1,15 +1,34 @@ import AbstractView from '../framework/view/abstract-view'; -const createLoadingTemplate = () => { - return ( - `

- Loading... +const Message = { + 'LOADING': 'Loading...', + 'LOADING_ERROR': 'Failed to load latest route information', +}; + +const createLoadingTemplate = ({ message }) => ( + `

+ ${message}

` - ); -} +); export default class LoadingView extends AbstractView { + #isLoading; + #isLoadingError; + + constructor({ isLoading, isLoadingError }) { + super(); + this.#isLoading = isLoading; + this.#isLoadingError = isLoadingError; + } + get template() { - return createLoadingTemplate(); + let message; + if (this.#isLoading) { + message = Message['LOADING']; + } else if (this.#isLoadingError) { + message = Message['LOADING_ERROR']; + } + + return createLoadingTemplate({ message }); } } diff --git a/src/view/message-view.js b/src/view/message-view.js index a94abf8..b31c586 100644 --- a/src/view/message-view.js +++ b/src/view/message-view.js @@ -1,21 +1,19 @@ -import AbstractView from "../framework/view/abstract-view.js"; -import { FilterType } from "../const.js"; +import AbstractView from '../framework/view/abstract-view.js'; +import { FilterType } from '../const.js'; const FilterMessage = { [FilterType.EVERYTHING]: 'Click New Event to create your first point', [FilterType.FUTURE]: 'There are no future events now', [FilterType.PRESENT]: 'There are no present events now', [FilterType.PAST]: 'There are no past events now' -} +}; -const createMessageTemplate = ({ message }) => { - return ( - `
+const createMessageTemplate = ({ message }) => ( + `

Trip events

${message}

` - ); -} +); export default class MessageView extends AbstractView { #filterType; diff --git a/src/view/new-point-button-view.js b/src/view/new-point-button-view.js index 4a35ce8..50c4c7b 100644 --- a/src/view/new-point-button-view.js +++ b/src/view/new-point-button-view.js @@ -1,10 +1,8 @@ -import AbstractView from "../framework/view/abstract-view.js"; +import AbstractView from '../framework/view/abstract-view.js'; -const createNewPointButtonTemplate = () => { - return ( - '' - ); -} +const createNewPointButtonTemplate = () => ( + '' +); export default class NewPointButtonView extends AbstractView { #handleClick = null; @@ -22,10 +20,10 @@ export default class NewPointButtonView extends AbstractView { setDisabled = (isDisabled) => { this.element.disabled = isDisabled; - } + }; #clickHandler = (evt) => { evt.preventDefault(); this.#handleClick(); - } + }; } diff --git a/src/view/point-list-view.js b/src/view/point-list-view.js index ff0f932..2b1b843 100644 --- a/src/view/point-list-view.js +++ b/src/view/point-list-view.js @@ -1,10 +1,8 @@ import AbstractView from '../framework/view/abstract-view.js'; -const createPointListTemplate = () => { - return ( - `
      ` - ); -}; +const createPointListTemplate = () => ( + '
        ' +); export default class PointListView extends AbstractView { get template() { diff --git a/src/view/point-view.js b/src/view/point-view.js index 0c1bc12..76ce55c 100644 --- a/src/view/point-view.js +++ b/src/view/point-view.js @@ -1,26 +1,24 @@ import AbstractView from '../framework/view/abstract-view.js'; -import { formatStringToDateTime, formatStringToShortDate, formatStringToTime, getPointDuration } from '../utils.js'; +import { formatStringToDateTime, formatStringToShortDate, formatStringToTime, getPointDuration } from '../utils/point.js'; import he from 'he'; const createPointOffersTemplate = ({ offersId, pointOffers }) => { const selectedOffers = pointOffers.filter((offer) => offersId.includes(offer.id)); - if (selectedOffers.length === 0) { + if (!selectedOffers.length) { return ''; } - const offerItems = selectedOffers.map(offer => { - return ( - `
      • + const offerItems = selectedOffers.map((offer) => ( + `
      • ${offer.title} +€  ${offer.price}
      • ` - ); - }).join(''); + )).join(''); return `
          ${offerItems}
        `; -} +}; const createPointTemplate = ({ point, pointDestination, pointOffers }) => { const { basePrice, dateFrom, dateTo, isFavorite, type } = point; @@ -59,7 +57,7 @@ const createPointTemplate = ({ point, pointDestination, pointOffers }) => {
    ` ); -} +}; export default class PointView extends AbstractView { #point = null; @@ -77,7 +75,7 @@ export default class PointView extends AbstractView { this.#onFavoriteClick = onFavoriteClick; this.element.querySelector('.event__rollup-btn').addEventListener('click', this.#editClickHandler); - this.element.querySelector('.event__favorite-icon').addEventListener('click', this.#favoriteClickHandler); + this.element.querySelector('.event__favorite-btn').addEventListener('click', this.#favoriteClickHandler); } get template() { @@ -90,11 +88,15 @@ export default class PointView extends AbstractView { #editClickHandler = (evt) => { evt.preventDefault(); - this.#onEditClick(); - } + const disabledResetButton = document.querySelector('.event__reset-btn[disabled]'); + const disabledSavingButton = document.querySelector('.event__save-btn[disabled]'); + if (!disabledResetButton && !disabledSavingButton) { + this.#onEditClick(); + } + }; #favoriteClickHandler = (evt) => { evt.preventDefault(); this.#onFavoriteClick(); - } + }; } diff --git a/src/view/sort-view.js b/src/view/sort-view.js index 2b41ce6..5f51acb 100644 --- a/src/view/sort-view.js +++ b/src/view/sort-view.js @@ -1,9 +1,8 @@ import AbstractView from '../framework/view/abstract-view.js'; const createSortItemsTemplate = ({ items }) => { - const sortItems = items.map(sortItem => { - return ( - `
    + const sortItems = items.map((sortItem) => ( + `
    { class="trip-sort__btn" for="sort-${sortItem.type}">${sortItem.type}
    ` - ) - }).join(''); + )).join(''); return sortItems; -} +}; -const createSortTemplate = ({ items }) => { - return ( - `
    +const createSortTemplate = ({ items }) => ( + ` ${createSortItemsTemplate({ items })}
    ` - ); -} +); export default class SortView extends AbstractView { #items = null; diff --git a/src/view/trip-info-view.js b/src/view/trip-info-view.js index a70cb81..449b912 100644 --- a/src/view/trip-info-view.js +++ b/src/view/trip-info-view.js @@ -1,21 +1,39 @@ import AbstractView from '../framework/view/abstract-view.js'; +import { getTripTitle, getTripDuration, getTripCost } from '../utils/trip-info.js'; -const createTripInfoTemplate = () => { - return ( - `
    +const createTripInfoTemplate = ({ title, duration, cost, isEmpty }) => ( + `${isEmpty + ? '
    ' + : ` +
    -

    Amsterdam — Chamonix — Geneva

    -

    Mar 18 — 20

    +

    ${title}

    +

    ${duration}

    - Total: € 1230 + Total: € ${cost}

    -
    ` - ); -} +
    `} + `); export default class TripInfoView extends AbstractView { + #destinations = null; + #offers = null; + #points = 0; + + constructor({ destinations, offers, points }) { + super(); + this.#destinations = destinations; + this.#offers = offers; + this.#points = points; + } + get template() { - return createTripInfoTemplate(); + return createTripInfoTemplate({ + title: getTripTitle(this.#points, this.#destinations), + duration: getTripDuration(this.#points), + cost: getTripCost(this.#points, this.#offers), + isEmpty: this.#points.length === 0 + }); } }