diff --git a/README.md b/README.md index 34719e139..aa5a94720 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,36 @@ MVP социальной сети для обмена фотографиями +## Реализованный функционал + +### Основные функции +- Авторизация и регистрация пользователей с загрузкой аватарок +- Просмотр ленты постов всех пользователей +- Добавление новых постов с изображениями и описанием +- Просмотр постов конкретного пользователя +- Система лайков с мгновенным обновлением +- Форматирование дат постов в относительном формате + +### Технические особенности +- Модульная архитектура приложения +- Компонентный подход к разработке +- Работа с REST API +- Загрузка изображений в облачное хранилище +- Динамическое обновление контента без перезагрузки страницы +- Локальное хранение данных пользователя +- Обработка ошибок и валидация форм + +### Интерфейсные улучшения +- Адаптивный и интуитивный интерфейс +- Информативные сообщения об ошибках +- Состояния загрузки для всех операций +- Интерактивные элементы с hover-эффектами +- Красивое отображение времени публикации + ## Первоначальная оценка -ХХХХ часов +8 часов ## Фактически затраченное время -YYYY часов +10 часов diff --git a/api.js b/api.js index 123a9e3b6..dc08332ff 100644 --- a/api.js +++ b/api.js @@ -1,6 +1,5 @@ -// Замени на свой, чтобы получить независимый от других набор данных. -// "боевая" версия инстапро лежит в ключе prod -const personalKey = "prod"; + +const personalKey = "dmitriy-skachkov"; const baseHost = "https://webdev-hw-api.vercel.app"; const postsHost = `${baseHost}/api/v1/${personalKey}/instapro`; @@ -15,7 +14,94 @@ export function getPosts({ token }) { if (response.status === 401) { throw new Error("Нет авторизации"); } + if (!response.ok) { + throw new Error(`Ошибка HTTP! статус: ${response.status}`); + } + return response.json(); + }) + .then((data) => { + return data.posts; + }); +} + +export function addPost({ token, description, imageUrl }) { + return fetch(postsHost, { + method: "POST", + body: JSON.stringify({ + description, + imageUrl, + }), + headers: { + Authorization: token, + }, + }) + .then((response) => { + if (response.status === 401) { + throw new Error("Нет авторизации"); + } + if (!response.ok) { + throw new Error(`Ошибка HTTP! статус: ${response.status}`); + } + return response.json(); + }); +} + +export function likePost({ token, postId }) { + return fetch(`${postsHost}/${postId}/like`, { + method: "POST", + headers: { + Authorization: token, + }, + }) + .then((response) => { + if (response.status === 401) { + throw new Error("Нет авторизации"); + } + if (response.status === 404) { + throw new Error("Пост не найден"); + } + if (!response.ok) { + throw new Error(`Ошибка лайка! статус: ${response.status}`); + } + return response.json(); + }); +} + +export function dislikePost({ token, postId }) { + return fetch(`${postsHost}/${postId}/dislike`, { + method: "POST", + headers: { + Authorization: token, + }, + }) + .then((response) => { + if (response.status === 401) { + throw new Error("Нет авторизации"); + } + if (response.status === 404) { + throw new Error("Пост не найден"); + } + if (!response.ok) { + throw new Error(`Ошибка дизлайка! статус: ${response.status}`); + } + return response.json(); + }); +} +export function getUserPosts({ token, userId }) { + return fetch(`${postsHost}/user-posts/${userId}`, { + method: "GET", + headers: { + Authorization: token, + }, + }) + .then((response) => { + if (response.status === 401) { + throw new Error("Нет авторизации"); + } + if (!response.ok) { + throw new Error(`Ошибка HTTP! статус: ${response.status}`); + } return response.json(); }) .then((data) => { @@ -32,12 +118,16 @@ export function registerUser({ login, password, name, imageUrl }) { name, imageUrl, }), - }).then((response) => { - if (response.status === 400) { - throw new Error("Такой пользователь уже существует"); - } - return response.json(); - }); + }) + .then((response) => { + if (response.status === 400) { + throw new Error("Такой пользователь уже существует"); + } + if (!response.ok) { + throw new Error(`Ошибка HTTP! статус: ${response.status}`); + } + return response.json(); + }); } export function loginUser({ login, password }) { @@ -47,15 +137,18 @@ export function loginUser({ login, password }) { login, password, }), - }).then((response) => { - if (response.status === 400) { - throw new Error("Неверный логин или пароль"); - } - return response.json(); - }); + }) + .then((response) => { + if (response.status === 400) { + throw new Error("Неверный логин или пароль"); + } + if (!response.ok) { + throw new Error(`Ошибка HTTP! статус: ${response.status}`); + } + return response.json(); + }); } -// Загружает картинку в облако, возвращает url загруженной картинки export function uploadImage({ file }) { const data = new FormData(); data.append("file", file); @@ -63,7 +156,11 @@ export function uploadImage({ file }) { return fetch(baseHost + "/api/upload/image", { method: "POST", body: data, - }).then((response) => { - return response.json(); - }); -} + }) + .then((response) => { + if (!response.ok) { + throw new Error(`Ошибка HTTP! статус: ${response.status}`); + } + return response.json(); + }); +} \ No newline at end of file diff --git a/components/add-post-page-component.js b/components/add-post-page-component.js index 8277f093b..5668167cf 100644 --- a/components/add-post-page-component.js +++ b/components/add-post-page-component.js @@ -1,23 +1,65 @@ +import { renderHeaderComponent } from "./header-component.js"; +import { renderUploadImageComponent } from "./upload-image-component.js"; + export function renderAddPostPageComponent({ appEl, onAddPostClick }) { + let imageUrl = ""; + let description = ""; + const render = () => { - // @TODO: Реализовать страницу добавления поста const appHtml = `
- Cтраница добавления поста - +
+

