Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 73 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,78 @@
# Instapro
# Instapro — социальная сеть SkyPro

MVP социальной сети для обмена фотографиями
Это клиентская часть приложения, написанная на чистом JavaScript.
Приложение позволяет:
- Регистрироваться и авторизовываться
- Просматривать ленту постов (общая и пользовательские)
- Ставить и убирать лайки
- Добавлять собственные посты с описанием и изображением
- Удалять свои посты с подтверждением через всплывающее окно

## Первоначальная оценка
---

ХХХХ часов
## 📌 Основные реализованные фичи

## Фактически затраченное время
✅ **Авторизация и регистрация**
✅ **Лента постов (общая и пользовательская)**
✅ **Добавление постов с загрузкой изображения**
✅ **Лайки/дизлайки с анимацией при наведении и клике**
✅ **Удаление поста с подтверждением через `confirm()`**
✅ **Адаптивный интерфейс**
✅ **Экранирование HTML-тегов в описании поста**

YYYY часов
---

## ⏱️ Затрачённое время

🕒 **Первоначальная оценка:** 8 часов
🕒 **Фактически затрачено:** ~12 часов

### Причины перерасхода времени:
- Разбор ошибок API (`400`, `401`)
- Исправление запросов без `Content-Type`
- Разбор ошибок с регистрацией и авторизацией
- Реализация анимаций и улучшений UX
- Устранение проблем с маршрутизацией и отрисовкой страниц

---

## 💡 Дополнительные улучшения

- 🎨 **Современный внешний вид:** добавлены отступы, стилизованные кнопки, тени, скругления
- 🖼 **Анимации лайков:** плавное увеличение при наведении и пульсация при клике
- 🔐 **Экранирование HTML в описании поста:** защита от XSS
- 📱 **Адаптивность:** корректное отображение на мобильных устройствах
- ❌ **Удаление поста с подтверждением**
- 👤 **Переход к постам пользователя по клику на имя**

---

## 🔧 Технические детали

- **API:** https://wedev-api.sky.pro
- **Авторизация:** Bearer токен
- **Формат данных:** JSON
- **Загрузка изображений:** через `FormData`
- **Клиентская маршрутизация:** реализована вручную через `goToPage(...)` и `renderApp()`

---

## 🛠 Как запустить проект:

1. Клонируйте репозиторий
2. Откройте `index.html` через Live Server или любой локальный сервер
3. Перейдите на главную страницу → всё работает!

---

## 📌 Вывод

Проект полностью соответствует техническому заданию и успешно прошёл все этапы разработки.

Были выполнены следующие доработки:
- Улучшен внешний вид и адаптивность
- Добавано экранирование HTML в постах
- Убраны все `console.log(...)`
- Оставлены только обработчики ошибок через `console.error(...)`

Проект готов к сдаче!
151 changes: 137 additions & 14 deletions api.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,140 @@
// Замени на свой, чтобы получить независимый от других набор данных.
// "боевая" версия инстапро лежит в ключе prod
const personalKey = "prod";
const baseHost = "https://webdev-hw-api.vercel.app";
const baseHost = "https://wedev-api.sky.pro";
const postsHost = `${baseHost}/api/v1/${personalKey}/instapro`;

export function getPosts({ token }) {
const headers = {};
if (token && token !== "Bearer undefined") {
headers.Authorization = token;
}
return fetch(postsHost, {
method: "GET",
headers: {
Authorization: token,
},
headers,
})
.then((response) => {
if (response.status === 401) {
throw new Error("Нет авторизации");
}
if (!response.ok) {
return response.text().then((text) => {
throw new Error(`Ошибка сервера: ${response.status} ${text}`);
});
}
return response.json();
})
.then((data) => {
return data.posts;
});
}

export function getUserPosts({ token, userId }) {
const headers = {};
if (token && token !== "Bearer undefined") {
headers.Authorization = token;
}
return fetch(`${postsHost}/user-posts/${userId}`, {
method: "GET",
headers,
})
.then((response) => {
if (response.status === 401) {
throw new Error("Нет авторизации");
}
if (!response.ok) {
return response.text().then((text) => {
throw new Error(`Ошибка сервера: ${response.status} ${text}`);
});
}
return response.json();
})
.then((data) => {
return data.posts;
});
}

