diff --git a/JS modules/addComment.js b/JS modules/addComment.js index 85174a73d..9014733e4 100644 --- a/JS modules/addComment.js +++ b/JS modules/addComment.js @@ -1,26 +1,53 @@ 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', async () => { - const nameValue = sanitize(nameInput.value); - const textValue = sanitize(textInput.value); - if (!nameValue || !textValue) { - alert('Напиши что-нибудь!'); + const handlePostClick = (retryCount = 0) => { + const textValue = textInput.value.trim(); + + if (textValue.length < 3) { + alert('Комментарий должен быть не короче 3 символов'); 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({ text: textValue }) + .then(() => loadComments()) + .then(() => { + renderComments(listRoot, getComments()); + 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/likeHandler.js b/JS modules/likeHandler.js index 5159d3e03..f3c74f594 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/loginView.js b/JS modules/loginView.js new file mode 100644 index 000000000..56e2d66b6 --- /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 bccb7fc1d..ad304ba27 100644 --- a/JS modules/main.js +++ b/JS modules/main.js @@ -1,18 +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 = '
Загружаем...
'; -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 be5da294c..f702451e6 100644 --- a/JS modules/state.js +++ b/JS modules/state.js @@ -1,35 +1,73 @@ -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 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) => { + if (!res.ok) { + if (res.status >= 500) throw new Error('Ошибка сервера'); + throw new Error('Ошибка запроса'); + } + 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({ 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.ok) { - const err = await res.json(); - throw new Error(err.error || 'Ошибка сервера'); - } -}; +} -export const toggleLike = (index) => { +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; 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; +} \ No newline at end of file diff --git a/JS modules/utils.js b/JS modules/utils.js index 5b5312af3..608b2b18b 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 2f559216a..e0e3526f3 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 edc120f8c..3f990cb18 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; +}