Добавить пост

+
+
Загрузка изображения...
+ + +
+
`; appEl.innerHTML = appHtml; + renderHeaderComponent({ + element: document.querySelector(".header-container"), + }); + + renderUploadImageComponent({ + element: document.querySelector(".upload-image-container"), + onImageUrlChange: (newImageUrl) => { + imageUrl = newImageUrl; + }, + }); + + const descriptionInput = document.getElementById("description-input"); + descriptionInput.addEventListener("input", (e) => { + description = e.target.value; + }); + document.getElementById("add-button").addEventListener("click", () => { + if (!imageUrl) { + alert("Не выбрана фотография"); + return; + } + + if (!description.trim()) { + alert("Добавьте описание фотографии"); + return; + } + onAddPostClick({ - description: "Описание картинки", - imageUrl: "https://image.png", + description: description.trim(), + imageUrl: imageUrl, }); }); }; render(); -} +} \ No newline at end of file diff --git a/components/auth-page-component.js b/components/auth-page-component.js index 0e93530ec..4e72f6a64 100644 --- a/components/auth-page-component.js +++ b/components/auth-page-component.js @@ -2,51 +2,23 @@ import { loginUser, registerUser } from "../api.js"; import { renderHeaderComponent } from "./header-component.js"; import { renderUploadImageComponent } from "./upload-image-component.js"; -/** - * Компонент страницы авторизации. - * Этот компонент предоставляет пользователю интерфейс для входа в систему или регистрации. - * Форма переключается между режимами "Вход" и "Регистрация". - * - * @param {HTMLElement} params.appEl - Корневой элемент приложения, в который будет рендериться страница. - * @param {Function} params.setUser - Функция, вызываемая при успешной авторизации или регистрации. - * Принимает объект пользователя в качестве аргумента. - */ export function renderAuthPageComponent({ appEl, setUser }) { - /** - * Флаг, указывающий текущий режим формы. - * Если `true`, форма находится в режиме входа. Если `false`, в режиме регистрации. - * @type {boolean} - */ let isLoginMode = true; - - /** - * URL изображения, загруженного пользователем при регистрации. - * Используется только в режиме регистрации. - * @type {string} - */ let imageUrl = ""; - /** - * Рендерит форму авторизации или регистрации. - * В зависимости от значения `isLoginMode` отображает соответствующий интерфейс. - */ const renderForm = () => { const appHtml = `

