diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..702b76a --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": ["eslint:recommended"], + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "warn", + "no-console": "warn", + "prefer-const": "error" + } +} \ No newline at end of file diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..a813c65 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false +} \ No newline at end of file diff --git a/index.html b/index.html index 3582f49..caf9a76 100644 --- a/index.html +++ b/index.html @@ -7,43 +7,16 @@ -
- +
+
+
Загрузка комментариев...
+ + +
@@ -61,224 +34,6 @@
+ - - \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..a3567cf --- /dev/null +++ b/main.js @@ -0,0 +1,3 @@ +import { initApp } from './modules/event-handlers.js'; + +document.addEventListener('DOMContentLoaded', initApp); \ No newline at end of file diff --git a/modules/api.js b/modules/api.js new file mode 100644 index 0000000..f60ab6d --- /dev/null +++ b/modules/api.js @@ -0,0 +1,90 @@ +import { delay } from './utils.js'; +import { getUserToken } from './auth.js'; + +const host = "https://wedev-api.sky.pro/api/v2/dmitriy-skachkov"; + +export function loadComments() { + return fetch(host + "/comments") + .then((response) => { + if (!response.ok) { + if (response.status >= 500) { + throw new Error('SERVER_ERROR'); + } + throw new Error('Ошибка загрузки комментариев'); + } + return response.json(); + }) + .then((data) => { + return data.comments.map(comment => { + return { + id: comment.id, + name: comment.author.name, + date: new Date(comment.date), + text: comment.text, + likes: comment.likes, + isLiked: false, + isLikeLoading: false + }; + }); + }) + .catch((error) => { + if (error.message === 'Failed to fetch') { + throw new Error('NETWORK_ERROR'); + } + throw error; + }); +} + +export const postComment = (text, forceError = false, retryCount = 0) => { + const token = getUserToken(); + + if (!token) { + return Promise.reject(new Error('Пользователь не авторизован')); + } + + const bodyData = forceError + ? JSON.stringify({ + text: text, + forceError: true + }) + : JSON.stringify({ + text: text + }); + + return fetch(host + "/comments", { + method: "POST", + headers: { + 'Authorization': `Bearer ${token}`, + // Убираем Content-Type для этого API, так как он его не принимает + }, + body: bodyData, + }) + .then((response) => { + if (!response.ok) { + if (response.status === 400) { + return response.json().then(errorData => { + throw new Error(`VALIDATION_ERROR:${errorData.error || 'Неверные данные'}`); + }); + } else if (response.status === 401) { + throw new Error('AUTH_ERROR'); + } else if (response.status >= 500) { + if (retryCount < 3) { + return delay(1000 * (retryCount + 1)).then(() => { + return postComment(text, forceError, retryCount + 1); + }); + } + throw new Error('SERVER_ERROR'); + } + return response.text().then(errorText => { + throw new Error(`Ошибка сервера: ${response.status} ${errorText}`); + }); + } + return response.json(); + }) + .catch((error) => { + if (error.message === 'Failed to fetch') { + throw new Error('NETWORK_ERROR'); + } + throw error; + }); +}; \ No newline at end of file diff --git a/modules/auth.js b/modules/auth.js new file mode 100644 index 0000000..86da1c5 --- /dev/null +++ b/modules/auth.js @@ -0,0 +1,117 @@ +import { renderApp } from './render.js'; + +const AUTH_STORAGE_KEY = 'userData'; + +export let currentUser = null; + +export function initAuth() { + const savedUser = localStorage.getItem(AUTH_STORAGE_KEY); + if (savedUser) { + try { + currentUser = JSON.parse(savedUser); + } catch (e) { + console.error('Ошибка при загрузке пользователя из localStorage:', e); + localStorage.removeItem(AUTH_STORAGE_KEY); + } + } +} + +export function saveUser(user) { + currentUser = user; + localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(user)); +} + +export function logout() { + currentUser = null; + localStorage.removeItem(AUTH_STORAGE_KEY); +} + +export function isLoggedIn() { + return currentUser !== null; +} + +export function getUserToken() { + return currentUser ? currentUser.token : null; +} + +export function getUserName() { + return currentUser ? currentUser.name : ''; +} + +export async function registerUser(login, name, password) { + try { + console.log('Отправка данных регистрации:', { login, name, password }); + + const response = await fetch('https://wedev-api.sky.pro/api/user', { + method: 'POST', + // Убираем все заголовки для этого API + body: JSON.stringify({ + login: login.trim(), + name: name.trim(), + password: password.trim() + }), + }); + + console.log('Статус ответа:', response.status); + + if (!response.ok) { + let errorMessage = 'Ошибка регистрации'; + + if (response.status === 400) { + const errorData = await response.json().catch(() => null); + errorMessage = errorData?.error || 'Пользователь с таким логином уже существует'; + } + + throw new Error(errorMessage); + } + + const data = await response.json(); + console.log('Успешная регистрация:', data); + saveUser(data.user); + return data.user; + } catch (error) { + console.error('Ошибка регистрации:', error); + if (error.message === 'Failed to fetch') { + throw new Error('Нет соединения с интернетом'); + } + throw error; + } +} + +export async function loginUser(login, password) { + try { + console.log('Отправка данных авторизации:', { login, password }); + + const response = await fetch('https://wedev-api.sky.pro/api/user/login', { + method: 'POST', + // Убираем все заголовки для этого API + body: JSON.stringify({ + login: login.trim(), + password: password.trim() + }), + }); + + console.log('Статус ответа:', response.status); + + if (!response.ok) { + let errorMessage = 'Ошибка авторизации'; + + if (response.status === 400) { + errorMessage = 'Неверный логин или пароль'; + } + + throw new Error(errorMessage); + } + + const data = await response.json(); + console.log('Успешная авторизация:', data); + saveUser(data.user); + return data.user; + } catch (error) { + console.error('Ошибка авторизации:', error); + if (error.message === 'Failed to fetch') { + throw new Error('Нет соединения с интернетом'); + } + throw error; + } +} \ No newline at end of file diff --git a/modules/comments.js b/modules/comments.js new file mode 100644 index 0000000..a3bd08f --- /dev/null +++ b/modules/comments.js @@ -0,0 +1,20 @@ +export let comments = []; +export let replyingToCommentId = null; +export let currentFormData = { text: '' }; + +export function updateComments(newComments) { + comments = newComments; +} + +export function setReplyingToCommentId(commentId) { + replyingToCommentId = commentId; +} + +export function setCurrentFormData(data) { + currentFormData = data; +} + +export function resetFormData() { + currentFormData = { text: '' }; + replyingToCommentId = null; +} \ No newline at end of file diff --git a/modules/event-handlers.js b/modules/event-handlers.js new file mode 100644 index 0000000..0f8cd63 --- /dev/null +++ b/modules/event-handlers.js @@ -0,0 +1,453 @@ +import { loadComments, postComment } from './api.js'; +import { comments, updateComments, setReplyingToCommentId, setCurrentFormData, resetFormData } from './comments.js'; +import { renderComments, renderApp, renderLoginForm, renderRegistrationForm } from './render.js'; +import { delay } from './utils.js'; +import { loginUser, registerUser, logout as authLogout, isLoggedIn } from './auth.js'; + +console.log('event-handlers.js загружен'); + +export function toggleLike(commentId) { + console.log('toggleLike вызван для комментария:', commentId, 'тип:', typeof commentId); + console.log('Пользователь авторизован:', isLoggedIn()); + + if (!isLoggedIn()) { + console.log('Пользователь не авторизован, показываем форму входа'); + showLoginForm(); + return; + } + + // Ищем комментарий по строковому ID + const comment = comments.find(c => c.id === commentId); + console.log('Найден комментарий:', comment); + + if (comment && !comment.isLikeLoading) { + console.log('Начинаем обработку лайка'); + comment.isLikeLoading = true; + renderComments(); + + delay(2000) + .then(() => { + console.log('Задержка завершена, обновляем лайки'); + comment.likes = comment.isLiked ? comment.likes - 1 : comment.likes + 1; + comment.isLiked = !comment.isLiked; + comment.isLikeLoading = false; + }) + .then(() => { + console.log('Рендерим комментарии после лайка'); + renderComments(); + }) + .catch((error) => { + console.error('Ошибка при установке лайка:', error); + comment.isLikeLoading = false; + renderComments(); + }); + } else { + console.log('Комментарий не найден или уже в процессе лайка'); + } +} + +export function replyToComment(commentId) { + console.log('replyToComment вызван для комментария:', commentId, 'тип:', typeof commentId); + console.log('Пользователь авторизован:', isLoggedIn()); + + if (!isLoggedIn()) { + console.log('Пользователь не авторизован, показываем форму входа'); + showLoginForm(); + return; + } + + // Ищем комментарий по строковому ID + const comment = comments.find(c => c.id === commentId); + console.log('Найден комментарий для ответа:', comment); + + const textInput = document.querySelector('.add-form-text'); + console.log('Поле ввода текста найдено:', textInput); + + if (comment && textInput) { + console.log('Устанавливаем данные для ответа'); + setReplyingToCommentId(commentId); + + const replyText = `> ${comment.name}:\n> ${comment.text}\n\n`; + console.log('Текст ответа:', replyText); + + textInput.value = replyText; + setCurrentFormData({ text: replyText }); + textInput.focus(); + + console.log('Ответ установлен в поле ввода'); + } else { + console.log('Комментарий или поле ввода не найдены'); + console.log('Доступные комментарии:', comments.map(c => ({ id: c.id, type: typeof c.id }))); + } +} + +// Глобальная функция для обработки кликов +window.handleCommentClick = function(event) { + console.log('handleCommentClick вызван', event.target); + + // Обработка лайков + if (event.target.classList.contains('like-button') || event.target.closest('.like-button')) { + console.log('Клик по лайку'); + const likeButton = event.target.classList.contains('like-button') ? event.target : event.target.closest('.like-button'); + if (likeButton && !likeButton.disabled) { + event.stopPropagation(); + // Берем ID как строку, не преобразуем в число + const commentId = likeButton.dataset.id; + console.log('ID комментария для лайка:', commentId, 'тип:', typeof commentId); + toggleLike(commentId); + } + return; + } + + // Обработка ответов на комментарии + const commentElement = event.target.closest('.comment'); + if (commentElement) { + console.log('Клик по комментарию', commentElement); + // Берем ID как строку, не преобразуем в число + const commentId = commentElement.dataset.id; + console.log('ID комментария для ответа:', commentId, 'тип:', typeof commentId); + replyToComment(commentId); + } +}; + +function addNewComment() { + console.log('addNewComment вызван'); + console.log('Пользователь авторизован:', isLoggedIn()); + + if (!isLoggedIn()) { + console.log('Пользователь не авторизован, показываем форму входа'); + showLoginForm(); + return; + } + + const textInput = document.querySelector('.add-form-text'); + const addButton = document.querySelector('.add-form-button'); + + let text = textInput.value.trim(); + console.log('Текст комментария:', text); + + if (text.length < 5) { + console.log('Текст слишком короткий'); + alert('Комментарий должен содержать не менее 5 символов'); + return; + } + + const originalText = addButton.textContent; + addButton.disabled = true; + addButton.textContent = 'Добавляем...'; + + setCurrentFormData({ text }); + + console.log('Отправка комментария...'); + postComment(text) + .then(() => { + console.log('Комментарий отправлен, загружаем обновленный список'); + return loadComments(); + }) + .then((updatedComments) => { + console.log('Комментарии загружены:', updatedComments); + updateComments(updatedComments); + renderComments(); + textInput.value = ''; + resetFormData(); + console.log('Комментарий успешно добавлен'); + }) + .catch((error) => { + console.error('Ошибка при добавлении комментария:', error); + textInput.value = currentFormData.text; + + if (error.message === 'NETWORK_ERROR') { + alert('Нет соединения с интернетом. Пожалуйста, проверьте подключение и попробуйте позже.'); + } else if (error.message.startsWith('VALIDATION_ERROR:')) { + const errorMsg = error.message.split(':')[1]; + alert(`Ошибка валидации: ${errorMsg}`); + } else if (error.message === 'SERVER_ERROR') { + alert('Ошибка сервера. Пожалуйста, попробуйте позже.'); + } else if (error.message === 'AUTH_ERROR') { + alert('Ошибка авторизации. Пожалуйста, войдите снова.'); + authLogout(); + renderApp(); + } else { + alert(`Не удалось отправить комментарий: ${error.message}`); + } + }) + .finally(() => { + addButton.disabled = false; + addButton.textContent = originalText; + }); +} + +function setupEventListeners() { + console.log('Настройка обработчиков событий...'); + + // Кнопка повторной загрузки + const retryButton = document.getElementById('retry-button'); + if (retryButton) { + console.log('Кнопка retry найдена'); + retryButton.addEventListener('click', handleRetry); + } else { + console.log('Кнопка retry не найдена'); + } + + // Форма добавления комментария + const addButton = document.querySelector('.add-form-button'); + const textInput = document.querySelector('.add-form-text'); + + if (addButton) { + console.log('Кнопка добавления найдена'); + addButton.addEventListener('click', addNewComment); + } else { + console.log('Кнопка добавления не найдена'); + } + + if (textInput) { + console.log('Поле ввода текста найдено'); + textInput.addEventListener('input', function() { + setCurrentFormData({ text: this.value }); + }); + + textInput.addEventListener('keydown', function(e) { + if (e.key === 'Enter' && e.ctrlKey) { + e.preventDefault(); + addNewComment(); + } + }); + } + + // Обработчики для комментариев (делегирование) + const commentsContainer = document.querySelector('.comments-container') || document.querySelector('.comments'); + if (commentsContainer) { + console.log('Контейнер комментариев найден, добавляем обработчик'); + commentsContainer.addEventListener('click', window.handleCommentClick); + } else { + console.log('Контейнер комментариев не найден'); + } + + // Кнопки авторизации + const loginButton = document.querySelector('.login-button'); + const registerButton = document.querySelector('.register-button'); + const logoutButton = document.querySelector('.logout-button'); + const loginLink = document.querySelector('.login-link'); + const registerLink = document.querySelector('.register-link'); + + if (loginButton) { + console.log('Кнопка входа найдена'); + loginButton.addEventListener('click', showLoginForm); + } + if (registerButton) { + console.log('Кнопка регистрации найдена'); + registerButton.addEventListener('click', showRegistrationForm); + } + if (logoutButton) { + console.log('Кнопка выхода найдена'); + logoutButton.addEventListener('click', logout); + } + if (loginLink) { + console.log('Ссылка входа найдена'); + loginLink.addEventListener('click', (e) => { e.preventDefault(); showLoginForm(); }); + } + if (registerLink) { + console.log('Ссылка регистрации найдена'); + registerLink.addEventListener('click', (e) => { e.preventDefault(); showRegistrationForm(); }); + } + + console.log('Обработчики событий настроены'); +} + +function handleRetry() { + console.log('Повторная загрузка комментариев'); + const errorMessageElement = document.getElementById('error-message'); + const loadingElement = document.getElementById('loading'); + const commentsList = document.querySelector('.comments'); + + errorMessageElement.style.display = 'none'; + loadingElement.style.display = 'block'; + + loadComments() + .then((commentsData) => { + updateComments(commentsData); + renderComments(); + loadingElement.style.display = 'none'; + commentsList.style.display = 'block'; + }) + .catch((error) => { + console.error('Ошибка при повторной загрузке:', error); + loadingElement.style.display = 'none'; + + if (error.message === 'NETWORK_ERROR') { + errorMessageElement.textContent = 'Нет соединения с интернетом. Пожалуйста, проверьте подключение.'; + } else { + errorMessageElement.textContent = 'Не удалось загрузить комментарии: ' + error.message; + } + + errorMessageElement.style.display = 'block'; + }); +} + +export function showLoginForm() { + console.log('Показываем форму входа'); + renderLoginForm(); + setupAuthFormListeners(); +} + +export function showRegistrationForm() { + console.log('Показываем форму регистрации'); + renderRegistrationForm(); + setupAuthFormListeners(); +} + +function setupAuthFormListeners() { + console.log('Настройка обработчиков форм авторизации'); + + const loginForm = document.getElementById('login-form'); + const registerForm = document.getElementById('register-form'); + const backButton = document.querySelector('.back-button'); + + if (loginForm) { + console.log('Форма входа найдена'); + loginForm.addEventListener('submit', handleLoginFormSubmit); + } + + if (registerForm) { + console.log('Форма регистрации найдена'); + registerForm.addEventListener('submit', handleRegisterFormSubmit); + } + + if (backButton) { + console.log('Кнопка назад найдена'); + backButton.addEventListener('click', () => { + renderApp(); + setupEventListeners(); + }); + } +} + +function handleLoginFormSubmit(event) { + event.preventDefault(); + console.log('Отправка формы входа'); + + const login = document.getElementById('login-username').value; + const password = document.getElementById('login-password').value; + const errorElement = document.getElementById('login-error'); + const button = event.target.querySelector('.submit-button'); + + handleLogin(login, password, button, errorElement); +} + +function handleRegisterFormSubmit(event) { + event.preventDefault(); + console.log('Отправка формы регистрации'); + + const login = document.getElementById('register-login').value; + const name = document.getElementById('register-name').value; + const password = document.getElementById('register-password').value; + const errorElement = document.getElementById('register-error'); + const button = event.target.querySelector('.submit-button'); + + handleRegister(login, name, password, button, errorElement); +} + +async function handleLogin(login, password, button, errorElement) { + console.log('Обработка входа:', login); + + const originalText = button.textContent; + button.disabled = true; + button.textContent = 'Вход...'; + errorElement.style.display = 'none'; + + try { + await loginUser(login, password); + console.log('Вход успешен'); + renderApp(); + setupEventListeners(); + initComments(); + } catch (error) { + console.error('Ошибка входа:', error); + errorElement.textContent = error.message; + errorElement.style.display = 'block'; + } finally { + button.disabled = false; + button.textContent = originalText; + } +} + +async function handleRegister(login, name, password, button, errorElement) { + console.log('Обработка регистрации:', login); + + const originalText = button.textContent; + button.disabled = true; + button.textContent = 'Регистрация...'; + errorElement.style.display = 'none'; + + try { + await registerUser(login, name, password); + console.log('Регистрация успешна'); + renderApp(); + setupEventListeners(); + initComments(); + } catch (error) { + console.error('Ошибка регистрации:', error); + errorElement.textContent = error.message; + errorElement.style.display = 'block'; + } finally { + button.disabled = false; + button.textContent = originalText; + } +} + +export function logout() { + console.log('Выход из системы'); + authLogout(); + renderApp(); + setupEventListeners(); +} + +function initComments() { + console.log('Инициализация комментариев'); + + const loadingElement = document.getElementById('loading'); + const commentsList = document.querySelector('.comments'); + const errorMessageElement = document.getElementById('error-message'); + + if (loadingElement && commentsList && errorMessageElement) { + loadingElement.style.display = 'block'; + commentsList.style.display = 'none'; + errorMessageElement.style.display = 'none'; + + loadComments() + .then((commentsData) => { + console.log('Комментарии загружены:', commentsData); + updateComments(commentsData); + renderComments(); + loadingElement.style.display = 'none'; + commentsList.style.display = 'block'; + }) + .catch((error) => { + console.error('Ошибка загрузки комментариев:', error); + loadingElement.style.display = 'none'; + + if (error.message === 'NETWORK_ERROR') { + errorMessageElement.textContent = 'Нет соединения с интернетом. Пожалуйста, проверьте подключение.'; + } else if (error.message === 'SERVER_ERROR') { + errorMessageElement.textContent = 'Ошибка сервера. Пожалуйста, попробуйте позже.'; + } else { + errorMessageElement.textContent = 'Не удалось загрузить комментарии: ' + error.message; + } + + errorMessageElement.style.display = 'block'; + }); + } else { + console.log('Элементы комментариев не найдены'); + } +} + +export function initApp() { + console.log('Инициализация приложения'); + + import('./auth.js').then(({ initAuth }) => { + initAuth(); + renderApp(); + setupEventListeners(); + initComments(); + }); +} \ No newline at end of file diff --git a/modules/render.js b/modules/render.js new file mode 100644 index 0000000..cc935c0 --- /dev/null +++ b/modules/render.js @@ -0,0 +1,211 @@ +import { comments } from './comments.js'; +import { escapeHtml } from './utils.js'; +import { isLoggedIn, getUserName } from './auth.js'; + +export function renderComments() { + console.log('renderComments вызван, комментариев:', comments.length); + + const commentsList = document.querySelector('.comments'); + + if (!commentsList) { + console.error('Элемент .comments не найден'); + return; + } + + let commentsHTML = ''; + + comments.forEach((comment) => { + const formattedDate = comment.date instanceof Date + ? comment.date.toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + : comment.date; + + const likeButtonClass = comment.isLiked ? '-active-like' : ''; + const loadingClass = comment.isLikeLoading ? '-loading-like' : ''; + const disabledAttr = comment.isLikeLoading ? 'disabled' : ''; + + commentsHTML += ` +
  • +
    +
    ${escapeHtml(comment.name)}
    +
    ${formattedDate}
    +
    +
    +
    + ${escapeHtml(comment.text)} +
    +
    + +
  • + `; + }); + + commentsList.innerHTML = commentsHTML; + console.log('Комментарии отрендерены, HTML установлен'); +} + +export function renderApp() { + console.log('renderApp вызван'); + + const container = document.querySelector('.container'); + const userName = getUserName(); + + container.innerHTML = ` +
    +

    Проект "Комменты"

    + +
    +
    +
    Загрузка комментариев...
    + + +
    + ${isLoggedIn() + ? `
    + +
    + +
    +
    ` + : `
    +

    Чтобы добавить комментарий, пожалуйста, или зарегистрируйтесь.

    +
    ` + } + `; + + console.log('Приложение отрендерено'); +} + +export function renderLoginForm() { + console.log('renderLoginForm вызван'); + + const container = document.querySelector('.container'); + + container.innerHTML = ` +
    +
    +

    Вход

    +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    +
    + `; + + console.log('Форма входа отрендерена'); +} + +export function renderRegistrationForm() { + console.log('renderRegistrationForm вызван'); + + const container = document.querySelector('.container'); + + container.innerHTML = ` +
    +
    +

    Регистрация

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    +
    + `; + + console.log('Форма регистрации отрендерена'); +} \ No newline at end of file diff --git a/modules/utils.js b/modules/utils.js new file mode 100644 index 0000000..8e3e93a --- /dev/null +++ b/modules/utils.js @@ -0,0 +1,17 @@ +export function escapeHtml(unsafe) { + if (!unsafe) return ''; + return unsafe + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +export function delay(interval = 300) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, interval); + }); +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..0c2080d --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "comments-project", + "version": "1.0.0", + "scripts": { + "format": "prettier --write .", + "lint": "eslint ." + }, + "devDependencies": { + "eslint": "^8.0.0", + "prettier": "^2.0.0" + } +} \ No newline at end of file diff --git a/styles.css b/styles.css index edc120f..cb3b29f 100644 --- a/styles.css +++ b/styles.css @@ -8,12 +8,69 @@ body { display: flex; flex-direction: column; align-items: center; - padding-top: 80px; + padding-top: 40px; padding-bottom: 200px; background: #202020; min-height: 100vh; } + .app-header { + display: flex; + justify-content: space-between; + align-items: center; + width: 596px; + margin-bottom: 40px; + } + + .app-header h1 { + margin: 0; + font-size: 24px; + } + + .user-info { + display: flex; + align-items: center; + gap: 15px; + } + + .auth-buttons { + display: flex; + gap: 10px; + } + + .login-button, + .register-button, + .logout-button { + background-color: #bcec30; + border: none; + border-radius: 8px; + padding: 8px 16px; + color: #202020; + cursor: pointer; + font-size: 14px; + } + + .login-button:hover, + .register-button:hover, + .logout-button:hover { + opacity: 0.9; + } + + .auth-required { + text-align: center; + padding: 20px; + color: #ccc; + } + + .auth-required a { + color: #bcec30; + text-decoration: none; + } + + .auth-required a:hover { + text-decoration: underline; + } + .comments, .comment { margin: 0; @@ -42,6 +99,7 @@ body { .comment { padding: 48px; + margin: 24px; } .comment-header { @@ -98,20 +156,11 @@ body { 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; @@ -134,4 +183,144 @@ body { .add-form-button:hover { opacity: 0.9; + } + + @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 { + text-align: center; + padding: 20px; + color: #666; + font-size: 18px; + } + + .add-form-button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .comments-container { + position: relative; + min-height: 200px; + } + + /* Стили для форм авторизации */ + .auth-form-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + min-height: 80vh; + } + + .auth-form { + background: radial-gradient( + 75.42% 75.42% at 50% 42.37%, + rgba(53, 53, 53, 0) 22.92%, + #7334ea 100% + ); + padding: 40px; + border-radius: 20px; + width: 400px; + box-shadow: 0px 20px 67px rgba(0, 0, 0, 0.08); + } + + .auth-form h2 { + text-align: center; + margin-bottom: 30px; + font-size: 28px; + } + + .form-group { + margin-bottom: 20px; + } + + .form-group label { + display: block; + margin-bottom: 8px; + font-size: 16px; + } + + .form-group input { + width: 100%; + padding: 12px; + border: none; + border-radius: 8px; + font-size: 16px; + box-sizing: border-box; + } + + .form-buttons { + display: flex; + gap: 15px; + margin-bottom: 20px; + } + + .submit-button, + .back-button { + flex: 1; + padding: 12px; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + } + + .submit-button { + background-color: #bcec30; + color: #202020; + } + + .back-button { + background-color: #666; + color: white; + } + + .submit-button:hover, + .back-button:hover { + opacity: 0.9; + } + + .submit-button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .auth-links { + text-align: center; + } + + .auth-links a { + color: #bcec30; + text-decoration: none; + } + + .auth-links a:hover { + text-decoration: underline; + } + + .error-message { + background-color: rgba(255, 0, 0, 0.1); + border: 1px solid rgba(255, 0, 0, 0.3); + border-radius: 8px; + padding: 12px; + margin-top: 15px; + color: #ff6b6b; } \ No newline at end of file