Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ https://glebkaf.github.io/webdev-dom-homework/
## Как разрабатывать

Открой index.html в браузере

19 changes: 14 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@
</head>

<body>
<div class="container">
<div id="app-container">
</div>
<script type="module" src="js/index.js"></script>
</body>

<!-- <body>
<div class="container"> -->
<!-- Список комментариев -->
<ul class="comments"></ul>
<!-- <ul class="comments"></ul> -->

<!-- Лоадер загрузки комментариев -->
<!-- <div class="comments-loading hidden">Пожалуйста подождите комментарии загружаются...</div> -->

<!-- Форма добавления нового комментария -->
<div class="add-form">
<!-- <div class="add-form">
<input type="text" class="add-form-name" placeholder="Введите ваше имя" />
<textarea
type="textarea"
class="add-form-text"
placeholder="Введите ваш коментарий"
placeholder="Введите ваш комментарий"
rows="4"
></textarea>
<div class="add-form-row">
Expand All @@ -26,5 +35,5 @@
</div>
</div>
<script type="module" src="js/index.js"></script>
</body>
</body> -->
</html>
Binary file added js/.DS_Store
Binary file not shown.
121 changes: 82 additions & 39 deletions js/addComment.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,96 @@
// js/addComment.js

import { comments } from './comments.js'
import { safeSymbol } from './safeSymbol.js'
import { renderComments } from './renderComments.js'
import {
sendComment,
loadComments,
OFFLINE_ERROR,
VALIDATION_ERROR,
SERVER_ERROR,
UNAUTHORIZED,
} from './api.js'
import { getUser } from './userState.js'

