diff --git a/.gitignore b/.gitignore index 9daa8247..91dfed8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ .DS_Store -node_modules +node_modules \ No newline at end of file diff --git a/README.md b/README.md index 216dfc2a..309e4d7a 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ MVP аналога популярной соц. сети для обмена ф ## Ссылка на приложение: -https:: +https: ## Первоначальная оценка -ХХХХ часов +36 часов ## Фактически затраченное время -YYYY часов +15 часов diff --git a/api.js b/api.js index 8997c44d..e9d3368a 100644 --- a/api.js +++ b/api.js @@ -1,7 +1,8 @@ // Замени на свой, чтобы получить независимый от других набор данных. // "боевая" версия инстапро лежит в ключе prod -const personalKey = "prod"; -const baseHost = "https://webdev-hw-api.vercel.app"; + +const personalKey = "aman"; +const baseHost = "https://wedev-api.sky.pro"; const postsHost = `${baseHost}/api/v1/${personalKey}/instapro`; export function getPosts({ token }) { @@ -15,7 +16,24 @@ export function getPosts({ token }) { if (response.status === 401) { throw new Error("Нет авторизации"); } + return response.json(); + }) + .then((data) => { + return data.posts; + }); +} +export function getUserPosts({ token, userId }) { + return fetch(`${postsHost}/user-posts/${userId}`, { + method: "GET", + headers: { + Authorization: token, + }, + }) + .then((response) => { + if (response.status === 401) { + throw new Error("Нет авторизации"); + } return response.json(); }) .then((data) => { @@ -68,3 +86,40 @@ export function uploadImage({ file }) { return response.json(); }); } + +export function getPostsFromUsers() { + return fetch(); +} + +export function addPost({ token, description, imageUrl }) { + return fetch(postsHost, { + method: "POST", + headers: { + Authorization: token, + }, + body: JSON.stringify({ + description, + imageUrl, + }), + }).then((response) => { + if (response.status === 400) { + throw new Error("Неверный логин или пароль"); + } + return response.json(); + }); +} + +export function clickLike({ token, postId, like }) { + return fetch(`${postsHost}/${postId}/${like}`, { + method: "POST", + headers: { + Authorization: token, + }, + }).then((response) => { + if (response.status === 401) { + alert("Только авторизованный пользователь может поставить лайк"); + throw new Error("Ты неавторизован"); + } + return response.json(); + }); +} diff --git a/components/add-post-page-component.js b/components/add-post-page-component.js index 59554d86..4ecdee8a 100644 --- a/components/add-post-page-component.js +++ b/components/add-post-page-component.js @@ -1,23 +1,65 @@ +import { renderHeaderComponent } from "./header-component.js"; +import { renderUploadImageComponent } from "./upload-image-component.js"; + export function renderAddPostPageComponent({ appEl, onAddPostClick }) { + let imageUrl = ""; + + const headerHtml = ` +
+
+ +
`; + + appEl.innerHTML = headerHtml; + + const postsList = document.querySelector(".posts"); + + // Страница добавления поста const render = () => { - // TODO: Реализовать страницу добавления поста const appHtml = ` -
-
- Cтраница добавления поста - -
- `; +
+

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

+
+
+
+ +
+
+ + +
`; - appEl.innerHTML = appHtml; + postsList.innerHTML = appHtml; + // Загрузка фотки + const uploadImageContainer = appEl.querySelector(".upload-image-container"); + + if (uploadImageContainer) { + renderUploadImageComponent({ + element: appEl.querySelector(".upload-image-container"), + onImageUrlChange(newImageUrl) { + imageUrl = newImageUrl; + }, + }); + } + + // Клик по кнопке «Добавить» — эта мрась не работает document.getElementById("add-button").addEventListener("click", () => { + const description = document.querySelector(".textarea").value; + onAddPostClick({ - description: "Описание картинки", - imageUrl: "https://image.png", + description: description, + imageUrl: imageUrl, }); }); }; + renderHeaderComponent(); render(); } diff --git a/components/auth-page-component.js b/components/auth-page-component.js index cf66a3ae..a125b559 100644 --- a/components/auth-page-component.js +++ b/components/auth-page-component.js @@ -1,4 +1,5 @@ import { loginUser, registerUser } from "../api.js"; +import { replaceSafe } from "../helpers.js"; import { renderHeaderComponent } from "./header-component.js"; import { renderUploadImageComponent } from "./upload-image-component.js"; @@ -6,6 +7,16 @@ export function renderAuthPageComponent({ appEl, setUser }) { let isLoginMode = true; let imageUrl = ""; + const headerHtml = ` +
+
+ +
`; + + appEl.innerHTML = headerHtml; + + const postsList = document.querySelector(".posts"); + const renderForm = () => { const appHtml = `
@@ -52,7 +63,7 @@ export function renderAuthPageComponent({ appEl, setUser }) {
`; - appEl.innerHTML = appHtml; + postsList.innerHTML = appHtml; // Не вызываем перерендер, чтобы не сбрасывалась заполненная форма // Точечно обновляем кусочек дом дерева @@ -60,10 +71,6 @@ export function renderAuthPageComponent({ appEl, setUser }) { appEl.querySelector(".form-error").textContent = message; }; - renderHeaderComponent({ - element: document.querySelector(".header-container"), - }); - const uploadImageContainer = appEl.querySelector(".upload-image-container"); if (uploadImageContainer) { @@ -127,9 +134,9 @@ export function renderAuthPageComponent({ appEl, setUser }) { } registerUser({ - login: login, + login: replaceSafe(login), password: password, - name: name, + name: replaceSafe(name), imageUrl, }) .then((user) => { @@ -148,5 +155,6 @@ export function renderAuthPageComponent({ appEl, setUser }) { }); }; + renderHeaderComponent(); renderForm(); } diff --git a/components/header-component.js b/components/header-component.js index 465fbb8f..1cf5ebe2 100644 --- a/components/header-component.js +++ b/components/header-component.js @@ -1,7 +1,9 @@ import { goToPage, logout, user } from "../index.js"; import { ADD_POSTS_PAGE, AUTH_PAGE, POSTS_PAGE } from "../routes.js"; -export function renderHeaderComponent({ element }) { +export function renderHeaderComponent() { + const element = document.querySelector(".header-container"); + element.innerHTML = ` - -`; +
`; element .querySelector(".add-or-login-button") diff --git a/components/like-event-component.js b/components/like-event-component.js new file mode 100644 index 00000000..b62edfcf --- /dev/null +++ b/components/like-event-component.js @@ -0,0 +1,32 @@ +import { renderPostsPageComponent } from "./posts-page-component.js"; +import { posts, getToken, setPosts } from "../index.js"; +import { clickLike } from "../api.js"; +import { POSTS_PAGE, USER_POSTS_PAGE } from "../routes.js"; +import { renderUserPageComponent } from "./user-page-component.js"; + +// Поставить лайк +export function likeEventListiner({ appEl, pageComponent }) { + const likeButtons = document.querySelectorAll(".like-button"); + + for (const likeButton of likeButtons) { + likeButton.addEventListener("click", (event) => { + event.stopPropagation(); + const postId = likeButton.dataset.postId; + const index = likeButton.dataset.index; + let like; + + posts[index].isLiked ? (like = "dislike") : (like = "like"); + + clickLike({ token: getToken(), postId, like }).then((updatedPost) => { + posts[index] = updatedPost.post; + setPosts(posts); + + if (POSTS_PAGE) { + renderPostsPageComponent({ appEl }); + } else if (USER_POSTS_PAGE) { + renderUserPageComponent({ appEl }); + } + }); + }); + } +} diff --git a/components/posts-page-component.js b/components/posts-page-component.js index 5b97fdfe..92cf359f 100644 --- a/components/posts-page-component.js +++ b/components/posts-page-component.js @@ -1,104 +1,81 @@ +import { replaceSafe } from "../helpers.js"; +import { goToPage, posts } from "../index.js"; import { USER_POSTS_PAGE } from "../routes.js"; import { renderHeaderComponent } from "./header-component.js"; -import { posts, goToPage } from "../index.js"; +import { likeEventListiner } from "./like-event-component.js"; +import { formatDistanceToNow } from "date-fns"; +import { ru } from "date-fns/locale"; export function renderPostsPageComponent({ appEl }) { - // TODO: реализовать рендер постов из api + // Вывод постов в консоли console.log("Актуальный список постов:", posts); + let pageComponent = true; /** * TODO: чтобы отформатировать дату создания поста в виде "19 минут назад" * можно использовать https://date-fns.org/v2.29.3/docs/formatDistanceToNow */ - const appHtml = ` + + const headerHtml = `
- +
`; - appEl.innerHTML = appHtml; + appEl.innerHTML = headerHtml; + + const postsList = document.querySelector(".posts"); - renderHeaderComponent({ - element: document.querySelector(".header-container"), - }); + const postHtml = posts + .map((post, index) => { + return ` +
  • +
    + +

    ${post.user.name}

    +
    +
    + +
    +
    +

    + Нравится: ${ + post.likes.length > 0 + ? `${post.likes[post.likes.length - 1].name} ${ + post.likes.length - 1 > 0 + ? "и ещё" + (post.likes.length - 1) + : "" + } ` + : "0" + } +

    +
    +

    + ${replaceSafe(post.user.name)} + ${replaceSafe(post.description)} +

    +

    ${formatDistanceToNow( + new Date(post.createdAt), + { + locale: ru, + } + )}

    +
  • `; + }) + .join(""); + postsList.innerHTML = postHtml; + + // Обработчик перехода в профиль пользователя for (let userEl of document.querySelectorAll(".post-header")) { userEl.addEventListener("click", () => { goToPage(USER_POSTS_PAGE, { @@ -106,4 +83,7 @@ export function renderPostsPageComponent({ appEl }) { }); }); } + + likeEventListiner({ appEl, pageComponent }); + renderHeaderComponent(); } diff --git a/components/upload-image-component.js b/components/upload-image-component.js index 2bdcdbf3..7f21593a 100644 --- a/components/upload-image-component.js +++ b/components/upload-image-component.js @@ -23,7 +23,6 @@ export function renderUploadImageComponent({ element, onImageUrlChange }) { /> Выберите фото - ` } diff --git a/components/user-page-component.js b/components/user-page-component.js new file mode 100644 index 00000000..2b106b4c --- /dev/null +++ b/components/user-page-component.js @@ -0,0 +1,74 @@ +import { posts } from "../index.js"; +import { formatDistanceToNow } from "date-fns"; +import { ru } from "date-fns/locale"; +import { replaceSafe } from "../helpers.js"; +import { renderHeaderComponent } from "./header-component.js"; +import { likeEventListiner } from "./like-event-component.js"; + +export function renderUserPageComponent({ appEl }) { + let pageComponent = false; + + const headerHtml = ` +
    +
    + +
    `; + + appEl.innerHTML = headerHtml; + + const postsList = document.querySelector(".posts"); + + const appHtml = posts.map((post, index) => { + return ` +