export function registerUser({ login, password, name, imageUrl }) {
return fetch(baseHost + "/api/user", {
export function addPost({ token, description, imageUrl }) {

if (!description || !imageUrl) {
throw new Error("Описание или URL изображения не переданы");
}

return fetch(postsHost, {
method: "POST",
headers: {
Authorization: token,
},
body: JSON.stringify({
login,
password,
name,
description,
imageUrl,
}),
}).then((response) => {
if (response.status === 400) {
throw new Error("Такой пользователь уже существует");
return response.json().then((data) => {
throw new Error(data.error || "Некорректные данные поста");
});
}
if (response.status === 401) {
throw new Error("Нет авторизации");
}
if (!response.ok) {
throw new Error(`Ошибка сервера: ${response.status}`);
}
return response.json();
});
}

export function likePost({ token, postId }) {
return fetch(`${postsHost}/${postId}/like`, {
method: "POST",
headers: {
Authorization: token,
},
})
.then((response) => {
if (response.status === 401) {
throw new Error("Нет авторизации");
}
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("Нет авторизации");
}
return response.json();
});
}

export function registerUser({ login, password, name, imageUrl }) {
const body = {
login,
password,
name,
};

if (imageUrl) {
body.imageUrl = imageUrl;
}

return fetch("https://wedev-api.sky.pro/api/user", {
method: "POST",
body: JSON.stringify(body),
}).then((response) => {
if (response.status === 400) {
return response.json().then((data) => {
throw new Error(data.error || "Ошибка регистрации");
});
}
if (!response.ok) {
throw new Error(`Ошибка сервера: ${response.status}`);
}
return response.json();
});
Expand All @@ -49,13 +149,17 @@ export function loginUser({ login, password }) {
}),
}).then((response) => {
if (response.status === 400) {
throw new Error("Неверный логин или пароль");
return response.json().then((data) => {
throw new Error(data.error || "Неверный логин или пароль");
});
}
if (!response.ok) {
throw new Error(`Ошибка сервера: ${response.status}`);
}
return response.json();
});
}

// Загружает картинку в облако, возвращает url загруженной картинки
export function uploadImage({ file }) {
const data = new FormData();
data.append("file", file);
Expand All @@ -64,6 +168,25 @@ export function uploadImage({ file }) {
method: "POST",
body: data,
}).then((response) => {
if (response.status !== 200) {
throw new Error("Ошибка загрузки изображения");
}
return response.json();
});
}

export function verifyToken({ token }) {
return fetch(baseHost + "/api/user/me", {
method: "GET",
headers: {
Authorization: token,
},
})
.then((response) => {
return response.status === 200;
})
.catch((error) => {
console.error("verifyToken error:", error);
return false;
});
}
68 changes: 55 additions & 13 deletions components/add-post-page-component.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,65 @@
import { renderHeaderComponent } from "./header-component.js";
import { renderUploadImageComponent } from "./upload-image-component.js";
import { addPost } from "../api.js";
import { goToPage, user, posts } from "../index.js";
import { POSTS_PAGE } from "../routes.js";

export function renderAddPostPageComponent({ appEl, onAddPostClick }) {
let imageUrl = "";

const render = () => {
// @TODO: Реализовать страницу добавления поста
const appHtml = `
<div class="page-container">
<div class="header-container"></div>
Cтраница добавления поста
<button class="button" id="add-button">Добавить</button>
</div>
`;
<div class="page-container">
<div class="header-container"></div>
<div class="form">
<h3 class="form-title">Добавить пост</h3>
<div class="form-inputs">
<div class="upload-image-container"></div>
<textarea id="description-input" class="textarea" placeholder="Описание поста"></textarea>
<button class="button" id="add-button">Добавить</button>
</div>
</div>
</div>`;

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;
},
});

document.getElementById("add-button").addEventListener("click", () => {
const description = document.getElementById("description-input").value.trim();

if (!description) {
alert("Введите описание поста");
return;
}
if (!imageUrl) {
alert("Выберите изображение");
return;
}

const token = user && user.token ? `Bearer ${user.token}` : undefined;


addPost({ token, description, imageUrl })
.then(() => {
goToPage(POSTS_PAGE);
})
.catch((error) => {
console.error("Ошибка при добавлении поста:", error);
alert("Ошибка при добавлении поста. Попробуйте снова.");
});
});
};

render();
}
}
Loading