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
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
# Instapro
# Проект InstaPro 📸

MVP социальной сети для обмена фотографиями
Учебный проект — аналог популярной социальной сети для обмена фотографиями. В ходе работы приложение было полностью переведено на динамическое взаимодействие с API, защищено от сбоев данных и оптимизировано для работы в различных режимах авторизации.

## Первоначальная оценка
## Реализованная функциональность

ХХХХ часов
- **Динамическая лента постов:** Реализован рендер всех публикаций пользователей, загружаемых с удаленного сервера.
- **Система лайков:** Добавлена возможность ставить и убирать лайки с мгновенным обновлением интерфейса (без перезагрузки всей страницы).
- **Относительные даты:** Время создания каждого поста переведено в удобный человекочитаемый формат (например, «19 минут назад», «3 часа назад»).
- **Защита от XSS-атак:** Внедрено экранирование HTML-тегов, блокирующее ввод вредоносного кода в описании постов.
- **Информирование об ошибках:** Добавлено корректное отображение ошибок («Неверный логин или пароль») прямо в интерфейсе формы авторизации при вводе некорректных данных.

## Фактически затраченное время
## Дополнительные фичи (Доп. задания)

YYYY часов
- **Гостевой режим для общей ленты:** Реализована возможность для неавторизованных пользователей просматривать главную страницу со всеми постами.
- **Гостевой режим для профилей:** Неавторизованные пользователи могут открывать и просматривать персональные ленты конкретных авторов.
- **Защита от несанкционированных действий:** Для гостей заблокирована отправка лайков — при клике на сердечко выводится предупреждающее уведомление без отправки некорректных запросов к API.
- **Защита от битых ссылок (Устойчивость бэкенда):** Внедрена строгая валидация URL-адресов картинок, которая перехватывает сломанные серверные ссылки (такие как фейковый домен `image.png`), предотвращая падение скриптов и забивание консоли ошибками `ERR_NAME_NOT_RESOLVED`.
- **Локальный модуль относительного времени:** Логика форматирования дат была написана вручную без утяжеления репозитория сторонними библиотеками, что исключило проблемы с блокировками CORS и внешних CDN.

## Временные затраты

- **Первоначальная оценка:** 12 часов.
- **Фактически затраченное время:** 29 часов (дополнительное время ушло на отладку сетевых ошибок, исправление некорректных данных с сервера и настройку локальной обработки дат).
2 changes: 1 addition & 1 deletion api.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Замени на свой, чтобы получить независимый от других набор данных.
// "боевая" версия инстапро лежит в ключе prod
const personalKey = "prod";
const personalKey = "Tyryshkin2";
const baseHost = "https://webdev-hw-api.vercel.app";
const postsHost = `${baseHost}/api/v1/${personalKey}/instapro`;

Expand Down
3 changes: 3 additions & 0 deletions components/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.DS_Store
dist/
57 changes: 53 additions & 4 deletions components/add-post-page-component.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,69 @@
import { renderUploadImageComponent } from "./upload-image-component.js";
export function renderAddPostPageComponent({ appEl, onAddPostClick }) {
const render = () => {
// Переменная для динамического сохранения ссылки на загруженное фото
let imageUrl = "";

// @TODO: Реализовать страницу добавления поста
const appHtml = `
<div class="page-container">
<div class="header-container"></div>
Cтраница добавления поста
<button class="button" id="add-button">Добавить</button>
<div class="form">
<h3 class="form-title">Добавить новый пост</h3>

<!-- Сюда встроенный метод смонтирует кнопку выбора файла и превью -->
<div class="upload-image-container"></div>

<div class="form-inputs">
<p class="form-label">Опишите фотографию:</p>
<textarea
class="input textarea"
id="description-input"
rows="4"
placeholder="Введите описание поста..."
></textarea>
</div>

<button class="button" id="add-button">Добавить</button>
</div>
</div>
`;

appEl.innerHTML = appHtml;

// 1. Находим контейнер и запускаем в нем готовый компонент загрузки фото
const uploadImageContainer = document.querySelector(
".upload-image-container",
);
if (uploadImageContainer) {
renderUploadImageComponent({
element: uploadImageContainer,
onImageUrlChange(newImageUrl) {
// Как только облако вернет ссылку на картинку, сохраняем её в переменную
imageUrl = newImageUrl;
},
});
}

// 2. Навешиваем обработчик клика на кнопку отправки
document.getElementById("add-button").addEventListener("click", () => {
const descriptionInput = document.getElementById("description-input");

// Простая валидация перед отправкой, чтобы бэкенд не ругался ошибками 400
if (!imageUrl) {
alert("Пожалуйста, выберите и загрузите фотографию");
return;
}

if (!descriptionInput.value.trim()) {
alert("Пожалуйста, добавьте описание к посту");
return;
}

// Передаем наверх реальные данные вместо старых текстовых заглушек
onAddPostClick({
description: "Описание картинки",
imageUrl: "https://image.png",
description: descriptionInput.value,
imageUrl: imageUrl,
});
});
};
Expand Down
32 changes: 32 additions & 0 deletions components/date-fns-local.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Простая локальная замена formatDistanceToNow для русского языка
export function formatDistanceToNow(date) {
const now = new Date();
const diffInSeconds = Math.floor((now - new Date(date)) / 1000);

if (diffInSeconds < 0) return "только что";

const minutes = Math.floor(diffInSeconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);

// Склонение числительных (минуты, часы, дни)
const getNoun = (number, one, two, five) => {
let n = Math.abs(number);
n %= 100;
if (n >= 5 && n <= 20) return five;
n %= 10;
if (n === 1) return one;
if (n >= 2 && n <= 4) return two;
return five;
};

if (minutes < 1) {
return "меньше минуты назад";
} else if (minutes < 60) {
return `${minutes} ${getNoun(minutes, "минуту", "минуты", "минут")} назад`;
} else if (hours < 24) {
return `${hours} ${getNoun(hours, "час", "часа", "часов")} назад`;
} else {
return `${days} ${getNoun(days, "день", "дня", "дней")} назад`;
}
}
Loading