diff --git a/README.md b/README.md
index 34719e139..e7111e9c5 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,18 @@
MVP социальной сети для обмена фотографиями
+В данном приложении было выполнено:
+
+1. Интеграция верстки списка постов с API,
+2. Реализация страницы добавления поста,
+3. Реализация страницы с постами конкретного юзера,
+4. Реализация функционала лайков,
+5. Реализация приложение в комфортный и современный стиль.
+
## Первоначальная оценка
-ХХХХ часов
+100 часов
## Фактически затраченное время
-YYYY часов
+80 часов
diff --git a/api.js b/api.js
index 123a9e3b6..0338fb7e1 100644
--- a/api.js
+++ b/api.js
@@ -1,7 +1,7 @@
// Замени на свой, чтобы получить независимый от других набор данных.
// "боевая" версия инстапро лежит в ключе prod
-const personalKey = "prod";
-const baseHost = "https://webdev-hw-api.vercel.app";
+const personalKey = "vasiliev_nikolay";
+const baseHost = "https://wedev-api.sky.pro";
const postsHost = `${baseHost}/api/v1/${personalKey}/instapro`;
export function getPosts({ token }) {
@@ -15,7 +15,30 @@ export function getPosts({ token }) {
if (response.status === 401) {
throw new Error("Нет авторизации");
}
+ if (!response.ok) {
+ throw new Error(`Ошибка сервера: ${response.status}`);
+ }
+ 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("Нет авторизации");
+ }
+ if (!response.ok) {
+ throw new Error(`Ошибка сервера: ${response.status}`);
+ }
return response.json();
})
.then((data) => {
@@ -23,9 +46,76 @@ export function getPosts({ token }) {
});
}
+
+export function addPost({ token, description, imageUrl }) {
+ // Отправляем как JSON
+ return fetch(postsHost, {
+ method: "POST",
+ headers: {
+ Authorization: token,
+
+ },
+ body: JSON.stringify({
+ description: description,
+ imageUrl: imageUrl,
+ }),
+ })
+ .then(async (response) => {
+ const text = await response.text();
+ console.log("addPost response status:", response.status);
+ console.log("addPost response body:", text);
+
+ if (response.status === 401) {
+ throw new Error("Нет авторизации");
+ }
+ if (!response.ok) {
+ throw new Error(`Ошибка сервера: ${response.status} - ${text}`);
+ }
+ return JSON.parse(text);
+ });
+}
+
+
+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}`);
+ }
+ 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}`);
+ }
+ return response.json();
+ });
+}
+
export function registerUser({ login, password, name, imageUrl }) {
return fetch(baseHost + "/api/user", {
method: "POST",
+ headers: {
+
+ },
body: JSON.stringify({
login,
password,
@@ -36,6 +126,9 @@ export function registerUser({ login, password, name, imageUrl }) {
if (response.status === 400) {
throw new Error("Такой пользователь уже существует");
}
+ if (!response.ok) {
+ throw new Error(`Ошибка сервера: ${response.status}`);
+ }
return response.json();
});
}
@@ -43,6 +136,9 @@ export function registerUser({ login, password, name, imageUrl }) {
export function loginUser({ login, password }) {
return fetch(baseHost + "/api/user/login", {
method: "POST",
+ headers: {
+
+ },
body: JSON.stringify({
login,
password,
@@ -51,6 +147,9 @@ export function loginUser({ login, password }) {
if (response.status === 400) {
throw new Error("Неверный логин или пароль");
}
+ if (!response.ok) {
+ throw new Error(`Ошибка сервера: ${response.status}`);
+ }
return response.json();
});
}
@@ -64,6 +163,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..c6db84ff1 100644
--- a/components/add-post-page-component.js
+++ b/components/add-post-page-component.js
@@ -1,23 +1,84 @@
+import { renderHeaderComponent } from "./header-component.js";
+import { renderUploadImageComponent } from "./upload-image-component.js";
+
+// Функция для очистки HTML-тегов
+function escapeHtml(unsafe) {
+ return unsafe
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
export function renderAddPostPageComponent({ appEl, onAddPostClick }) {
+ let imageUrl = "";
+ let isLoading = false;
+
const render = () => {
- // @TODO: Реализовать страницу добавления поста
- const appHtml = `
-
-
- Cтраница добавления поста
-
-
- `;
+ appEl.innerHTML = `
+
+ `;
- appEl.innerHTML = appHtml;
+ renderHeaderComponent({
+ element: document.querySelector(".header-container"),
+ });
+
+ renderUploadImageComponent({
+ element: document.getElementById("upload-image-container"),
+ onImageUrlChange(url) {
+ imageUrl = url;
+ },
+ });
document.getElementById("add-button").addEventListener("click", () => {
- onAddPostClick({
- description: "Описание картинки",
- imageUrl: "https://image.png",
- });
+ const descriptionInput = document.getElementById("post-description");
+ const description = descriptionInput ? descriptionInput.value.trim() : "";
+ const errorEl = document.getElementById("form-error");
+
+ if (!imageUrl) {
+ errorEl.textContent = "Пожалуйста, загрузите фото";
+ errorEl.style.display = "block";
+ return;
+ }
+
+ if (!description) {
+ errorEl.textContent = "Пожалуйста, введите описание";
+ errorEl.style.display = "block";
+ return;
+ }
+
+ // Экранируем HTML-теги из описания перед отправкой
+ const sanitizedDescription = escapeHtml(description);
+
+ errorEl.style.display = "none";
+ isLoading = true;
+ render();
+
+ onAddPostClick({ description: sanitizedDescription, imageUrl });
});
};
render();
-}
+}
\ No newline at end of file
diff --git a/components/posts-page-component.js b/components/posts-page-component.js
index 662ccdbdc..d10a23339 100644
--- a/components/posts-page-component.js
+++ b/components/posts-page-component.js
@@ -1,109 +1,144 @@
+import { formatDistanceToNow } from "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/esm/index.js";
+import { ru } from "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/esm/locale/index.js";
import { USER_POSTS_PAGE } from "../routes.js";
import { renderHeaderComponent } from "./header-component.js";
-import { posts, goToPage } from "../index.js";
-
-export function renderPostsPageComponent({ appEl }) {
- // @TODO: реализовать рендер постов из api
- console.log("Актуальный список постов:", posts);
-
- /**
- * @TODO: чтобы отформатировать дату создания поста в виде "19 минут назад"
- * можно использовать https://date-fns.org/v2.29.3/docs/formatDistanceToNow
- */
- const appHtml = `
-
-
-
- -
-
-
-

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

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

-
-
-
-
- Нравится: 0
-
-
-
- Варварва Н.
- Голова
-
-
- 8 дней назад
-
-
-
-
`;
-
- appEl.innerHTML = appHtml;
-
- renderHeaderComponent({
- element: document.querySelector(".header-container"),
+import { goToPage, user } from "../index.js";
+import { likePost, dislikePost } from "../api.js";
+
+function escapeHtml(unsafe) {
+ if (!unsafe) return "";
+ return unsafe
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function getPostId(post) {
+ return post.id || post._id;
+}
+
+function getUserId(userObj) {
+ if (!userObj) return undefined;
+ return userObj.id || userObj._id;
+}
+
+function isPostLikedByUser(post, userId) {
+ if (!userId || !post.likes?.length) return false;
+ return post.likes.some((like) => {
+ if (typeof like === "string") return like === userId;
+ if (like.userId) return like.userId === userId;
+ if (like.user?.id) return like.user.id === userId;
+ if (like.user?._id) return like.user._id === userId;
+ if (like.id) return like.id === userId;
+ if (like._id) return like._id === userId;
+ return false;
});
+}
- for (let userEl of document.querySelectorAll(".post-header")) {
- userEl.addEventListener("click", () => {
- goToPage(USER_POSTS_PAGE, {
- userId: userEl.dataset.userId,
- });
+export function renderPostsPageComponent({ appEl, posts }) {
+ const getToken = () => (user ? `Bearer ${user.token}` : undefined);
+
+ const render = (currentPosts) => {
+ const userId = getUserId(user);
+
+ const postsHtml = currentPosts
+ .map((post) => {
+ const postId = getPostId(post);
+ const isLiked = isPostLikedByUser(post, userId);
+ const likeImg = isLiked
+ ? "./assets/images/like-active.svg"
+ : "./assets/images/like-not-active.svg";
+ const timeAgo = formatDistanceToNow(new Date(post.createdAt), {
+ addSuffix: true,
+ locale: ru,
+ });
+ const escapedDescription = escapeHtml(post.description || "");
+ const escapedUserName = escapeHtml(post.user?.name || "");
+
+ return `
+
+
+
+

+
+
+
+
+ Нравится: ${post.likes?.length || 0}
+
+
+
+ ${escapedUserName}
+ ${escapedDescription}
+
+ ${timeAgo}
+
+ `;
+ })
+ .join("");
+
+ appEl.innerHTML = `
+
+ `;
+
+ renderHeaderComponent({
+ element: document.querySelector(".header-container"),
});
- }
-}
+
+ // Переход на страницу юзера
+ for (let userEl of document.querySelectorAll(".post-header")) {
+ userEl.addEventListener("click", () => {
+ const userId = userEl.dataset.userId;
+ if (userId) goToPage(USER_POSTS_PAGE, { userId });
+ });
+ }
+
+ // Лайки
+ for (let likeBtn of document.querySelectorAll(".like-button")) {
+ likeBtn.addEventListener("click", () => {
+ if (!user) {
+ goToPage("auth");
+ return;
+ }
+
+ const postId = likeBtn.dataset.postId;
+ const post = currentPosts.find((p) => getPostId(p) === postId);
+ if (!post) return;
+
+ const isLiked = isPostLikedByUser(post, getUserId(user));
+ const action = isLiked ? dislikePost : likePost;
+
+ likeBtn.disabled = true;
+
+ action({ token: getToken(), postId })
+ .then((data) => {
+ console.log("API ответ:", data);
+ const updatedPost = data.post || data;
+ const idx = currentPosts.findIndex((p) => getPostId(p) === postId);
+ if (idx !== -1) {
+ currentPosts[idx] = { ...currentPosts[idx], ...updatedPost };
+ }
+ render(currentPosts);
+ })
+ .catch((err) => {
+ console.error("Ошибка лайка:", err);
+ likeBtn.disabled = false;
+ });
+ });
+ }
+ };
+
+ render([...posts]);
+}
\ 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..efe9742ad
--- /dev/null
+++ b/components/user-posts-page-component.js
@@ -0,0 +1,144 @@
+import { formatDistanceToNow } from "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/esm/index.js";
+import { ru } from "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/esm/locale/index.js";
+import { renderHeaderComponent } from "./header-component.js";
+import { likePost, dislikePost } from "../api.js";
+import { user, goToPage } from "../index.js";
+import { POSTS_PAGE } from "../routes.js";
+
+function escapeHtml(unsafe) {
+ if (!unsafe) return "";
+ return unsafe
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function getPostId(post) {
+ return post.id || post._id;
+}
+
+function getUserId(userObj) {
+ if (!userObj) return undefined;
+ return userObj.id || userObj._id;
+}
+
+function isPostLikedByUser(post, userId) {
+ if (!userId || !post.likes?.length) return false;
+ return post.likes.some((like) => {
+ if (typeof like === "string") return like === userId;
+ if (like.userId) return like.userId === userId;
+ if (like.user?.id) return like.user.id === userId;
+ if (like.user?._id) return like.user._id === userId;
+ if (like.id) return like.id === userId;
+ if (like._id) return like._id === userId;
+ return false;
+ });
+}
+
+export function renderUserPostsPageComponent({ appEl, posts, userId }) {
+ const getToken = () => (user ? `Bearer ${user.token}` : undefined);
+
+ const render = (currentPosts) => {
+ const currentUserId = getUserId(user);
+ const firstPost = currentPosts[0];
+ const postUser = firstPost?.user;
+ const escapedUserName = postUser ? escapeHtml(postUser.name || "") : "";
+
+ const userHeaderHtml = postUser
+ ? `
+
+ `
+ : "";
+
+ const postsHtml = currentPosts
+ .map((post) => {
+ const postId = getPostId(post);
+ const isLiked = isPostLikedByUser(post, currentUserId);
+ const likeImg = isLiked
+ ? "./assets/images/like-active.svg"
+ : "./assets/images/like-not-active.svg";
+ const timeAgo = formatDistanceToNow(new Date(post.createdAt), {
+ addSuffix: true,
+ locale: ru,
+ });
+ const escapedDescription = escapeHtml(post.description || "");
+ const escapedUserNamePost = escapeHtml(post.user?.name || "");
+
+ return `
+
+
+

+
+
+
+
+ Нравится: ${post.likes?.length || 0}
+
+
+
+ ${escapedUserNamePost}
+ ${escapedDescription}
+
+ ${timeAgo}
+
+ `;
+ })
+ .join("");
+
+ appEl.innerHTML = `
+
+
+ ${userHeaderHtml}
+
+ ${currentPosts.length === 0 ? "У этого пользователя нет постов
" : postsHtml}
+
+
+ `;
+
+ renderHeaderComponent({
+ element: document.querySelector(".header-container"),
+ });
+
+ // Лайки
+ for (let likeBtn of document.querySelectorAll(".like-button")) {
+ likeBtn.addEventListener("click", () => {
+ if (!user) {
+ goToPage(POSTS_PAGE);
+ return;
+ }
+
+ const postId = likeBtn.dataset.postId;
+ const post = currentPosts.find((p) => getPostId(p) === postId);
+ if (!post) return;
+
+ const isLiked = isPostLikedByUser(post, getUserId(user));
+ const action = isLiked ? dislikePost : likePost;
+
+ likeBtn.disabled = true;
+
+ action({ token: getToken(), postId })
+ .then((data) => {
+ const updatedPost = data.post || data;
+ const idx = currentPosts.findIndex((p) => getPostId(p) === postId);
+ if (idx !== -1) {
+ currentPosts[idx] = { ...currentPosts[idx], ...updatedPost };
+ }
+ render(currentPosts);
+ })
+ .catch((err) => {
+ console.error("Ошибка лайка:", err);
+ likeBtn.disabled = false;
+ });
+ });
+ }
+ };
+
+ render([...posts]);
+}
\ No newline at end of file
diff --git a/index.js b/index.js
index 7f1817c75..4264d8f34 100644
--- a/index.js
+++ b/index.js
@@ -1,4 +1,4 @@
-import { getPosts } from "./api.js";
+import { getPosts, getUserPosts, 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,
@@ -45,7 +46,6 @@ export const goToPage = (newPage, data) => {
].includes(newPage)
) {
if (newPage === ADD_POSTS_PAGE) {
- /* Если пользователь не авторизован, то отправляем его на страницу авторизации перед добавлением поста */
page = user ? ADD_POSTS_PAGE : AUTH_PAGE;
return renderApp();
}
@@ -67,11 +67,19 @@ export const goToPage = (newPage, data) => {
}
if (newPage === USER_POSTS_PAGE) {
- // @@TODO: реализовать получение постов юзера из API
- console.log("Открываю страницу пользователя: ", data.userId);
- page = USER_POSTS_PAGE;
- posts = [];
- return renderApp();
+ page = LOADING_PAGE;
+ renderApp();
+
+ return getUserPosts({ token: getToken(), userId: data.userId })
+ .then((userPosts) => {
+ page = USER_POSTS_PAGE;
+ posts = userPosts;
+ renderApp({ userId: data.userId });
+ })
+ .catch((error) => {
+ console.error(error);
+ goToPage(POSTS_PAGE);
+ });
}
page = newPage;
@@ -83,8 +91,9 @@ export const goToPage = (newPage, data) => {
throw new Error("страницы не существует");
};
-const renderApp = () => {
+const renderApp = (data = {}) => {
const appEl = document.getElementById("app");
+
if (page === LOADING_PAGE) {
return renderLoadingPageComponent({
appEl,
@@ -110,9 +119,17 @@ const renderApp = () => {
return renderAddPostPageComponent({
appEl,
onAddPostClick({ description, imageUrl }) {
- // @TODO: реализовать добавление поста в API
- console.log("Добавляю пост...", { description, imageUrl });
- goToPage(POSTS_PAGE);
+ console.log('Пытаюсь добавить пост:', { description, imageUrl });
+ console.log('Токен:', getToken());
+ addPost({ token: getToken(), description, imageUrl })
+ .then((result) => {
+ console.log('Пост добавлен успешно:', result);
+ goToPage(POSTS_PAGE);
+ })
+ .catch((error) => {
+ console.error("Ошибка при добавлении поста:", error);
+ goToPage(POSTS_PAGE);
+ });
},
});
}
@@ -120,13 +137,16 @@ const renderApp = () => {
if (page === POSTS_PAGE) {
return renderPostsPageComponent({
appEl,
+ posts,
});
}
if (page === USER_POSTS_PAGE) {
- // @TODO: реализовать страницу с фотографиями отдельного пользвателя
- appEl.innerHTML = "Здесь будет страница фотографий пользователя";
- return;
+ return renderUserPostsPageComponent({
+ appEl,
+ posts,
+ userId: data.userId,
+ });
}
};
diff --git a/styles.css b/styles.css
index 57bc08b16..d8d89b822 100644
--- a/styles.css
+++ b/styles.css
@@ -1,26 +1,55 @@
+/* ===== BASE ===== */
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ background-color: #fafafa;
+ color: #262626;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+}
+
.page-container {
+ max-width: 600px;
+ margin: 0 auto;
padding: 0 20px;
}
+/* ===== HEADER ===== */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
- border-bottom: 1px solid #e0e0e0;
- padding: 20px 16px;
+ border-bottom: 1px solid #dbdbdb;
+ padding: 12px 16px;
margin-left: -20px;
margin-right: -20px;
+ background: #fff;
+ position: sticky;
+ top: 0;
+ z-index: 100;
}
.logo {
margin: 0;
- font-size: 28px;
+ font-size: 24px;
+ font-weight: 700;
cursor: pointer;
width: 130px;
+ font-style: italic;
+ letter-spacing: -0.5px;
+ color: #262626;
+ user-select: none;
+ transition: opacity 0.2s;
+}
+
+.logo:hover {
+ opacity: 0.7;
}
.header-container {
- margin-bottom: 8px;
+ margin-bottom: 12px;
}
.header-button {
@@ -29,15 +58,23 @@
background-color: transparent;
font-size: 14px;
line-height: 18px;
- font-weight: 500;
+ font-weight: 600;
cursor: pointer;
+ color: #262626;
+ transition: opacity 0.2s;
+}
+
+.header-button:hover {
+ opacity: 0.6;
}
.logout-button {
width: 130px;
text-align: right;
+ color: #ed4956;
}
+/* ===== POSTS LIST ===== */
.posts {
display: flex;
flex-direction: column;
@@ -46,113 +83,218 @@
margin: 0;
}
+.post {
+ background: #fff;
+ border: 1px solid #dbdbdb;
+ border-radius: 8px;
+ margin-bottom: 24px;
+ overflow: hidden;
+ animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(12px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
.post-header {
display: flex;
flex-direction: row;
align-items: center;
- margin-bottom: 10px;
+ padding: 12px 16px;
cursor: pointer;
+ transition: background 0.15s;
+}
+
+.post-header:hover {
+ background: #f9f9f9;
}
.post-header__user-image {
- width: 40px;
- height: 40px;
+ width: 36px;
+ height: 36px;
object-fit: cover;
- border-radius: 40px;
+ border-radius: 50%;
margin-right: 10px;
+ border: 2px solid #dbdbdb;
+}
+
+.post-header__user-name {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: #262626;
}
.post-image-container {
- margin-left: -20px;
- margin-right: -20px;
- height: 500px;
- display: flex;
- justify-content: center;
- background-color: #e0e0e0;
+ width: 100%;
+ background-color: #efefef;
+ overflow: hidden;
}
.post-image {
width: 100%;
- height: 100%;
- max-width: 500px;
+ max-height: 600px;
object-fit: cover;
+ display: block;
+ transition: transform 0.3s ease;
+}
+
+.post-image:hover {
+ transform: scale(1.01);
}
.post-likes {
display: flex;
flex-direction: row;
align-items: center;
+ padding: 4px 16px 0;
}
.like-button {
border: none;
background-color: transparent;
- padding: 8px;
- padding-left: 0px;
- padding-bottom: 5px;
+ padding: 8px 8px 5px 0;
cursor: pointer;
+ transition: transform 0.15s;
+}
+
+.like-button:hover {
+ transform: scale(1.2);
+}
+
+.like-button:active {
+ transform: scale(0.9), ;
+}
+
+.like-button:disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+
+.like-button img {
+ width: 24px;
+ height: 24px;
+ display: block;
+}
+
+.post-likes-text {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 600;
}
.user-name {
- font-weight: 500;
+ font-weight: 600;
}
.post-text {
font-size: 14px;
- line-height: 18px;
- margin-top: 5px;
+ line-height: 20px;
+ margin: 4px 0 6px;
+ padding: 0 16px;
+ word-break: break-word;
}
.post-date {
- color: #8a8a8a;
-}
-
-.post + .post {
- margin-top: 20px;
+ color: #8e8e8e;
+ font-size: 11px;
+ letter-spacing: 0.2px;
+ padding: 0 16px 12px;
+ margin: 0;
+ text-transform: uppercase;
}
+/* ===== USER POSTS PAGE HEADER ===== */
.posts-user-header {
display: flex;
flex-direction: row;
align-items: center;
- margin-bottom: 12px;
+ padding: 20px 0 16px;
+ border-bottom: 1px solid #dbdbdb;
+ margin-bottom: 20px;
}
.posts-user-header__user-image {
- width: 70px;
- height: 70px;
+ width: 80px;
+ height: 80px;
object-fit: cover;
- border-radius: 40px;
- margin-right: 10px;
+ border-radius: 50%;
+ margin-right: 20px;
+ border: 3px solid #dbdbdb;
}
.posts-user-header__user-name {
- font-size: 28px;
- line-height: 35px;
+ font-size: 24px;
+ font-weight: 600;
+ margin: 0;
}
+/* ===== LOADING PAGE ===== */
.loading-page {
display: flex;
+ flex-direction: column;
justify-content: center;
- margin-top: 100px;
+ align-items: center;
+ margin-top: 120px;
+ gap: 16px;
+}
+
+.loading-spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid #dbdbdb;
+ border-top-color: #262626;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
}
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.loading-text {
+ font-size: 14px;
+ color: #8e8e8e;
+}
+
+/* ===== ADD POST SIGN ===== */
.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-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23262626' stroke-width='2'%3E%3Crect x='3' y='3' width='18' height='18' rx='3'/%3E%3Cline x1='12' y1='8' x2='12' y2='16'/%3E%3Cline x1='8' y1='12' x2='16' y2='12'/%3E%3C/svg%3E");
+ height: 28px;
+ width: 28px;
+ background-size: cover;
}
+/* ===== FORM ===== */
.form {
display: flex;
flex-direction: column;
width: 100%;
+ max-width: 420px;
+ margin: 40px auto 0;
+ background: #fff;
+ border: 1px solid #dbdbdb;
+ border-radius: 8px;
+ padding: 32px 40px;
}
.form-title {
- font-size: 28px;
- line-height: 35px;
+ font-size: 26px;
+ line-height: 32px;
text-align: center;
+ margin: 0 0 24px;
+ font-style: italic;
+ font-weight: 700;
}
.form-inputs {
@@ -162,32 +304,40 @@
}
.form-error {
- color: red;
+ color: #ed4956;
+ font-size: 13px;
+ text-align: center;
+ min-height: 16px;
}
.form-footer {
- margin-top: 50px;
+ margin-top: 24px;
display: flex;
flex-direction: column;
gap: 10px;
+ border-top: 1px solid #dbdbdb;
+ padding-top: 16px;
}
.form-footer-title {
text-align: center;
+ font-size: 14px;
+ margin: 0;
}
-.file-upload-image-conrainer {
+/* ===== FILE UPLOAD ===== */
+.file-upload-image-container {
display: flex;
align-items: center;
+ gap: 12px;
}
.file-upload-image {
- width: 100px;
- height: 100px;
+ width: 80px;
+ height: 80px;
object-fit: cover;
- margin-right: 10px;
- border: 1px solid gray;
- border-radius: 5px;
+ border-radius: 6px;
+ border: 1px solid #dbdbdb;
}
.file-upload-label {
@@ -196,3 +346,39 @@
width: 100%;
text-align: center;
}
+
+/* ===== EMPTY STATE ===== */
+.empty-state {
+ text-align: center;
+ padding: 60px 20px;
+ color: #8e8e8e;
+}
+
+.empty-state-icon {
+ font-size: 48px;
+ margin-bottom: 12px;
+}
+
+.empty-state-text {
+ font-size: 16px;
+ margin: 0;
+}
+
+textarea.input {
+ resize: vertical;
+ min-height: 80px;
+ font-family: inherit;
+}
+
+/* ===== RESPONSIVE ===== */
+@media (max-width: 480px) {
+ .form {
+ padding: 24px 20px;
+ border: none;
+ margin-top: 0;
+ }
+
+ .post-image {
+ max-height: 400px;
+ }
+}
\ No newline at end of file