diff --git a/index.html b/index.html index b6f7d35..6c543ab 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,9 @@ + + + Кекстаграм @@ -30,7 +33,7 @@

Фотографии других

Загрузка фотографии

-
+
@@ -116,10 +119,10 @@

Загрузка фотограф
- +
- +
@@ -234,5 +237,7 @@

Не удалось загрузить данны

+ + diff --git a/js/api.js b/js/api.js new file mode 100644 index 0000000..addb914 --- /dev/null +++ b/js/api.js @@ -0,0 +1,24 @@ +const BASE_URL = 'https://32.javascript.htmlacademy.pro/kekstagram'; +const API_ROUTE = { + GET: `${BASE_URL}/data`, + POST: `${BASE_URL}/`, +}; + + +const getData = (successCallback, errorCallback) => fetch(API_ROUTE.GET) + .then((response) => { + if (!response.ok) { + throw new Error(`Ошибка сети: ${response.status} ${response.statusText}`); + } + return response.json(); + }) + .then(successCallback) + .catch(errorCallback); + +const sendData = (formData) => + fetch(API_ROUTE.POST, { + method: 'POST', + body: formData, + }); + +export { getData, sendData }; diff --git a/js/filters.js b/js/filters.js new file mode 100644 index 0000000..e1476b3 --- /dev/null +++ b/js/filters.js @@ -0,0 +1,59 @@ +import { sortArrayDescending, shuffleArray, debounce } from './util.js'; +import { renderThumbnailListWithRetry } from './render-thumbnails.js'; + +const RERENDER_DELAY = 500; +const PICTURE_COUNT = 10; + +const imgFilters = document.querySelector('.img-filters'); + +const clearThumbnailList = () => { + const thumbnails = document.querySelectorAll('.picture'); + thumbnails.forEach((element) => { + element.remove(); + }); +}; + +const changeThumbnailList = (evt, data) => { + const filterActions = { + 'filter-default': () => data, + 'filter-random': () => shuffleArray(data.slice()).slice(0, PICTURE_COUNT), + 'filter-discussed': () => sortArrayDescending(data.slice(), (item) => item.comments.length), + }; + + const filterButton = Object.keys(filterActions).find((filter) => evt.target.closest(`#${filter}`)); + + if (filterButton) { + clearThumbnailList(); + const filteredPhotoData = filterActions[filterButton](); + renderThumbnailListWithRetry(filteredPhotoData); + } +}; + +const setFiltersClick = (data) => { + const handleThumbnailChange = debounce((evt) => { + changeThumbnailList(evt, data); + }, RERENDER_DELAY); + + imgFilters.addEventListener('click', (evt) => { + if (evt.target.classList.contains('img-filters__button')) { + const buttons = imgFilters.querySelectorAll('.img-filters__button'); + const button = evt.target.closest('.img-filters__button'); + + if ( + (button && !button.classList.contains('img-filters__button--active')) + ) { + buttons.forEach((btn) => btn.classList.remove('img-filters__button--active')); + button.classList.add('img-filters__button--active'); + + handleThumbnailChange(evt); + } + } + }); +}; + +const showFilters = (data) => { + imgFilters.classList.remove('img-filters--inactive'); + setFiltersClick(data); +}; + +export { showFilters }; diff --git a/js/form-img-upload-scale.js b/js/form-img-upload-scale.js new file mode 100644 index 0000000..77178c4 --- /dev/null +++ b/js/form-img-upload-scale.js @@ -0,0 +1,32 @@ +const imgUploadScale = document.querySelector('.img-upload__scale'); +const controlValue = imgUploadScale.querySelector('.scale__control--value'); +const imgUploadPreview = document.querySelector('.img-upload__preview img'); + +const STEP = 25; + +const updateImageScale = (formStep) => { + const value = parseInt(controlValue.value.slice(0, -1), 10); + let scaleValue = value + formStep; + + if (scaleValue < 25) { + scaleValue = 25; + } else if (scaleValue > 100) { + scaleValue = 100; + } + + const transformValue = `scale(${scaleValue / 100})`; + imgUploadPreview.style.transform = transformValue; + controlValue.value = `${scaleValue}%`; +}; + +const onFormClickScaleButtons = (evt) => { + const clickedElementClassList = evt.target.classList; + + if (clickedElementClassList.contains('scale__control--bigger')) { + updateImageScale(STEP); + } else if (clickedElementClassList.contains('scale__control--smaller')) { + updateImageScale(-STEP); + } +}; + +export { onFormClickScaleButtons, updateImageScale }; diff --git a/js/form-img-upload-sending-data.js b/js/form-img-upload-sending-data.js new file mode 100644 index 0000000..e6302e9 --- /dev/null +++ b/js/form-img-upload-sending-data.js @@ -0,0 +1,114 @@ +import { validateHashtags, getErrorText, validateDescription } from './form-img-upload-validate.js'; +import { isEscapeKey } from './util.js'; +import { sendData } from './api.js'; + +const form = document.querySelector('.img-upload__form'); +const formImgUploadOverlay = form.querySelector('.img-upload__overlay'); +const templateSuccess = document.querySelector('#success'); +const templateError = document.querySelector('#error'); +const submitButton = form.querySelector('.img-upload__submit'); + +let notificationElement; +let notificationCancel; +let isResponseError; + +const pristine = new Pristine(form, { + classTo: 'img-upload__field-wrapper', + errorClass: 'img-upload__field-wrapper--error', + errorTextParent: 'img-upload__field-wrapper', +}); + +pristine.addValidator(form.querySelector('.text__hashtags'), validateHashtags, getErrorText); +pristine.addValidator(form.querySelector('.text__description'), validateDescription, getErrorText); + +const resetPristin = () => { + pristine.reset(); +}; + + +const closeNotification = () => { + if (isResponseError) { + formImgUploadOverlay.classList.remove('hidden'); + } + if (notificationCancel) { + notificationCancel.removeEventListener('click', onNotificationClickCancel); + notificationElement.removeEventListener('click', onNotificationClickElsewhere); + } + document.removeEventListener('keydown', onNotificationEsc); + if (notificationElement) { + notificationElement.remove(); + notificationElement = null; + } +}; + +const showNotification = (isError) => { + const clone = document.importNode(isError ? templateError.content : templateSuccess.content, true); + + if (notificationElement) { + document.body.removeChild(notificationElement); + } + + notificationElement = document.createElement('div'); + notificationElement.appendChild(clone); + document.body.appendChild(notificationElement); + + notificationCancel = document.querySelector(isError ? '.error__button' : '.success__button'); + notificationCancel.addEventListener('click', onNotificationClickCancel); + document.addEventListener('keydown', onNotificationEsc); + notificationElement.addEventListener('click', onNotificationClickElsewhere); +}; + +const blockSubmitButton = () => { + submitButton.disabled = true; + submitButton.textContent = 'Публикую'; +}; + +const unblockSubmitButton = () => { + submitButton.disabled = false; + submitButton.textContent = 'Опубликовать'; +}; + +const setUserFormSubmit = (onSuccess) => { + form.addEventListener('submit', async (evt) => { + evt.preventDefault(); + + const isValid = pristine.validate(); + if (!isValid) { + return; + } + + const formData = new FormData(evt.target); + blockSubmitButton(); + + try { + const response = await sendData(formData); + isResponseError = !response.ok; + showNotification(isResponseError); + onSuccess(isResponseError); + } finally { + unblockSubmitButton(); + } + }); +}; + +function onNotificationClickCancel (evt) { + evt.preventDefault(); + closeNotification(); +} + +function onNotificationClickElsewhere (evt) { + if (!evt.target.closest('.error__inner') && isResponseError) { + closeNotification(); + } else if (!evt.target.closest('.success__inner') && !isResponseError) { + closeNotification(); + } +} + +function onNotificationEsc (evt){ + if (isEscapeKey(evt)) { + evt.preventDefault(); + closeNotification(); + } +} + +export { setUserFormSubmit, resetPristin }; diff --git a/js/form-img-upload-slider.js b/js/form-img-upload-slider.js new file mode 100644 index 0000000..edb20e4 --- /dev/null +++ b/js/form-img-upload-slider.js @@ -0,0 +1,78 @@ +const filtersData = { + none: { range: { min: 0, max: 1 }, step: 0.1 }, + chrome: { range: { min: 0, max: 1 }, step: 0.1 }, + sepia: { range: { min: 0, max: 1 }, step: 0.1 }, + marvin: { range: { min: 0, max: 100 }, step: 1 }, + phobos: { range: { min: 0, max: 3 }, step: 0.1 }, + heat: { range: { min: 1, max: 3 }, step: 0.1 } +}; + +let currentFilter; + +const formImgUploadWrapper = document.querySelector('.img-upload__wrapper'); +const slider = formImgUploadWrapper.querySelector('.img-upload__effect-level'); +const levelSlider = formImgUploadWrapper.querySelector('.effect-level__slider'); +const levelValue = formImgUploadWrapper.querySelector('.effect-level__value'); +const imgUploadPreview = formImgUploadWrapper.querySelector('.img-upload__preview img'); +const noneRadioButton = formImgUploadWrapper.querySelector('#effect-none'); + +const changeFilter = (value) => { + switch (currentFilter) { + case 'none': imgUploadPreview.style.filter = ''; break; + case 'chrome': imgUploadPreview.style.filter = `grayscale(${value})`; break; + case 'sepia': imgUploadPreview.style.filter = `sepia(${value})`; break; + case 'marvin': imgUploadPreview.style.filter = `invert(${value}%)`; break; + case 'phobos': imgUploadPreview.style.filter = `blur(${value}px)`; break; + case 'heat': imgUploadPreview.style.filter = `brightness(${value})`; break; + } +}; + +noUiSlider.create(levelSlider, { + range: { + min: filtersData.none.range.min, + max: filtersData.none.range.max + }, + start: filtersData.none.range.max, + step: filtersData.none.step, + connect: 'lower', + format: { + to: (value) => (Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)), + from: (value) => parseFloat(value) + } +}); + +levelSlider.noUiSlider.on('update', () => { + levelValue.value = levelSlider.noUiSlider.get(); + changeFilter(levelValue.value); +}); + +levelSlider.setAttribute('disabled', true); + +const resetFilter = () => { + currentFilter = 'none'; + noneRadioButton.checked = true; + levelSlider.noUiSlider.set(0); + levelSlider.setAttribute('disabled', true); +}; + +const onFormClickFilter = (evt) => { + slider.classList.remove('hidden'); + const clickedElementId = evt.target.id.split('-')[1]; + if (clickedElementId !== 'none') { + levelSlider.removeAttribute('disabled'); + currentFilter = clickedElementId; + levelSlider.noUiSlider.updateOptions({ + range: { + min: filtersData[clickedElementId].range.min, + max: filtersData[clickedElementId].range.max + }, + start: filtersData[clickedElementId].range.max, + step: filtersData[clickedElementId].step + }); + } else { + slider.classList.add('hidden'); + resetFilter(); + } +}; + +export { onFormClickFilter, resetFilter }; diff --git a/js/form-img-upload-validate.js b/js/form-img-upload-validate.js new file mode 100644 index 0000000..358d461 --- /dev/null +++ b/js/form-img-upload-validate.js @@ -0,0 +1,62 @@ +import { hasDuplicates } from './util.js'; + +const PATTERN = /^#[a-zа-яё0-9]+$/; +const HASHTAGS_MAX = 5; +let errorText = ''; + +const validateHashtags = (value) => { + errorText = ''; + + if (value === '') { + return true; + } + + const hashtagsString = value.trim().toLowerCase(); + const hashtags = hashtagsString.split(/\s+/).filter(Boolean); + + if (hashtags.length > HASHTAGS_MAX) { + errorText = `Нельзя указать больше ${HASHTAGS_MAX} хэштегов`; + return false; + } + + if (hasDuplicates(hashtags)) { + errorText = 'Хэштеги не должны повторяться'; + return false; + } + + for (const hashtag of hashtags) { + if (!hashtag.startsWith('#')) { + errorText = 'Хэштег начинается с символа #'; + return false; + } + if (!PATTERN.test(hashtag)) { + errorText = 'Хэштег должен состоять из букв и чисел'; + return false; + } + if (hashtag === '#') { + errorText = 'Хеш-тег не может состоять только из одной решётки'; + return false; + } + if (hashtag.length > 20) { + errorText = 'Максимальная длина хэштега - 20 символов'; + return false; + } + } + + return true; +}; + +const validateDescription = (value) => { + errorText = ''; + + if (value.length > 140) { + errorText = 'Длина комментария не более 140 символов'; + return false; + } + + return true; +}; + +const getErrorText = () => errorText; + +export { validateHashtags, getErrorText, validateDescription }; diff --git a/js/form-img-upload.js b/js/form-img-upload.js new file mode 100644 index 0000000..311ab86 --- /dev/null +++ b/js/form-img-upload.js @@ -0,0 +1,121 @@ +import { isEscapeKey } from './util.js'; +import { onFormClickScaleButtons, updateImageScale } from './form-img-upload-scale.js'; +import { onFormClickFilter, resetFilter } from './form-img-upload-slider.js'; +import { resetPristin } from './form-img-upload-sending-data.js'; + +const form = document.querySelector('.img-upload__form'); +const formImgUploadInput = form.querySelector('.img-upload__input'); + +const formImgUploadOverlay = form.querySelector('.img-upload__overlay'); +const formImgUploadWrapper = formImgUploadOverlay.querySelector('.img-upload__wrapper'); +const formImgPreviewContainer = formImgUploadWrapper.querySelector('.img-upload__preview-container'); +const formImgUploadPreview = formImgPreviewContainer.querySelector('.img-upload__preview'); +const formImgUploadCancel = formImgUploadOverlay.querySelector('.img-upload__cancel'); +const formImgUploadScale = formImgUploadWrapper.querySelector('.img-upload__scale'); +const imgUploadEffects = formImgUploadWrapper.querySelector('.img-upload__effects'); +const slider = formImgUploadWrapper.querySelector('.img-upload__effect-level'); +const previews = formImgUploadWrapper.querySelectorAll('.effects__preview'); + + +const showError = () => { + const existingError = document.querySelector('.data-error'); + if (existingError) { + existingError.remove(); + } + + const errorTemplate = document.getElementById('data-error'); + const clone = document.importNode(errorTemplate.content, true); + const errorElement = document.createElement('div'); + errorElement.appendChild(clone); + document.body.appendChild(errorElement); + const errorText = document.body.querySelector('.data-error__title'); + errorText.textContent = 'Не верный формат файла. Загружайте: .jpg / .jpeg / .png'; + + setTimeout(() => { + errorElement.remove(); + }, 2000); +}; + +const openForm = () => { + const file = formImgUploadInput.files[0]; + const fileName = file ? file.name.toLowerCase() : ''; + + if (file && (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.png'))) { + const fileURL = URL.createObjectURL(file); + formImgUploadPreview.querySelector('img').src = fileURL; + + previews.forEach((preview) => { + preview.style.backgroundImage = `url(${fileURL})`; + preview.style.backgroundSize = 'cover'; + }); + + document.body.classList.add('modal-open'); + formImgUploadOverlay.classList.remove('hidden'); + slider.classList.add('hidden'); + + formImgUploadCancel.addEventListener('click', onFormClickCancel); + document.addEventListener('keydown', onFormEsc); + + formImgUploadScale.addEventListener('click', onFormClickScaleButtons); + imgUploadEffects.addEventListener('change', onFormClickFilter); + } else { + showError(); + formImgUploadInput.value = ''; + } +}; + +const resetForm = () => { + form.reset(); + formImgUploadPreview.querySelector('img').src = 'img/upload-default-image.jpg'; + previews.forEach((preview) => { + preview.style.backgroundImage = ''; + preview.style.backgroundSize = ''; + }); + resetFilter(); + updateImageScale(100); + resetPristin(); +}; + +const closeForm = (isError) => { + document.body.classList.remove('modal-open'); + formImgUploadOverlay.classList.add('hidden'); + + if (!isError) { + formImgUploadCancel.removeEventListener('click', onFormClickCancel); + document.removeEventListener('keydown', onFormEsc); + formImgUploadScale.removeEventListener('click', onFormClickScaleButtons); + imgUploadEffects.removeEventListener('change', onFormClickFilter); + + resetForm(); + } +}; + +function onFormEsc (evt) { + + if (isEscapeKey(evt) && !formImgUploadOverlay.classList.contains('hidden')) { + + const hashtagsInput = form.querySelector('.text__hashtags'); + const descriptionInput = form.querySelector('.text__description'); + + if (document.activeElement === hashtagsInput || document.activeElement === descriptionInput) { + + evt.preventDefault(); + return; + } + + evt.preventDefault(); + closeForm(); + } +} + +function onFormClickCancel (evt) { + evt.preventDefault(); + closeForm(); +} + +formImgUploadInput.addEventListener('change', (evt) => { + evt.preventDefault(); + openForm(); +}); + +export { closeForm }; diff --git a/js/main.js b/js/main.js index e69de29..fef65ca 100644 --- a/js/main.js +++ b/js/main.js @@ -0,0 +1,18 @@ +import { getData } from './api.js'; +import { closeForm } from './form-img-upload.js'; +import { setUserFormSubmit } from './form-img-upload-sending-data.js'; +import { renderThumbnailListWithRetry, showError, showFilters, setThumbnailsClick } from './render-thumbnails.js'; + + +getData( + (data) => { + renderThumbnailListWithRetry(data); + setThumbnailsClick(data); + showFilters(data); + }, + () => { + showError(); + } +); + +setUserFormSubmit(closeForm); diff --git a/js/render-big-picture.js b/js/render-big-picture.js new file mode 100644 index 0000000..bb273d5 --- /dev/null +++ b/js/render-big-picture.js @@ -0,0 +1,55 @@ +import { isEscapeKey } from './util.js'; +import { renderFirstsComments, renderMoreComments } from './render-comments.js'; + +const modalBigPhoto = document.querySelector('.big-picture'); +const bigPhoto = modalBigPhoto.querySelector('.big-picture__img'); +const bigPictureSocial = modalBigPhoto.querySelector('.big-picture__social'); +const commentsLoader = bigPictureSocial.querySelector('.comments-loader'); + +const findPhotoObject = (pictureId, photoData) => + photoData.find((item) => item.id === parseInt(pictureId, 10)); + +const onModalClickLoadMore = () => renderMoreComments(); + +const openModalBigPhoto = (pictureId, photoData) => { + modalBigPhoto.classList.remove('hidden'); + document.body.classList.add('modal-open'); + + document.addEventListener('keydown', onModalKeydownEsc); + modalBigPhoto.addEventListener('click', onModalClickElsewhere); + commentsLoader.addEventListener('click', onModalClickLoadMore); + + const photoObject = findPhotoObject(pictureId, photoData); + + bigPhoto.querySelector('img').src = photoObject.url; + bigPhoto.querySelector('img').alt = photoObject.description; + bigPictureSocial.querySelector('.likes-count').textContent = photoObject.likes; + bigPictureSocial.querySelector('.social__comment-total-count').textContent = photoObject.comments.length; + bigPictureSocial.querySelector('.social__caption').textContent = photoObject.description; + + renderFirstsComments(photoObject.comments); +}; + +const closeModalBigPhoto = () => { + modalBigPhoto.classList.add('hidden'); + document.body.classList.remove('modal-open'); + + document.removeEventListener('keydown', onModalKeydownEsc); + modalBigPhoto.removeEventListener('click', onModalClickElsewhere); + commentsLoader.removeEventListener('click', onModalClickLoadMore); +}; + +function onModalKeydownEsc (evt){ + if (isEscapeKey(evt)) { + evt.preventDefault(); + closeModalBigPhoto(); + } +} + +function onModalClickElsewhere (evt) { + if (!evt.target.closest('.big-picture__preview')) { + closeModalBigPhoto(); + } +} + +export { openModalBigPhoto, closeModalBigPhoto }; diff --git a/js/render-comments.js b/js/render-comments.js new file mode 100644 index 0000000..9d38df2 --- /dev/null +++ b/js/render-comments.js @@ -0,0 +1,57 @@ +const COMMENTS_COUNT = 5; +let comments = []; + +const bigPictureSocial = document.querySelector('.big-picture__social'); +const socialComments = bigPictureSocial.querySelector('.social__comments'); +const commentsTemplate = socialComments.querySelector('.social__comment'); +const commentsLoader = bigPictureSocial.querySelector('.comments-loader'); + +const commentsFragment = document.createDocumentFragment(); + +const createCommentsFragment = (index) => { + const commentsElement = commentsTemplate.cloneNode(true); + commentsElement.querySelector('img').src = comments[index].avatar; + commentsElement.querySelector('img').alt = comments[index].name; + commentsElement.querySelector('.social__text').textContent = comments[index].message; + commentsFragment.appendChild(commentsElement); +}; + +const renderSocialCommentShownCount = () => { + const commentsShown = socialComments.children.length; + bigPictureSocial.querySelector('.social__comment-shown-count').textContent = commentsShown; +}; + +const renderCommentsLoader = () => { + const commentsTotal = comments.length; + const commentsShown = socialComments.children.length; + commentsLoader.classList.remove('hidden'); + + if (commentsShown >= commentsTotal) { + commentsLoader.classList.add('hidden'); + } +}; + +const renderFirstsComments = (commentsArray) => { + socialComments.innerHTML = ''; + comments = commentsArray; + + for (let i = 0; i < COMMENTS_COUNT && i < comments.length; i++) { + createCommentsFragment(i); + } + socialComments.appendChild(commentsFragment); + renderSocialCommentShownCount(); + renderCommentsLoader(); +}; + +const renderMoreComments = () => { + const lastIndex = socialComments.children.length; + + for (let i = lastIndex; i < lastIndex + COMMENTS_COUNT && i < comments.length; i++) { + createCommentsFragment(i); + } + socialComments.appendChild(commentsFragment); + renderSocialCommentShownCount(); + renderCommentsLoader(); +}; + +export { renderFirstsComments, renderMoreComments }; diff --git a/js/render-thumbnails.js b/js/render-thumbnails.js new file mode 100644 index 0000000..3b9913d --- /dev/null +++ b/js/render-thumbnails.js @@ -0,0 +1,73 @@ +import { openModalBigPhoto, closeModalBigPhoto } from './render-big-picture.js'; +import { showFilters } from './filters.js'; + +const thumbnailContainer = document.querySelector('.pictures'); +const modalBigPicture = document.querySelector('.big-picture'); +const bigPictureCancel = modalBigPicture.querySelector('.big-picture__cancel'); +const thumbnailTemplate = document.querySelector('#picture') + .content + .querySelector('.picture'); + +const renderThumbnailList = (data) => { + const photoListFragment = document.createDocumentFragment(); + + data.forEach((element) => { + const photoElement = thumbnailTemplate.cloneNode(true); + photoElement.dataset.pictureId = element.id; + photoElement.querySelector('img').src = element.url; + photoElement.querySelector('img').alt = element.description; + photoElement.querySelector('.picture__likes').textContent = element.likes; + photoElement.querySelector('.picture__comments').textContent = element.comments.length; + photoListFragment.appendChild(photoElement); + }); + + thumbnailContainer.appendChild(photoListFragment); +}; + +const renderThumbnailListWithRetry = (data) => { + const attemptRender = (retries) => { + try { + renderThumbnailList(data); + } catch (error) { + if (retries > 0) { + setTimeout(() => { + attemptRender(retries - 1); + }, 500); + } else { + throw new Error('Не удалось выполнить renderThumbnailList'); + } + } + }; + + attemptRender(2); +}; + +const setThumbnailsClick = (data) => { + thumbnailContainer.addEventListener('click', (evt) => { + const clickedThumbnail = evt.target.closest('.picture'); + + if (clickedThumbnail) { + evt.preventDefault(); + openModalBigPhoto(clickedThumbnail.dataset.pictureId, data); + } + }); + + bigPictureCancel.addEventListener('click', () => { + closeModalBigPhoto(); + }); +}; + +const showError = () => { + const errorTemplate = document.getElementById('data-error'); + const clone = document.importNode(errorTemplate.content, true); + const errorElement = document.createElement('div'); + errorElement.appendChild(clone); + document.body.appendChild(errorElement); + + setTimeout(() => { + errorElement.remove(); + }, 5000); +}; + + +export { renderThumbnailListWithRetry, showError, showFilters, setThumbnailsClick }; diff --git a/js/util.js b/js/util.js new file mode 100644 index 0000000..3799527 --- /dev/null +++ b/js/util.js @@ -0,0 +1,37 @@ +const getRandomNumber = (from, to) => { + const lower = Math.ceil(Math.min(from, to)); + const upper = Math.floor(Math.max(from, to)); + const result = Math.random() * (upper - lower + 1) + lower; + return Math.floor(result); +}; + +const isEscapeKey = (evt) => evt.keyCode === 27; + +const hasDuplicates = (array) => new Set(array).size !== array.length; + +const shuffleArray = (array) => { + for (let i = array.length - 1; i > 0; i--) { + const j = getRandomNumber(0, i + 1); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +}; + +const sortArrayDescending = (array, compareFunction) => + [...array].sort((a, b) => compareFunction(b) - compareFunction(a)); + +const debounce = (cb, timeoutDelay) => { + let timeoutId; + return (...rest) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => cb.apply(this, rest), timeoutDelay); + }; +}; + +export { + isEscapeKey, + hasDuplicates, + shuffleArray, + sortArrayDescending, + debounce +}; diff --git a/package.json b/package.json index 230ab63..b3d70be 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "kekstagram", + "type": "module", "version": "30.0.0", "private": true, "description": "Личный проект «Кекстаграм» от HTML Academy",