diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..40b878db5b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000000..0fe5deb5b9 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,2 @@ +tabWidth: 4 +singleQuote: true diff --git a/JS modules/addComment.js b/JS modules/addComment.js new file mode 100644 index 0000000000..85174a73d2 --- /dev/null +++ b/JS modules/addComment.js @@ -0,0 +1,26 @@ +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('Напиши что-нибудь!'); + 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; + } + }); +} diff --git a/JS modules/likeHandler.js b/JS modules/likeHandler.js new file mode 100644 index 0000000000..5159d3e03c --- /dev/null +++ b/JS modules/likeHandler.js @@ -0,0 +1,14 @@ +// Лайки +import { toggleLike, getComments } from './state.js'; +import { renderComments } from './view.js'; + +export function initLikeHandler({ listRoot }) { + listRoot.addEventListener('click', (e) => { + const likeBtn = e.target.closest('.like-button'); + if (!likeBtn) return; + const i = Number(likeBtn.dataset.index); + if (Number.isNaN(i)) return; + toggleLike(i); + renderComments(listRoot, getComments()); + }); +} diff --git a/JS modules/main.js b/JS modules/main.js new file mode 100644 index 0000000000..bccb7fc1dc --- /dev/null +++ b/JS modules/main.js @@ -0,0 +1,18 @@ +import { getComments, loadComments } from './state.js'; +import { renderComments } from './view.js'; +import { initAddComment } from './addComment.js'; +import { initLikeHandler } from './likeHandler.js'; +import { initQuoteHandler } from './quoteHandler.js'; + +const nameInput = document.getElementById('name'); +const textInput = document.getElementById('comment'); +const button = document.getElementById('button'); +const listRoot = document.querySelector('.comments'); + +initAddComment({ nameInput, textInput, button, listRoot }); +initLikeHandler({ listRoot }); +initQuoteHandler({ listRoot, textInput }); + +loadComments().then(() => { + renderComments(listRoot, getComments()); +}); diff --git a/JS modules/quoteHandler.js b/JS modules/quoteHandler.js new file mode 100644 index 0000000000..f0866709b3 --- /dev/null +++ b/JS modules/quoteHandler.js @@ -0,0 +1,21 @@ +// Цитирование по клику +export function initQuoteHandler({ listRoot, textInput }) { + listRoot.addEventListener('click', (e) => { + const item = e.target.closest('.comment'); + if (!item || !listRoot.contains(item)) return; + + const author = + item + .querySelector('.comment-header > div:first-child') + ?.textContent.trim() ?? ''; + const text = + item.querySelector('.comment-text')?.textContent.trim() ?? ''; + const quote = `@${author}: "${text}" `; + textInput.value = quote; + textInput.focus(); + textInput.setSelectionRange( + textInput.value.length, + textInput.value.length, + ); + }); +} diff --git a/JS modules/state.js b/JS modules/state.js new file mode 100644 index 0000000000..be5da294cf --- /dev/null +++ b/JS modules/state.js @@ -0,0 +1,35 @@ +const API_URL = 'https://wedev-api.sky.pro/api/v1/origami/comments'; + +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 const saveComment = async ({ name, text }) => { + const res = await fetch(API_URL, { + method: 'POST', + body: JSON.stringify({ name, text }), + }); + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || 'Ошибка сервера'); + } +}; + +export const toggleLike = (index) => { + const c = comments[index]; + if (!c) return; + c.isLiked = !c.isLiked; + c.likes += c.isLiked ? 1 : -1; +}; diff --git a/JS modules/utils.js b/JS modules/utils.js new file mode 100644 index 0000000000..5b5312af3c --- /dev/null +++ b/JS modules/utils.js @@ -0,0 +1,23 @@ +// Время и экранирование +export function nowRu() { + const now = new Date(); + const dateStr = now.toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: '2-digit', + }); + const timeStr = now.toLocaleTimeString('ru-RU', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + return `${dateStr} ${timeStr}`; +} + +export function sanitize(str) { + return String(str) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .trim(); +} diff --git a/JS modules/view.js b/JS modules/view.js new file mode 100644 index 0000000000..2f559216a7 --- /dev/null +++ b/JS modules/view.js @@ -0,0 +1,24 @@ +// Рендер списка +export function renderComments(rootEl, comments) { + const items = comments.map( + (item, index) => ` +