From 1207a3ed626f58853e6e4d779c649b7ca58311cf Mon Sep 17 00:00:00 2001 From: Noa <144594801+NoaVel@users.noreply.github.com> Date: Wed, 22 Apr 2026 01:10:40 +0300 Subject: [PATCH 1/3] feat(upload): implement image upload form with hashtag validation --- index.html | 4 +- js/check-hashtag-validity.js | 30 +++++++++ js/main.js | 2 + js/upload-photo-form.js | 114 +++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 js/check-hashtag-validity.js create mode 100644 js/upload-photo-form.js diff --git a/index.html b/index.html index a1aea97..88f6628 100644 --- a/index.html +++ b/index.html @@ -30,7 +30,7 @@

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

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

-
+
@@ -233,7 +233,7 @@

Изображение успешно загруже

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

- + diff --git a/js/check-hashtag-validity.js b/js/check-hashtag-validity.js new file mode 100644 index 0000000..5ebbc31 --- /dev/null +++ b/js/check-hashtag-validity.js @@ -0,0 +1,30 @@ +const MAX_HASHTAG_COUNT = 5; +const MAX_HASHTAG_LENGTH = 20; +const HASHTAG_REGEXP = /^#[a-zа-яё0-9]+$/i; + +const normalizeHashtags = (value) => value + .trim() + .toLowerCase() + .split(/\s+/) + .filter((hashtag) => hashtag.length > 0); + +const hasValidHashtagCount = (value) => + normalizeHashtags(value).length <= MAX_HASHTAG_COUNT; + +const hasValidHashtagLength = (value) => + normalizeHashtags(value).every((hashtag) => hashtag.length <= MAX_HASHTAG_LENGTH); + +const hasValidHashtagFormat = (value) => + normalizeHashtags(value).every((hashtag) => HASHTAG_REGEXP.test(hashtag)); + +const hasUniqueHashtags = (value) => { + const hashtags = normalizeHashtags(value); + return hashtags.length === new Set(hashtags).size; +}; + +export { + hasValidHashtagCount, + hasValidHashtagLength, + hasValidHashtagFormat, + hasUniqueHashtags +}; diff --git a/js/main.js b/js/main.js index b9e0f5c..ce58f80 100644 --- a/js/main.js +++ b/js/main.js @@ -1,5 +1,7 @@ import { photos } from './create-array-photos.js'; import { renderThumbnails } from './render-thumbnails.js'; +import { initUploadPhotoForm } from './upload-photo-form.js'; renderThumbnails(photos); +initUploadPhotoForm(); diff --git a/js/upload-photo-form.js b/js/upload-photo-form.js new file mode 100644 index 0000000..57034b7 --- /dev/null +++ b/js/upload-photo-form.js @@ -0,0 +1,114 @@ +import { + hasValidHashtagCount, + hasValidHashtagLength, + hasValidHashtagFormat, + hasUniqueHashtags +} from './check-hashtag-validity.js'; + +const MAX_COMMENT_LENGTH = 140; + +const bodyNode = document.body; +const uploadFormNode = document.querySelector('.img-upload__form'); +const uploadFileControl = uploadFormNode.querySelector('#upload-file'); +const uploadOverlayNode = uploadFormNode.querySelector('.img-upload__overlay'); +const uploadCancelButtonNode = uploadFormNode.querySelector('#upload-cancel'); +const hashtagsInputNode = uploadFormNode.querySelector('.text__hashtags'); +const descriptionInputNode = uploadFormNode.querySelector('.text__description'); + +const pristine = new Pristine(uploadFormNode, { + classTo: 'img-upload__field-wrapper', + errorTextParent: 'img-upload__field-wrapper', + errorTextTag: 'div', + errorTextClass: 'img-upload__field-wrapper--error' +}); + +const isEscapeKey = (evt) => evt.key === 'Escape'; + +const hasValidCommentLength = (value) => value.length <= MAX_COMMENT_LENGTH; + +const resetUploadPhotoForm = () => { + uploadFormNode.reset(); + uploadFileControl.value = ''; + pristine.reset(); +}; + +const stopEscapePropagation = (evt) => { + if (isEscapeKey(evt)) { + evt.stopPropagation(); + } +}; + +const closeUploadPhotoForm = () => { + uploadOverlayNode.classList.add('hidden'); + bodyNode.classList.remove('modal-open'); + document.removeEventListener('keydown', onDocumentKeydown); + resetUploadPhotoForm(); +}; + +const onDocumentKeydown = (evt) => { + if (isEscapeKey(evt)) { + evt.preventDefault(); + closeUploadPhotoForm(); + } +}; + +const openUploadPhotoForm = () => { + uploadOverlayNode.classList.remove('hidden'); + bodyNode.classList.add('modal-open'); + document.addEventListener('keydown', onDocumentKeydown); +}; + +const onUploadFileControlChange = () => { + openUploadPhotoForm(); +}; + +const onUploadCancelButtonClick = (evt) => { + evt.preventDefault(); + closeUploadPhotoForm(); +}; + +const onUploadFormSubmit = (evt) => { + if (!pristine.validate()) { + evt.preventDefault(); + } +}; + +pristine.addValidator( + hashtagsInputNode, + hasValidHashtagCount, + 'Нельзя указать больше пяти хэштегов' +); + +pristine.addValidator( + hashtagsInputNode, + hasValidHashtagLength, + 'Максимальная длина одного хэштега — 20 символов' +); + +pristine.addValidator( + hashtagsInputNode, + hasValidHashtagFormat, + 'Хэштег должен начинаться с # и содержать только буквы и цифры' +); + +pristine.addValidator( + hashtagsInputNode, + hasUniqueHashtags, + 'Хэштеги не должны повторяться' +); + +pristine.addValidator( + descriptionInputNode, + hasValidCommentLength, + 'Длина комментария не может составлять больше 140 символов' +); + +const initUploadPhotoForm = () => { + uploadFileControl.addEventListener('change', onUploadFileControlChange); + uploadCancelButtonNode.addEventListener('click', onUploadCancelButtonClick); + uploadFormNode.addEventListener('submit', onUploadFormSubmit); + hashtagsInputNode.addEventListener('keydown', stopEscapePropagation); + descriptionInputNode.addEventListener('keydown', stopEscapePropagation); +}; + +export { initUploadPhotoForm }; From 79f7dff09590a8778428c2d6b27c3455c26908dd Mon Sep 17 00:00:00 2001 From: Noa <144594801+NoaVel@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:04:19 +0300 Subject: [PATCH 2/3] refactor(upload): make escape handler lint-friendly --- js/upload-photo-form.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/js/upload-photo-form.js b/js/upload-photo-form.js index 57034b7..2f97ffb 100644 --- a/js/upload-photo-form.js +++ b/js/upload-photo-form.js @@ -38,17 +38,13 @@ const stopEscapePropagation = (evt) => { } }; -const closeUploadPhotoForm = () => { - uploadOverlayNode.classList.add('hidden'); - bodyNode.classList.remove('modal-open'); - document.removeEventListener('keydown', onDocumentKeydown); - resetUploadPhotoForm(); -}; - const onDocumentKeydown = (evt) => { if (isEscapeKey(evt)) { evt.preventDefault(); - closeUploadPhotoForm(); + uploadOverlayNode.classList.add('hidden'); + bodyNode.classList.remove('modal-open'); + document.removeEventListener('keydown', onDocumentKeydown); + resetUploadPhotoForm(); } }; @@ -58,6 +54,13 @@ const openUploadPhotoForm = () => { document.addEventListener('keydown', onDocumentKeydown); }; +const closeUploadPhotoForm = () => { + uploadOverlayNode.classList.add('hidden'); + bodyNode.classList.remove('modal-open'); + document.removeEventListener('keydown', onDocumentKeydown); + resetUploadPhotoForm(); +}; + const onUploadFileControlChange = () => { openUploadPhotoForm(); }; From f97d4ec0f51eb676473322f522fcf94deeca70d1 Mon Sep 17 00:00:00 2001 From: Noa <144594801+NoaVel@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:15:59 +0300 Subject: [PATCH 3/3] refactor(full-size): make big picture handlers lint-friendly --- js/full-size-picture.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/js/full-size-picture.js b/js/full-size-picture.js index 12036b3..2a4e316 100644 --- a/js/full-size-picture.js +++ b/js/full-size-picture.js @@ -7,6 +7,16 @@ const likesCountNode = bigPictureNode.querySelector('.likes-count'); const commentsCaptionNode = bigPictureNode.querySelector('.social__caption'); const bigPictureCancelNode = bigPictureNode.querySelector('.big-picture__cancel'); +const onEscKeydown = (evt) => { + if (evt.key === 'Escape') { + evt.preventDefault(); + clearComments(); + bigPictureNode.classList.add('hidden'); + document.body.classList.remove('modal-open'); + document.removeEventListener('keydown', onEscKeydown); + } +}; + const closeBigPicture = () => { clearComments(); bigPictureNode.classList.add('hidden'); @@ -14,13 +24,6 @@ const closeBigPicture = () => { document.removeEventListener('keydown', onEscKeydown); }; -const onEscKeydown = (evt) => { - if (evt.key === 'Escape') { - evt.preventDefault(); - closeBigPicture(); - } -}; - const onBigPictureCancelClick = (evt) => { evt.preventDefault(); closeBigPicture();