diff --git a/api.js b/api.js
index 123a9e3b6..8908b3491 100644
--- a/api.js
+++ b/api.js
@@ -1,5 +1,5 @@
-// Замени на свой, чтобы получить независимый от других набор данных.
-// "боевая" версия инстапро лежит в ключе prod
+// api.js - ПОЛНОСТЬЮ ИСПРАВЛЕННАЯ ВЕРСИЯ
+
const personalKey = "prod";
const baseHost = "https://webdev-hw-api.vercel.app";
const postsHost = `${baseHost}/api/v1/${personalKey}/instapro`;
@@ -15,11 +15,113 @@ export function getPosts({ token }) {
if (response.status === 401) {
throw new Error("Нет авторизации");
}
+ if (!response.ok) {
+ throw new Error(`Ошибка ${response.status}: ${response.statusText}`);
+ }
+ return response.json();
+ })
+ .then((data) => {
+ console.log("getPosts response:", data);
+ return data.posts || [];
+ });
+}
+export function getUserPosts({ token, userId }) {
+ return fetch(postsHost, {
+ method: "GET",
+ headers: {
+ Authorization: token,
+ },
+ })
+ .then((response) => {
+ if (response.status === 401) {
+ throw new Error("Нет авторизации");
+ }
+ if (!response.ok) {
+ throw new Error(`Ошибка ${response.status}: ${response.statusText}`);
+ }
return response.json();
})
.then((data) => {
- return data.posts;
+ console.log("getUserPosts response:", data);
+ const allPosts = data.posts || [];
+ return allPosts.filter(post => post.user?.id === userId);
+ });
+}
+
+export function likePost({ token, postId }) {
+ return fetch(`${postsHost}/${postId}/like`, {
+ method: "POST",
+ headers: {
+ Authorization: token,
+ },
+ })
+ .then((response) => {
+ if (response.status === 401) {
+ throw new Error("Нет авторизации");
+ }
+ if (!response.ok) {
+ throw new Error(`Ошибка ${response.status}: ${response.statusText}`);
+ }
+ return response.json();
+ });
+}
+
+export function dislikePost({ token, postId }) {
+ return fetch(`${postsHost}/${postId}/dislike`, {
+ method: "POST",
+ headers: {
+ Authorization: token,
+ },
+ })
+ .then((response) => {
+ if (response.status === 401) {
+ throw new Error("Нет авторизации");
+ }
+ if (!response.ok) {
+ throw new Error(`Ошибка ${response.status}: ${response.statusText}`);
+ }
+ return response.json();
+ });
+}
+
+export function addPost({ token, description, imageUrl }) {
+ if (!description || !imageUrl) {
+ return Promise.reject(new Error("Описание и изображение обязательны"));
+ }
+
+ console.log("Sending post data:", { description, imageUrl });
+
+ return fetch(postsHost, {
+ method: "POST",
+ headers: {
+ Authorization: token,
+ },
+ body: JSON.stringify({
+ description: description.trim(),
+ imageUrl: imageUrl.trim(),
+ }),
+ })
+ .then(async (response) => {
+ const responseText = await response.text();
+ console.log("Response status:", response.status);
+ console.log("Response text:", responseText);
+
+ if (response.status === 401) {
+ throw new Error("Нет авторизации");
+ }
+ if (response.status === 400) {
+ throw new Error("Неверные данные поста");
+ }
+ if (!response.ok) {
+ throw new Error(`Ошибка ${response.status}: ${responseText}`);
+ }
+
+ try {
+ return JSON.parse(responseText);
+ } catch (e) {
+ return { success: true };
+ }
});
}
@@ -32,12 +134,22 @@ export function registerUser({ login, password, name, imageUrl }) {
name,
imageUrl,
}),
- }).then((response) => {
- if (response.status === 400) {
- throw new Error("Такой пользователь уже существует");
- }
- return response.json();
- });
+ })
+ .then(async (response) => {
+ const responseText = await response.text();
+ console.log("Register response:", response.status, responseText);
+
+ if (response.status === 400) {
+ throw new Error("Такой пользователь уже существует");
+ }
+ if (!response.ok) {
+ throw new Error(`Ошибка ${response.status}: ${responseText}`);
+ }
+
+ const data = JSON.parse(responseText);
+ // Возвращаем пользователя в правильном формате
+ return { user: data.user };
+ });
}
export function loginUser({ login, password }) {
@@ -47,15 +159,28 @@ export function loginUser({ login, password }) {
login,
password,
}),
- }).then((response) => {
- if (response.status === 400) {
- throw new Error("Неверный логин или пароль");
- }
- return response.json();
- });
+ })
+ .then(async (response) => {
+ const responseText = await response.text();
+ console.log("Login response:", response.status, responseText);
+
+ if (response.status === 400) {
+ throw new Error("Неверный логин или пароль");
+ }
+ if (response.status === 401) {
+ throw new Error("Не авторизован");
+ }
+ if (!response.ok) {
+ throw new Error(`Ошибка ${response.status}: ${responseText}`);
+ }
+
+ const data = JSON.parse(responseText);
+ console.log("Parsed login data:", data);
+ // Возвращаем пользователя в правильном формате
+ return { user: data.user };
+ });
}
-// Загружает картинку в облако, возвращает url загруженной картинки
export function uploadImage({ file }) {
const data = new FormData();
data.append("file", file);
@@ -64,6 +189,9 @@ export function uploadImage({ file }) {
method: "POST",
body: data,
}).then((response) => {
+ if (!response.ok) {
+ throw new Error(`Ошибка загрузки изображения: ${response.status}`);
+ }
return response.json();
});
-}
+}
\ No newline at end of file
diff --git a/components/add-post-page-component.js b/components/add-post-page-component.js
index 8277f093b..c4adb299d 100644
--- a/components/add-post-page-component.js
+++ b/components/add-post-page-component.js
@@ -1,23 +1,109 @@
+import { renderHeaderComponent } from "./header-component.js";
+import { renderUploadImageComponent } from "./upload-image-component.js";
+import { goToPage } from "../index.js";
+import { POSTS_PAGE } from "../routes.js";
+
export function renderAddPostPageComponent({ appEl, onAddPostClick }) {
+ let imageUrl = "";
+ let description = "";
+
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"),
+ });
+
+ const uploadImageContainer = appEl.querySelector(".upload-image-container");
+ renderUploadImageComponent({
+ element: uploadImageContainer,
+ onImageUrlChange(newImageUrl) {
+ imageUrl = newImageUrl;
+ console.log("Image URL updated:", imageUrl);
+ const addButton = document.getElementById("add-button");
+ if (addButton) {
+ addButton.disabled = !imageUrl || !description;
+ }
+ },
+ });
+
+ const descriptionInput = document.getElementById("description-input");
+ const addButton = document.getElementById("add-button");
+ const cancelButton = document.getElementById("cancel-button");
+ const errorDiv = document.querySelector(".form-error");
+
+ descriptionInput.addEventListener("input", (e) => {
+ description = e.target.value;
+ console.log("Description updated:", description);
+ if (addButton) {
+ addButton.disabled = !imageUrl || !description.trim();
+ }
+ if (errorDiv) {
+ errorDiv.textContent = "";
+ }
+ });
+
+ addButton.addEventListener("click", async () => {
+ console.log("Add button clicked");
+ console.log("Current imageUrl:", imageUrl);
+ console.log("Current description:", description);
+
+ if (!imageUrl) {
+ errorDiv.textContent = "Пожалуйста, выберите изображение";
+ return;
+ }
+
+ if (!description.trim()) {
+ errorDiv.textContent = "Пожалуйста, введите описание";
+ return;
+ }
+
+ addButton.disabled = true;
+ addButton.textContent = "Публикация...";
+
+ try {
+ const postData = {
+ description: description.trim(),
+ imageUrl: imageUrl,
+ };
+ console.log("Sending post data:", postData);
+
+ await onAddPostClick(postData);
+ console.log("Post added successfully");
+ goToPage(POSTS_PAGE);
+ } catch (error) {
+ console.error("Ошибка при добавлении поста:", error);
+ errorDiv.textContent = error.message || "Ошибка при публикации";
+ addButton.disabled = false;
+ addButton.textContent = "Опубликовать";
+ }
+ });
+
+ cancelButton.addEventListener("click", () => {
+ goToPage(POSTS_PAGE);
});
};
render();
-}
+}
\ No newline at end of file
diff --git a/components/date-utils.js b/components/date-utils.js
new file mode 100644
index 000000000..44c9b7152
--- /dev/null
+++ b/components/date-utils.js
@@ -0,0 +1,45 @@
+export function formatDistanceToNow(date) {
+ const now = new Date();
+ const diffInSeconds = Math.floor((now - date) / 1000);
+
+ if (diffInSeconds < 0) return "только что";
+
+ const intervals = [
+ { label: "год", seconds: 31536000, threshold: 5 },
+ { label: "месяц", seconds: 2592000, threshold: 11 },
+ { label: "день", seconds: 86400, threshold: 6 },
+ { label: "час", seconds: 3600, threshold: 23 },
+ { label: "минуту", seconds: 60, threshold: 59 }
+ ];
+
+ for (const interval of intervals) {
+ const count = Math.floor(diffInSeconds / interval.seconds);
+ if (count >= 1) {
+ if (count === 1) {
+ return `${count} ${interval.label} назад`;
+ }
+ let label = interval.label;
+ if (label === "минуту") label = "минут";
+ if (label === "час") label = "часов";
+ if (label === "день") {
+ if (count % 10 === 1 && count % 100 !== 11) label = "день";
+ else if (count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20)) label = "дня";
+ else label = "дней";
+ }
+ if (label === "месяц") {
+ if (count % 10 === 1 && count % 100 !== 11) label = "месяц";
+ else if (count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20)) label = "месяца";
+ else label = "месяцев";
+ }
+ if (label === "год") {
+ if (count % 10 === 1 && count % 100 !== 11) label = "год";
+ else if (count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20)) label = "года";
+ else label = "лет";
+ }
+
+ return `${count} ${label} назад`;
+ }
+ }
+
+ return "только что";
+}
\ No newline at end of file
diff --git a/components/posts-page-component.js b/components/posts-page-component.js
index 662ccdbdc..b4faf881f 100644
--- a/components/posts-page-component.js
+++ b/components/posts-page-component.js
@@ -1,109 +1,129 @@
import { USER_POSTS_PAGE } from "../routes.js";
import { renderHeaderComponent } from "./header-component.js";
-import { posts, goToPage } from "../index.js";
+import { goToPage, user } from "../index.js";
+import { likePost, dislikePost } from "../api.js";
+import { formatDistanceToNow } from "./date-utils.js";
-export function renderPostsPageComponent({ appEl }) {
- // @TODO: реализовать рендер постов из api
- console.log("Актуальный список постов:", posts);
+const DEFAULT_AVATAR = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%23cccccc'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='%23666666' font-size='20'%3E👤%3C/text%3E%3C/svg%3E";
- /**
- * @TODO: чтобы отформатировать дату создания поста в виде "19 минут назад"
- * можно использовать https://date-fns.org/v2.29.3/docs/formatDistanceToNow
- */
- const appHtml = `
-
-
-
- -
-
-
-

-
-
-
-
- Нравится: 2
-
-
-
- Иван Иваныч
- Ромашка, ромашка...
-
-
- 19 минут назад
-
-
- -
-
-
-
-
-

-
-
-
-
- Нравится: 35
-
-
-
- Варварва Н.
- Нарисовала картину, посмотрите какая красивая
-
-
- 3 часа назад
-
-
- -
-
-
-
-
-

-
-
-
-
- Нравится: 0
-
-
-
- Варварва Н.
- Голова
-
-
- 8 дней назад
-
-
-
-
`;
+export function renderPostsPageComponent({ appEl, posts }) {
+ const token = user ? `Bearer ${user.token}` : undefined;
- appEl.innerHTML = appHtml;
+ const renderPosts = () => {
+ const postsHtml = posts.map(post => createPostHTML(post, token)).join('');
+
+ const appHtml = `
+
+ `;
- renderHeaderComponent({
- element: document.querySelector(".header-container"),
- });
+ appEl.innerHTML = appHtml;
- for (let userEl of document.querySelectorAll(".post-header")) {
- userEl.addEventListener("click", () => {
- goToPage(USER_POSTS_PAGE, {
- userId: userEl.dataset.userId,
+ renderHeaderComponent({
+ element: document.querySelector(".header-container"),
+ });
+
+ document.querySelectorAll('.like-button').forEach(button => {
+ button.addEventListener('click', async (event) => {
+ event.stopPropagation();
+ const postId = button.dataset.postId;
+ const isLiked = button.classList.contains('liked');
+
+ if (!token) {
+ alert("Войдите в систему, чтобы ставить лайки");
+ return;
+ }
+
+ try {
+ if (isLiked) {
+ await dislikePost({ token, postId });
+ } else {
+ await likePost({ token, postId });
+ }
+
+ const { getPosts } = await import("../api.js");
+ const newPosts = await getPosts({ token });
+ window.posts = newPosts;
+ renderPostsPageComponent({ appEl, posts: newPosts });
+ } catch (error) {
+ console.error("Ошибка при изменении лайка:", error);
+ alert("Не удалось поставить лайк. Попробуйте позже.");
+ }
+ });
+ });
+
+ document.querySelectorAll(".post-header").forEach((header) => {
+ header.addEventListener("click", () => {
+ const userId = header.dataset.userId;
+ if (userId) {
+ goToPage(USER_POSTS_PAGE, { userId });
+ }
});
});
- }
+ };
+
+ renderPosts();
}
+
+function createPostHTML(post, token) {
+ const date = post.createdAt ? new Date(post.createdAt) : new Date();
+ const timeAgo = formatDistanceToNow(date);
+
+ const isLiked = post.isLiked || false;
+ // ИСПРАВЛЕНО: если likes - массив, берем его длину, иначе само значение
+ const likeCount = Array.isArray(post.likes) ? post.likes.length : (post.likes || 0);
+
+ const userAvatar = post.user?.imageUrl || DEFAULT_AVATAR;
+
+ const likeButtonHtml = token ? `
+
+ ` : `
+
+ 🤍 ${likeCount}
+
+ `;
+
+ return `
+
+
+
+ ${post.imageUrl ? `
+
+

+
+ ` : ''}
+
+
+ ${likeButtonHtml}
+
+
+
+ ${escapeHtml(post.user?.name || 'Аноним')}
+ ${escapeHtml(post.description || '')}
+
+
+
+ ${timeAgo}
+
+
+ `;
+}
+
+function escapeHtml(str) {
+ if (!str) return '';
+ const div = document.createElement('div');
+ div.textContent = str;
+ return div.innerHTML;
+}
\ No newline at end of file
diff --git a/components/user-posts-page-component.js b/components/user-posts-page-component.js
new file mode 100644
index 000000000..215cb4e5c
--- /dev/null
+++ b/components/user-posts-page-component.js
@@ -0,0 +1,180 @@
+import { renderHeaderComponent } from "./header-component.js";
+import { goToPage, user } from "../index.js";
+import { getUserPosts, likePost, dislikePost } from "../api.js";
+import { formatDistanceToNow } from "./date-utils.js";
+
+const DEFAULT_AVATAR = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%23cccccc'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='%23666666' font-size='20'%3E👤%3C/text%3E%3C/svg%3E";
+const DEFAULT_COVER = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='70' height='70' viewBox='0 0 70 70'%3E%3Crect width='70' height='70' fill='%23cccccc'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='%23666666' font-size='35'%3E👤%3C/text%3E%3C/svg%3E";
+
+export function renderUserPostsPageComponent({ appEl, userId }) {
+ const token = user ? `Bearer ${user.token}` : undefined;
+ let posts = [];
+
+ const render = async () => {
+ appEl.innerHTML = `
+
+ `;
+
+ renderHeaderComponent({
+ element: document.querySelector(".header-container"),
+ });
+
+ try {
+ posts = await getUserPosts({ token, userId });
+ renderPosts();
+ } catch (error) {
+ console.error("Ошибка загрузки постов пользователя:", error);
+ appEl.innerHTML = `
+
+
+
+
❌ Ошибка загрузки постов
+
+
+
+ `;
+
+ renderHeaderComponent({
+ element: document.querySelector(".header-container"),
+ });
+
+ document.getElementById("retry-button")?.addEventListener("click", () => {
+ renderUserPostsPageComponent({ appEl, userId });
+ });
+ }
+ };
+
+ const renderPosts = () => {
+ if (!posts || posts.length === 0) {
+ appEl.innerHTML = `
+
+
+
+
📭 У пользователя пока нет постов
+
+
+ `;
+ renderHeaderComponent({
+ element: document.querySelector(".header-container"),
+ });
+ return;
+ }
+
+ const userInfo = posts[0]?.user;
+ const userAvatar = userInfo?.imageUrl || DEFAULT_COVER;
+ const postsHtml = posts.map(post => createPostHTML(post, token)).join('');
+
+ const appHtml = `
+
+ `;
+
+ appEl.innerHTML = appHtml;
+
+ renderHeaderComponent({
+ element: document.querySelector(".header-container"),
+ });
+
+ document.querySelectorAll('.like-button').forEach(button => {
+ button.addEventListener('click', async (event) => {
+ event.stopPropagation();
+ const postId = button.dataset.postId;
+ const isLiked = button.classList.contains('liked');
+
+ if (!token) {
+ alert("Войдите в систему, чтобы ставить лайки");
+ return;
+ }
+
+ try {
+ if (isLiked) {
+ await dislikePost({ token, postId });
+ } else {
+ await likePost({ token, postId });
+ }
+
+ posts = await getUserPosts({ token, userId });
+ renderPosts();
+ } catch (error) {
+ console.error("Ошибка при изменении лайка:", error);
+ alert("Не удалось поставить лайк. Попробуйте позже.");
+ }
+ });
+ });
+ };
+
+ render();
+}
+
+function createPostHTML(post, token) {
+ const date = post.createdAt ? new Date(post.createdAt) : new Date();
+ const timeAgo = formatDistanceToNow(date);
+
+ const isLiked = post.isLiked || false;
+ // ИСПРАВЛЕНО: если likes - массив, берем его длину, иначе само значение
+ const likeCount = Array.isArray(post.likes) ? post.likes.length : (post.likes || 0);
+ const userAvatar = post.user?.imageUrl || DEFAULT_AVATAR;
+
+ const likeButtonHtml = token ? `
+
+ ` : `
+
+ 🤍 ${likeCount}
+
+ `;
+
+ return `
+
+
+
+ ${post.imageUrl ? `
+
+

+
+ ` : ''}
+
+
+ ${likeButtonHtml}
+
+
+
+ ${escapeHtml(post.user?.name || 'Аноним')}
+ ${escapeHtml(post.description || '')}
+
+
+
+ ${timeAgo}
+
+
+ `;
+}
+
+function escapeHtml(str) {
+ if (!str) return '';
+ const div = document.createElement('div');
+ div.textContent = str;
+ return div.innerHTML;
+}
\ No newline at end of file
diff --git a/index.js b/index.js
index 7f1817c75..24aeab324 100644
--- a/index.js
+++ b/index.js
@@ -1,4 +1,4 @@
-import { getPosts } from "./api.js";
+import { getPosts, addPost } from "./api.js";
import { renderAddPostPageComponent } from "./components/add-post-page-component.js";
import { renderAuthPageComponent } from "./components/auth-page-component.js";
import {
@@ -10,6 +10,7 @@ import {
} from "./routes.js";
import { renderPostsPageComponent } from "./components/posts-page-component.js";
import { renderLoadingPageComponent } from "./components/loading-page-component.js";
+import { renderUserPostsPageComponent } from "./components/user-posts-page-component.js";
import {
getUserFromLocalStorage,
removeUserFromLocalStorage,
@@ -31,9 +32,6 @@ export const logout = () => {
goToPage(POSTS_PAGE);
};
-/**
- * Включает страницу приложения
- */
export const goToPage = (newPage, data) => {
if (
[
@@ -45,7 +43,6 @@ export const goToPage = (newPage, data) => {
].includes(newPage)
) {
if (newPage === ADD_POSTS_PAGE) {
- /* Если пользователь не авторизован, то отправляем его на страницу авторизации перед добавлением поста */
page = user ? ADD_POSTS_PAGE : AUTH_PAGE;
return renderApp();
}
@@ -62,21 +59,20 @@ export const goToPage = (newPage, data) => {
})
.catch((error) => {
console.error(error);
- goToPage(POSTS_PAGE);
+ page = POSTS_PAGE;
+ posts = [];
+ renderApp();
});
}
if (newPage === USER_POSTS_PAGE) {
- // @@TODO: реализовать получение постов юзера из API
- console.log("Открываю страницу пользователя: ", data.userId);
page = USER_POSTS_PAGE;
- posts = [];
+ window.currentUserId = data?.userId;
return renderApp();
}
page = newPage;
renderApp();
-
return;
}
@@ -85,6 +81,7 @@ export const goToPage = (newPage, data) => {
const renderApp = () => {
const appEl = document.getElementById("app");
+
if (page === LOADING_PAGE) {
return renderLoadingPageComponent({
appEl,
@@ -109,10 +106,19 @@ const renderApp = () => {
if (page === ADD_POSTS_PAGE) {
return renderAddPostPageComponent({
appEl,
- onAddPostClick({ description, imageUrl }) {
- // @TODO: реализовать добавление поста в API
- console.log("Добавляю пост...", { description, imageUrl });
- goToPage(POSTS_PAGE);
+ onAddPostClick: async ({ description, imageUrl }) => {
+ try {
+ const token = getToken();
+ if (!token) {
+ throw new Error("Необходимо авторизоваться");
+ }
+
+ await addPost({ token, description, imageUrl });
+ goToPage(POSTS_PAGE);
+ } catch (error) {
+ console.error("Ошибка при добавлении поста:", error);
+ throw error;
+ }
},
});
}
@@ -120,14 +126,17 @@ const renderApp = () => {
if (page === POSTS_PAGE) {
return renderPostsPageComponent({
appEl,
+ posts,
});
}
if (page === USER_POSTS_PAGE) {
- // @TODO: реализовать страницу с фотографиями отдельного пользвателя
- appEl.innerHTML = "Здесь будет страница фотографий пользователя";
- return;
+ return renderUserPostsPageComponent({
+ appEl,
+ userId: window.currentUserId,
+ });
}
};
-goToPage(POSTS_PAGE);
+// Запускаем приложение
+goToPage(POSTS_PAGE);
\ No newline at end of file
diff --git a/styles.css b/styles.css
index 57bc08b16..8221ddfa6 100644
--- a/styles.css
+++ b/styles.css
@@ -196,3 +196,42 @@
width: 100%;
text-align: center;
}
+.error-container {
+ text-align: center;
+ padding: 40px;
+}
+
+.empty-state {
+ text-align: center;
+ padding: 40px;
+ color: #666;
+}
+
+.like-button {
+ cursor: pointer;
+ transition: transform 0.2s;
+}
+
+.like-button:hover {
+ transform: scale(1.1);
+}
+
+.like-button.liked img {
+ filter: drop-shadow(0 0 2px rgba(255, 0, 0, 0.5));
+}
+
+.like-placeholder {
+ width: 32px;
+ height: 32px;
+}
+
+.textarea {
+ font-family: StratosSkyeng;
+ resize: vertical;
+}
+
+.posts-user-header {
+ margin-bottom: 20px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid #e0e0e0;
+}
\ No newline at end of file