export async function addComment(commentInput) {
const user = getUser()
if (!user) {
alert('Для добавления комментария требуется авторизация.')
return
}

const commentText = safeSymbol(commentInput.value.trim())

// ДОБАВЛЕНИЕ НОВЫХ КОММЕНТАРИЕВ
export function addComment(nameInput, commentInput) {
const name = safeSymbol(nameInput.value.trim())
const comment = safeSymbol(commentInput.value.trim())
let hasError = false

// ВАЛИДАЦИЯ
if (!name) {
nameInput.value = ''
nameInput.placeholder = 'Введите имя!'
nameInput.classList.add('error')
hasError = true
} else {
nameInput.classList.remove('error')
nameInput.placeholder = 'Введите ваше имя'
if (commentText.length < 3) {
alert('Комментарий должен быть не короче 3 символов')
return
}

if (!comment) {
const formEl = document.querySelector('#add-comment-form')
const sendingEl = document.querySelector('.comments-loading')

// Скрываем форму
if (formEl) formEl.style.visibility = 'hidden'
if (formEl) formEl.style.pointerEvents = 'none'

sendingEl.textContent = 'Комментарий добавляется...'
sendingEl.classList.remove('hidden')

try {
// Отправляем. Передаем только текст, имя берется с сервера.
await sendComment({ text: commentText })

// V2 API не возвращает новый комментарий, поэтому перезагружаем весь список!
const newComments = await loadComments()

// Удаляем старый массив и загружаем новый
comments.length = 0
newComments.forEach((c) => {
comments.push({
id: c.id,
name: c.author?.name || c.name || 'Аноним',
text: c.text,
date: new Date(c.date).toLocaleString(),
likes: c.likes || 0,
isLiked: c.isLiked || false,
isLikeLoading: false,
})
})

renderComments()

// Если ок чистим форму
commentInput.value = ''
commentInput.placeholder = 'Введите комментарий!'
commentInput.classList.add('error')
hasError = true
} else {
commentInput.classList.remove('error')
commentInput.placeholder = 'Введите ваш комментарий'
}
} catch (error) {
console.error(error)

if (hasError) return
if (error.message === OFFLINE_ERROR) {
alert('Кажется, у вас отключен интернет, попробуйте позже')
return
}

// ДОБАВЛЕНИЕ НОВОГО КОММЕНТАРИЯ В МАССИВ
const now = new Date()
const pad = (n) => String(n).padStart(2, '0')
const date = `${pad(now.getDate())}.${pad(now.getMonth() + 1)}.${String(now.getFullYear()).slice(-2)} ${pad(now.getHours())}:${pad(now.getMinutes())}`
if (error.message === SERVER_ERROR) {
alert('Сервер сломался, попробуй позже')
return
}

comments.push({
name,
date,
text: comment,
likes: 0,
isLiked: false,
})
if (error.message === VALIDATION_ERROR) {
alert('Текст комментария не прошел валидацию.')
return
}

renderComments()
if (error.message === UNAUTHORIZED) {
alert('Ошибка авторизации. Войдите снова.')
return
}

nameInput.value = ''
commentInput.value = ''
nameInput.classList.remove('error')
commentInput.classList.remove('error')
alert('Произошла ошибка: ' + error.message)
} finally {
// Возврат формы в рабочее состояние
if (formEl) formEl.style.visibility = 'visible'
if (formEl) formEl.style.pointerEvents = 'auto'
sendingEl.classList.add('hidden')
}
}
139 changes: 139 additions & 0 deletions js/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { getUser } from './userState.js'

// --- API CONFIG V2
const commentsHost = 'https://wedev-api.sky.pro/api/v2'
const userHost = 'https://wedev-api.sky.pro/api/user'
const personalKey = 'max-parf'
export const COMMENTS_URL = `${commentsHost}/${personalKey}/comments`

// --- КОНСТАНТЫ ОШИБОК
export const OFFLINE_ERROR = 'OFFLINE_ERROR'
export const VALIDATION_ERROR = 'VALIDATION_ERROR'
export const BAD_REQUEST = 'BAD_REQUEST'
export const SERVER_ERROR = 'SERVER_ERROR'
export const UNKNOWN_ERROR = 'UNKNOWN_ERROR'
export const UNAUTHORIZED = 'UNAUTHORIZED' // 401 ошибка

// Хелпер для авторизованных запросов
const getAuthHeaders = () => {
const token = getUser()?.token
return token ? { Authorization: `Bearer ${token}` } : {}
}

// УТИЛИТА RETRY ТОЛЬКО ДЛЯ 500
async function requestWithRetry(requestFn, retries = 3) {
try {
return await requestFn()
} catch (error) {
// Повторяем только при ошибке сервера
if (error.message === SERVER_ERROR && retries > 0) {
return await requestWithRetry(requestFn, retries - 1)
}
throw error
}
}

// --- АВТОРИЗАЦИЯ И РЕГИСТРАЦИЯ ---

// POST — Логин
export async function login({ login, password }) {
const response = await fetch(`${userHost}/login`, {
method: 'POST',
body: JSON.stringify({ login, password }),
})

// Если 400, ищем причину
if (response.status === 400) {
const errorBody = await response.json()
// Выбрасываем точный текст ошибки, если он есть
throw new Error(errorBody.error || BAD_REQUEST)
}
if (!response.ok) {
throw new Error(UNKNOWN_ERROR)
}
return response.json()
}

// POST — Регистрация
export async function register({ login, name, password }) {
const response = await fetch(`${userHost}`, {
method: 'POST',
body: JSON.stringify({ login, name, password }),
})

// Если 400, ищем причину
if (response.status === 400) {
const errorBody = await response.json()
// Выбрасываем точный текст ошибки, если он есть
throw new Error(errorBody.error || BAD_REQUEST)
}
if (!response.ok) {
throw new Error(UNKNOWN_ERROR)
}
return response.json()
}

// --- КОММЕНТАРИИ ---

// GET — загрузка комментариев (v2)
export async function loadComments() {
const response = await fetch(COMMENTS_URL)

if (!navigator.onLine) throw new Error(OFFLINE_ERROR)
if (response.status === 500) throw new Error(SERVER_ERROR)
if (!response.ok) throw new Error(UNKNOWN_ERROR)

const data = await response.json()
return data.comments
}

// POST — добавление комментария (v2, нужен токен, принимаем только text)
export async function sendComment({ text }) {
return requestWithRetry(async () => {
try {
const response = await fetch(COMMENTS_URL, {
method: 'POST',
headers: {
// ВОТ ОНО!!! ЗЛО - Content-Type, API его отклоняет
// 'Content-Type': 'application/json',
...getAuthHeaders(),
},
body: JSON.stringify({ text }),
})

if (response.status === 401) throw new Error(UNAUTHORIZED)

if (response.status === 400) {
// Читаем тело ответа, ищем точную причину
const errorBody = await response.json()
throw new Error(errorBody.error || VALIDATION_ERROR)
}

if (response.status === 500) throw new Error(SERVER_ERROR)

if (!response.ok) throw new Error(UNKNOWN_ERROR)

return { result: 'ok' }
} catch (e) {
if (e.message.startsWith('Failed to fetch') || e instanceof TypeError) {
console.warn('Network error:', e)
throw new Error(OFFLINE_ERROR)
}
throw e
}
})
}
// POST — Переключить лайк (v2, нужен токен)
export async function toggleLikeAPI(commentId) {
const response = await fetch(`${COMMENTS_URL}/${commentId}/toggle-like`, {
method: 'POST',
headers: {
...getAuthHeaders(),
},
})

if (response.status === 401) throw new Error(UNAUTHORIZED)
if (!response.ok) throw new Error(UNKNOWN_ERROR)

return response.json()
}
19 changes: 2 additions & 17 deletions js/comments.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,2 @@
// МАССИВ КОММЕНТАРИЕВ
export const comments = [
{
name: 'Глеб Фокин',
date: '12.02.22 12:18',
text: 'Это будет первый комментарий на этой странице',
likes: 3,
isLiked: false,
},
{
name: 'Варвара Н.',
date: '13.02.22 19:22',
text: 'Мне нравится как оформлена эта страница! ❤',
likes: 75,
isLiked: true,
},
]
// Глобальный массив комментариев, который будет заполнен данными с сервера.
export const comments = []
4 changes: 4 additions & 0 deletions js/delay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// иммитация задержки лайка
export function delay(interval = 300) {
return new Promise((resolve) => setTimeout(resolve, interval))
}
15 changes: 12 additions & 3 deletions js/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { initHandlers } from './initHandlers.js'
import { loadUserFromLocalStorage, getUser } from './userState.js'
import { renderMainPage } from './renderApp.js'
import { fetchAndRenderMainPage } from './initHandlers.js'

// ТОЧКА ВХОДА
document.addEventListener('DOMContentLoaded', () => {
initHandlers()
document.addEventListener('DOMContentLoaded', async () => {
// 1. Загрузка пользователя из LocalStorage
loadUserFromLocalStorage()

// 2. Рендеринг основной структуры (Показывает форму или ссылку)
renderMainPage(!!getUser())

// 3. Загрузка данных и финальный рендеринг списка
await fetchAndRenderMainPage()
})
Loading