diff --git a/.eslintignore b/.eslintignore index 6de001d..956aac3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ webpack.config.js +src/mock/* diff --git a/package-lock.json b/package-lock.json index 7075a01..b164ac5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "big-trip", "version": "22.0.0", + "dependencies": { + "dayjs": "1.11.6" + }, "devDependencies": { "@babel/core": "7.21.4", "@babel/preset-env": "7.21.4", @@ -15,6 +18,7 @@ "eslint": "8.38.0", "eslint-config-htmlacademy": "9.0.0", "html-webpack-plugin": "5.5.1", + "lodash": "4.17.21", "webpack": "5.92.0", "webpack-cli": "5.1.4", "webpack-dev-server": "4.13.3" @@ -3265,6 +3269,11 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/dayjs": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.6.tgz", + "integrity": "sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index c6b700c..2862c3c 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,15 @@ "eslint": "8.38.0", "eslint-config-htmlacademy": "9.0.0", "html-webpack-plugin": "5.5.1", + "lodash": "4.17.21", "webpack": "5.92.0", "webpack-cli": "5.1.4", "webpack-dev-server": "4.13.3" }, "engines": { "node": "20" + }, + "dependencies": { + "dayjs": "1.11.6" } } diff --git a/src/mock/destinations.js b/src/mock/destinations.js new file mode 100644 index 0000000..e6f2d9e --- /dev/null +++ b/src/mock/destinations.js @@ -0,0 +1,31 @@ +export const destinations = [ + { + id: 'bfa5cb75-a1fe-4b77-a83c-0e528e910e04', + description: 'St. Petersburg, is a beautiful city, a true asian pearl, with crowded streets.', + name: 'St. Petersburg', + pictures: [ + { + src: 'http://picsum.photos/300/200?r=0.0762563005163317', + description: 'Chamonix parliament building' + }] + }, + { + id: 'bfa5cb75-a1fe-4b77-a83c-0e528e910e05', + description: 'Paris is a major railway, highway, and air-transport hub served by two international airports.', + name: 'Paris', + pictures: [ + { + src: 'http://picsum.photos/300/200?r=0.0762563005163317', + description: 'Chamonix parliament building' + }] + }, + { + id: 'bfa5cb75-a1fe-4b77-a83c-0e528e910e06', + description: 'Moscow is a very green city, if compared to other cities of comparable size in Western Europe and North America.', + name: 'Moscow', + pictures: [ + { + src: 'http://picsum.photos/300/200?r=0.0762563005163317', + description: 'Chamonix parliament building' + }] + }]; diff --git a/src/mock/offers.js b/src/mock/offers.js new file mode 100644 index 0000000..a034b7d --- /dev/null +++ b/src/mock/offers.js @@ -0,0 +1,153 @@ +export const offers = [ + { + type: 'taxi', + offers: [ + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa31', + title: 'taxi offer 1', + price: 120 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa32', + title: 'taxi offer 2', + price: 130 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa33', + title: 'taxi offer 3', + price: 140 + }] + }, + { + type: 'bus', + offers: [ + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa34', + title: 'bus offer 1', + price: 150 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa35', + title: 'bus offer 2', + price: 160 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa36', + title: 'bus offer 3', + price: 170 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa37', + title: 'bus offer 4', + price: 180 + }] + }, + { + type: 'train', + offers: [ + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa38', + title: 'train offer 1', + price: 190 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa39', + title: 'train offer 2', + price: 200 + }] + }, + { + type: 'ship', + offers: [ + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa30', + title: 'ship offer 1', + price: 210 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa41', + title: 'ship offer 2', + price: 220 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa42', + title: 'ship offer 3', + price: 230 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa43', + title: 'ship offer 4', + price: 240 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa44', + title: 'ship offer 5', + price: 250 + }] + }, + { + type: 'drive', + offers: [ + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa45', + title: 'drive offer 1', + price: 260 + }] + }, + { + type: 'flight', + offers: [ + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa46', + title: 'flight offer 1', + price: 270 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa47', + title: 'flight offer 2', + price: 280 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa48', + title: 'flight offer 3', + price: 290 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa49', + title: 'flight offer 4', + price: 300 + }] + }, + { + type: 'check-in', + offers: [ + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa40', + title: 'chick-in offer 1', + price: 312 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa51', + title: 'chick-in offer 2', + price: 320 + }, + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa52', + title: 'chick-in offer 3', + price: 330 + }] + }, + { + type: 'sightseeing', + offers: [] + }, + { + type: 'restaurant', + offers: [ + { + id: 'b4c3e4e6-9053-42ce-b747-e281314baa53', + title: 'restaurant offer 1', + price: 340 + }] + } +]; diff --git a/src/mock/points.js b/src/mock/points.js new file mode 100644 index 0000000..d823685 --- /dev/null +++ b/src/mock/points.js @@ -0,0 +1,52 @@ +import { toCamelCase } from '../util.js' + +export const points = [ + { + id: 'f4b62099-293f-4c3d-a702-94eec4a2808c', + basePrice: 11300, + dateFrom: '2019-07-10T00:55:56.845Z', + dateTo: '2019-07-11T11:01:13.375Z', + destination: 'bfa5cb75-a1fe-4b77-a83c-0e528e910e04', + isFavorite: false, + offers: [ + 'b4c3e4e6-9053-42ce-b747-e281314baa31' + ], + type: 'taxi' + }, + { + id: 'f4b62099-293f-4c3d-a702-94eec4a2808d', + basePrice: 11200, + dateFrom: '2019-07-12T22:02:56.845Z', + dateTo: '2019-07-13T11:03:13.375Z', + destination: 'bfa5cb75-a1fe-4b77-a83c-0e528e910e05', + isFavorite: true, + offers: [ + 'b4c3e4e6-9053-42ce-b747-e281314baa34', + 'b4c3e4e6-9053-42ce-b747-e281314baa35', + 'b4c3e4e6-9053-42ce-b747-e281314baa36' + ], + type: 'bus' + }, + { + id: 'f4b62099-293f-4c3d-a702-94eec4a2808e', + basePrice: 11100, + dateFrom: '2019-07-14T22:55:56.845Z', + dateTo: '2019-07-15T11:22:13.375Z', + destination: 'bfa5cb75-a1fe-4b77-a83c-0e528e910e06', + isFavorite: false, + offers: [], + type: 'ship' + }]; + +export const defaultPoint = [ + { + id: '000', + basePrice: '000', + dateFrom: '2001-01-01T00:00:00.845Z', + dateTo: '2001-01-01T00:00:00.845Z', + destination: '', + isFavorite: false, + offers: [], + type: 'flight' + } +] diff --git a/src/model/point-model.js b/src/model/point-model.js new file mode 100644 index 0000000..231a807 --- /dev/null +++ b/src/model/point-model.js @@ -0,0 +1,35 @@ +import { points, defaultPoint } from '../mock/points.js'; +import { destinations } from '../mock/destinations.js'; +import { offers } from '../mock/offers.js'; + +export default class PointModel { + constructor() { + this.points = []; + this.destinations = []; + this.offers = []; + this.defaultPoint = []; + } + + init() { + this.points = points; + this.destinations = destinations; + this.offers = offers; + this.defaultPoint = defaultPoint; + } + + getPoints() { + return this.points; + } + + getDestinations() { + return this.destinations; + } + + getOffers() { + return this.offers; + } + + getDefaultPoint() { + return this.defaultPoint; + } +} diff --git a/src/presenter/header-presenter.js b/src/presenter/header-presenter.js index 8f2b758..60a9121 100644 --- a/src/presenter/header-presenter.js +++ b/src/presenter/header-presenter.js @@ -1,6 +1,6 @@ import { render, RenderPosition } from '../render.js'; -import Filters from '../view/filters.js'; -import TripInfo from '../view/trip-info.js'; +import FiltersView from '../view/filters-view.js'; +import TripInfoView from '../view/trip-info-view.js'; export default class HeaderPresenter { constructor ({ container }) { @@ -8,10 +8,10 @@ export default class HeaderPresenter { } initInfo () { - render (new TripInfo, this.container, RenderPosition.AFTERBEGIN); + render (new TripInfoView, this.container, RenderPosition.AFTERBEGIN); } initFilters () { - render(new Filters, this.container); + render(new FiltersView, this.container); } } diff --git a/src/presenter/trip-presenter.js b/src/presenter/trip-presenter.js index b3bd27f..4c63f24 100644 --- a/src/presenter/trip-presenter.js +++ b/src/presenter/trip-presenter.js @@ -1,25 +1,33 @@ import { render } from '../render.js'; -import CreateForm from '../view/create-form.js'; -import EditForm from '../view/edit-form.js'; -import Sorting from '../view/sorting.js'; -import TripList from '../view/trip-list.js'; -import TripPoint from '../view/trip-point.js'; +import CreateFormView from '../view/create-form-view.js'; +import EditFormView from '../view/edit-form-view.js'; +import SortingView from '../view/sorting-view.js'; +import TripListView from '../view/trip-list-view.js'; +import TripPointView from '../view/trip-point-view.js'; +import PointModel from '../model/point-model.js'; export default class TripPresenter { - tripList = new TripList(); + tripList = new TripListView(); - constructor ({ container }) { + constructor ({ container, pointModel = new PointModel }) { this.container = container; + this.pointModel = pointModel; } init () { - render(new Sorting, this.container); + this.pointModel.init(); + const points = this.pointModel.getPoints(); + const destinations = this.pointModel.getDestinations(); + const offers = this.pointModel.getOffers(); + const defaultPoint = this.pointModel.getDefaultPoint(); + + render(new SortingView, this.container); render(this.tripList, this.container); - render(new EditForm, this.tripList.getElement()); - render(new CreateForm, this.tripList.getElement()); + render(new EditFormView(points[0], destinations, offers), this.tripList.getElement()); + render(new CreateFormView(defaultPoint[0], destinations, offers), this.tripList.getElement()); - for (let i = 0; i < 3; i++) { - render (new TripPoint, this.tripList.getElement()); - } + points.forEach((point) => { + render(new TripPointView(point, destinations, offers), this.tripList.getElement()); + }); } } diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..0e3229f --- /dev/null +++ b/src/util.js @@ -0,0 +1,28 @@ +import dayjs from 'dayjs'; +import lodash from 'lodash'; + +const humanizeDueDate = (dueDate, dateFormat) => dueDate ? dayjs(dueDate).format(dateFormat) : ''; + +const capitalize = (str) => str[0].toUpperCase() + str.slice(1); + +const findDuration = (date1, date2) => { + let minutesDuration = dayjs(date1).diff(dayjs(date2), 'm'); + const minutesAfterHours = minutesDuration % 60; + + if (minutesDuration >= 60 && minutesAfterHours !== 0) { + minutesDuration /= 60; + + return `${Math.floor(minutesDuration)}H ${minutesAfterHours}M`; + } + + if (minutesDuration >= 60) { + minutesDuration /= 60; + } + + return `${Math.floor(minutesDuration)}H`; +}; + +const toCamelCase = (str) => lodash.camelCase(str); + + +export { humanizeDueDate, capitalize, findDuration, toCamelCase }; diff --git a/src/view/create-form.js b/src/view/create-form-view.js similarity index 68% rename from src/view/create-form.js rename to src/view/create-form-view.js index 044dd82..2f7d775 100644 --- a/src/view/create-form.js +++ b/src/view/create-form-view.js @@ -1,13 +1,22 @@ import { createElement } from '../render.js'; +import { capitalize, humanizeDueDate } from '../util.js'; -const createFormTemplate = () => ` -
  • +const dateFormat = { + FULL_DATE: 'DD/MM/YY H:mm' +}; + +const createFormTemplate = (point, destinations, offers) => { + const { dateFrom, dateTo, type } = point; + const offersByType = offers.find((offer) => offer.type === point.type).offers; + const destinationDescription = 'Choose your destiny'; + + return `
  • @@ -65,22 +74,20 @@ const createFormTemplate = () => `
    - + - - - + ${destinations.map((dest) => ``).join('')}
    - + - +
    @@ -99,56 +106,21 @@ const createFormTemplate = () => `

    Offers

    + ${offersByType.map((offer) => `
    - - -
    - -
    - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    +
    `).join('')}

    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.

    +

    ${destinationDescription || ''}

    @@ -163,10 +135,17 @@ const createFormTemplate = () => `
  • `; +}; + +export default class CreateFormView { + constructor(points, destinations, offers) { + this.points = points; + this.destinations = destinations; + this.offers = offers; + } -export default class CreateForm { getTemplate () { - return createFormTemplate(); + return createFormTemplate(this.points, this.destinations, this.offers); } getElement () { diff --git a/src/view/edit-form.js b/src/view/edit-form-view.js similarity index 65% rename from src/view/edit-form.js rename to src/view/edit-form-view.js index 5ffc10e..d5c5557 100644 --- a/src/view/edit-form.js +++ b/src/view/edit-form-view.js @@ -1,13 +1,23 @@ import { createElement } from '../render.js'; +import { capitalize, humanizeDueDate } from '../util.js'; -const createEditFormTemplate = () => ` -
  • +const dateFormat = { + FULL_DATE: 'DD/MM/YY H:mm' +}; + +const createEditFormTemplate = (point, destionations, offers) => { + const { basePrice, dateFrom, dateTo, type } = point; + const offersByType = offers.find((offer) => offer.type === point.type).offers; + const pointDestination = destionations.find((destination) => destination.id === point.destination).name; + const destinationDescription = destionations.find((destination) => destination.id === point.destination).description; + + return `
  • @@ -65,22 +75,20 @@ const createEditFormTemplate = () => `
    - + - - - + ${destionations.map((dest) => ``).join('')}
    - + - +
    @@ -88,7 +96,7 @@ const createEditFormTemplate = () => ` Price € - +
    @@ -102,64 +110,36 @@ const createEditFormTemplate = () => `

    Offers

    + ${offersByType.map((offer) => `
    - - -
    - -
    - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    +
    `).join('')}

    Destination

    -

    Chamonix-Mont-Blanc (usually shortened to Chamonix) is a resort area near the junction of France, Switzerland and Italy. At the base of Mont Blanc, the highest summit in the Alps, it's renowned for its skiing.

    +

    ${destinationDescription}

  • `; +}; + +export default class EditFormView { + constructor(points, destionations, offers) { + this.points = points; + this.destionations = destionations; + this.offers = offers; + } -export default class EditForm { getTemplate() { - return createEditFormTemplate(); + return createEditFormTemplate(this.points, this.destionations, this.offers); } getElement() { diff --git a/src/view/filters.js b/src/view/filters-view.js similarity index 85% rename from src/view/filters.js rename to src/view/filters-view.js index c620868..b62fe62 100644 --- a/src/view/filters.js +++ b/src/view/filters-view.js @@ -1,6 +1,6 @@ import { createElement } from '../render.js'; -const FILTERS_TYPES = ['Everything', 'Future', 'Present', 'Past']; +const filtersTypes = ['Everything', 'Future', 'Present', 'Past']; const createFilterElement = (filterType) => `
    @@ -14,12 +14,12 @@ const createFilterTemplate = () => `
    - ${FILTERS_TYPES.map((filterType) => createFilterElement(filterType)).join('')} + ${filtersTypes.map((filterType) => createFilterElement(filterType)).join('')} `; -export default class Filters { +export default class FiltersView { getTemplate () { return createFilterTemplate(); } diff --git a/src/view/sorting.js b/src/view/sorting-view.js similarity index 82% rename from src/view/sorting.js rename to src/view/sorting-view.js index cb0a903..258afee 100644 --- a/src/view/sorting.js +++ b/src/view/sorting-view.js @@ -1,6 +1,6 @@ import { createElement } from '../render.js'; -const SORTING_TYPES = ['Day', 'Event', 'Time', 'Price', 'Offers']; +const sortingTypes = ['Day', 'Event', 'Time', 'Price', 'Offers']; const findCheckedElement = (element) => element === 'Price' ? 'checked' : ''; @@ -12,10 +12,10 @@ const createSortingElement = (sortingType) => ` const createSortingTemplate = () => `
    - ${SORTING_TYPES.map((sortingType) => createSortingElement(sortingType)).join('')} + ${sortingTypes.map((sortingType) => createSortingElement(sortingType)).join('')}
    `; -export default class Sorting { +export default class SortingView { getTemplate () { return createSortingTemplate(); } diff --git a/src/view/trip-info.js b/src/view/trip-info-view.js similarity index 95% rename from src/view/trip-info.js rename to src/view/trip-info-view.js index 09f7309..6cb3033 100644 --- a/src/view/trip-info.js +++ b/src/view/trip-info-view.js @@ -13,7 +13,7 @@ const createTripInfoTemplate = () => `

    `; -export default class TripInfo { +export default class TripInfoView { getTemplate() { return createTripInfoTemplate(); } diff --git a/src/view/trip-list.js b/src/view/trip-list-view.js similarity index 91% rename from src/view/trip-list.js rename to src/view/trip-list-view.js index 0715fd5..e72f9a8 100644 --- a/src/view/trip-list.js +++ b/src/view/trip-list-view.js @@ -4,7 +4,7 @@ const createTripListTemplate = () => ` `; -export default class TripList { +export default class TripListView { getTemplate () { return createTripListTemplate(); } diff --git a/src/view/trip-point-view.js b/src/view/trip-point-view.js new file mode 100644 index 0000000..19354b3 --- /dev/null +++ b/src/view/trip-point-view.js @@ -0,0 +1,78 @@ +import { createElement } from '../render.js'; +import { capitalize, humanizeDueDate, findDuration } from '../util.js'; + +const dateFormat = { + MMMD: 'MMM D', + HMM: 'H:mm', + DURATION: 'duration' +}; + +const createTripPointTemplate = (point, destionations, offers) => { + const { basePrice, isFavorite, dateFrom, dateTo, type } = point; + const offersByType = offers.find((offer) => offer.type === point.type).offers; + const offersForPoint = offersByType.filter((offerByType) => point.offers.includes(offerByType.id)); + const pointDestination = destionations.find((destination) => destination.id === point.destination).name; + + return `
  • +
    + +
    + Event type icon +
    +

    ${capitalize(point.type)} ${pointDestination}

    +
    +

    + + — + +

    +

    ${findDuration(dateTo, dateFrom)}

    +
    +

    + € ${basePrice} +

    +

    Offers:

    + + + +
    +
  • `; +}; + +export default class TripPointView { + constructor(points, destionations, offers) { + this.points = points; + this.destionations = destionations; + this.offers = offers; + } + + getTemplate() { + return createTripPointTemplate(this.points, this.destionations, this.offers); + } + + getElement() { + if (!this.element) { + this.element = createElement(this.getTemplate()); + + return this.element; + } + } + + removeElement() { + this.element = null; + } +} diff --git a/src/view/trip-point.js b/src/view/trip-point.js deleted file mode 100644 index 7469aea..0000000 --- a/src/view/trip-point.js +++ /dev/null @@ -1,58 +0,0 @@ -import { createElement } from '../render.js'; - -const createTripPointTemplate = () => ` -
  • -
    - -
    - Event type icon -
    -

    Taxi Amsterdam

    -
    -

    - - — - -

    -

    30M

    -
    -

    - € 20 -

    -

    Offers:

    - - - -
    -
  • `; - -export default class TripPoint { - getTemplate() { - return createTripPointTemplate(); - } - - getElement() { - if (!this.element) { - this.element = createElement(this.getTemplate()); - - return this.element; - } - } - - removeElement() { - this.element = null; - } -}