- ${ - isLoginMode - ? "Вход в Instapro" - : "Регистрация в Instapro" - } + ${isLoginMode ? "Вход в Instapro" : "Регистрация в Instapro"}

${ !isLoginMode ? ` -
+
Выберите фото профиля
` : "" @@ -72,20 +44,26 @@ export function renderAuthPageComponent({ appEl, setUser }) { appEl.innerHTML = appHtml; - /** - * Устанавливает сообщение об ошибке в форме. - * @param {string} message - Текст сообщения об ошибке. - */ const setError = (message) => { - appEl.querySelector(".form-error").textContent = message; + const errorEl = appEl.querySelector(".form-error"); + errorEl.textContent = message; + }; + + const setLoading = (isLoading) => { + const button = appEl.querySelector("#login-button"); + if (isLoading) { + button.disabled = true; + button.textContent = "Загрузка..."; + } else { + button.disabled = false; + button.textContent = isLoginMode ? "Войти" : "Зарегистрироваться"; + } }; - // Рендерим заголовок страницы renderHeaderComponent({ element: document.querySelector(".header-container"), }); - // Если режим регистрации, рендерим компонент загрузки изображения const uploadImageContainer = appEl.querySelector(".upload-image-container"); if (uploadImageContainer) { renderUploadImageComponent({ @@ -96,22 +74,23 @@ export function renderAuthPageComponent({ appEl, setUser }) { }); } - // Обработка клика на кнопку входа/регистрации document.getElementById("login-button").addEventListener("click", () => { setError(""); + setLoading(true); - if (isLoginMode) { - // Обработка входа - const login = document.getElementById("login-input").value; - const password = document.getElementById("password-input").value; + const login = document.getElementById("login-input").value.trim(); + const password = document.getElementById("password-input").value; + if (isLoginMode) { if (!login) { - alert("Введите логин"); + setError("Введите логин"); + setLoading(false); return; } if (!password) { - alert("Введите пароль"); + setError("Введите пароль"); + setLoading(false); return; } @@ -122,30 +101,38 @@ export function renderAuthPageComponent({ appEl, setUser }) { .catch((error) => { console.warn(error); setError(error.message); + setLoading(false); }); } else { - // Обработка регистрации - const login = document.getElementById("login-input").value; - const name = document.getElementById("name-input").value; - const password = document.getElementById("password-input").value; + const name = document.getElementById("name-input").value.trim(); if (!name) { - alert("Введите имя"); + setError("Введите имя"); + setLoading(false); return; } if (!login) { - alert("Введите логин"); + setError("Введите логин"); + setLoading(false); return; } if (!password) { - alert("Введите пароль"); + setError("Введите пароль"); + setLoading(false); return; } if (!imageUrl) { - alert("Не выбрана фотография"); + setError("Не выбрана фотография профиля"); + setLoading(false); + return; + } + + if (password.length < 3) { + setError("Пароль должен содержать минимум 3 символа"); + setLoading(false); return; } @@ -156,17 +143,21 @@ export function renderAuthPageComponent({ appEl, setUser }) { .catch((error) => { console.warn(error); setError(error.message); + setLoading(false); }); } }); - // Обработка переключения режима (вход ↔ регистрация) document.getElementById("toggle-button").addEventListener("click", () => { isLoginMode = !isLoginMode; - renderForm(); // Перерисовываем форму с новым режимом + renderForm(); }); + + const firstInput = appEl.querySelector('input'); + if (firstInput) { + firstInput.focus(); + } }; - // Инициализация формы renderForm(); -} +} \ No newline at end of file diff --git a/components/header-component.js b/components/header-component.js index e733a39dc..7280156b8 100644 --- a/components/header-component.js +++ b/components/header-component.js @@ -1,17 +1,7 @@ import { goToPage, logout, user } from "../index.js"; import { ADD_POSTS_PAGE, AUTH_PAGE, POSTS_PAGE } from "../routes.js"; -/** - * Компонент заголовка страницы. - * Этот компонент отображает шапку страницы с логотипом, кнопкой добавления постов/входа и кнопкой выхода (если пользователь авторизован). - * - * @param {HTMLElement} params.element - HTML-элемент, в который будет рендериться заголовок. - * @returns {HTMLElement} Возвращает элемент заголовка после рендеринга. - */ export function renderHeaderComponent({ element }) { - /** - * Рендерит содержимое заголовка. - */ element.innerHTML = ` `; - /** - * Обработчик клика по кнопке "Добавить пост"/"Войти". - * Если пользователь авторизован, перенаправляет на страницу добавления постов. - * Если пользователь не авторизован, перенаправляет на страницу авторизации. - */ element .querySelector(".add-or-login-button") .addEventListener("click", () => { @@ -45,19 +30,11 @@ export function renderHeaderComponent({ element }) { } }); - /** - * Обработчик клика по логотипу. - * Перенаправляет на страницу с постами. - */ element.querySelector(".logo").addEventListener("click", () => { goToPage(POSTS_PAGE); }); - /** - * Обработчик клика по кнопке "Выйти". - * Если кнопка существует (т.е. пользователь авторизован), вызывает функцию `logout`. - */ element.querySelector(".logout-button")?.addEventListener("click", logout); return element; -} +} \ No newline at end of file diff --git a/components/loading-page-component.js b/components/loading-page-component.js index c93522573..704fd9449 100644 --- a/components/loading-page-component.js +++ b/components/loading-page-component.js @@ -1,19 +1,6 @@ import { renderHeaderComponent } from "./header-component.js"; -/** - * Компонент страницы загрузки. - * Этот компонент отображает страницу с индикатором загрузки и заголовком. - * Используется для отображения промежуточного состояния, пока выполняется загрузка данных или другой процесс. - * - * @param {HTMLElement} params.appEl - Корневой элемент приложения, в который будет рендериться страница загрузки. - * @param {Object} params.user - Объект пользователя, содержащий данные о текущем авторизованном пользователе (если он есть). - * @param {Function} params.goToPage - Функция для навигации по страницам. - */ export function renderLoadingPageComponent({ appEl, user, goToPage }) { - /** - * HTML-разметка страницы загрузки. - * Содержит контейнер заголовка и индикатор загрузки. - */ const appHtml = `
@@ -22,16 +9,11 @@ export function renderLoadingPageComponent({ appEl, user, goToPage }) {
`; - // Устанавливаем разметку в корневой элемент приложения appEl.innerHTML = appHtml; - /** - * Рендеринг заголовка с использованием компонента `renderHeaderComponent`. - * Передаются данные пользователя и функция навигации. - */ renderHeaderComponent({ user, element: document.querySelector(".header-container"), goToPage, }); -} +} \ No newline at end of file diff --git a/components/posts-page-component.js b/components/posts-page-component.js index 662ccdbdc..c33a82aaa 100644 --- a/components/posts-page-component.js +++ b/components/posts-page-component.js @@ -1,97 +1,49 @@ import { USER_POSTS_PAGE } from "../routes.js"; import { renderHeaderComponent } from "./header-component.js"; -import { posts, goToPage } from "../index.js"; +import { posts, goToPage, user } from "../index.js"; +import { likePost, dislikePost } from "../api.js"; +import { formatDate, initLikeButtonHandlers } from "../helpers.js"; export function renderPostsPageComponent({ appEl }) { - // @TODO: реализовать рендер постов из api console.log("Актуальный список постов:", posts); - /** - * @TODO: чтобы отформатировать дату создания поста в виде "19 минут назад" - * можно использовать https://date-fns.org/v2.29.3/docs/formatDistanceToNow - */ const appHtml = ` -
-
-
    -
  • -
    - -

    Иван Иваныч

    -
    -
    - -
    -
    - -

    - Нравится: 2 -

    -
    -

    - Иван Иваныч - Ромашка, ромашка... -

    - -
  • -
  • -
    - -

    Варварва Н.

    -
    - - -
    - -
    -
    - -

    - Нравится: 35 -

    -
    -

    - Варварва Н. - Нарисовала картину, посмотрите какая красивая -

    - -
  • -
  • -
    - -

    Варварва Н.

    -
    - - -
    - -
    -
    - -

    - Нравится: 0 -

    -
    -

    - Варварва Н. - Голова -

    - -
  • -
-
`; +
+
+
    + ${posts.map(post => { + if (!post || !post.user) { + console.error("Некорректный пост:", post); + return ''; + } + return ` +
  • +
    + +

    ${post.user.name}

    +
    +
    + +
    +
    + +

    + Нравится: ${post.likes ? post.likes.length : 0} +

    +
    +

    + ${post.user.name} + ${post.description} +

    + +
  • + `}).join('')} +
+
`; appEl.innerHTML = appHtml; @@ -106,4 +58,38 @@ export function renderPostsPageComponent({ appEl }) { }); }); } -} + + initLikeButtonHandlers({ + appEl, + posts, + user, + onLikeUpdate: async (postId, postIndex) => { + const token = `Bearer ${user.token}`; + const post = posts[postIndex]; + + try { + let response; + if (post.isLiked) { + response = await dislikePost({ token, postId }); + } else { + response = await likePost({ token, postId }); + } + + console.log("Ответ от API:", response); + + if (response && response.post) { + posts[postIndex] = response.post; + } else if (response) { + posts[postIndex] = response; + } else { + throw new Error("Пустой ответ от сервера"); + } + + renderPostsPageComponent({ appEl }); + } catch (error) { + console.error("Ошибка при лайке:", error); + alert("Не удалось обновить лайк: " + error.message); + } + } + }); +} \ No newline at end of file diff --git a/components/upload-image-component.js b/components/upload-image-component.js index e2cdf2018..46829d61e 100644 --- a/components/upload-image-component.js +++ b/components/upload-image-component.js @@ -1,27 +1,9 @@ import { uploadImage } from "../api.js"; -/** - * Компонент загрузки изображения. - * Этот компонент позволяет пользователю загружать изображение и отображать его превью. - * Если изображение уже загружено, пользователь может заменить его. - * - * @param {HTMLElement} params.element - HTML-элемент, в который будет рендериться компонент. - * @param {Function} params.onImageUrlChange - Функция, вызываемая при изменении URL изображения. - * Принимает один аргумент - новый URL изображения или пустую строку. - */ export function renderUploadImageComponent({ element, onImageUrlChange }) { - /** - * URL текущего изображения. - * Изначально пуст, пока пользователь не загрузит изображение. - * @type {string} - */ let imageUrl = ""; + let isLoading = false; - /** - * Функция рендеринга компонента. - * Отображает интерфейс компонента в зависимости от состояния: - * либо форма выбора файла, либо превью загруженного изображения с кнопкой замены. - */ const render = () => { element.innerHTML = `
@@ -30,51 +12,72 @@ export function renderUploadImageComponent({ element, onImageUrlChange }) { ? `
Загруженное изображение - +
` : ` -
`; - // Обработчик выбора файла const fileInputElement = element.querySelector(".file-upload-input"); fileInputElement?.addEventListener("change", () => { const file = fileInputElement.files[0]; if (file) { - const labelEl = document.querySelector(".file-upload-label"); - labelEl.setAttribute("disabled", true); - labelEl.textContent = "Загружаю файл..."; - - // Загружаем изображение с помощью API - uploadImage({ file }).then(({ fileUrl }) => { - imageUrl = fileUrl; // Сохраняем URL загруженного изображения - onImageUrlChange(imageUrl); // Уведомляем о изменении URL изображения - render(); // Перерисовываем компонент с новым состоянием - }); + // Проверка типа файла + if (!file.type.startsWith('image/')) { + alert("Пожалуйста, выберите файл изображения (JPG, PNG, GIF)"); + return; + } + + // Проверка размера файла (максимум 5MB) + if (file.size > 5 * 1024 * 1024) { + alert("Файл слишком большой. Максимальный размер: 5MB"); + return; + } + + isLoading = true; + render(); + + uploadImage({ file }) + .then(({ fileUrl }) => { + imageUrl = fileUrl; + isLoading = false; + onImageUrlChange(imageUrl); + render(); + }) + .catch((error) => { + console.error("Ошибка загрузки изображения:", error); + alert("Не удалось загрузить изображение. Попробуйте еще раз."); + isLoading = false; + render(); + }); } }); - // Обработчик удаления изображения element .querySelector(".file-upload-remove-button") ?.addEventListener("click", () => { - imageUrl = ""; // Сбрасываем URL изображения - onImageUrlChange(imageUrl); // Уведомляем об изменении URL изображения - render(); // Перерисовываем компонент + if (!isLoading) { + imageUrl = ""; + onImageUrlChange(imageUrl); + render(); + } }); }; - // Инициализация компонента render(); -} +} \ No newline at end of file diff --git a/components/user-posts-page-component.js b/components/user-posts-page-component.js new file mode 100644 index 000000000..ab3f964ad --- /dev/null +++ b/components/user-posts-page-component.js @@ -0,0 +1,101 @@ +import { renderHeaderComponent } from "./header-component.js"; +import { posts, goToPage, user } from "../index.js"; +import { likePost, dislikePost } from "../api.js"; +import { formatDate, initLikeButtonHandlers } from "../helpers.js"; + +export function renderUserPostsPageComponent({ appEl, userId }) { + const userPosts = posts.filter(post => post && post.user && post.user.id === userId); + + if (userPosts.length === 0) { + appEl.innerHTML = ` +
+
+

У пользователя пока нет постов

+
+ `; + renderHeaderComponent({ + element: document.querySelector(".header-container"), + }); + return; + } + + const userData = userPosts[0].user; + + const appHtml = ` +
+
+
+ +

${userData.name}

+
+
    + ${userPosts.map(post => { + if (!post || !post.user) { + console.error("Некорректный пост:", post); + return ''; + } + return ` +
  • +
    + +
    +
    + +

    + Нравится: ${post.likes ? post.likes.length : 0} +

    +
    +

    + ${post.user.name} + ${post.description} +

    + +
  • + `}).join('')} +
+
`; + + appEl.innerHTML = appHtml; + + renderHeaderComponent({ + element: document.querySelector(".header-container"), + }); + + initLikeButtonHandlers({ + appEl, + posts, + user, + onLikeUpdate: async (postId, postIndex) => { + const token = `Bearer ${user.token}`; + const post = posts[postIndex]; + + try { + let response; + if (post.isLiked) { + response = await dislikePost({ token, postId }); + } else { + response = await likePost({ token, postId }); + } + + console.log("Ответ от API:", response); + + if (response && response.post) { + posts[postIndex] = response.post; + } else if (response) { + posts[postIndex] = response; + } else { + throw new Error("Пустой ответ от сервера"); + } + + renderUserPostsPageComponent({ appEl, userId }); + } catch (error) { + console.error("Ошибка при лайке:", error); + alert("Не удалось обновить лайк: " + error.message); + } + } + }); +} \ No newline at end of file diff --git a/helpers.js b/helpers.js index b47d3b777..fb2479ab1 100644 --- a/helpers.js +++ b/helpers.js @@ -13,3 +13,72 @@ export function getUserFromLocalStorage(user) { export function removeUserFromLocalStorage(user) { window.localStorage.removeItem("user"); } + +export function formatDate(createdAt) { + const now = new Date(); + const postDate = new Date(createdAt); + const diffInSeconds = Math.floor((now - postDate) / 1000); + + if (diffInSeconds < 60) { + return 'только что'; + } else if (diffInSeconds < 3600) { + const minutes = Math.floor(diffInSeconds / 60); + return `${minutes} ${getMinutesText(minutes)} назад`; + } else if (diffInSeconds < 86400) { + const hours = Math.floor(diffInSeconds / 3600); + return `${hours} ${getHoursText(hours)} назад`; + } else if (diffInSeconds < 2592000) { + const days = Math.floor(diffInSeconds / 86400); + return `${days} ${getDaysText(days)} назад`; + } else { + const months = Math.floor(diffInSeconds / 2592000); + return `${months} ${getMonthsText(months)} назад`; + } +} + +function getMinutesText(minutes) { + if (minutes === 1) return 'минуту'; + if (minutes >= 2 && minutes <= 4) return 'минуты'; + return 'минут'; +} + +function getHoursText(hours) { + if (hours === 1) return 'час'; + if (hours >= 2 && hours <= 4) return 'часа'; + return 'часов'; +} + +function getDaysText(days) { + if (days === 1) return 'день'; + if (days >= 2 && days <= 4) return 'дня'; + return 'дней'; +} + +function getMonthsText(months) { + if (months === 1) return 'месяц'; + if (months >= 2 && months <= 4) return 'месяца'; + return 'месяцев'; +} + +export function initLikeButtonHandlers({ appEl, posts, user, onLikeUpdate }) { + const likeButtons = appEl.querySelectorAll(".like-button"); + + likeButtons.forEach(likeButton => { + likeButton.addEventListener("click", async () => { + const postId = likeButton.dataset.postId; + + if (!user) { + alert("Для лайков нужно авторизоваться"); + return; + } + + const postIndex = posts.findIndex(p => p.id === postId); + if (postIndex === -1) { + console.error("Пост не найден:", postId); + return; + } + + await onLikeUpdate(postId, postIndex); + }); + }); +} \ No newline at end of file diff --git a/index.js b/index.js index 7f1817c75..6343e75df 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -import { getPosts } from "./api.js"; +import { getPosts, addPost, getUserPosts } from "./api.js"; import { renderAddPostPageComponent } from "./components/add-post-page-component.js"; import { renderAuthPageComponent } from "./components/auth-page-component.js"; import { @@ -10,6 +10,7 @@ import { } from "./routes.js"; import { renderPostsPageComponent } from "./components/posts-page-component.js"; import { renderLoadingPageComponent } from "./components/loading-page-component.js"; +import { renderUserPostsPageComponent } from "./components/user-posts-page-component.js"; import { getUserFromLocalStorage, removeUserFromLocalStorage, @@ -20,6 +21,8 @@ export let user = getUserFromLocalStorage(); export let page = null; export let posts = []; +let data = {}; + const getToken = () => { const token = user ? `Bearer ${user.token}` : undefined; return token; @@ -31,10 +34,11 @@ export const logout = () => { goToPage(POSTS_PAGE); }; -/** - * Включает страницу приложения - */ -export const goToPage = (newPage, data) => { +export const goToPage = (newPage, newData) => { + if (newData) { + data = newData; + } + if ( [ POSTS_PAGE, @@ -45,7 +49,6 @@ export const goToPage = (newPage, data) => { ].includes(newPage) ) { if (newPage === ADD_POSTS_PAGE) { - /* Если пользователь не авторизован, то отправляем его на страницу авторизации перед добавлением поста */ page = user ? ADD_POSTS_PAGE : AUTH_PAGE; return renderApp(); } @@ -61,17 +64,35 @@ export const goToPage = (newPage, data) => { renderApp(); }) .catch((error) => { - console.error(error); - goToPage(POSTS_PAGE); + console.error("Ошибка загрузки постов:", error); + page = POSTS_PAGE; + posts = []; + renderApp(); }); } if (newPage === USER_POSTS_PAGE) { - // @@TODO: реализовать получение постов юзера из API - console.log("Открываю страницу пользователя: ", data.userId); - page = USER_POSTS_PAGE; - posts = []; - return renderApp(); + if (!data?.userId) { + console.error("Не передан userId для страницы пользователя"); + goToPage(POSTS_PAGE); + return; + } + + page = LOADING_PAGE; + renderApp(); + + return getUserPosts({ token: getToken(), userId: data.userId }) + .then((newPosts) => { + page = USER_POSTS_PAGE; + posts = newPosts; + renderApp(); + }) + .catch((error) => { + console.error("Ошибка загрузки постов пользователя:", error); + page = USER_POSTS_PAGE; + posts = []; + renderApp(); + }); } page = newPage; @@ -85,6 +106,11 @@ export const goToPage = (newPage, data) => { const renderApp = () => { const appEl = document.getElementById("app"); + if (!appEl) { + console.error("Элемент app не найден"); + return; + } + if (page === LOADING_PAGE) { return renderLoadingPageComponent({ appEl, @@ -110,9 +136,19 @@ const renderApp = () => { return renderAddPostPageComponent({ appEl, onAddPostClick({ description, imageUrl }) { - // @TODO: реализовать добавление поста в API - console.log("Добавляю пост...", { description, imageUrl }); - goToPage(POSTS_PAGE); + addPost({ + token: getToken(), + description, + imageUrl + }) + .then((newPost) => { + posts.unshift(newPost); + goToPage(POSTS_PAGE); + }) + .catch((error) => { + console.error("Ошибка при добавлении поста:", error); + alert("Не удалось добавить пост. Попробуйте еще раз."); + }); }, }); } @@ -124,10 +160,13 @@ const renderApp = () => { } if (page === USER_POSTS_PAGE) { - // @TODO: реализовать страницу с фотографиями отдельного пользвателя - appEl.innerHTML = "Здесь будет страница фотографий пользователя"; - return; + return renderUserPostsPageComponent({ + appEl, + userId: data?.userId, + }); } }; -goToPage(POSTS_PAGE); +document.addEventListener('DOMContentLoaded', () => { + goToPage(POSTS_PAGE); +}); \ No newline at end of file diff --git a/routes.js b/routes.js index 725f8eb1a..f7521ea5b 100644 --- a/routes.js +++ b/routes.js @@ -1,6 +1,5 @@ -/* Файл со списком страниц приложения */ export const POSTS_PAGE = "posts"; export const USER_POSTS_PAGE = "user-posts"; export const AUTH_PAGE = "auth"; export const ADD_POSTS_PAGE = "add-post"; -export const LOADING_PAGE = "loading"; +export const LOADING_PAGE = "loading"; \ No newline at end of file diff --git a/styles.css b/styles.css index 57bc08b16..535f2749f 100644 --- a/styles.css +++ b/styles.css @@ -196,3 +196,45 @@ width: 100%; text-align: center; } + +.file-upload-hint { + font-size: 12px; + color: #666; + margin-top: 5px; + text-align: center; +} + +.upload-progress { + color: #565eef; + font-size: 14px; + margin-top: 10px; + text-align: center; +} + +.file-upload-label[disabled], +.file-upload-remove-button[disabled] { + opacity: 0.6; + cursor: not-allowed; +} + +.form-error { + color: #ff3333; + font-size: 14px; + margin: 10px 0; + min-height: 20px; + text-align: center; +} + +.file-upload-image { + max-width: 200px; + max-height: 200px; + border-radius: 8px; + margin-bottom: 10px; +} + +.file-upload-image-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} \ No newline at end of file