From c406fcdef9b0e997bfa2009bf2648a30168c2d8f Mon Sep 17 00:00:00 2001 From: Origami Date: Mon, 6 Apr 2026 18:43:35 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BB=D0=BE=D0=B0=D0=B4=D0=B5=D1=80=D1=8B=20=D0=B8=20=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BB=D0=B0=D0=B9?= =?UTF-8?q?=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- JS modules/addComment.js | 32 ++++--- JS modules/likeHandler.js | 15 +++- JS modules/main.js | 2 + JS modules/state.js | 53 +++++++----- JS modules/utils.js | 8 ++ JS modules/view.js | 2 +- styles.css | 174 +++++++++++++++++++++++++++----------- 7 files changed, 202 insertions(+), 84 deletions(-) diff --git a/JS modules/addComment.js b/JS modules/addComment.js index 85174a73d2..90dc98fb62 100644 --- a/JS modules/addComment.js +++ b/JS modules/addComment.js @@ -3,24 +3,32 @@ import { sanitize } from './utils.js'; import { renderComments } from './view.js'; export function initAddComment({ nameInput, textInput, button, listRoot }) { - button.addEventListener('click', async () => { + button.addEventListener('click', () => { const nameValue = sanitize(nameInput.value); const textValue = sanitize(textInput.value); if (!nameValue || !textValue) { alert('Напиши что-нибудь!'); return; } + button.disabled = true; - try { - await saveComment({ name: nameValue, text: textValue }); - await loadComments(); - renderComments(listRoot, getComments()); - nameInput.value = ''; - textInput.value = ''; - } catch (e) { - alert(e.message); - } finally { - button.disabled = false; - } + button.textContent = 'Отправляем...'; + + saveComment({ name: nameValue, text: textValue }) + .then(() => { + return loadComments(); + }) + .then(() => { + renderComments(listRoot, getComments()); + nameInput.value = ''; + textInput.value = ''; + }) + .catch((e) => { + alert(e.message); + }) + .finally(() => { + button.disabled = false; + button.textContent = 'Написать'; + }); }); } diff --git a/JS modules/likeHandler.js b/JS modules/likeHandler.js index 5159d3e03c..f3c74f594c 100644 --- a/JS modules/likeHandler.js +++ b/JS modules/likeHandler.js @@ -1,6 +1,7 @@ // Лайки -import { toggleLike, getComments } from './state.js'; +import { toggleLike, setLikeLoading, getComments } from './state.js'; import { renderComments } from './view.js'; +import { delay } from './utils.js'; export function initLikeHandler({ listRoot }) { listRoot.addEventListener('click', (e) => { @@ -8,7 +9,17 @@ export function initLikeHandler({ listRoot }) { if (!likeBtn) return; const i = Number(likeBtn.dataset.index); if (Number.isNaN(i)) return; - toggleLike(i); + + const comments = getComments(); + if (comments[i].isLikeLoading) return; + + setLikeLoading(i, true); renderComments(listRoot, getComments()); + + delay(2000).then(() => { + toggleLike(i); + setLikeLoading(i, false); + renderComments(listRoot, getComments()); + }); }); } diff --git a/JS modules/main.js b/JS modules/main.js index bccb7fc1dc..9c4cab6bc3 100644 --- a/JS modules/main.js +++ b/JS modules/main.js @@ -13,6 +13,8 @@ initAddComment({ nameInput, textInput, button, listRoot }); initLikeHandler({ listRoot }); initQuoteHandler({ listRoot, textInput }); +listRoot.innerHTML = '
Загружаем комментарии...
'; + loadComments().then(() => { renderComments(listRoot, getComments()); }); diff --git a/JS modules/state.js b/JS modules/state.js index be5da294cf..46d06d559f 100644 --- a/JS modules/state.js +++ b/JS modules/state.js @@ -4,32 +4,45 @@ let comments = []; export const getComments = () => comments.slice(); -export const loadComments = async () => { - const res = await fetch(API_URL); - const data = await res.json(); - comments = data.comments.map((c) => ({ - name: c.author.name, - text: c.text, - date: new Date(c.date).toLocaleString('ru-RU'), - likes: c.likes, - isLiked: false, // лайки локальные, сбрасываем при загрузке - })); -}; +export function loadComments() { + return fetch(API_URL) + .then((res) => { + return res.json(); + }) + .then((data) => { + comments = data.comments.map((c) => ({ + name: c.author.name, + text: c.text, + date: new Date(c.date).toLocaleString('ru-RU'), + likes: c.likes, + isLiked: false, + isLikeLoading: false, + })); + }); +} -export const saveComment = async ({ name, text }) => { - const res = await fetch(API_URL, { +export function saveComment({ name, text }) { + return fetch(API_URL, { method: 'POST', body: JSON.stringify({ name, text }), + }).then((res) => { + if (!res.ok) { + return res.json().then((err) => { + throw new Error(err.error || 'Ошибка сервера'); + }); + } }); - if (!res.ok) { - const err = await res.json(); - throw new Error(err.error || 'Ошибка сервера'); - } -}; +} -export const toggleLike = (index) => { +export function toggleLike(index) { const c = comments[index]; if (!c) return; c.isLiked = !c.isLiked; c.likes += c.isLiked ? 1 : -1; -}; +} + +export function setLikeLoading(index, value) { + const c = comments[index]; + if (!c) return; + c.isLikeLoading = value; +} diff --git a/JS modules/utils.js b/JS modules/utils.js index 5b5312af3c..608b2b18b0 100644 --- a/JS modules/utils.js +++ b/JS modules/utils.js @@ -21,3 +21,11 @@ export function sanitize(str) { .replaceAll('>', '>') .trim(); } + +export function delay(interval = 300) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, interval); + }); +} diff --git a/JS modules/view.js b/JS modules/view.js index 2f559216a7..e0e3526f3f 100644 --- a/JS modules/view.js +++ b/JS modules/view.js @@ -15,7 +15,7 @@ export function renderComments(rootEl, comments) { ${item.likes} + }${item.isLikeLoading ? ' -loading-like' : ''}" data-index="${index}"> `, diff --git a/styles.css b/styles.css index edc120f8cd..3f990cb181 100644 --- a/styles.css +++ b/styles.css @@ -1,8 +1,8 @@ body { margin: 0; - } +} - .container { +.container { font-family: Helvetica; color: #ffffff; display: flex; @@ -12,117 +12,117 @@ body { padding-bottom: 200px; background: #202020; min-height: 100vh; - } +} - .comments, - .comment { +.comments, +.comment { margin: 0; padding: 0; list-style: none; - } +} - .comment, - .add-form { +.comment, +.add-form { width: 596px; box-sizing: border-box; background: radial-gradient( - 75.42% 75.42% at 50% 42.37%, - rgba(53, 53, 53, 0) 22.92%, - #7334ea 100% + 75.42% 75.42% at 50% 42.37%, + rgba(53, 53, 53, 0) 22.92%, + #7334ea 100% ); filter: drop-shadow(0px 20px 67px rgba(0, 0, 0, 0.08)); border-radius: 20px; - } +} - .comments { +.comments { display: flex; flex-direction: column; gap: 24px; - } +} - .comment { +.comment { padding: 48px; - } +} - .comment-header { +.comment-header { font-size: 16px; display: flex; justify-content: space-between; - } +} - .comment-footer { +.comment-footer { display: flex; justify-content: flex-end; - } +} - .comment-body { +.comment-body { margin-top: 32px; margin-bottom: 32px; - } +} - .comment-text { +.comment-text { font-size: 32px; - } +} - .likes { +.likes { display: flex; align-items: center; - } +} - .like-button { +.like-button { all: unset; cursor: pointer; - } +} - .likes-counter { +.likes-counter { font-size: 26px; margin-right: 8px; - } +} - .like-button { +.like-button { margin-left: 10px; background-image: url("data:image/svg+xml,%3Csvg width='22' height='20' viewBox='0 0 22 20' fill='none' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath d='M11.11 16.9482L11 17.0572L10.879 16.9482C5.654 12.2507 2.2 9.14441 2.2 5.99455C2.2 3.81471 3.85 2.17984 6.05 2.17984C7.744 2.17984 9.394 3.26975 9.977 4.75204H12.023C12.606 3.26975 14.256 2.17984 15.95 2.17984C18.15 2.17984 19.8 3.81471 19.8 5.99455C19.8 9.14441 16.346 12.2507 11.11 16.9482ZM15.95 0C14.036 0 12.199 0.882834 11 2.26703C9.801 0.882834 7.964 0 6.05 0C2.662 0 0 2.6267 0 5.99455C0 10.1035 3.74 13.4714 9.405 18.5613L11 20L12.595 18.5613C18.26 13.4714 22 10.1035 22 5.99455C22 2.6267 19.338 0 15.95 0Z' fill='%23BCEC30' /%3E%3C/svg%3E"); background-repeat: no-repeat; width: 22px; height: 22px; - } +} - .-active-like { +.-active-like { background-image: url("data:image/svg+xml,%3Csvg width='22' height='20' viewBox='0 0 22 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.95 0C14.036 0 12.199 0.882834 11 2.26703C9.801 0.882834 7.964 0 6.05 0C2.662 0 0 2.6267 0 5.99455C0 10.1035 3.74 13.4714 9.405 18.5613L11 20L12.595 18.5613C18.26 13.4714 22 10.1035 22 5.99455C22 2.6267 19.338 0 15.95 0Z' fill='%23BCEC30'/%3E%3C/svg%3E"); - } +} - .add-form { +.add-form { padding: 20px; margin-top: 48px; display: flex; flex-direction: column; - } +} - .add-form-name, - .add-form-text { +.add-form-name, +.add-form-text { font-size: 16px; font-family: Helvetica; border-radius: 8px; border: none; - } +} - .add-form-name { +.add-form-name { width: 300px; padding: 11px 22px; - } +} - .add-form-text { +.add-form-text { margin-top: 12px; padding: 22px; resize: none; - } +} - .add-form-row { +.add-form-row { display: flex; justify-content: flex-end; - } +} - .add-form-button { +.add-form-button { margin-top: 24px; font-size: 24px; padding: 10px 20px; @@ -130,8 +130,84 @@ body { border: none; border-radius: 18px; cursor: pointer; - } +} - .add-form-button:hover { +.add-form-button:hover { opacity: 0.9; - } \ No newline at end of file +} + +@keyframes rotating { + from { + transform: rotate(0deg); + } + 25% { + transform: rotate(30deg); + } + 75% { + transform: rotate(-30deg); + } + to { + transform: rotate(0deg); + } +} + +.-loading-like { + animation: rotating 2s linear infinite; +} + +.loading { + font-size: 24px; + text-align: center; + padding: 40px; + color: #bcec30; +} + +.add-form { + padding: 20px; + margin-top: 48px; + display: flex; + flex-direction: column; +} + +.add-form-name, +.add-form-text { + font-size: 16px; + font-family: Helvetica; + border-radius: 8px; + border: none; +} + +.add-form-name { + width: 300px; + padding: 11px 22px; +} + +.add-form-text { + margin-top: 12px; + padding: 22px; + resize: none; +} + +.add-form-row { + display: flex; + justify-content: flex-end; +} + +.add-form-button { + margin-top: 24px; + font-size: 24px; + padding: 10px 20px; + background-color: #bcec30; + border: none; + border-radius: 18px; + cursor: pointer; +} + +.add-form-button:hover { + opacity: 0.9; +} + +.add-form-button:disabled { + opacity: 0.6; + cursor: default; +} From 5153e92009c36488260a845f02d44c1c95401b1e Mon Sep 17 00:00:00 2001 From: Origami Date: Sun, 26 Apr 2026 20:44:14 +0300 Subject: [PATCH 2/3] HW ERRORS --- JS modules/addComment.js | 44 ++++++++++++++++++++++++++++------------ JS modules/state.js | 16 ++++++++++++--- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/JS modules/addComment.js b/JS modules/addComment.js index 90dc98fb62..4a2a171361 100644 --- a/JS modules/addComment.js +++ b/JS modules/addComment.js @@ -1,13 +1,10 @@ -import { saveComment, loadComments, getComments } from './state.js'; -import { sanitize } from './utils.js'; -import { renderComments } from './view.js'; - export function initAddComment({ nameInput, textInput, button, listRoot }) { - button.addEventListener('click', () => { - const nameValue = sanitize(nameInput.value); - const textValue = sanitize(textInput.value); - if (!nameValue || !textValue) { - alert('Напиши что-нибудь!'); + const handlePostClick = (retryCount = 0) => { + const nameValue = nameInput.value.trim(); + const textValue = textInput.value.trim(); + + if (nameValue.length < 3 || textValue.length < 3) { + alert('Имя и комментарий должны быть не короче 3 символов'); return; } @@ -15,20 +12,41 @@ export function initAddComment({ nameInput, textInput, button, listRoot }) { button.textContent = 'Отправляем...'; saveComment({ name: nameValue, text: textValue }) - .then(() => { - return loadComments(); - }) + .then(() => loadComments()) .then(() => { renderComments(listRoot, getComments()); nameInput.value = ''; textInput.value = ''; }) .catch((e) => { + if (e.message === 'Failed to fetch') { + alert('Кажется, у вас сломался интернет, попробуйте позже'); + return; + } + + if (e.message === 'Ошибка сервера') { + if (retryCount < 3) { + setTimeout(() => { + handlePostClick(retryCount + 1); + }, 1000); + } else { + alert('Сервер сломался, попробуй позже'); + } + return; + } + + if (e.message === 'Ошибка запроса') { + alert('Ошибка запроса'); + return; + } + alert(e.message); }) .finally(() => { button.disabled = false; button.textContent = 'Написать'; }); - }); + }; + + button.addEventListener('click', () => handlePostClick()); } diff --git a/JS modules/state.js b/JS modules/state.js index 46d06d559f..20e9c30ef5 100644 --- a/JS modules/state.js +++ b/JS modules/state.js @@ -7,6 +7,12 @@ export const getComments = () => comments.slice(); export function loadComments() { return fetch(API_URL) .then((res) => { + if (!res.ok) { + if (res.status >= 500) { + throw new Error('Ошибка сервера'); + } + throw new Error('Ошибка запроса'); + } return res.json(); }) .then((data) => { @@ -27,9 +33,13 @@ export function saveComment({ name, text }) { body: JSON.stringify({ name, text }), }).then((res) => { if (!res.ok) { - return res.json().then((err) => { - throw new Error(err.error || 'Ошибка сервера'); - }); + if (res.status >= 500) { + throw new Error('Ошибка сервера'); + } + if (res.status === 400) { + throw new Error('Ошибка запроса'); + } + throw new Error('Неизвестная ошибка'); } }); } From edba8c3fa6bdee5e1ebe43c4654b2f70b68d7019 Mon Sep 17 00:00:00 2001 From: Origami Date: Sun, 26 Apr 2026 20:58:46 +0300 Subject: [PATCH 3/3] add auth --- JS modules/addComment.js | 11 +++---- JS modules/loginView.js | 16 ++++++++++ JS modules/main.js | 64 ++++++++++++++++++++++++++++++++-------- JS modules/state.js | 43 ++++++++++++++++++--------- 4 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 JS modules/loginView.js diff --git a/JS modules/addComment.js b/JS modules/addComment.js index 4a2a171361..9014733e46 100644 --- a/JS modules/addComment.js +++ b/JS modules/addComment.js @@ -1,21 +1,22 @@ +import { saveComment, loadComments, getComments } from './state.js'; +import { renderComments } from './view.js'; + export function initAddComment({ nameInput, textInput, button, listRoot }) { const handlePostClick = (retryCount = 0) => { - const nameValue = nameInput.value.trim(); const textValue = textInput.value.trim(); - if (nameValue.length < 3 || textValue.length < 3) { - alert('Имя и комментарий должны быть не короче 3 символов'); + if (textValue.length < 3) { + alert('Комментарий должен быть не короче 3 символов'); return; } button.disabled = true; button.textContent = 'Отправляем...'; - saveComment({ name: nameValue, text: textValue }) + saveComment({ text: textValue }) .then(() => loadComments()) .then(() => { renderComments(listRoot, getComments()); - nameInput.value = ''; textInput.value = ''; }) .catch((e) => { diff --git a/JS modules/loginView.js b/JS modules/loginView.js new file mode 100644 index 0000000000..56e2d66b6a --- /dev/null +++ b/JS modules/loginView.js @@ -0,0 +1,16 @@ +export function renderLogin(root, onLogin) { + root.innerHTML = ` +
+ + + +
+ `; + + document.getElementById('login-btn').addEventListener('click', () => { + const login = document.getElementById('login').value; + const password = document.getElementById('password').value; + + onLogin({ login, password }); + }); +} diff --git a/JS modules/main.js b/JS modules/main.js index 9c4cab6bc3..ad304ba27c 100644 --- a/JS modules/main.js +++ b/JS modules/main.js @@ -1,20 +1,58 @@ -import { getComments, loadComments } from './state.js'; +import { getComments, loadComments, login, getUser } from './state.js'; import { renderComments } from './view.js'; import { initAddComment } from './addComment.js'; -import { initLikeHandler } from './likeHandler.js'; -import { initQuoteHandler } from './quoteHandler.js'; +import { renderLogin } from './loginView.js'; -const nameInput = document.getElementById('name'); -const textInput = document.getElementById('comment'); -const button = document.getElementById('button'); const listRoot = document.querySelector('.comments'); +const appRoot = document.querySelector('.container'); +const form = document.querySelector('.add-form'); -initAddComment({ nameInput, textInput, button, listRoot }); -initLikeHandler({ listRoot }); -initQuoteHandler({ listRoot, textInput }); +function renderApp() { + listRoot.innerHTML = '
Загружаем...
'; -listRoot.innerHTML = '
Загружаем комментарии...
'; + loadComments() + .then(() => { + renderComments(listRoot, getComments()); -loadComments().then(() => { - renderComments(listRoot, getComments()); -}); + if (!getUser()) { + form.style.display = 'none'; + + const link = document.createElement('div'); + link.innerHTML = + 'Чтобы добавить комментарий, авторизуйтесь'; + + link.addEventListener('click', (e) => { + e.preventDefault(); + renderLogin(appRoot, handleLogin); + }); + + appRoot.appendChild(link); + } else { + form.style.display = 'block'; + + const nameInput = document.getElementById('name'); + const textInput = document.getElementById('comment'); + const button = document.getElementById('button'); + + nameInput.value = getUser().name; + nameInput.setAttribute('readonly', true); + + initAddComment({ nameInput, textInput, button, listRoot }); + } + }) + .catch(() => { + alert('Ошибка загрузки'); + }); +} + +function handleLogin({ login: userLogin, password }) { + login({ login: userLogin, password }) + .then(() => { + location.reload(); + }) + .catch(() => { + alert('Неверные данные'); + }); +} + +renderApp(); diff --git a/JS modules/state.js b/JS modules/state.js index 20e9c30ef5..f702451e6b 100644 --- a/JS modules/state.js +++ b/JS modules/state.js @@ -1,16 +1,17 @@ -const API_URL = 'https://wedev-api.sky.pro/api/v1/origami/comments'; +const API_URL = 'https://wedev-api.sky.pro/api/v2/origami/comments'; let comments = []; +let token = null; +let user = null; export const getComments = () => comments.slice(); +export const getUser = () => user; export function loadComments() { return fetch(API_URL) .then((res) => { if (!res.ok) { - if (res.status >= 500) { - throw new Error('Ошибка сервера'); - } + if (res.status >= 500) throw new Error('Ошибка сервера'); throw new Error('Ошибка запроса'); } return res.json(); @@ -27,23 +28,37 @@ export function loadComments() { }); } -export function saveComment({ name, text }) { +export function saveComment({ text }) { return fetch(API_URL, { method: 'POST', - body: JSON.stringify({ name, text }), + headers: { + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ text }), }).then((res) => { if (!res.ok) { - if (res.status >= 500) { - throw new Error('Ошибка сервера'); - } - if (res.status === 400) { - throw new Error('Ошибка запроса'); - } - throw new Error('Неизвестная ошибка'); + if (res.status >= 500) throw new Error('Ошибка сервера'); + if (res.status === 400) throw new Error('Ошибка запроса'); + throw new Error('Ошибка'); } }); } +export function login({ login, password }) { + return fetch('https://wedev-api.sky.pro/api/v2/origami/login', { + method: 'POST', + body: JSON.stringify({ login, password }), + }) + .then((res) => { + if (!res.ok) throw new Error('Неверные данные'); + return res.json(); + }) + .then((data) => { + token = data.user.token; + user = data.user; + }); +} + export function toggleLike(index) { const c = comments[index]; if (!c) return; @@ -55,4 +70,4 @@ export function setLikeLoading(index, value) { const c = comments[index]; if (!c) return; c.isLikeLoading = value; -} +} \ No newline at end of file