From 1b70dab3b939f6292ae81dcef6db3aa68291c91f Mon Sep 17 00:00:00 2001 From: Aleksandr Lagutin Date: Thu, 27 Mar 2025 10:03:25 +0300 Subject: [PATCH 1/4] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34719e139..672560552 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ MVP социальной сети для обмена фотографиями ## Первоначальная оценка -ХХХХ часов +40 часов ## Фактически затраченное время From acc99fddb00169be0e6d1a7caebd1925b20eb546 Mon Sep 17 00:00:00 2001 From: AlexxGutt Date: Tue, 6 May 2025 17:19:43 +0300 Subject: [PATCH 2/4] Rendering of posts from API has been implemented --- components/posts-page-component.js | 81 ++++++------------------------ 1 file changed, 16 insertions(+), 65 deletions(-) diff --git a/components/posts-page-component.js b/components/posts-page-component.js index 662ccdbdc..5557d08e5 100644 --- a/components/posts-page-component.js +++ b/components/posts-page-component.js @@ -10,17 +10,21 @@ export function renderPostsPageComponent({ appEl }) { * @TODO: чтобы отформатировать дату создания поста в виде "19 минут назад" * можно использовать https://date-fns.org/v2.29.3/docs/formatDistanceToNow */ - const appHtml = ` -
+ + const appHtml = posts + .map((post, index) => { + return ` +
    -
  • +
  • - -

    Иван Иваныч

    + +

    ${post.user.name}

    - +

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

    - -
  • -
  • -
    - -

    Варварва Н.

    -
    - - -
    - -
    -
    - -

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

    -
    -

    - Варварва Н. - Нарисовала картину, посмотрите какая красивая + ${post.user.name} + ${post.description}

    -
  • -
  • -
    - -

    Варварва Н.

    -
    - - -
    - -
    -
    - -

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

    -
    -

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

    - -
  • -
-
`; - + `; + }) + .join(""); appEl.innerHTML = appHtml; renderHeaderComponent({ From 34bc896bd38d74a3bae48f5873bb9fd4704e550f Mon Sep 17 00:00:00 2001 From: AlexxGutt Date: Tue, 12 Aug 2025 12:05:20 +0300 Subject: [PATCH 3/4] Done --- README.md | 10 ++- api.js | 100 ++++++++++++++++++++- components/add-post-page-component.js | 62 ++++++++++--- components/posts-page-component.js | 125 +++++++++++++++++--------- components/user-page-component.js | 79 ++++++++++++++++ index.js | 46 ++++++---- styles.css | 80 +++++++++++++++-- 7 files changed, 417 insertions(+), 85 deletions(-) create mode 100644 components/user-page-component.js diff --git a/README.md b/README.md index 672560552..e0cb5fbe4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # Instapro -MVP социальной сети для обмена фотографиями +Реализовано: +страница добалнения поста +функционал лайков +страница автора поста +получение списка постов по API ## Первоначальная оценка @@ -8,4 +12,6 @@ MVP социальной сети для обмена фотографиями ## Фактически затраченное время -YYYY часов +38 часов + +Доп. функции не реализовывались... diff --git a/api.js b/api.js index 123a9e3b6..7c83859ca 100644 --- a/api.js +++ b/api.js @@ -1,6 +1,4 @@ -// Замени на свой, чтобы получить независимый от других набор данных. -// "боевая" версия инстапро лежит в ключе prod -const personalKey = "prod"; +const personalKey = "keyProd"; const baseHost = "https://webdev-hw-api.vercel.app"; const postsHost = `${baseHost}/api/v1/${personalKey}/instapro`; @@ -23,6 +21,39 @@ export function getPosts({ token }) { }); } +export function addPost({ token, description, imageUrl }) { + const url = `${baseHost}/api/v1/${personalKey}/instapro`; + + if (!description || !imageUrl) { + throw new Error("Необходимо передать описание и ссылку на изображение"); + } + + return fetch(url, { + method: "POST", + headers: { + "Authorization": token, + }, + body: JSON.stringify({ description, imageUrl }), + }) + .then((response) => { + console.log("Статус ответа:", response.status); + if (!response.ok) { + return response.text().then((text) => { + throw new Error(`Ошибка при добавлении поста: ${text}`); + }); + } + return response.json(); + }) + .then((data) => { + console.log("Пост успешно добавлен:", data); + return data; + }) + .catch((error) => { + console.error("Ошибка при добавлении поста:", error); + throw error; + }); +} + export function registerUser({ login, password, name, imageUrl }) { return fetch(baseHost + "/api/user", { method: "POST", @@ -55,7 +86,6 @@ export function loginUser({ login, password }) { }); } -// Загружает картинку в облако, возвращает url загруженной картинки export function uploadImage({ file }) { const data = new FormData(); data.append("file", file); @@ -67,3 +97,65 @@ export function uploadImage({ file }) { return response.json(); }); } + +export function getUserPosts({ token, userId }) { + return fetch(`${postsHost}/user-posts/${userId}`, { + method: "GET", + headers: { + Authorization: token, + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error("Ошибка загрузки постов пользователя"); + } + return response.json(); + }) + .then((data) => data.posts); +} + +export function likePost({ postId, token }) { + return fetch(`${postsHost}/${postId}/like`, { + method: "POST", + headers: { + Authorization: token, + }, + }).then((response) => { + if (!response.ok) { + throw new Error("Ошибка при лайке поста"); + } + return response.json(); + }); +} + +export function dislikePost({ postId, token }) { + return fetch(`${postsHost}/${postId}/dislike`, { + method: "POST", + headers: { + Authorization: token, + }, + }).then((response) => { + if (!response.ok) { + throw new Error("Ошибка при дизлайке поста"); + } + return response.json(); + }); +} + +export function toggleLike({ postId, token, isLiked }) { + const url = `${postsHost}/${postId}/${isLiked ? "dislike" : "like"}`; + + console.log(`Sending request to: ${url}`); + + return fetch(url, { + method: "POST", + headers: { + Authorization: token, + }, + }).then((response) => { + if (!response.ok) { + throw new Error("Ошибка при лайке поста"); + } + return response.json(); + }); +} diff --git a/components/add-post-page-component.js b/components/add-post-page-component.js index 8277f093b..dab8c6c4b 100644 --- a/components/add-post-page-component.js +++ b/components/add-post-page-component.js @@ -1,21 +1,59 @@ +import { renderHeaderComponent } from "./header-component.js"; +import { renderUploadImageComponent } from "./upload-image-component.js"; + export function renderAddPostPageComponent({ appEl, onAddPostClick }) { const render = () => { - // @TODO: Реализовать страницу добавления поста const appHtml = ` -
-
- Cтраница добавления поста - -
- `; - +
+
+
+

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

+
+
+
+ + +
+
+
+ `; appEl.innerHTML = appHtml; - document.getElementById("add-button").addEventListener("click", () => { - onAddPostClick({ - description: "Описание картинки", - imageUrl: "https://image.png", + renderHeaderComponent({ + element: document.querySelector(".header-container"), + }); + + let previewUrl = null; + + const uploadImageContainer = appEl.querySelector(".upload-image-container"); + if (uploadImageContainer) { + renderUploadImageComponent({ + element: uploadImageContainer, + onImageUrlChange(newImageUrl) { + previewUrl = newImageUrl; + }, }); + } + + document.getElementById("add-button").addEventListener("click", () => { + const description = document + .getElementById("post-description") + .value.trim(); + + if (!previewUrl) { + alert("Пожалуйста, выберите фото"); + return; + } + + if (!description) { + alert("Пожалуйста, добавьте описание"); + return; + } + + onAddPostClick({ description, imageUrl: previewUrl }); }); }; diff --git a/components/posts-page-component.js b/components/posts-page-component.js index 5557d08e5..0c6845548 100644 --- a/components/posts-page-component.js +++ b/components/posts-page-component.js @@ -1,60 +1,97 @@ import { USER_POSTS_PAGE } from "../routes.js"; import { renderHeaderComponent } from "./header-component.js"; -import { posts, goToPage } from "../index.js"; +import * as state from "../index.js"; +import { toggleLike } from "../api.js"; export function renderPostsPageComponent({ appEl }) { - // @TODO: реализовать рендер постов из api - console.log("Актуальный список постов:", posts); + const postsHtml = state.posts.length + ? state.posts + .map((post) => { + const formattedDate = post.createdAt + ? new Date(post.createdAt).toLocaleString() + : ""; + return ` +
  • +
    + +

    ${post.user.name}

    +
    +
    + +
    +
    + +

    + Нравится: ${ + Array.isArray(post.likes) ? post.likes.length : post.likes + } +

    +
    +

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

    + +
  • + `; + }) + .join("") + : "

    Нет постов

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

      ${post.user.name}

      -
      -
      - -
      -
      - -

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

      -
      -

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

      - -
    • `; - }) - .join(""); +
      +
        + ${postsHtml} +
      +
    + `; + appEl.innerHTML = appHtml; + document.querySelectorAll(".like-button").forEach((button) => { + button.addEventListener("click", () => { + const postId = button.dataset.postId; + const post = state.posts.find((p) => p.id === postId); + if (!post) return; - renderHeaderComponent({ - element: document.querySelector(".header-container"), + toggleLike({ + postId, + token: state.getToken(), + isLiked: post.isLiked, + }) + .then((updatedPost) => { + const updatedPosts = state.posts.map((p) => { + if (p.id === postId) { + return { + ...p, + isLiked: updatedPost.post.isLiked, + likes: updatedPost.post.likes.length, + }; + } + return p; + }); + state.posts.splice(0, state.posts.length, ...updatedPosts); + renderPostsPageComponent({ appEl }); + }) + .catch((error) => { + console.error("Ошибка при обновлении лайка:", error); + alert("Не удалось обновить лайк"); + }); + }); }); - for (let userEl of document.querySelectorAll(".post-header")) { userEl.addEventListener("click", () => { - goToPage(USER_POSTS_PAGE, { + state.goToPage(USER_POSTS_PAGE, { userId: userEl.dataset.userId, }); }); } + + renderHeaderComponent({ + element: document.querySelector(".header-container"), + }); } diff --git a/components/user-page-component.js b/components/user-page-component.js new file mode 100644 index 000000000..306082b48 --- /dev/null +++ b/components/user-page-component.js @@ -0,0 +1,79 @@ +import { renderHeaderComponent } from "./header-component.js"; +import { posts } from "../index.js"; +import { likePost, dislikePost } from "../api.js"; +import { getToken } from "../index.js"; + +export function renderUserPostsPageComponent({ appEl }) { + const postsHtml = posts.length + ? posts + .map((post) => { + const formattedDate = new Date(post.createdAt).toLocaleString(); + return ` +
  • +
    + Пост +
    +
    + +

    + Нравится: ${ + Array.isArray(post.likes) ? post.likes.length : post.likes + } +

    +
    +

    ${post.description}

    + +
  • + `; + }) + .join("") + : "

    Нет постов

    "; + + const userImageUrl = + posts.length > 0 ? posts[0].user.imageUrl : "default-avatar.png"; + const userName = + posts.length > 0 ? posts[0].user.name : "Неизвестный пользователь"; + + const appHtml = ` +
    +
    +
    + ${userName} +

    ${userName}

    +
    +
      + ${postsHtml} +
    +
    + `; + + appEl.innerHTML = appHtml; + + document.querySelectorAll(".like-button").forEach((button) => { + button.addEventListener("click", () => { + const postId = button.dataset.postId; + const post = posts.find((p) => p.id === postId); + if (!post) return; + + const action = post.isLiked ? dislikePost : likePost; + + action({ postId, token: getToken() }) + .then((updatedPost) => { + const index = posts.findIndex((p) => p.id === postId); + posts[index] = updatedPost.post; + renderUserPostsPageComponent({ appEl }); + }) + .catch((error) => { + console.error("Ошибка при обновлении лайка:", error); + }); + }); + }); + + renderHeaderComponent({ + element: document.querySelector(".header-container"), + }); +} diff --git a/index.js b/index.js index 7f1817c75..98accab8e 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -import { getPosts } 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 { @@ -9,18 +9,20 @@ import { USER_POSTS_PAGE, } from "./routes.js"; import { renderPostsPageComponent } from "./components/posts-page-component.js"; +import { addPost } from "./api.js"; import { renderLoadingPageComponent } from "./components/loading-page-component.js"; import { getUserFromLocalStorage, removeUserFromLocalStorage, saveUserToLocalStorage, } from "./helpers.js"; +import { renderUserPostsPageComponent } from "./components/user-page-component.js"; export let user = getUserFromLocalStorage(); export let page = null; export let posts = []; -const getToken = () => { +export const getToken = () => { const token = user ? `Bearer ${user.token}` : undefined; return token; }; @@ -31,9 +33,6 @@ export const logout = () => { goToPage(POSTS_PAGE); }; -/** - * Включает страницу приложения - */ export const goToPage = (newPage, data) => { if ( [ @@ -45,7 +44,6 @@ export const goToPage = (newPage, data) => { ].includes(newPage) ) { if (newPage === ADD_POSTS_PAGE) { - /* Если пользователь не авторизован, то отправляем его на страницу авторизации перед добавлением поста */ page = user ? ADD_POSTS_PAGE : AUTH_PAGE; return renderApp(); } @@ -67,11 +65,21 @@ export const goToPage = (newPage, data) => { } if (newPage === USER_POSTS_PAGE) { - // @@TODO: реализовать получение постов юзера из API + page = LOADING_PAGE; + renderApp(); + console.log("Открываю страницу пользователя: ", data.userId); - page = USER_POSTS_PAGE; - posts = []; - return renderApp(); + + return getUserPosts({ token: getToken(), userId: data.userId }) + .then((newPosts) => { + page = USER_POSTS_PAGE; + posts = newPosts; + renderApp(); + }) + .catch((error) => { + console.error(error); + goToPage(POSTS_PAGE); + }); } page = newPage; @@ -110,9 +118,17 @@ const renderApp = () => { return renderAddPostPageComponent({ appEl, onAddPostClick({ description, imageUrl }) { - // @TODO: реализовать добавление поста в API console.log("Добавляю пост...", { description, imageUrl }); - goToPage(POSTS_PAGE); + const token = getToken(); + addPost({ token, description, imageUrl }) + .then((response) => { + console.log("Пост успешно добавлен:", response); + goToPage(POSTS_PAGE); + }) + .catch((error) => { + console.error("Ошибка при добавлении поста:", error); + alert(`Ошибка при добавлении поста: ${error.message}`); + }); }, }); } @@ -124,9 +140,9 @@ const renderApp = () => { } if (page === USER_POSTS_PAGE) { - // @TODO: реализовать страницу с фотографиями отдельного пользвателя - appEl.innerHTML = "Здесь будет страница фотографий пользователя"; - return; + return renderUserPostsPageComponent({ + appEl, + }); } }; diff --git a/styles.css b/styles.css index 57bc08b16..2ada07840 100644 --- a/styles.css +++ b/styles.css @@ -1,3 +1,7 @@ +html { + scrollbar-width: none; +} + .page-container { padding: 0 20px; } @@ -14,7 +18,9 @@ .logo { margin: 0; - font-size: 28px; + font-size: 32px; + font-weight: 700; + font-family: StratosSkyeng; cursor: pointer; width: 130px; } @@ -23,6 +29,20 @@ margin-bottom: 8px; } +.page-container { + padding: 0 20px; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #e0e0e0; + padding: 20px 16px; + margin-left: -20px; + margin-right: -20px; +} + .header-button { padding: 0; border: none; @@ -62,6 +82,11 @@ margin-right: 10px; } +.post-header__user-name { + font-size: 18px; + line-height: 30px; +} + .post-image-container { margin-left: -20px; margin-right: -20px; @@ -73,8 +98,8 @@ .post-image { width: 100%; - height: 100%; - max-width: 500px; + height: auto; + max-width: 425px; object-fit: cover; } @@ -119,16 +144,16 @@ } .posts-user-header__user-image { - width: 70px; - height: 70px; + width: 40px; + height: 40px; object-fit: cover; border-radius: 40px; margin-right: 10px; } .posts-user-header__user-name { - font-size: 28px; - line-height: 35px; + font-size: 18px; + line-height: 30px; } .loading-page { @@ -191,8 +216,47 @@ } .file-upload-label { - display: inline-block; + display: block; box-sizing: border-box; width: 100%; text-align: center; } + +.file-upload-preview-container { + display: flex; + align-items: center; + justify-content: flex-start; + width: auto; +} + +.file-upload-preview-container .replace-photo-button { + justify-content: flex-start; +} + +.upload-preview { + width: 100px; + height: 100px; + display: block; +} + +.replace-photo-button { + font-size: 14px; + padding: 5px 10px; + cursor: pointer; +} + +.upload-image-container { + display: block; +} + +.form-button { + height: min-content; +} + +.location { + justify-content: left; +} + +.center { + justify-content: center; +} From a72939b420d8c13aaa1be02afd366225ebbd6fa6 Mon Sep 17 00:00:00 2001 From: AlexxGutt Date: Tue, 12 Aug 2025 12:08:54 +0300 Subject: [PATCH 4/4] 1 --- api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.js b/api.js index 7c83859ca..4f3b06c33 100644 --- a/api.js +++ b/api.js @@ -1,4 +1,4 @@ -const personalKey = "keyProd"; +const personalKey = "prod"; const baseHost = "https://webdev-hw-api.vercel.app"; const postsHost = `${baseHost}/api/v1/${personalKey}/instapro`;