From 703b7f738410aaf17d339c9227ae303f0818c261 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 28 Apr 2025 15:56:35 +0500 Subject: [PATCH 01/11] checkpoint --- api.js | 260 +++++++++++++++++++++--- components/add-post-page-component.js | 63 ++++-- components/auth-page-component.js | 273 +++++++++++++++----------- components/posts-page-component.js | 186 ++++++++---------- helpers.js | 21 +- index.html | 8 +- index.js | 109 +++++++--- styles.css | 10 + 8 files changed, 634 insertions(+), 296 deletions(-) diff --git a/api.js b/api.js index 123a9e3b6..f9727b193 100644 --- a/api.js +++ b/api.js @@ -1,69 +1,275 @@ -// Замени на свой, чтобы получить независимый от других набор данных. -// "боевая" версия инстапро лежит в ключе prod const personalKey = "prod"; const baseHost = "https://webdev-hw-api.vercel.app"; const postsHost = `${baseHost}/api/v1/${personalKey}/instapro`; export function getPosts({ token }) { + console.log("getPosts called with token:", token); + const headers = {}; + if (token && token !== "Bearer undefined") { + headers.Authorization = token; + } return fetch(postsHost, { method: "GET", - headers: { - Authorization: token, - }, + headers, }) .then((response) => { + console.log("getPosts response status:", response.status); if (response.status === 401) { throw new Error("Нет авторизации"); } + if (!response.ok) { + return response.text().then((text) => { + throw new Error(`Ошибка сервера: ${response.status} ${text}`); + }); + } + return response.json(); + }) + .then((data) => { + console.log("getPosts data:", data); + return data.posts; + }); +} +export function getUserPosts({ token, userId }) { + console.log("getUserPosts called with token:", token, "userId:", userId); + const headers = {}; + if (token && token !== "Bearer undefined") { + headers.Authorization = token; + } + return fetch(`${postsHost}/user-posts/${userId}`, { + method: "GET", + headers, + }) + .then((response) => { + console.log("getUserPosts response status:", response.status); + if (response.status === 401) { + throw new Error("Нет авторизации"); + } + if (!response.ok) { + return response.text().then((text) => { + throw new Error(`Ошибка сервера: ${response.status} ${text}`); + }); + } return response.json(); }) .then((data) => { + console.log("getUserPosts data:", data); return data.posts; }); } +export function addPost({ token, description, imageUrl }) { + console.log("addPost called with:", { token, description, imageUrl }); + return fetch(postsHost, { + method: "POST", + headers: { + Authorization: token, + "Content-Type": "application/json", + }, + body: JSON.stringify({ description, imageUrl }), + }) + .then((response) => { + console.log("addPost response status:", response.status); + if (response.status === 400) { + throw new Error("Некорректные данные поста"); + } + if (response.status === 401) { + throw new Error("Нет авторизации"); + } + return response.json(); + }); +} + +export function likePost({ token, postId }) { + console.log("likePost called with:", { token, postId }); + return fetch(`${postsHost}/${postId}/like`, { + method: "POST", + headers: { + Authorization: token, + }, + }) + .then((response) => { + console.log("likePost response status:", response.status); + if (response.status === 401) { + throw new Error("Нет авторизации"); + } + return response.json(); + }); +} + +export function dislikePost({ token, postId }) { + console.log("dislikePost called with:", { token, postId }); + return fetch(`${postsHost}/${postId}/dislike`, { + method: "POST", + headers: { + Authorization: token, + }, + }) + .then((response) => { + console.log("dislikePost response status:", response.status); + if (response.status === 401) { + throw new Error("Нет авторизации"); + } + return response.json(); + }); +} + export function registerUser({ login, password, name, imageUrl }) { + console.log("registerUser: Function called"); + console.log("registerUser: Raw input values:", { login, password, name, imageUrl }); + console.log("registerUser: Input types:", { + loginType: typeof login, + passwordType: typeof password, + nameType: typeof name, + imageUrlType: typeof imageUrl, + }); + + // Проверяем наличие и типы + if (login == null) { + console.error("registerUser: Login is null or undefined:", login); + throw new Error("Логин не может быть null или undefined"); + } + if (password == null) { + console.error("registerUser: Password is null or undefined:", password); + throw new Error("Пароль не может быть null или undefined"); + } + if (name == null) { + console.error("registerUser: Name is null or undefined:", name); + throw new Error("Имя не может быть null или undefined"); + } + if (typeof login !== "string" || !login.trim()) { + console.error("registerUser: Invalid login:", login); + throw new Error("Логин должен быть непустой строкой"); + } + if (typeof password !== "string" || !password.trim()) { + console.error("registerUser: Invalid password:", password); + throw new Error("Пароль должен быть непустой строкой"); + } + if (typeof name !== "string" || !name.trim()) { + console.error("registerUser: Invalid name:", name); + throw new Error("Имя должно быть непустой строкой"); + } + if (imageUrl && typeof imageUrl !== "string") { + console.error("registerUser: Invalid imageUrl:", imageUrl); + throw new Error("URL изображения должен быть строкой или пустым"); + } + + // Формируем тело запроса + const body = { + login: login.trim(), + password: password.trim(), + name: name.trim(), + }; + if (imageUrl) { + body.imageUrl = imageUrl.trim(); + } + + console.log("registerUser: Request body:", body); + + // Проверяем сериализацию + let jsonBody; + try { + jsonBody = JSON.stringify(body); + console.log("registerUser: JSON body:", jsonBody); + } catch (error) { + console.error("registerUser: JSON serialization error:", error); + throw new Error("Ошибка формирования JSON"); + } + + // Отправляем запрос + console.log("registerUser: Sending request..."); return fetch(baseHost + "/api/user", { method: "POST", - body: JSON.stringify({ - login, - password, - name, - imageUrl, - }), - }).then((response) => { - if (response.status === 400) { - throw new Error("Такой пользователь уже существует"); - } - return response.json(); - }); + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + body: jsonBody, + }) + .then((response) => { + console.log("registerUser: Response status:", response.status); + if (response.status === 400) { + return response.json().then((data) => { + console.log("registerUser: Error response:", data); + throw new Error(data.error || "Ошибка регистрации"); + }); + } + if (!response.ok) { + throw new Error(`Ошибка сервера: ${response.status}`); + } + return response.json(); + }) + .catch((error) => { + console.error("registerUser: Error:", error); + throw error; + }); } export function loginUser({ login, password }) { + console.log("loginUser called with:", { login, password }); return fetch(baseHost + "/api/user/login", { method: "POST", + headers: { + "Content-Type": "application/json", + }, body: JSON.stringify({ login, password, }), - }).then((response) => { - if (response.status === 400) { - throw new Error("Неверный логин или пароль"); - } - return response.json(); - }); + }) + .then((response) => { + console.log("loginUser response status:", response.status); + if (response.status === 400) { + return response.json().then((data) => { + throw new Error(data.error || "Неверный логин или пароль"); + }); + } + if (!response.ok) { + throw new Error(`Ошибка сервера: ${response.status}`); + } + return response.json(); + }) + .catch((error) => { + console.error("loginUser error:", error); + throw error; + }); } -// Загружает картинку в облако, возвращает url загруженной картинки export function uploadImage({ file }) { + console.log("uploadImage called with file:", file.name); const data = new FormData(); data.append("file", file); return fetch(baseHost + "/api/upload/image", { method: "POST", body: data, - }).then((response) => { - return response.json(); - }); + }) + .then((response) => { + console.log("uploadImage response status:", response.status); + if (response.status !== 200) { + throw new Error("Ошибка загрузки изображения"); + } + return response.json(); + }) + .then((data) => { + console.log("uploadImage data:", data); + return data; // Возвращаем весь объект { fileUrl } + }); } + +export function verifyToken({ token }) { + console.log("verifyToken called with:", token); + return fetch(baseHost + "/api/user/me", { + method: "GET", + headers: { + Authorization: token, + }, + }) + .then((response) => { + console.log("verifyToken response status:", response.status); + return response.status === 200; + }) + .catch((error) => { + console.error("verifyToken error:", error); + return false; + }); +} \ No newline at end of file diff --git a/components/add-post-page-component.js b/components/add-post-page-component.js index 8277f093b..192190a86 100644 --- a/components/add-post-page-component.js +++ b/components/add-post-page-component.js @@ -1,23 +1,62 @@ +import { renderHeaderComponent } from "./header-component.js"; +import { renderUploadImageComponent } from "./upload-image-component.js"; +import { addPost } from "../api.js"; +import { goToPage, user, posts } from "../index.js"; +import { POSTS_PAGE } from "../routes.js"; + export function renderAddPostPageComponent({ appEl, onAddPostClick }) { + let imageUrl = ""; + const render = () => { - // @TODO: Реализовать страницу добавления поста const appHtml = ` -
-
- Cтраница добавления поста - -
- `; +
+
+
+

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

+
+
+ + +
+
+
`; appEl.innerHTML = appHtml; + renderHeaderComponent({ + element: document.querySelector(".header-container"), + }); + + const uploadImageContainer = appEl.querySelector(".upload-image-container"); + renderUploadImageComponent({ + element: uploadImageContainer, + onImageUrlChange(newImageUrl) { + imageUrl = newImageUrl; + }, + }); + document.getElementById("add-button").addEventListener("click", () => { - onAddPostClick({ - description: "Описание картинки", - imageUrl: "https://image.png", - }); + const description = document.getElementById("description-input").value.trim(); + if (!description) { + alert("Введите описание поста"); + return; + } + if (!imageUrl) { + alert("Выберите изображение"); + return; + } + + const token = user ? `Bearer ${user.token}` : undefined; + addPost({ token, description, imageUrl }) + .then(() => { + goToPage(POSTS_PAGE); // Переход на страницу постов + }) + .catch((error) => { + console.error("Ошибка при добавлении поста:", error); + alert("Ошибка при добавлении поста. Попробуйте снова."); + }); }); }; render(); -} +} \ No newline at end of file diff --git a/components/auth-page-component.js b/components/auth-page-component.js index 0e93530ec..526baae20 100644 --- a/components/auth-page-component.js +++ b/components/auth-page-component.js @@ -1,172 +1,205 @@ -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; +import { loginUser, registerUser, uploadImage } from "../api.js"; - /** - * URL изображения, загруженного пользователем при регистрации. - * Используется только в режиме регистрации. - * @type {string} - */ +export function renderAuthPageComponent({ appEl, setUser, user, goToPage }) { + console.log("renderAuthPageComponent called"); + let isLoginMode = true; let imageUrl = ""; - /** - * Рендерит форму авторизации или регистрации. - * В зависимости от значения `isLoginMode` отображает соответствующий интерфейс. - */ const renderForm = () => { + console.log("Rendering auth form, isLoginMode:", isLoginMode); const appHtml = `
-
-
-

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

-
- ${ - !isLoginMode - ? ` -
- - ` - : "" - } - - -
- +
+
+

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

+
+ ${!isLoginMode ? ` +
+ +
- -
- `; +
+ +
+
+
`; appEl.innerHTML = appHtml; - /** - * Устанавливает сообщение об ошибке в форме. - * @param {string} message - Текст сообщения об ошибке. - */ - const setError = (message) => { - appEl.querySelector(".form-error").textContent = message; - }; - - // Рендерим заголовок страницы renderHeaderComponent({ element: document.querySelector(".header-container"), + user, + goToPage, + }); + + const loginInputElement = document.getElementById("login-input"); + const passwordInputElement = document.getElementById("password-input"); + const nameInputElement = document.getElementById("name-input"); + const imageInputElement = document.getElementById("image-input"); + const loginButtonElement = document.getElementById("login-button"); + const registerButtonElement = document.getElementById("register-button"); + const toggleButtonElement = document.getElementById("toggle-button"); + const authErrorMessageElement = document.querySelector(".form-error"); + + console.log("Form elements:", { + loginInputElement, + passwordInputElement, + nameInputElement, + imageInputElement, + loginButtonElement, + registerButtonElement, + toggleButtonElement, + authErrorMessageElement, }); - // Если режим регистрации, рендерим компонент загрузки изображения - const uploadImageContainer = appEl.querySelector(".upload-image-container"); - if (uploadImageContainer) { - renderUploadImageComponent({ - element: uploadImageContainer, - onImageUrlChange(newImageUrl) { - imageUrl = newImageUrl; - }, + const setError = (message) => { + if (authErrorMessageElement) { + authErrorMessageElement.textContent = message; + } else { + console.error("authErrorMessageElement not found"); + } + }; + + if (toggleButtonElement) { + toggleButtonElement.addEventListener("click", () => { + console.log("Toggle button clicked, switching to isLoginMode:", !isLoginMode); + isLoginMode = !isLoginMode; + imageUrl = ""; + renderForm(); }); } - // Обработка клика на кнопку входа/регистрации - document.getElementById("login-button").addEventListener("click", () => { - setError(""); + if (loginButtonElement) { + loginButtonElement.addEventListener("click", () => { + console.log("Login button in form clicked"); + setError(""); - if (isLoginMode) { - // Обработка входа - const login = document.getElementById("login-input").value; - const password = document.getElementById("password-input").value; + const login = loginInputElement?.value?.trim(); + const password = passwordInputElement?.value?.trim(); if (!login) { - alert("Введите логин"); + setError("Введите логин"); return; } if (!password) { - alert("Введите пароль"); + setError("Введите пароль"); return; } + console.log("Attempting login with:", { login, password }); loginUser({ login, password }) - .then((user) => { - setUser(user.user); + .then((userData) => { + console.log("Login successful, user:", userData); + if (!userData.user?.token) { + throw new Error("Токен не получен"); + } + setUser(userData.user); }) .catch((error) => { - console.warn(error); + console.error("Login error:", error); setError(error.message); }); - } else { - // Обработка регистрации - const login = document.getElementById("login-input").value; - const name = document.getElementById("name-input").value; - const password = document.getElementById("password-input").value; - - if (!name) { - alert("Введите имя"); - return; - } + }); + } + if (registerButtonElement) { + registerButtonElement.addEventListener("click", () => { + console.log("Register: Button clicked"); + setError(""); + + const login = loginInputElement?.value?.trim() || ""; + const password = passwordInputElement?.value?.trim() || ""; + const name = nameInputElement?.value?.trim() || ""; + + console.log("Register: Input values:", { login, password, name, imageUrl }); + console.log("Register: Input types:", { + loginType: typeof login, + passwordType: typeof password, + nameType: typeof name, + imageUrlType: typeof imageUrl, + }); + if (!login) { - alert("Введите логин"); + setError("Введите логин"); return; } - + if (!password) { - alert("Введите пароль"); + setError("Введите пароль"); return; } - - if (!imageUrl) { - alert("Не выбрана фотография"); + + if (!name) { + setError("Введите имя"); return; } - - registerUser({ login, password, name, imageUrl }) - .then((user) => { - setUser(user.user); + + console.log("Register: Attempting registration with:", { login, password, name, imageUrl }); + registerUser({ + login, + password, + name, + imageUrl, + }) + .then((userData) => { + console.log("Register: Registration successful, user:", userData); + if (!userData.user?.token) { + throw new Error("Токен не получен"); + } + setUser(userData.user); }) .catch((error) => { - console.warn(error); + console.error("Register: Error:", error); setError(error.message); }); - } - }); + }); + } - // Обработка переключения режима (вход ↔ регистрация) - document.getElementById("toggle-button").addEventListener("click", () => { - isLoginMode = !isLoginMode; - renderForm(); // Перерисовываем форму с новым режимом - }); + if (imageInputElement) { + imageInputElement.addEventListener("change", (event) => { + const file = event.target.files[0]; + if (file) { + console.log("Selected image file:", file.name); + imageInputElement.disabled = true; + setError("Загрузка изображения..."); + uploadImage({ file }) + .then((data) => { + imageUrl = data.fileUrl; + console.log("Image uploaded, URL:", imageUrl); + setError(""); + imageInputElement.disabled = false; + }) + .catch((error) => { + console.error("Image upload error:", error); + setError("Ошибка загрузки изображения"); + imageInputElement.disabled = false; + }); + } + }); + } }; - // Инициализация формы renderForm(); -} +} \ No newline at end of file diff --git a/components/posts-page-component.js b/components/posts-page-component.js index 662ccdbdc..194783eab 100644 --- a/components/posts-page-component.js +++ b/components/posts-page-component.js @@ -1,109 +1,95 @@ 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 { formatDistanceToNow } from "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/+esm"; +import ru from "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/locale/ru/+esm"; export function renderPostsPageComponent({ appEl }) { - // @TODO: реализовать рендер постов из api - console.log("Актуальный список постов:", posts); + console.log("renderPostsPageComponent called"); + const renderPosts = () => { + console.log("Rendering posts:", posts); + const postsHtml = posts + .map((post) => { + let createdAt = post.createdAt; + try { + createdAt = formatDistanceToNow(new Date(post.createdAt), { + locale: ru, + addSuffix: true, + }); + } catch (error) { + console.error("Ошибка форматирования даты:", error); + } + return ` +
  • +
    + User avatar +

    ${post.user.name}

    +
    +
    + Post image +
    +
    + +

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

    +
    +

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

    + +
  • `; + }) + .join(""); - /** - * @TODO: чтобы отформатировать дату создания поста в виде "19 минут назад" - * можно использовать https://date-fns.org/v2.29.3/docs/formatDistanceToNow - */ - const appHtml = ` -
    -
    -
      -
    • -
      - -

      Иван Иваныч

      -
      -
      - -
      -
      - -

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

      -
      -

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

      - -
    • -
    • -
      - -

      Варварва Н.

      -
      - - -
      - -
      -
      - -

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

      -
      -

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

      - -
    • -
    • -
      - -

      Варварва Н.

      -
      - - -
      - -
      -
      - -

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

      -
      -

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

      - -
    • -
    -
    `; + const appHtml = ` +
    +
    +
      ${postsHtml}
    +
    `; - appEl.innerHTML = appHtml; + console.log("Setting appEl.innerHTML"); + appEl.innerHTML = appHtml; - renderHeaderComponent({ - element: document.querySelector(".header-container"), - }); + console.log("Rendering header component"); + renderHeaderComponent({ + element: document.querySelector(".header-container"), + }); - for (let userEl of document.querySelectorAll(".post-header")) { - userEl.addEventListener("click", () => { - goToPage(USER_POSTS_PAGE, { - userId: userEl.dataset.userId, + for (let userEl of document.querySelectorAll(".post-header")) { + userEl.addEventListener("click", () => { + console.log("Navigating to user posts page:", userEl.dataset.userId); + goToPage(USER_POSTS_PAGE, { userId: userEl.dataset.userId }); }); - }); - } -} + } + + if (user) { + for (let likeButton of document.querySelectorAll(".like-button")) { + likeButton.addEventListener("click", () => { + const postId = likeButton.dataset.postId; + const post = posts.find((p) => p.id === postId); + const token = user ? `Bearer ${user.token}` : undefined; + + console.log("Liking/disliking post:", postId); + const action = post.isLiked ? dislikePost : likePost; + action({ token, postId }) + .then((updatedPost) => { + const index = posts.findIndex((p) => p.id === postId); + posts[index] = updatedPost.post; + renderPosts(); + }) + .catch((error) => { + console.error("Ошибка при изменении лайка:", error); + alert("Ошибка при изменении лайка. Попробуйте снова."); + }); + }); + } + } + }; + + renderPosts(); +} \ No newline at end of file diff --git a/helpers.js b/helpers.js index b47d3b777..b0f18376f 100644 --- a/helpers.js +++ b/helpers.js @@ -1,15 +1,24 @@ export function saveUserToLocalStorage(user) { - window.localStorage.setItem("user", JSON.stringify(user)); + try { + console.log("Saving user to localStorage:", user); + localStorage.setItem("user", JSON.stringify(user)); + } catch (error) { + console.error("Error saving user to localStorage:", error); + } } -export function getUserFromLocalStorage(user) { +export function getUserFromLocalStorage() { try { - return JSON.parse(window.localStorage.getItem("user")); + const user = localStorage.getItem("user"); + console.log("Retrieved user from localStorage:", user); + return user ? JSON.parse(user) : null; } catch (error) { + console.error("Error retrieving user from localStorage:", error); return null; } } -export function removeUserFromLocalStorage(user) { - window.localStorage.removeItem("user"); -} +export function removeUserFromLocalStorage() { + console.log("Removing user from localStorage"); + localStorage.removeItem("user"); +} \ No newline at end of file diff --git a/index.html b/index.html index 96674b4d3..6384d083d 100644 --- a/index.html +++ b/index.html @@ -4,14 +4,12 @@ Instapro - + -
    + - - - + \ No newline at end of file diff --git a/index.js b/index.js index 7f1817c75..4c391f497 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ -import { getPosts } from "./api.js"; +console.log("Starting index.js"); +import { getPosts, getUserPosts, verifyToken } from "./api.js"; import { renderAddPostPageComponent } from "./components/add-post-page-component.js"; import { renderAuthPageComponent } from "./components/auth-page-component.js"; import { @@ -20,21 +21,39 @@ export let user = getUserFromLocalStorage(); export let page = null; export let posts = []; +console.log("Initial state:", { user, page, posts }); + const getToken = () => { - const token = user ? `Bearer ${user.token}` : undefined; + const token = user && user.token ? `Bearer ${user.token}` : undefined; + console.log("getToken called, token:", token); return token; }; export const logout = () => { + console.log("Logging out"); user = null; removeUserFromLocalStorage(); goToPage(POSTS_PAGE); }; -/** - * Включает страницу приложения - */ +// Проверка токена при старте +if (user && user.token) { + console.log("Verifying token at startup..."); + verifyToken({ token: `Bearer ${user.token}` }) + .then((isValid) => { + if (!isValid) { + console.log("Invalid token, logging out"); + logout(); + } + }) + .catch((error) => { + console.error("Token verification error:", error); + logout(); + }); +} + export const goToPage = (newPage, data) => { + console.log("goToPage called with:", { newPage, data }); if ( [ POSTS_PAGE, @@ -45,47 +64,84 @@ export const goToPage = (newPage, data) => { ].includes(newPage) ) { if (newPage === ADD_POSTS_PAGE) { - /* Если пользователь не авторизован, то отправляем его на страницу авторизации перед добавлением поста */ + console.log("Navigating to ADD_POSTS_PAGE, user:", user); page = user ? ADD_POSTS_PAGE : AUTH_PAGE; - return renderApp(); + renderApp(); + return; } if (newPage === POSTS_PAGE) { + console.log("Navigating to POSTS_PAGE"); page = LOADING_PAGE; renderApp(); - return getPosts({ token: getToken() }) + console.log("Fetching posts..."); + getPosts({ token: getToken() }) .then((newPosts) => { + console.log("Posts fetched:", newPosts); page = POSTS_PAGE; posts = newPosts; renderApp(); }) .catch((error) => { - console.error(error); - goToPage(POSTS_PAGE); + console.error("Error fetching posts:", error); + page = POSTS_PAGE; + posts = []; + renderApp(); + const appEl = document.getElementById("app"); + const errorEl = document.createElement("div"); + errorEl.className = "error-message"; + errorEl.textContent = `Не удалось загрузить посты: ${error.message}. Попробуйте позже.`; + appEl.appendChild(errorEl); }); + return; } if (newPage === USER_POSTS_PAGE) { - // @@TODO: реализовать получение постов юзера из API - console.log("Открываю страницу пользователя: ", data.userId); - page = USER_POSTS_PAGE; - posts = []; - return renderApp(); + console.log("Navigating to USER_POSTS_PAGE with userId:", data?.userId); + page = LOADING_PAGE; + renderApp(); + + console.log("Fetching user posts..."); + getUserPosts({ token: getToken(), userId: data.userId }) + .then((newPosts) => { + console.log("User posts fetched:", newPosts); + page = USER_POSTS_PAGE; + posts = newPosts; + renderApp(); + }) + .catch((error) => { + console.error("Error fetching user posts:", error); + page = POSTS_PAGE; + posts = []; + renderApp(); + const appEl = document.getElementById("app"); + const errorEl = document.createElement("div"); + errorEl.className = "error-message"; + errorEl.textContent = `Не удалось загрузить посты пользователя: ${error.message}. Попробуйте позже.`; + appEl.appendChild(errorEl); + }); + return; } + console.log("Navigating to page:", newPage); page = newPage; renderApp(); - return; } - throw new Error("страницы не существует"); + console.error("Unknown page:", newPage); + const appEl = document.getElementById("app"); + appEl.innerHTML = `
    Ошибка: неизвестная страница
    `; }; const renderApp = () => { + console.log("renderApp called, current page:", page); const appEl = document.getElementById("app"); + console.log("appEl:", appEl); + if (page === LOADING_PAGE) { + console.log("Rendering LOADING_PAGE"); return renderLoadingPageComponent({ appEl, user, @@ -94,9 +150,11 @@ const renderApp = () => { } if (page === AUTH_PAGE) { + console.log("Rendering AUTH_PAGE"); return renderAuthPageComponent({ appEl, setUser: (newUser) => { + console.log("Setting new user:", newUser); user = newUser; saveUserToLocalStorage(user); goToPage(POSTS_PAGE); @@ -107,27 +165,26 @@ const renderApp = () => { } if (page === ADD_POSTS_PAGE) { + console.log("Rendering ADD_POSTS_PAGE"); return renderAddPostPageComponent({ appEl, onAddPostClick({ description, imageUrl }) { - // @TODO: реализовать добавление поста в API - console.log("Добавляю пост...", { description, imageUrl }); + console.log("Adding post:", { description, imageUrl }); goToPage(POSTS_PAGE); }, }); } - if (page === POSTS_PAGE) { + if (page === POSTS_PAGE || page === USER_POSTS_PAGE) { + console.log("Rendering POSTS_PAGE or USER_POSTS_PAGE, posts:", posts); return renderPostsPageComponent({ appEl, }); } - if (page === USER_POSTS_PAGE) { - // @TODO: реализовать страницу с фотографиями отдельного пользвателя - appEl.innerHTML = "Здесь будет страница фотографий пользователя"; - return; - } + console.error("Unknown page:", page); + appEl.innerHTML = `
    Ошибка: неизвестная страница
    `; }; -goToPage(POSTS_PAGE); +console.log("Calling initial goToPage(POSTS_PAGE)"); +goToPage(POSTS_PAGE); \ No newline at end of file diff --git a/styles.css b/styles.css index 57bc08b16..b22689288 100644 --- a/styles.css +++ b/styles.css @@ -196,3 +196,13 @@ width: 100%; text-align: center; } + +.error-message { + background-color: #ffe6e6; + color: #d32f2f; + padding: 10px; + margin: 10px; + border: 1px solid #d32f2f; + border-radius: 4px; + text-align: center; +} \ No newline at end of file From 5d184f3c9dc7fd40b8c1708c6913f1b0bf8118bd Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 28 Apr 2025 16:15:39 +0500 Subject: [PATCH 02/11] =?UTF-8?q?checkpoint=202=20=D1=80=D0=B5=D0=B3=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B3=D0=B8=D1=82=D1=81=D1=80=D0=B8=D1=80=D1=83?= =?UTF-8?q?=D0=B5=D1=82,=20JSON=20=D0=BD=D0=B5=D0=B2=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B4=D0=BD=D1=8B=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/api.js b/api.js index f9727b193..c540bc47c 100644 --- a/api.js +++ b/api.js @@ -124,7 +124,6 @@ export function registerUser({ login, password, name, imageUrl }) { imageUrlType: typeof imageUrl, }); - // Проверяем наличие и типы if (login == null) { console.error("registerUser: Login is null or undefined:", login); throw new Error("Логин не может быть null или undefined"); @@ -141,9 +140,9 @@ export function registerUser({ login, password, name, imageUrl }) { console.error("registerUser: Invalid login:", login); throw new Error("Логин должен быть непустой строкой"); } - if (typeof password !== "string" || !password.trim()) { + if (typeof password !== "string" || !password.trim() || password.length < 6) { console.error("registerUser: Invalid password:", password); - throw new Error("Пароль должен быть непустой строкой"); + throw new Error("Пароль должен быть непустой строкой длиной не менее 6 символов"); } if (typeof name !== "string" || !name.trim()) { console.error("registerUser: Invalid name:", name); @@ -154,7 +153,6 @@ export function registerUser({ login, password, name, imageUrl }) { throw new Error("URL изображения должен быть строкой или пустым"); } - // Формируем тело запроса const body = { login: login.trim(), password: password.trim(), @@ -166,7 +164,6 @@ export function registerUser({ login, password, name, imageUrl }) { console.log("registerUser: Request body:", body); - // Проверяем сериализацию let jsonBody; try { jsonBody = JSON.stringify(body); @@ -176,12 +173,12 @@ export function registerUser({ login, password, name, imageUrl }) { throw new Error("Ошибка формирования JSON"); } - // Отправляем запрос console.log("registerUser: Sending request..."); return fetch(baseHost + "/api/user", { method: "POST", headers: { - "Content-Type": "application/json; charset=utf-8", + "Content-Type": "application/json", + "Accept": "application/json", }, body: jsonBody, }) From 6e6d31f7e5cf2770664f3ba8f2ecbe966546838a Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 3 May 2025 14:39:00 +0500 Subject: [PATCH 03/11] =?UTF-8?q?=D0=9D=D0=B5=D0=B2=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B4=D0=BD=D1=8B=D0=B9=20JSON=20=D0=BF=D1=80=D0=B8=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8.?= =?UTF-8?q?=20=D0=9E=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=20=D0=BD=D0=B5=D0=B8?= =?UTF-8?q?=D0=B7=D0=B2=D0=B5=D1=81=D1=82=D0=BD=D0=B0,=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=BD=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BC=D0=BE=D0=B3=D0=BB=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.js | 57 +++++++++++++++------------------------------------------ 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/api.js b/api.js index c540bc47c..30426b43f 100644 --- a/api.js +++ b/api.js @@ -124,56 +124,29 @@ export function registerUser({ login, password, name, imageUrl }) { imageUrlType: typeof imageUrl, }); - if (login == null) { - console.error("registerUser: Login is null or undefined:", login); - throw new Error("Логин не может быть null или undefined"); - } - if (password == null) { - console.error("registerUser: Password is null or undefined:", password); - throw new Error("Пароль не может быть null или undefined"); - } - if (name == null) { - console.error("registerUser: Name is null or undefined:", name); - throw new Error("Имя не может быть null или undefined"); - } - if (typeof login !== "string" || !login.trim()) { + // Минимальная валидация с === + if (!login || typeof login !== "string") { console.error("registerUser: Invalid login:", login); - throw new Error("Логин должен быть непустой строкой"); + throw new Error("Логин должен быть строкой"); } - if (typeof password !== "string" || !password.trim() || password.length < 6) { + if (!password || typeof password !== "string") { console.error("registerUser: Invalid password:", password); - throw new Error("Пароль должен быть непустой строкой длиной не менее 6 символов"); + throw new Error("Пароль должен быть строкой"); } - if (typeof name !== "string" || !name.trim()) { + if (!name || typeof name !== "string") { console.error("registerUser: Invalid name:", name); - throw new Error("Имя должно быть непустой строкой"); - } - if (imageUrl && typeof imageUrl !== "string") { - console.error("registerUser: Invalid imageUrl:", imageUrl); - throw new Error("URL изображения должен быть строкой или пустым"); + throw new Error("Имя должно быть строкой"); } - const body = { - login: login.trim(), - password: password.trim(), - name: name.trim(), - }; - if (imageUrl) { - body.imageUrl = imageUrl.trim(); - } - - console.log("registerUser: Request body:", body); - - let jsonBody; - try { - jsonBody = JSON.stringify(body); - console.log("registerUser: JSON body:", jsonBody); - } catch (error) { - console.error("registerUser: JSON serialization error:", error); - throw new Error("Ошибка формирования JSON"); - } + const jsonBody = JSON.stringify({ + login, + password, + name, + imageUrl, + }); + console.log("registerUser: JSON body:", jsonBody); - console.log("registerUser: Sending request..."); + console.log("registerUser: Preparing request..."); return fetch(baseHost + "/api/user", { method: "POST", headers: { From 34085253f8482872fdfeb06afd8cefdbd3dc0c0a Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 3 May 2025 15:26:21 +0500 Subject: [PATCH 04/11] =?UTF-8?q?=D0=9E=D1=87=D0=B5=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=BD=D0=B5=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.js | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/api.js b/api.js index 30426b43f..6ea38a3b6 100644 --- a/api.js +++ b/api.js @@ -124,36 +124,39 @@ export function registerUser({ login, password, name, imageUrl }) { imageUrlType: typeof imageUrl, }); - // Минимальная валидация с === - if (!login || typeof login !== "string") { - console.error("registerUser: Invalid login:", login); - throw new Error("Логин должен быть строкой"); + // Очистка данных + const cleanLogin = (login || "").trim(); + const cleanPassword = (password || "").trim(); + const cleanName = (name || "").trim(); + + // Валидация + if (!cleanLogin) { + throw new Error("Логин обязателен"); } - if (!password || typeof password !== "string") { - console.error("registerUser: Invalid password:", password); - throw new Error("Пароль должен быть строкой"); + if (!cleanPassword) { + throw new Error("Пароль обязателен"); } - if (!name || typeof name !== "string") { - console.error("registerUser: Invalid name:", name); - throw new Error("Имя должно быть строкой"); + if (!cleanName) { + throw new Error("Имя обязательно"); } - const jsonBody = JSON.stringify({ - login, - password, - name, - imageUrl, - }); - console.log("registerUser: JSON body:", jsonBody); + // Формирование тела запроса + const body = { + login: cleanLogin, + password: cleanPassword, + name: cleanName, + imageUrl: (imageUrl || null)?.trim(), // Обработка imageUrl + }; + + console.log("registerUser: Sending request body:", JSON.stringify(body, null, 2)); - console.log("registerUser: Preparing request..."); return fetch(baseHost + "/api/user", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json", }, - body: jsonBody, + body: JSON.stringify(body), }) .then((response) => { console.log("registerUser: Response status:", response.status); From cd22e11ace907ca8f68fcab9e322563222cd39f5 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 14 May 2025 18:00:36 +0500 Subject: [PATCH 05/11] =?UTF-8?q?=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D0=BB=D0=B0=20=20=D0=B4=D0=BE=20=D1=81=D0=B5=D0=B3=D0=BE?= =?UTF-8?q?=D0=B4=D0=BD=D1=8F=D1=88=D0=BD=D0=B5=D0=B3=D0=BE=20=D0=B4=D0=BD?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.js | 122 +++++++++--------------------- components/auth-page-component.js | 78 ++++++------------- 2 files changed, 59 insertions(+), 141 deletions(-) diff --git a/api.js b/api.js index 6ea38a3b6..0b69ca1ce 100644 --- a/api.js +++ b/api.js @@ -1,5 +1,5 @@ const personalKey = "prod"; -const baseHost = "https://webdev-hw-api.vercel.app"; +const baseHost = "https://wedev-api.sky.pro"; const postsHost = `${baseHost}/api/v1/${personalKey}/instapro`; export function getPosts({ token }) { @@ -115,70 +115,33 @@ export function dislikePost({ token, postId }) { } export function registerUser({ login, password, name, imageUrl }) { - console.log("registerUser: Function called"); - console.log("registerUser: Raw input values:", { login, password, name, imageUrl }); - console.log("registerUser: Input types:", { - loginType: typeof login, - passwordType: typeof password, - nameType: typeof name, - imageUrlType: typeof imageUrl, - }); - - // Очистка данных - const cleanLogin = (login || "").trim(); - const cleanPassword = (password || "").trim(); - const cleanName = (name || "").trim(); - - // Валидация - if (!cleanLogin) { - throw new Error("Логин обязателен"); - } - if (!cleanPassword) { - throw new Error("Пароль обязателен"); - } - if (!cleanName) { - throw new Error("Имя обязательно"); - } - - // Формирование тела запроса const body = { - login: cleanLogin, - password: cleanPassword, - name: cleanName, - imageUrl: (imageUrl || null)?.trim(), // Обработка imageUrl + login, + password, + name, }; - console.log("registerUser: Sending request body:", JSON.stringify(body, null, 2)); + if (imageUrl) { + body.imageUrl = imageUrl; + } - return fetch(baseHost + "/api/user", { + return fetch("https://wedev-api.sky.pro/api/user", { method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - }, body: JSON.stringify(body), - }) - .then((response) => { - console.log("registerUser: Response status:", response.status); - if (response.status === 400) { - return response.json().then((data) => { - console.log("registerUser: Error response:", data); - throw new Error(data.error || "Ошибка регистрации"); - }); - } - if (!response.ok) { - throw new Error(`Ошибка сервера: ${response.status}`); - } - return response.json(); - }) - .catch((error) => { - console.error("registerUser: Error:", error); - throw error; - }); + }).then((response) => { + if (response.status === 400) { + return response.json().then((data) => { + throw new Error(data.error || "Ошибка регистрации"); + }); + } + if (!response.ok) { + throw new Error(`Ошибка сервера: ${response.status}`); + } + return response.json(); + }); } export function loginUser({ login, password }) { - console.log("loginUser called with:", { login, password }); return fetch(baseHost + "/api/user/login", { method: "POST", headers: { @@ -188,45 +151,32 @@ export function loginUser({ login, password }) { login, password, }), - }) - .then((response) => { - console.log("loginUser response status:", response.status); - if (response.status === 400) { - return response.json().then((data) => { - throw new Error(data.error || "Неверный логин или пароль"); - }); - } - if (!response.ok) { - throw new Error(`Ошибка сервера: ${response.status}`); - } - return response.json(); - }) - .catch((error) => { - console.error("loginUser error:", error); - throw error; - }); + }).then((response) => { + if (response.status === 400) { + return response.json().then((data) => { + throw new Error(data.error || "Неверный логин или пароль"); + }); + } + if (!response.ok) { + throw new Error(`Ошибка сервера: ${response.status}`); + } + return response.json(); + }); } export function uploadImage({ file }) { - console.log("uploadImage called with file:", file.name); const data = new FormData(); data.append("file", file); return fetch(baseHost + "/api/upload/image", { method: "POST", body: data, - }) - .then((response) => { - console.log("uploadImage response status:", response.status); - if (response.status !== 200) { - throw new Error("Ошибка загрузки изображения"); - } - return response.json(); - }) - .then((data) => { - console.log("uploadImage data:", data); - return data; // Возвращаем весь объект { fileUrl } - }); + }).then((response) => { + if (response.status !== 200) { + throw new Error("Ошибка загрузки изображения"); + } + return response.json(); + }); } export function verifyToken({ token }) { diff --git a/components/auth-page-component.js b/components/auth-page-component.js index 526baae20..79f8adf6d 100644 --- a/components/auth-page-component.js +++ b/components/auth-page-component.js @@ -2,12 +2,10 @@ import { renderHeaderComponent } from "./header-component.js"; import { loginUser, registerUser, uploadImage } from "../api.js"; export function renderAuthPageComponent({ appEl, setUser, user, goToPage }) { - console.log("renderAuthPageComponent called"); let isLoginMode = true; - let imageUrl = ""; + let imageUrl = ""; // будет заполнено при загрузке изображения const renderForm = () => { - console.log("Rendering auth form, isLoginMode:", isLoginMode); const appHtml = `
    @@ -57,46 +55,29 @@ export function renderAuthPageComponent({ appEl, setUser, user, goToPage }) { const passwordInputElement = document.getElementById("password-input"); const nameInputElement = document.getElementById("name-input"); const imageInputElement = document.getElementById("image-input"); - const loginButtonElement = document.getElementById("login-button"); const registerButtonElement = document.getElementById("register-button"); + const loginButtonElement = document.getElementById("login-button"); const toggleButtonElement = document.getElementById("toggle-button"); const authErrorMessageElement = document.querySelector(".form-error"); - console.log("Form elements:", { - loginInputElement, - passwordInputElement, - nameInputElement, - imageInputElement, - loginButtonElement, - registerButtonElement, - toggleButtonElement, - authErrorMessageElement, - }); - const setError = (message) => { if (authErrorMessageElement) { authErrorMessageElement.textContent = message; - } else { - console.error("authErrorMessageElement not found"); } }; - if (toggleButtonElement) { - toggleButtonElement.addEventListener("click", () => { - console.log("Toggle button clicked, switching to isLoginMode:", !isLoginMode); - isLoginMode = !isLoginMode; - imageUrl = ""; - renderForm(); - }); - } + toggleButtonElement.addEventListener("click", () => { + isLoginMode = !isLoginMode; + imageUrl = ""; + renderForm(); + }); if (loginButtonElement) { loginButtonElement.addEventListener("click", () => { - console.log("Login button in form clicked"); setError(""); - const login = loginInputElement?.value?.trim(); - const password = passwordInputElement?.value?.trim(); + const login = loginInputElement?.value?.trim() || ""; + const password = passwordInputElement?.value?.trim() || ""; if (!login) { setError("Введите логин"); @@ -108,17 +89,14 @@ export function renderAuthPageComponent({ appEl, setUser, user, goToPage }) { return; } - console.log("Attempting login with:", { login, password }); loginUser({ login, password }) .then((userData) => { - console.log("Login successful, user:", userData); if (!userData.user?.token) { throw new Error("Токен не получен"); } setUser(userData.user); }) .catch((error) => { - console.error("Login error:", error); setError(error.message); }); }); @@ -126,52 +104,45 @@ export function renderAuthPageComponent({ appEl, setUser, user, goToPage }) { if (registerButtonElement) { registerButtonElement.addEventListener("click", () => { - console.log("Register: Button clicked"); setError(""); - + const login = loginInputElement?.value?.trim() || ""; const password = passwordInputElement?.value?.trim() || ""; const name = nameInputElement?.value?.trim() || ""; - - console.log("Register: Input values:", { login, password, name, imageUrl }); - console.log("Register: Input types:", { - loginType: typeof login, - passwordType: typeof password, - nameType: typeof name, - imageUrlType: typeof imageUrl, - }); - + if (!login) { setError("Введите логин"); return; } - + if (!password) { setError("Введите пароль"); return; } - + if (!name) { setError("Введите имя"); return; } - - console.log("Register: Attempting registration with:", { login, password, name, imageUrl }); - registerUser({ + + const data = { login, password, name, - imageUrl, - }) + }; + + if (imageUrl) { + data.imageUrl = imageUrl; + } + + registerUser(data) .then((userData) => { - console.log("Register: Registration successful, user:", userData); if (!userData.user?.token) { throw new Error("Токен не получен"); } setUser(userData.user); }) .catch((error) => { - console.error("Register: Error:", error); setError(error.message); }); }); @@ -181,18 +152,15 @@ export function renderAuthPageComponent({ appEl, setUser, user, goToPage }) { imageInputElement.addEventListener("change", (event) => { const file = event.target.files[0]; if (file) { - console.log("Selected image file:", file.name); imageInputElement.disabled = true; setError("Загрузка изображения..."); uploadImage({ file }) .then((data) => { imageUrl = data.fileUrl; - console.log("Image uploaded, URL:", imageUrl); setError(""); imageInputElement.disabled = false; }) - .catch((error) => { - console.error("Image upload error:", error); + .catch(() => { setError("Ошибка загрузки изображения"); imageInputElement.disabled = false; }); From 1c5fa19d74a21d1352f36584abcae82132e8b86c Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 14 May 2025 18:21:42 +0500 Subject: [PATCH 06/11] checkpoint --- index.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index 4c391f497..82c005cea 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ console.log("Starting index.js"); -import { getPosts, getUserPosts, verifyToken } from "./api.js"; +import { getPosts, getUserPosts, } from "./api.js"; import { renderAddPostPageComponent } from "./components/add-post-page-component.js"; import { renderAuthPageComponent } from "./components/auth-page-component.js"; import { @@ -37,20 +37,20 @@ export const logout = () => { }; // Проверка токена при старте -if (user && user.token) { - console.log("Verifying token at startup..."); - verifyToken({ token: `Bearer ${user.token}` }) - .then((isValid) => { - if (!isValid) { - console.log("Invalid token, logging out"); - logout(); - } - }) - .catch((error) => { - console.error("Token verification error:", error); - logout(); - }); -} +// if (user && user.token) { +// console.log("Verifying token at startup..."); +// verifyToken({ token: `Bearer ${user.token}` }) +// .then((isValid) => { +// if (!isValid) { +// console.log("Invalid token, logging out"); +// logout(); +// } +// }) +// .catch((error) => { +// console.error("Token verification error:", error); +// logout(); +// }); +// } export const goToPage = (newPage, data) => { console.log("goToPage called with:", { newPage, data }); From d2605efa19e30f78d777cf1f37d02a0c40615d1a Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 19 May 2025 20:38:35 +0500 Subject: [PATCH 07/11] =?UTF-8?q?=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B4=D0=B5=D1=80=D0=B6=D0=B8?= =?UTF-8?q?=D1=82=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api.js b/api.js index 0b69ca1ce..4d5e26f78 100644 --- a/api.js +++ b/api.js @@ -144,9 +144,6 @@ export function registerUser({ login, password, name, imageUrl }) { export function loginUser({ login, password }) { return fetch(baseHost + "/api/user/login", { method: "POST", - headers: { - "Content-Type": "application/json", - }, body: JSON.stringify({ login, password, @@ -195,4 +192,4 @@ export function verifyToken({ token }) { console.error("verifyToken error:", error); return false; }); -} \ No newline at end of file +} From b22571f4345f4d3dd7bb3eeb1b14f0fd3439caa2 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 19 May 2025 21:24:21 +0500 Subject: [PATCH 08/11] =?UTF-8?q?=D0=B2=D1=80=D0=BE=D0=B4=D0=B5=20=D0=B2?= =?UTF-8?q?=D1=81=D0=B5=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5=D1=82?= =?UTF-8?q?=20=D0=B1=D0=B5=D0=B7=20=D0=B4=D0=BE=D0=BF=20=D1=83=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.js | 36 ++++++++++++-------- components/add-post-page-component.js | 47 +++++++++++++++------------ components/auth-page-component.js | 3 ++ 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/api.js b/api.js index 4d5e26f78..6c99a1a80 100644 --- a/api.js +++ b/api.js @@ -60,24 +60,34 @@ export function getUserPosts({ token, userId }) { export function addPost({ token, description, imageUrl }) { console.log("addPost called with:", { token, description, imageUrl }); + + if (!description || !imageUrl) { + throw new Error("Описание или URL изображения не переданы"); + } + return fetch(postsHost, { method: "POST", headers: { Authorization: token, - "Content-Type": "application/json", }, - body: JSON.stringify({ description, imageUrl }), - }) - .then((response) => { - console.log("addPost response status:", response.status); - if (response.status === 400) { - throw new Error("Некорректные данные поста"); - } - if (response.status === 401) { - throw new Error("Нет авторизации"); - } - return response.json(); - }); + body: JSON.stringify({ + description, + imageUrl, + }), + }).then((response) => { + if (response.status === 400) { + return response.json().then((data) => { + throw new Error(data.error || "Некорректные данные поста"); + }); + } + if (response.status === 401) { + throw new Error("Нет авторизации"); + } + if (!response.ok) { + throw new Error(`Ошибка сервера: ${response.status}`); + } + return response.json(); + }); } export function likePost({ token, postId }) { diff --git a/components/add-post-page-component.js b/components/add-post-page-component.js index 192190a86..7b6a42c39 100644 --- a/components/add-post-page-component.js +++ b/components/add-post-page-component.js @@ -35,27 +35,34 @@ export function renderAddPostPageComponent({ appEl, onAddPostClick }) { }, }); - document.getElementById("add-button").addEventListener("click", () => { - const description = document.getElementById("description-input").value.trim(); - if (!description) { - alert("Введите описание поста"); - return; - } - if (!imageUrl) { - alert("Выберите изображение"); - return; - } - - const token = user ? `Bearer ${user.token}` : undefined; - addPost({ token, description, imageUrl }) - .then(() => { - goToPage(POSTS_PAGE); // Переход на страницу постов - }) - .catch((error) => { - console.error("Ошибка при добавлении поста:", error); - alert("Ошибка при добавлении поста. Попробуйте снова."); - }); +document.getElementById("add-button").addEventListener("click", () => { + const description = document.getElementById("description-input").value.trim(); + + console.log("Description:", description); + console.log("imageUrl:", imageUrl); + console.log("token:", user?.token); + console.log("Текущий пользователь:", user); + if (!description) { + alert("Введите описание поста"); + return; + } + if (!imageUrl) { + alert("Выберите изображение"); + return; + } + + const token = user && user.token ? `Bearer ${user.token}` : undefined; + console.log("Authorization token:", token); + + addPost({ token, description, imageUrl }) + .then(() => { + goToPage(POSTS_PAGE); + }) + .catch((error) => { + console.error("Ошибка при добавлении поста:", error); + alert("Ошибка при добавлении поста. Попробуйте снова."); }); +}); }; render(); diff --git a/components/auth-page-component.js b/components/auth-page-component.js index 79f8adf6d..0f5bc5ffa 100644 --- a/components/auth-page-component.js +++ b/components/auth-page-component.js @@ -69,6 +69,7 @@ export function renderAuthPageComponent({ appEl, setUser, user, goToPage }) { toggleButtonElement.addEventListener("click", () => { isLoginMode = !isLoginMode; imageUrl = ""; + setError(""); // Сбрасываем ошибку при смене формы renderForm(); }); @@ -135,6 +136,8 @@ export function renderAuthPageComponent({ appEl, setUser, user, goToPage }) { data.imageUrl = imageUrl; } + console.log("Register: Sending data:", data); // Отладочный лог + registerUser(data) .then((userData) => { if (!userData.user?.token) { From 3657220106f13e9e776398fde7c59053df69f0eb Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 19 May 2025 22:04:32 +0500 Subject: [PATCH 09/11] =?UTF-8?q?=D0=B2=D1=80=D0=BE=D0=B4=D0=B5=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=87=D0=B0=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=B8=D1=8F=20+=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B0=20=D0=B8=20=D0=BF=D1=83=D0=BB=D1=8C=D1=81?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BB=D0=B0=D0=B9=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/posts-page-component.js | 108 +++++++++++++++++++---------- styles.css | 27 +++++++- 2 files changed, 97 insertions(+), 38 deletions(-) diff --git a/components/posts-page-component.js b/components/posts-page-component.js index 194783eab..2b8bc6378 100644 --- a/components/posts-page-component.js +++ b/components/posts-page-component.js @@ -2,24 +2,17 @@ import { USER_POSTS_PAGE } from "../routes.js"; import { renderHeaderComponent } from "./header-component.js"; import { posts, goToPage, user } from "../index.js"; import { likePost, dislikePost } from "../api.js"; -import { formatDistanceToNow } from "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/+esm"; -import ru from "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/locale/ru/+esm"; export function renderPostsPageComponent({ appEl }) { - console.log("renderPostsPageComponent called"); const renderPosts = () => { console.log("Rendering posts:", posts); + const postsHtml = posts .map((post) => { - let createdAt = post.createdAt; - try { - createdAt = formatDistanceToNow(new Date(post.createdAt), { - locale: ru, - addSuffix: true, - }); - } catch (error) { - console.error("Ошибка форматирования даты:", error); - } + // Упрощённый вывод даты + const date = new Date(post.createdAt); + const createdAt = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; + return `
  • @@ -30,7 +23,7 @@ export function renderPostsPageComponent({ appEl }) { Post image
    -

    @@ -42,6 +35,10 @@ export function renderPostsPageComponent({ appEl }) { ${post.description}

    + + ${user && post.user.login === user.login ? ` + + ` : ""}
  • `; }) .join(""); @@ -52,42 +49,79 @@ export function renderPostsPageComponent({ appEl }) {
      ${postsHtml}
    `; - console.log("Setting appEl.innerHTML"); appEl.innerHTML = appHtml; - console.log("Rendering header component"); renderHeaderComponent({ element: document.querySelector(".header-container"), + user, + goToPage, }); + // Переход к пользователю for (let userEl of document.querySelectorAll(".post-header")) { userEl.addEventListener("click", () => { - console.log("Navigating to user posts page:", userEl.dataset.userId); - goToPage(USER_POSTS_PAGE, { userId: userEl.dataset.userId }); + const userId = userEl.dataset.userId; + console.log("Navigating to user posts page:", userId); + goToPage(USER_POSTS_PAGE, { userId }); }); } - if (user) { - for (let likeButton of document.querySelectorAll(".like-button")) { - likeButton.addEventListener("click", () => { - const postId = likeButton.dataset.postId; - const post = posts.find((p) => p.id === postId); - const token = user ? `Bearer ${user.token}` : undefined; - - console.log("Liking/disliking post:", postId); - const action = post.isLiked ? dislikePost : likePost; - action({ token, postId }) - .then((updatedPost) => { - const index = posts.findIndex((p) => p.id === postId); - posts[index] = updatedPost.post; - renderPosts(); - }) - .catch((error) => { - console.error("Ошибка при изменении лайка:", error); - alert("Ошибка при изменении лайка. Попробуйте снова."); - }); - }); + // Лайки + for (let likeButton of document.querySelectorAll(".like-button")) { + likeButton.addEventListener("click", () => { + const postId = likeButton.dataset.postId; + const post = posts.find((p) => p.id === postId); + const token = user ? `Bearer ${user.token}` : undefined; + + console.log("Liking/disliking post:", postId); + + const action = post.isLiked ? dislikePost : likePost; + + action({ token, postId }).then(({ post: updatedPost }) => { + const index = posts.findIndex((p) => p.id === postId); + posts[index] = updatedPost; + + // Получаем элемент из DOM + const buttonEl = document.querySelector(`[data-post-id="${postId}"]`); + const imageEl = buttonEl?.querySelector("img"); + + if (imageEl) { + // Меняем изображение на active/not-active + imageEl.src = "./assets/images/like-" + (updatedPost.isLiked ? "active" : "not-active") + ".svg"; + + // Запускаем анимацию пульсации + imageEl.classList.add("liked"); + setTimeout(() => imageEl.classList.remove("liked"), 300); } + + renderPosts(); // Перерисовываем посты для обновления количества лайков + }); + }); +} + + // Удаление поста + for (let deleteButton of document.querySelectorAll(".delete-button")) { + deleteButton.addEventListener("click", (event) => { + const postId = deleteButton.dataset.postId; + + if (!confirm("Вы точно хотите удалить пост?")) return; + + fetch(`https://wedev-api.sky.pro/api/v1/prod/instapro/ ${postId}`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${user.token}`, + }, + }) + .then(() => { + const index = posts.findIndex((p) => p.id === postId); + posts.splice(index, 1); + renderPosts(); + }) + .catch((error) => { + console.error("Ошибка удаления поста:", error); + alert("Не удалось удалить пост"); + }); + }); } }; diff --git a/styles.css b/styles.css index b22689288..ef0e5311c 100644 --- a/styles.css +++ b/styles.css @@ -205,4 +205,29 @@ border: 1px solid #d32f2f; border-radius: 4px; text-align: center; -} \ No newline at end of file +} + +/* Анимация лайка */ +.like-button img { + transition: transform 0.2s ease, filter 0.2s ease; + width: 24px; + height: 24px; +} + +/* Увеличение при наведении */ +.like-button:hover img { + transform: scale(1.2); + filter: brightness(0.9) saturate(1.2); +} + +/* Пульсация при клике */ +.like-button img.liked { + animation: pulse 0.3s ease; + filter: none; +} + +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} From 5000f89ee22bbb52240cbe6daf6e7dc75937690f Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 19 May 2025 22:12:37 +0500 Subject: [PATCH 10/11] =?UTF-8?q?=D0=A4=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 34719e139..f7acccdfe 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,74 @@ -# Instapro +# Instapro — социальная сеть SkyPro -MVP социальной сети для обмена фотографиями +Это клиентская часть приложения, написанная на чистом JavaScript. +Приложение позволяет: +- Регистрироваться и авторизоваться +- Просматривать ленту постов +- Ставить лайки и дизлайки +- Добавлять собственные посты с описанием и изображением +- Удалять свои посты с подтверждением (всплывающее окно) -## Первоначальная оценка +--- -ХХХХ часов +## Реализованная функциональность -## Фактически затраченное время +✅ **Авторизация и регистрация** +✅ **Лента постов (общая и пользовательская)** +✅ **Добавление постов с изображением** +✅ **Лайки/дизлайки с анимацией** +✅ **Удаление постов с подтверждением через `confirm()`** +✅ **Адаптивный интерфейс** -YYYY часов +--- + +## Затраченное время + +🕒 **Первоначальная оценка:** 8 часов +🕒 **Фактически затрачено:** ~12 часов + +> Причины перерасхода: +- Долгий разбор ошибок API +- Проблемы с импортом `date-fns` +- Отладка запросов к серверу +- Добавление анимаций и улучшений UX + +--- + +## 💡 Дополнительные фичи + +- Анимация лайка при наведении и клике (`@keyframes`, `transition`) +- Подтверждение удаления поста через модалку браузера (`confirm(...)`) +- Переход к постам пользователя по клику на имя +- Отображение даты создания поста (упрощённый формат: `03.05.2025 14:30:00`) +- Обновление лайков без перезагрузки страницы + +--- + +## 🔧 Технические детали + +- API: https://wedev-api.sky.pro +- Авторизация: Bearer токен +- Формат данных: JSON +- Загрузка изображений: `FormData` +- Клиентская маршрутизация: реализована вручную через `goToPage(...)` и `renderApp()` + +--- + +## 🛠 Как запустить проект: + +1. Клонируйте репозиторий +2. Установите Live Server или любой локальный сервер +3. Запустите командой `npm run start` или откройте `index.html` через Live Server +4. Перейдите на главную страницу → всё работает! + +--- + +## 📌 Вывод + +Проект полностью соответствует техническому заданию. +Дополнительно были добавлены элементы UI/UX: +- Анимации +- Модалки +- Удобный интерфейс для взаимодействия с постами + +Проект готов к сдаче! \ No newline at end of file From bf593d410d6c2e7ae0e4bc4d3996c4cae163a7b3 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 21 May 2025 13:17:45 +0500 Subject: [PATCH 11/11] =?UTF-8?q?=D0=A4=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8.=20?= =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BA=D0=BE=D0=BD=D1=81=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=20=D0=BB=D0=BE=D0=B3=D0=B8,=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5,=20=D0=B2=D0=B8?= =?UTF-8?q?=D0=B7=D1=83=D0=B0=D0=BB+=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B8?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D1=81=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 74 +++--- api.js | 13 -- components/add-post-page-component.js | 6 +- components/auth-page-component.js | 2 - components/posts-page-component.js | 17 +- helpers.js | 3 - index.js | 39 ---- styles.css | 314 +++++++++++++++++--------- 8 files changed, 259 insertions(+), 209 deletions(-) diff --git a/README.md b/README.md index f7acccdfe..6d3a0294f 100644 --- a/README.md +++ b/README.md @@ -2,73 +2,77 @@ Это клиентская часть приложения, написанная на чистом JavaScript. Приложение позволяет: -- Регистрироваться и авторизоваться -- Просматривать ленту постов -- Ставить лайки и дизлайки +- Регистрироваться и авторизовываться +- Просматривать ленту постов (общая и пользовательские) +- Ставить и убирать лайки - Добавлять собственные посты с описанием и изображением -- Удалять свои посты с подтверждением (всплывающее окно) +- Удалять свои посты с подтверждением через всплывающее окно --- -## Реализованная функциональность +## 📌 Основные реализованные фичи ✅ **Авторизация и регистрация** -✅ **Лента постов (общая и пользовательская)** -✅ **Добавление постов с изображением** -✅ **Лайки/дизлайки с анимацией** -✅ **Удаление постов с подтверждением через `confirm()`** -✅ **Адаптивный интерфейс** +✅ **Лента постов (общая и пользовательская)** +✅ **Добавление постов с загрузкой изображения** +✅ **Лайки/дизлайки с анимацией при наведении и клике** +✅ **Удаление поста с подтверждением через `confirm()`** +✅ **Адаптивный интерфейс** +✅ **Экранирование HTML-тегов в описании поста** --- -## Затраченное время +## ⏱️ Затрачённое время 🕒 **Первоначальная оценка:** 8 часов 🕒 **Фактически затрачено:** ~12 часов -> Причины перерасхода: -- Долгий разбор ошибок API -- Проблемы с импортом `date-fns` -- Отладка запросов к серверу -- Добавление анимаций и улучшений UX +### Причины перерасхода времени: +- Разбор ошибок API (`400`, `401`) +- Исправление запросов без `Content-Type` +- Разбор ошибок с регистрацией и авторизацией +- Реализация анимаций и улучшений UX +- Устранение проблем с маршрутизацией и отрисовкой страниц --- -## 💡 Дополнительные фичи +## 💡 Дополнительные улучшения -- Анимация лайка при наведении и клике (`@keyframes`, `transition`) -- Подтверждение удаления поста через модалку браузера (`confirm(...)`) -- Переход к постам пользователя по клику на имя -- Отображение даты создания поста (упрощённый формат: `03.05.2025 14:30:00`) -- Обновление лайков без перезагрузки страницы +- 🎨 **Современный внешний вид:** добавлены отступы, стилизованные кнопки, тени, скругления +- 🖼 **Анимации лайков:** плавное увеличение при наведении и пульсация при клике +- 🔐 **Экранирование HTML в описании поста:** защита от XSS +- 📱 **Адаптивность:** корректное отображение на мобильных устройствах +- ❌ **Удаление поста с подтверждением** +- 👤 **Переход к постам пользователя по клику на имя** --- ## 🔧 Технические детали -- API: https://wedev-api.sky.pro -- Авторизация: Bearer токен -- Формат данных: JSON -- Загрузка изображений: `FormData` -- Клиентская маршрутизация: реализована вручную через `goToPage(...)` и `renderApp()` +- **API:** https://wedev-api.sky.pro +- **Авторизация:** Bearer токен +- **Формат данных:** JSON +- **Загрузка изображений:** через `FormData` +- **Клиентская маршрутизация:** реализована вручную через `goToPage(...)` и `renderApp()` --- ## 🛠 Как запустить проект: 1. Клонируйте репозиторий -2. Установите Live Server или любой локальный сервер -3. Запустите командой `npm run start` или откройте `index.html` через Live Server -4. Перейдите на главную страницу → всё работает! +2. Откройте `index.html` через Live Server или любой локальный сервер +3. Перейдите на главную страницу → всё работает! --- ## 📌 Вывод -Проект полностью соответствует техническому заданию. -Дополнительно были добавлены элементы UI/UX: -- Анимации -- Модалки -- Удобный интерфейс для взаимодействия с постами +Проект полностью соответствует техническому заданию и успешно прошёл все этапы разработки. + +Были выполнены следующие доработки: +- Улучшен внешний вид и адаптивность +- Добавано экранирование HTML в постах +- Убраны все `console.log(...)` +- Оставлены только обработчики ошибок через `console.error(...)` Проект готов к сдаче! \ No newline at end of file diff --git a/api.js b/api.js index 6c99a1a80..403eebcb4 100644 --- a/api.js +++ b/api.js @@ -3,7 +3,6 @@ const baseHost = "https://wedev-api.sky.pro"; const postsHost = `${baseHost}/api/v1/${personalKey}/instapro`; export function getPosts({ token }) { - console.log("getPosts called with token:", token); const headers = {}; if (token && token !== "Bearer undefined") { headers.Authorization = token; @@ -13,7 +12,6 @@ export function getPosts({ token }) { headers, }) .then((response) => { - console.log("getPosts response status:", response.status); if (response.status === 401) { throw new Error("Нет авторизации"); } @@ -25,13 +23,11 @@ export function getPosts({ token }) { return response.json(); }) .then((data) => { - console.log("getPosts data:", data); return data.posts; }); } export function getUserPosts({ token, userId }) { - console.log("getUserPosts called with token:", token, "userId:", userId); const headers = {}; if (token && token !== "Bearer undefined") { headers.Authorization = token; @@ -41,7 +37,6 @@ export function getUserPosts({ token, userId }) { headers, }) .then((response) => { - console.log("getUserPosts response status:", response.status); if (response.status === 401) { throw new Error("Нет авторизации"); } @@ -53,13 +48,11 @@ export function getUserPosts({ token, userId }) { return response.json(); }) .then((data) => { - console.log("getUserPosts data:", data); return data.posts; }); } export function addPost({ token, description, imageUrl }) { - console.log("addPost called with:", { token, description, imageUrl }); if (!description || !imageUrl) { throw new Error("Описание или URL изображения не переданы"); @@ -91,7 +84,6 @@ export function addPost({ token, description, imageUrl }) { } export function likePost({ token, postId }) { - console.log("likePost called with:", { token, postId }); return fetch(`${postsHost}/${postId}/like`, { method: "POST", headers: { @@ -99,7 +91,6 @@ export function likePost({ token, postId }) { }, }) .then((response) => { - console.log("likePost response status:", response.status); if (response.status === 401) { throw new Error("Нет авторизации"); } @@ -108,7 +99,6 @@ export function likePost({ token, postId }) { } export function dislikePost({ token, postId }) { - console.log("dislikePost called with:", { token, postId }); return fetch(`${postsHost}/${postId}/dislike`, { method: "POST", headers: { @@ -116,7 +106,6 @@ export function dislikePost({ token, postId }) { }, }) .then((response) => { - console.log("dislikePost response status:", response.status); if (response.status === 401) { throw new Error("Нет авторизации"); } @@ -187,7 +176,6 @@ export function uploadImage({ file }) { } export function verifyToken({ token }) { - console.log("verifyToken called with:", token); return fetch(baseHost + "/api/user/me", { method: "GET", headers: { @@ -195,7 +183,6 @@ export function verifyToken({ token }) { }, }) .then((response) => { - console.log("verifyToken response status:", response.status); return response.status === 200; }) .catch((error) => { diff --git a/components/add-post-page-component.js b/components/add-post-page-component.js index 7b6a42c39..063f3b29f 100644 --- a/components/add-post-page-component.js +++ b/components/add-post-page-component.js @@ -38,10 +38,6 @@ export function renderAddPostPageComponent({ appEl, onAddPostClick }) { document.getElementById("add-button").addEventListener("click", () => { const description = document.getElementById("description-input").value.trim(); - console.log("Description:", description); - console.log("imageUrl:", imageUrl); - console.log("token:", user?.token); - console.log("Текущий пользователь:", user); if (!description) { alert("Введите описание поста"); return; @@ -52,7 +48,7 @@ document.getElementById("add-button").addEventListener("click", () => { } const token = user && user.token ? `Bearer ${user.token}` : undefined; - console.log("Authorization token:", token); + addPost({ token, description, imageUrl }) .then(() => { diff --git a/components/auth-page-component.js b/components/auth-page-component.js index 0f5bc5ffa..fbd466f9b 100644 --- a/components/auth-page-component.js +++ b/components/auth-page-component.js @@ -136,8 +136,6 @@ export function renderAuthPageComponent({ appEl, setUser, user, goToPage }) { data.imageUrl = imageUrl; } - console.log("Register: Sending data:", data); // Отладочный лог - registerUser(data) .then((userData) => { if (!userData.user?.token) { diff --git a/components/posts-page-component.js b/components/posts-page-component.js index 2b8bc6378..281b395a9 100644 --- a/components/posts-page-component.js +++ b/components/posts-page-component.js @@ -3,10 +3,18 @@ import { renderHeaderComponent } from "./header-component.js"; import { posts, goToPage, user } from "../index.js"; import { likePost, dislikePost } from "../api.js"; +function escapeHTML(str) { + return str.replace(/[&<>'"]/g, (match) => ({ + "&": "&", + "<": "<", + ">": ">", + "'": "'", + '"': """, + }[match])); +} + export function renderPostsPageComponent({ appEl }) { const renderPosts = () => { - console.log("Rendering posts:", posts); - const postsHtml = posts .map((post) => { // Упрощённый вывод даты @@ -32,7 +40,7 @@ export function renderPostsPageComponent({ appEl }) {

    ${post.user.name} - ${post.description} + ${escapeHTML(post.description)}

    @@ -61,7 +69,6 @@ export function renderPostsPageComponent({ appEl }) { for (let userEl of document.querySelectorAll(".post-header")) { userEl.addEventListener("click", () => { const userId = userEl.dataset.userId; - console.log("Navigating to user posts page:", userId); goToPage(USER_POSTS_PAGE, { userId }); }); } @@ -73,8 +80,6 @@ export function renderPostsPageComponent({ appEl }) { const post = posts.find((p) => p.id === postId); const token = user ? `Bearer ${user.token}` : undefined; - console.log("Liking/disliking post:", postId); - const action = post.isLiked ? dislikePost : likePost; action({ token, postId }).then(({ post: updatedPost }) => { diff --git a/helpers.js b/helpers.js index b0f18376f..781aadf85 100644 --- a/helpers.js +++ b/helpers.js @@ -1,6 +1,5 @@ export function saveUserToLocalStorage(user) { try { - console.log("Saving user to localStorage:", user); localStorage.setItem("user", JSON.stringify(user)); } catch (error) { console.error("Error saving user to localStorage:", error); @@ -10,7 +9,6 @@ export function saveUserToLocalStorage(user) { export function getUserFromLocalStorage() { try { const user = localStorage.getItem("user"); - console.log("Retrieved user from localStorage:", user); return user ? JSON.parse(user) : null; } catch (error) { console.error("Error retrieving user from localStorage:", error); @@ -19,6 +17,5 @@ export function getUserFromLocalStorage() { } export function removeUserFromLocalStorage() { - console.log("Removing user from localStorage"); localStorage.removeItem("user"); } \ No newline at end of file diff --git a/index.js b/index.js index 82c005cea..ad63b35f2 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,3 @@ -console.log("Starting index.js"); import { getPosts, getUserPosts, } from "./api.js"; import { renderAddPostPageComponent } from "./components/add-post-page-component.js"; import { renderAuthPageComponent } from "./components/auth-page-component.js"; @@ -21,39 +20,19 @@ export let user = getUserFromLocalStorage(); export let page = null; export let posts = []; -console.log("Initial state:", { user, page, posts }); - const getToken = () => { const token = user && user.token ? `Bearer ${user.token}` : undefined; - console.log("getToken called, token:", token); return token; }; export const logout = () => { - console.log("Logging out"); user = null; removeUserFromLocalStorage(); goToPage(POSTS_PAGE); }; -// Проверка токена при старте -// if (user && user.token) { -// console.log("Verifying token at startup..."); -// verifyToken({ token: `Bearer ${user.token}` }) -// .then((isValid) => { -// if (!isValid) { -// console.log("Invalid token, logging out"); -// logout(); -// } -// }) -// .catch((error) => { -// console.error("Token verification error:", error); -// logout(); -// }); -// } export const goToPage = (newPage, data) => { - console.log("goToPage called with:", { newPage, data }); if ( [ POSTS_PAGE, @@ -64,21 +43,17 @@ export const goToPage = (newPage, data) => { ].includes(newPage) ) { if (newPage === ADD_POSTS_PAGE) { - console.log("Navigating to ADD_POSTS_PAGE, user:", user); page = user ? ADD_POSTS_PAGE : AUTH_PAGE; renderApp(); return; } if (newPage === POSTS_PAGE) { - console.log("Navigating to POSTS_PAGE"); page = LOADING_PAGE; renderApp(); - console.log("Fetching posts..."); getPosts({ token: getToken() }) .then((newPosts) => { - console.log("Posts fetched:", newPosts); page = POSTS_PAGE; posts = newPosts; renderApp(); @@ -98,14 +73,11 @@ export const goToPage = (newPage, data) => { } if (newPage === USER_POSTS_PAGE) { - console.log("Navigating to USER_POSTS_PAGE with userId:", data?.userId); page = LOADING_PAGE; renderApp(); - console.log("Fetching user posts..."); getUserPosts({ token: getToken(), userId: data.userId }) .then((newPosts) => { - console.log("User posts fetched:", newPosts); page = USER_POSTS_PAGE; posts = newPosts; renderApp(); @@ -124,7 +96,6 @@ export const goToPage = (newPage, data) => { return; } - console.log("Navigating to page:", newPage); page = newPage; renderApp(); return; @@ -136,12 +107,9 @@ export const goToPage = (newPage, data) => { }; const renderApp = () => { - console.log("renderApp called, current page:", page); const appEl = document.getElementById("app"); - console.log("appEl:", appEl); if (page === LOADING_PAGE) { - console.log("Rendering LOADING_PAGE"); return renderLoadingPageComponent({ appEl, user, @@ -150,11 +118,9 @@ const renderApp = () => { } if (page === AUTH_PAGE) { - console.log("Rendering AUTH_PAGE"); return renderAuthPageComponent({ appEl, setUser: (newUser) => { - console.log("Setting new user:", newUser); user = newUser; saveUserToLocalStorage(user); goToPage(POSTS_PAGE); @@ -165,26 +131,21 @@ const renderApp = () => { } if (page === ADD_POSTS_PAGE) { - console.log("Rendering ADD_POSTS_PAGE"); return renderAddPostPageComponent({ appEl, onAddPostClick({ description, imageUrl }) { - console.log("Adding post:", { description, imageUrl }); goToPage(POSTS_PAGE); }, }); } if (page === POSTS_PAGE || page === USER_POSTS_PAGE) { - console.log("Rendering POSTS_PAGE or USER_POSTS_PAGE, posts:", posts); return renderPostsPageComponent({ appEl, }); } - console.error("Unknown page:", page); appEl.innerHTML = `
    Ошибка: неизвестная страница
    `; }; -console.log("Calling initial goToPage(POSTS_PAGE)"); goToPage(POSTS_PAGE); \ No newline at end of file diff --git a/styles.css b/styles.css index ef0e5311c..1d47c514f 100644 --- a/styles.css +++ b/styles.css @@ -1,7 +1,26 @@ +/* === Общие стили === */ + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Segoe UI", sans-serif; + background-color: #fafafa; + color: #222; +} + +/* === Контейнер страницы === */ + .page-container { + max-width: 600px; + margin: 0 auto; padding: 0 20px; } +/* === Шапка === */ + .page-header { display: flex; justify-content: space-between; @@ -10,6 +29,7 @@ padding: 20px 16px; margin-left: -20px; margin-right: -20px; + background-color: #fff; } .logo { @@ -19,10 +39,6 @@ width: 130px; } -.header-container { - margin-bottom: 8px; -} - .header-button { padding: 0; border: none; @@ -31,6 +47,7 @@ line-height: 18px; font-weight: 500; cursor: pointer; + color: #000; } .logout-button { @@ -38,19 +55,90 @@ text-align: right; } +/* === Форма регистрации/авторизации === */ + +.form { + display: flex; + flex-direction: column; + gap: 16px; + background-color: #fff; + padding: 20px; + border-radius: 8px; + margin-top: 20px; +} + +.form-title { + font-size: 28px; + line-height: 35px; + text-align: center; + margin-bottom: 20px; +} + +.form-inputs { + display: flex; + flex-direction: column; + gap: 10px; +} + +.input { + padding: 10px; + border: 1px solid #ccc; + border-radius: 6px; + font-size: 16px; +} + +.button { + padding: 12px; + background-color: #000; + color: #fff; + border: none; + border-radius: 6px; + font-size: 16px; + cursor: pointer; + margin-top: 8px; +} + +.button--link { + background-color: transparent; + color: #000; + font-size: 14px; + margin-top: 0; +} + +.form-buttons { + margin-top: 16px; + text-align: center; +} + +.form-error { + color: red; + font-size: 14px; + text-align: center; +} + +/* === Посты === */ + .posts { display: flex; flex-direction: column; list-style: none; padding: 0; - margin: 0; + margin-top: 20px; + gap: 20px; +} + +.post { + background-color: #fff; + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); } .post-header { display: flex; - flex-direction: row; align-items: center; - margin-bottom: 10px; + gap: 12px; + margin-bottom: 16px; cursor: pointer; } @@ -58,127 +146,130 @@ width: 40px; height: 40px; object-fit: cover; - border-radius: 40px; - margin-right: 10px; + border-radius: 50%; +} + +.post-header__user-name { + font-weight: bold; + font-size: 16px; } .post-image-container { - margin-left: -20px; - margin-right: -20px; - height: 500px; - display: flex; - justify-content: center; - background-color: #e0e0e0; + position: relative; + overflow: hidden; + border-radius: 8px; + margin-bottom: 16px; } .post-image { width: 100%; - height: 100%; - max-width: 500px; - object-fit: cover; + height: auto; + display: block; } .post-likes { display: flex; - flex-direction: row; align-items: center; + gap: 8px; + margin-bottom: 8px; } .like-button { + background: transparent; border: none; - background-color: transparent; - padding: 8px; - padding-left: 0px; - padding-bottom: 5px; + padding: 0; cursor: pointer; } -.user-name { - font-weight: 500; +.like-button img { + transition: transform 0.2s ease, filter 0.2s ease; + width: 24px; + height: 24px; } -.post-text { - font-size: 14px; - line-height: 18px; - margin-top: 5px; +.like-button:hover img { + transform: scale(1.2); + filter: brightness(0.9) saturate(1.2); } -.post-date { - color: #8a8a8a; +.like-button img.liked { + animation: pulse 0.3s ease; + filter: none; } -.post + .post { - margin-top: 20px; +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } } -.posts-user-header { - display: flex; - flex-direction: row; - align-items: center; - margin-bottom: 12px; +.post-likes-text { + font-size: 14px; + margin: 0; } -.posts-user-header__user-image { - width: 70px; - height: 70px; - object-fit: cover; - border-radius: 40px; - margin-right: 10px; +.post-text { + font-size: 16px; + line-height: 1.4; + margin: 10px 0; } -.posts-user-header__user-name { - font-size: 28px; - line-height: 35px; +.user-name { + font-weight: 500; + margin-right: 8px; } -.loading-page { - display: flex; - justify-content: center; - margin-top: 100px; +.post-date { + font-size: 12px; + color: #888; + margin-top: 8px; } +.delete-button { + margin-top: 12px; + background: transparent; + color: red; + border: none; + font-size: 14px; + cursor: pointer; +} + +/* === Загрузка поста === */ + .add-post-sign { background-image: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 122.88 122.88'%3E%3Ctitle%3Eadd%3C/title%3E%3Cpath d='M61.44,0A61.46,61.46,0,1,1,18,18,61.25,61.25,0,0,1,61.44,0ZM88.6,56.82v9.24a4,4,0,0,1-4,4H70V84.62a4,4,0,0,1-4,4H56.82a4,4,0,0,1-4-4V70H38.26a4,4,0,0,1-4-4V56.82a4,4,0,0,1,4-4H52.84V38.26a4,4,0,0,1,4-4h9.24a4,4,0,0,1,4,4V52.84H84.62a4,4,0,0,1,4,4Zm8.83-31.37a50.92,50.92,0,1,0,14.9,36,50.78,50.78,0,0,0-14.9-36Z'/%3E%3C/svg%3E"); height: 30px; width: 30px; + background-repeat: no-repeat; + background-size: contain; + background-position: center; } -.form { - display: flex; - flex-direction: column; - width: 100%; -} - -.form-title { - font-size: 28px; - line-height: 35px; - text-align: center; -} +/* === Форма добавления поста === */ -.form-inputs { - display: flex; - flex-direction: column; - gap: 10px; +.upload-image-container { + margin-bottom: 16px; } -.form-error { - color: red; -} - -.form-footer { - margin-top: 50px; - display: flex; - flex-direction: column; - gap: 10px; +.file-upload-label { + display: inline-block; + width: 100%; + padding: 10px; + text-align: center; + border: 1px dashed #ccc; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; } -.form-footer-title { - text-align: center; +.file-upload-label:hover { + background-color: #f5f5f5; } -.file-upload-image-conrainer { +.file-upload-image-container { display: flex; align-items: center; + flex-wrap: wrap; } .file-upload-image { @@ -186,48 +277,59 @@ height: 100px; object-fit: cover; margin-right: 10px; - border: 1px solid gray; border-radius: 5px; + border: 1px solid #ddd; } -.file-upload-label { - display: inline-block; - box-sizing: border-box; - width: 100%; - text-align: center; +.textarea { + resize: vertical; + min-height: 100px; + padding: 12px; + font-size: 16px; + border: 1px solid #ccc; + border-radius: 6px; + margin-bottom: 16px; } +/* === Ошибки и уведомления === */ + .error-message { background-color: #ffe6e6; color: #d32f2f; padding: 10px; - margin: 10px; + margin: 10px 0; border: 1px solid #d32f2f; border-radius: 4px; text-align: center; } -/* Анимация лайка */ -.like-button img { - transition: transform 0.2s ease, filter 0.2s ease; - width: 24px; - height: 24px; -} +/* === Адаптивность (мобильные) === */ -/* Увеличение при наведении */ -.like-button:hover img { - transform: scale(1.2); - filter: brightness(0.9) saturate(1.2); -} +@media (max-width: 600px) { + .page-container { + padding: 0 10px; + } -/* Пульсация при клике */ -.like-button img.liked { - animation: pulse 0.3s ease; - filter: none; -} + .post-header { + flex-direction: row; + align-items: center; + } -@keyframes pulse { - 0% { transform: scale(1); } - 50% { transform: scale(1.2); } - 100% { transform: scale(1); } -} + .post-header__user-name { + font-size: 16px; + } + + .post-image { + width: 100%; + height: auto; + } + + .like-button img { + width: 20px; + height: 20px; + } + + .delete-button { + font-size: 14px; + } +} \ No newline at end of file