${
!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
-
-
-
- Иван Иваныч
- Ромашка, ромашка...
-
-
- 19 минут назад
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Нравится: 35
-
-
-
- Варварва Н.
- Нарисовала картину, посмотрите какая красивая
-
-
- 3 часа назад
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Нравится: 0
-
-
-
- Варварва Н.
- Голова
-
-
- 8 дней назад
-
-
-
-
`;
+
+
+
+ ${posts.map(post => {
+ if (!post || !post.user) {
+ console.error("Некорректный пост:", post);
+ return '';
+ }
+ return `
+
+
+
+
+
+
+
+
+
+
+ Нравится: ${post.likes ? post.likes.length : 0}
+
+
+
+ ${post.user.name}
+ ${post.description}
+
+
+ ${formatDate(post.createdAt)}
+
+
+ `}).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 }) {
? `
-
Заменить фото
+
${isLoading ? 'Загрузка...' : 'Заменить фото'}
`
: `
-
+
- Выберите фото
+ ${isLoading ? 'Загрузка...' : 'Выберите фото'}
+ Можно загрузить JPG, PNG или GIF
`
}
+ ${isLoading ? 'Загружаем изображение...
' : ''}
`;
- // Обработчик выбора файла
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 = `
+
+
+
+
+ ${userPosts.map(post => {
+ if (!post || !post.user) {
+ console.error("Некорректный пост:", post);
+ return '';
+ }
+ return `
+
+
+
+
+
+
+
+
+
+ Нравится: ${post.likes ? post.likes.length : 0}
+
+
+
+ ${post.user.name}
+ ${post.description}
+
+
+ ${formatDate(post.createdAt)}
+
+
+ `}).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