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..4f3b06c33 100644
--- a/api.js
+++ b/api.js
@@ -1,5 +1,3 @@
-// Замени на свой, чтобы получить независимый от других набор данных.
-// "боевая" версия инстапро лежит в ключе prod
const personalKey = "prod";
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 662ccdbdc..0c6845548 100644
--- a/components/posts-page-component.js
+++ b/components/posts-page-component.js
@@ -1,109 +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 `
+
+
+
+
+
+
+
+
+
+
+ Нравится: ${
+ Array.isArray(post.likes) ? post.likes.length : post.likes
+ }
+
+
+
+ ${post.user.name}
+ ${post.description}
+
+ ${formattedDate}
+
+ `;
+ })
+ .join("")
+ : "Нет постов
";
- /**
- * @TODO: чтобы отформатировать дату создания поста в виде "19 минут назад"
- * можно использовать https://date-fns.org/v2.29.3/docs/formatDistanceToNow
- */
const appHtml = `
-
-
-
-
-
-
-
-
-
-
-
-
-
- Нравится: 2
-
-
-
- Иван Иваныч
- Ромашка, ромашка...
-
-
- 19 минут назад
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Нравится: 35
-
-
-
- Варварва Н.
- Нарисовала картину, посмотрите какая красивая
-
-
- 3 часа назад
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Нравится: 0
-
-
-
- Варварва Н.
- Голова
-
-
- 8 дней назад
-
-
-
-
`;
+
+ `;
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}
+ ${formattedDate}
+
+ `;
+ })
+ .join("")
+ : "Нет постов
";
+
+ const userImageUrl =
+ posts.length > 0 ? posts[0].user.imageUrl : "default-avatar.png";
+ const userName =
+ posts.length > 0 ? posts[0].user.name : "Неизвестный пользователь";
+
+ const appHtml = `
+
+ `;
+
+ 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;
+}