From 44bb63705397a3dc40e4cb8fbdccd37ee645b394 Mon Sep 17 00:00:00 2001 From: Stepan Vasilyev Date: Fri, 2 Jan 2026 20:10:10 +0500 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B8=D1=82=20=D0=BA=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/photo-filters.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/js/photo-filters.js b/js/photo-filters.js index e4febdd..f80183e 100644 --- a/js/photo-filters.js +++ b/js/photo-filters.js @@ -66,12 +66,7 @@ const createSlider = () => { start: 1, connect: 'lower', format: { - to: (value) => { - if (Number.isInteger(value)) { - return value; - } - return value.toFixed(1); - }, + to: (value) => Number.isInteger(value) ? value : value.toFixed(1), from: (value) => parseFloat(value), }, }); From 1f2ff9632c91d8ba4471bbbee98b64f51c9eb5fd Mon Sep 17 00:00:00 2001 From: Stepan Vasilyev Date: Sat, 3 Jan 2026 00:42:17 +0500 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D1=82=20=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D0=B5=20=D1=81=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/api.js | 29 ++++++++++++++++++ js/create-photos.js | 49 ------------------------------- js/data.js | 32 -------------------- js/form-validation.js | 16 ++-------- js/form.js | 38 ++++++++++++++++++++++-- js/gallery.js | 30 +++++++++++++++---- js/info-modal-send.js | 54 ++++++++++++++++++++++++++++++++++ js/photo-modal.js | 19 ++---------- js/utils.js | 68 ++++++++++++------------------------------- 9 files changed, 167 insertions(+), 168 deletions(-) create mode 100644 js/api.js delete mode 100644 js/create-photos.js delete mode 100644 js/data.js create mode 100644 js/info-modal-send.js diff --git a/js/api.js b/js/api.js new file mode 100644 index 0000000..3b2f1c9 --- /dev/null +++ b/js/api.js @@ -0,0 +1,29 @@ +const BASE_URL = 'https://31.javascript.htmlacademy.pro/kekstagram'; +const Route = { + GET_DATA: '/data', + SEND_DATA: '', +}; +const ErrorText = { + GET_DATA: 'Не удалось загрузить данные. Попробуйте обновить страницу', + SEND_DATA: 'Не удалось отправить форму. Попробуйте ещё раз', +}; +const Method = { + GET: 'GET', + POST: 'POST', +}; + +const load = (route, errorText, method = Method.GET, body = null) => fetch(`${BASE_URL}${route}`, {method, body}).then((response) => { + if (!response.ok) { + throw new Error(`Произошла ошибка ${response.status}: ${response.statusText}`); + } + + return response.json(); +}).catch((err) => { + throw new Error(errorText ?? err.message); +}); + +const getData = () => load(Route.GET_DATA, ErrorText.GET_DATA); + +const sendData = (body) => load(Route.SEND_DATA, ErrorText.SEND_DATA, Method.POST, body); + +export { getData, sendData }; diff --git a/js/create-photos.js b/js/create-photos.js deleted file mode 100644 index 3971bb5..0000000 --- a/js/create-photos.js +++ /dev/null @@ -1,49 +0,0 @@ -import { getRandomInteger, getRandomArrayElement, getRandomArrayElements, createRandomIdGenetrator } from './utils.js'; -import { createPhotosData } from './data.js'; - -const PHOTOS_COUNT = 25; -const MAX_COMMENT_MESSAGES_COUNT = 2; - -const CommentsIdRange = { - MIN: 1, - MAX: 1000 -}; - -const AvatarIdRange = { - MIN: 1, - MAX: 6 -}; - -const LikesCountRange = { - MIN: 15, - MAX: 200 -}; - -const CommentsCountRange = { - MIN: 0, - MAX: 30 -}; - -const { COMMENTS, DESCRIPTIONS, NAMES} = createPhotosData(); - -let urlId = 0; -const getCommentId = createRandomIdGenetrator(CommentsIdRange.MIN, CommentsIdRange.MAX); - -const createComment = () => ({ - id: getCommentId(), - avatar: `img/avatar-${getRandomInteger(AvatarIdRange.MIN, AvatarIdRange.MAX)}.svg`, - message: getRandomArrayElements(COMMENTS, MAX_COMMENT_MESSAGES_COUNT), - name: getRandomArrayElement(NAMES), -}); - -const createPhoto = (currentId) => ({ - id: ++currentId, - url: `photos/${++urlId}.jpg`, - description: getRandomArrayElement(DESCRIPTIONS), - likes: getRandomInteger(LikesCountRange.MIN, LikesCountRange.MAX), - comments: Array.from({length: getRandomInteger(CommentsCountRange.MIN, CommentsCountRange.MAX)}, createComment) -}); - -const createPhotos = () => Array.from({length: PHOTOS_COUNT}, (_, index) => createPhoto(index)); - -export { createPhotos }; diff --git a/js/data.js b/js/data.js deleted file mode 100644 index 084e51e..0000000 --- a/js/data.js +++ /dev/null @@ -1,32 +0,0 @@ -const createPhotosData = () => { - const COMMENTS = [ - 'Всё отлично!', - 'В целом всё неплохо. Но не всё.', - 'Когда вы делаете фотографию, хорошо бы убирать палец из кадра. В конце концов это просто непрофессионально.', - 'Моя бабушка случайно чихнула с фотоаппаратом в руках и у неё получилась фотография лучше.', - 'Я поскользнулся на банановой кожуре и уронил фотоаппарат на кота и у меня получилась фотография лучше.', - 'Лица у людей на фотке перекошены, как будто их избивают. Как можно было поймать такой неудачный момент?!' - ]; - - const DESCRIPTIONS = [ - 'Отличный был концерт!', - 'Это я на море! Пью джус!', - 'Кот опять учудил', - 'О, на улице погооодааа... ужаснаааяяя', - 'Я уже устал придумывать описания', - 'Россия для грустных' - ]; - - const NAMES = [ - 'Паша Техник', - 'Джуди Хопс', - 'Мисс Барашкис', - 'Билли Бутчер', - 'Габи Салис', - 'Валера' - ]; - - return { COMMENTS, DESCRIPTIONS, NAMES }; -}; - -export { createPhotosData }; diff --git a/js/form-validation.js b/js/form-validation.js index 3a0e0be..46878e6 100644 --- a/js/form-validation.js +++ b/js/form-validation.js @@ -65,18 +65,6 @@ const resetValidation = () => { pristine.reset(); }; -const initFormValidation = () => { - if (!formElement) { - return; - } - - formElement.addEventListener('submit', (evt) => { - evt.preventDefault(); - - if (pristine.validate()) { - formElement.submit(); - } - }); -}; +const isFormValid = () => pristine.validate(); -export { initFormValidation, resetValidation }; +export { isFormValid, resetValidation }; diff --git a/js/form.js b/js/form.js index e4d8f8d..380606c 100644 --- a/js/form.js +++ b/js/form.js @@ -1,7 +1,14 @@ import { isEscKey } from './utils.js'; -import { initFormValidation, resetValidation } from './form-validation.js'; +import { renderSendInfoModal } from './info-modal-send.js'; +import { isFormValid, resetValidation } from './form-validation.js'; import { initScalePhoto, resetScaleValue } from './scale-photo.js'; import { initPhotoFilters, resetPhotoFilters } from './photo-filters.js'; +import { sendData } from './api.js'; + +const SubmitButtonText = { + IDLE: 'Отправить', + SENDING: 'Отправляю...' +}; const bodyElement = document.querySelector('body'); const formElement = bodyElement.querySelector('.img-upload__form'); @@ -9,6 +16,7 @@ const modalFormElement = formElement.querySelector('.img-upload__overlay'); const uploadControlElement = formElement.querySelector('.img-upload__input'); const modalFormCloseElement = modalFormElement.querySelector('.img-upload__cancel'); const sliderWrapperElement = modalFormElement.querySelector('.img-upload__effect-level'); +const submitButtonElement = modalFormElement.querySelector('.img-upload__submit'); const onDocumentKeydown = (evt) => { if (isEscKey(evt)) { @@ -21,6 +29,16 @@ const onDocumentKeydown = (evt) => { } }; +const disableSubmitButton = () => { + submitButtonElement.disabled = true; + submitButtonElement.textContent = SubmitButtonText.SENDING; +}; + +const enableSubmitButton = () => { + submitButtonElement.disabled = false; + submitButtonElement.textContent = SubmitButtonText.IDLE; +}; + function openModalForm() { modalFormElement.classList.remove('hidden'); bodyElement.classList.add('modal-open'); @@ -43,6 +61,23 @@ const initModalForm = () => { return; } + formElement.addEventListener('submit', (evt) => { + evt.preventDefault(); + + if (isFormValid()) { + const formData = new FormData(evt.target); + disableSubmitButton(); + sendData(formData).then(() => { + closeModalForm(); + renderSendInfoModal('success'); + }).catch(() => { + renderSendInfoModal('error'); + }).finally(() => { + enableSubmitButton(); + }); + } + }); + uploadControlElement.addEventListener('change', () => { openModalForm(); }); @@ -51,7 +86,6 @@ const initModalForm = () => { closeModalForm(); }); - initFormValidation(); initScalePhoto(); initPhotoFilters(); }; diff --git a/js/gallery.js b/js/gallery.js index 9cadf16..518c3a1 100644 --- a/js/gallery.js +++ b/js/gallery.js @@ -1,14 +1,34 @@ +import { findElementById } from './utils.js'; +import { showAlertTemporarily, findTemplateById } from './utils.js'; import { renderThumbnails } from './render-thumbnails.js'; -import { createPhotos } from './create-photos.js'; -import { initPhotoModal } from './photo-modal.js'; +import { registerPhotoModalEvents, openPhotoModal } from './photo-modal.js'; +import { getData } from './api.js'; const thumbnailsContainerElement = document.querySelector('.pictures'); -const photos = createPhotos(); +const errorTemplateElement = findTemplateById('data-error'); const initGallery = () => { - renderThumbnails(photos, thumbnailsContainerElement); - initPhotoModal(photos, thumbnailsContainerElement); + getData().then((photos) => { + renderThumbnails(photos, thumbnailsContainerElement); + registerPhotoModalEvents(); + + thumbnailsContainerElement.addEventListener('click', (evt) => { + const targetThumbnailElement = evt.target.closest('.picture'); + + if (targetThumbnailElement) { + evt.preventDefault(); + + const targetThumbnailElementId = Number(targetThumbnailElement.dataset.id); + const targetPhoto = findElementById(targetThumbnailElementId, photos); + + openPhotoModal(targetPhoto); + } + }); + }).catch(() => { + showAlertTemporarily(errorTemplateElement); + }); + }; export { initGallery }; diff --git a/js/info-modal-send.js b/js/info-modal-send.js new file mode 100644 index 0000000..663cc9e --- /dev/null +++ b/js/info-modal-send.js @@ -0,0 +1,54 @@ +import { isEscKey, findTemplateById } from './utils.js'; + +const bodyElement = document.querySelector('body'); +const errorTemplateElement = findTemplateById('error'); +const successTemplateElement = findTemplateById('success'); + +let modalElement; +let currentStatus; + +const onBodyClick = (evt) => { + if (!evt.target.closest(`.${currentStatus}__inner`)) { + removeSendInfoModal(); + } +}; + +const onBodyKeydown = (evt) => { + if (isEscKey(evt)) { + evt.preventDefault(); + removeSendInfoModal(); + evt.stopPropagation(); + } +}; + +const createSendInfoModal = (status, message) => { + const templateElement = status === 'success' ? successTemplateElement : errorTemplateElement; + const templateModalElement = templateElement.cloneNode(true); + const closeElement = templateModalElement.querySelector(`.${status}__button`); + + if (message) { + templateModalElement.querySelector(`.${status}__title`).textContnet = message; + } + + closeElement.addEventListener('click', () => { + removeSendInfoModal(); + }); + + return templateModalElement; +}; + +function renderSendInfoModal(status, message) { + modalElement = createSendInfoModal(status, message); + currentStatus = status; + bodyElement.append(modalElement); + bodyElement.addEventListener('keydown', onBodyKeydown); + bodyElement.addEventListener('click', onBodyClick); +} + +function removeSendInfoModal() { + modalElement.remove(); + bodyElement.removeEventListener('keydown', onBodyKeydown); + bodyElement.removeEventListener('click', onBodyClick); +} + +export { renderSendInfoModal }; diff --git a/js/photo-modal.js b/js/photo-modal.js index f4b6539..224db64 100644 --- a/js/photo-modal.js +++ b/js/photo-modal.js @@ -1,4 +1,4 @@ -import { findElementById, isEscKey } from './utils.js'; +import { isEscKey } from './utils.js'; import { renderComments } from './render-comments.js'; const COMMENTS_CHUNK_SIZE = 5; @@ -65,24 +65,11 @@ function closePhotoModal() { bodyElement.classList.remove('modal-open'); } -const initPhotoModal = (photos, container) => { +const registerPhotoModalEvents = () => { if (!modalElement) { return; } - container.addEventListener('click', (evt) => { - const targetThumbnailElement = evt.target.closest('.picture'); - - if (targetThumbnailElement) { - evt.preventDefault(); - - const targetThumbnailElementId = Number(targetThumbnailElement.dataset.id); - const targetPhoto = findElementById(targetThumbnailElementId, photos); - - openPhotoModal(targetPhoto); - } - }); - modalCommentsLoaderElement.addEventListener('click', () => { renderNextComments(); }); @@ -92,4 +79,4 @@ const initPhotoModal = (photos, container) => { }); }; -export { initPhotoModal }; +export { registerPhotoModalEvents, openPhotoModal }; diff --git a/js/utils.js b/js/utils.js index 10f7c3e..846372b 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,52 +1,5 @@ -const getRandomInteger = (a, b) => { - const lower = Math.ceil(Math.min(a, b)); - const upper = Math.floor(Math.max(a, b)); - const result = Math.random() * (upper - lower + 1) + lower; - return Math.floor(result); -}; - -const getRandomArrayElement = (elements) => elements[getRandomInteger(0, elements.length - 1)]; - -const getRandomArrayElements = (elements, maxItems) => { - const usedElements = []; - - const iterations = getRandomInteger(1, maxItems); - - if (usedElements.length >= iterations) { - return; - } - - for (let i = 1; i <= iterations; i++) { - let currentElement = getRandomArrayElement(elements); - - while (usedElements.includes(currentElement)) { - currentElement = getRandomArrayElement(elements); - } - - usedElements.push(currentElement); - } - - return usedElements.join(' '); -}; - -const createRandomIdGenetrator = (a, b) => { - const values = []; - - return () => { - let currentValue = getRandomInteger(a, b); - - if (values.length >= Math.max(a, b) - Math.min(a, b) + 1) { - window.console.error('Перебраны все значения из диапазона'); - return null; - } - while (values.includes(currentValue)) { - currentValue = getRandomInteger(a, b); - } - values.push(currentValue); - - return currentValue; - }; -}; +const ALERT_SHOW_TIME = 5000; +const bodyElement = document.querySelector('body'); const findTemplateById = (id) => { const template = document.querySelector(`#${id}`); @@ -66,4 +19,19 @@ const isEscKey = (evt) => evt.key === 'Escape'; const findElementById = (id, array) => array.find((element) => element.id === id); -export { getRandomInteger, getRandomArrayElement, getRandomArrayElements, createRandomIdGenetrator, findTemplateById, isEscKey, findElementById }; +const showAlertTemporarily = (template, message) => { + const templateElement = template.cloneNode(true); + + if (message) { + templateElement.querySelector('.data-error__title').textContnet = message; + } + + bodyElement.append(templateElement); + + setTimeout(() => { + templateElement.remove(); + }, ALERT_SHOW_TIME); +}; + + +export { findTemplateById, isEscKey, findElementById, showAlertTemporarily }; From af54b731d55d65258ed7ceb690808324b299688a Mon Sep 17 00:00:00 2001 From: Stepan Vasilyev Date: Sat, 3 Jan 2026 13:26:02 +0500 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B8=D1=82=20=D0=BA=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/info-modal-send.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/js/info-modal-send.js b/js/info-modal-send.js index 663cc9e..1797226 100644 --- a/js/info-modal-send.js +++ b/js/info-modal-send.js @@ -21,13 +21,13 @@ const onBodyKeydown = (evt) => { } }; -const createSendInfoModal = (status, message) => { - const templateElement = status === 'success' ? successTemplateElement : errorTemplateElement; +const createSendInfoModal = (message) => { + const templateElement = currentStatus === 'success' ? successTemplateElement : errorTemplateElement; const templateModalElement = templateElement.cloneNode(true); - const closeElement = templateModalElement.querySelector(`.${status}__button`); + const closeElement = templateModalElement.querySelector(`.${currentStatus}__button`); if (message) { - templateModalElement.querySelector(`.${status}__title`).textContnet = message; + templateModalElement.querySelector(`.${currentStatus}__title`).textContnet = message; } closeElement.addEventListener('click', () => { @@ -38,8 +38,8 @@ const createSendInfoModal = (status, message) => { }; function renderSendInfoModal(status, message) { - modalElement = createSendInfoModal(status, message); currentStatus = status; + modalElement = createSendInfoModal(message); bodyElement.append(modalElement); bodyElement.addEventListener('keydown', onBodyKeydown); bodyElement.addEventListener('click', onBodyClick);