From 205de7ab1179e538a92c703c2fe95ac79426b58b Mon Sep 17 00:00:00 2001 From: nik Date: Sun, 17 May 2026 09:36:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D1=83=D1=80=D1=81=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 +- api.js | 108 ++++++++- components/add-post-page-component.js | 89 ++++++-- components/posts-page-component.js | 243 +++++++++++--------- components/user-posts-page-component.js | 144 ++++++++++++ index.js | 48 ++-- styles.css | 284 ++++++++++++++++++++---- 7 files changed, 742 insertions(+), 186 deletions(-) create mode 100644 components/user-posts-page-component.js 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 = ` -
-
- -
`; - - 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 ` +
  • +
    + ${escapedUserName} +

    ${escapedUserName}

    +
    +
    + ${escapedDescription} +
    +
    + +

    + Нравится: ${post.likes?.length || 0} +

    +
    +

    + ${escapedUserName} + ${escapedDescription} +

    + +
  • + `; + }) + .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 + ? ` +
    + ${escapedUserName} +

    ${escapedUserName}

    +
    + ` + : ""; + + 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 ` +
  • +
    + ${escapedDescription} +
    +
    + +

    + Нравится: ${post.likes?.length || 0} +

    +
    +

    + ${escapedUserNamePost} + ${escapedDescription} +

    + +
  • + `; + }) + .join(""); + + appEl.innerHTML = ` +
    +
    + ${userHeaderHtml} + +
    + `; + + 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