SlimusMinus 9ccad319cd Merge pull request #76 from SlimusMinus/fix-lan_lat_post
fixed lan and lon on post
2026-10-06 01:25:37 +03:00
2026-06-29 01:56:25 +03:00
2026-08-31 01:53:12 +03:00
2026-10-06 01:24:28 +03:00
2026-10-06 01:00:10 +03:00
2026-10-06 01:00:10 +03:00
2026-10-06 01:00:10 +03:00
2026-10-06 01:00:10 +03:00

Находка

Платформа для поиска потерянных вещей и питомцев. SPA на React 18 с JWT-аутентификацией и автоматическим обновлением токена, чатом на WebSocket (STOMP/SockJS), картами Яндекса, загрузкой изображений, админ-панелью рекламы и модерации объявлений, i18n (ru/en) и светлой/тёмной темой.


Содержание

  1. Быстрый старт
  2. Переменные окружения
  3. Стек и зависимости
  4. Скрипты
  5. Структура проекта
  6. Маршруты
  7. Ключевые компоненты
  8. Контексты
  9. Хуки
  10. API-эндпоинты
  11. WebSocket
  12. Админские эндпоинты
  13. Стили и темы
  14. Основные сценарии
  15. Известные проблемы и предупреждения

1. Быстрый старт

Требуется Node.js 18+ (проверено на Node 24).

npm install     # зависимости (см. раздел о .npmrc)
npm start       # dev-сервер на http://localhost:3000
npm run build   # production-сборка в build/

Фронтенд ожидает работающий бэкенд (по умолчанию http://localhost:8080). Если нужно обращаться к другому адресу — скопируйте .env.example в .env и задайте REACT_APP_API_URL.


2. Переменные окружения

Файл .env не коммитится (см. .gitignore), шаблон — .env.example.

Переменная Обязательна По умолчанию Описание
REACT_APP_API_URL да http://localhost:8080 Базовый URL бэкенда
REACT_APP_YANDEX_GEOCODER_KEY для карты — Яндекс.Геокодер: обратное геокодирование координат → адрес
REACT_APP_YANDEX_MAPS_KEY для карты — Публичный ключ Яндекс.Карт, подставляется в public/index.html
REACT_APP_SUPPORT_EMAIL нет есть значение по умолчанию Почта поддержки в модалке «Поддержка»

Безопасность. Ключи Яндекса — публичные, но попадание реального .env в git означает утечку в историю коммитов. Если файл уже коммитился, перевыпустите ключи: добавление .env в .gitignore не удаляет его из истории.


3. Стек и зависимости

Пакет Назначение
react / react-dom 18 UI
react-router-dom 7 Роутинг (BrowserRouter)
@stomp/stompjs + sockjs-client WebSocket (STOMP) для чата и непрочитанных
framer-motion Анимации модалок и переходов
react-hook-form + @hookform/resolvers + zod 4 Формы и их валидация
i18next + react-i18next Локализация ru/en
@emoji-mart/react + @emoji-mart/data + emoji-mart Эмодзи-пикер в чате
react-scripts 5.0.1 Сборка/дев-сервер (CRA)

Карты Яндекса подключаются напрямую через глобальный window.ymaps (см. src/utils/ymapsApi.js), без обёртки — она ждёт готовности API с таймаутом и отдаёт ошибку вместо вечного спиннера.

Dev-зависимости: typescript 5.9, @types/react 18, @types/react-dom, @types/node, @testing-library/*.

.npmrc и конфликт peer-зависимостей

В репозитории лежит .npmrc с legacy-peer-deps=true. Причина: react-scripts@5.0.1 объявляет typescript опциональным peer-диапазоном ^3.2.1 || ^4, а проекту нужен TypeScript 5 (на TS 4 не собираются @types/react 18.3 и zod 4). Конфликт формальный: CRA типы не проверяет — их вырезает babel, а проверка запускается отдельно через npm run typecheck.

Побочный эффект legacy-peer-deps: npm не ставит peer-зависимости автоматически, поэтому emoji-mart объявлен в dependencies явно (его требует @emoji-mart/react).


4. Скрипты

Скрипт Что делает
npm start Dev-сервер с hot reload
npm run build Production-сборка в build/
npm test Jest в watch-режиме
npm run test:ci Тесты один раз (--watchAll=false) — для CI
npm run lint ESLint по src, --max-warnings=0
npm run typecheck tsc --noEmit по tsconfig.json

Перед коммитом стоит прогнать все четыре проверки: lint, typecheck, test:ci, build.


5. Структура проекта

src/
├── index.js                          # Точка входа
├── App.js                            # Провайдеры + маршруты
├── i18n.js                           # Инициализация i18next, persist языка
├── legacy.d.ts                       # Типы ассетов (*.css, *.png, ...)
│
├── api/
│   ├── apiClient.jsx                 # apiFetch: Authorization + refresh токена по 401
│   ├── adminAdsApi.js                # Кампании, реклама, фуллскрин-реклама
│   ├── adminContentApi.js            # Админские объявления: список, статус, удаление
│   └── adsApi.js                     # Фуллскрин-реклама, пресigned-выгрузка видео
│
├── config/
│   ├── authConstants.js              # API_URL
│   ├── feedConstants.js              # Пункты меню, категории, пустая форма
│   └── landingConstants.js           # Контент лендинга
│
├── context/
│   ├── AuthContext.jsx               # user, token, isAuthenticated, isAdmin, login/logout
│   ├── ThemeContext.jsx              # Светлая/тёмная тема
│   └── UnreadContext.jsx             # Счётчики непрочитанных чатов
│
├── stores/
│   ├── activeChatStore.js            # Активный чат
│   └── chatPostStore.js              # Кэш «объявление ↔ чат»
│
├── hooks/                            # см. раздел 9
│
├── utils/
│   ├── adSession.js                  # ID рекламной сессии (устойчив к запрету localStorage)
│   ├── campaignStatus.js             # Статусы кампаний
│   ├── chatDisplay.jsx               # Данные для списка чатов
│   ├── compressImage.js              # Сжатие JPEG до 1200px
│   ├── formatPhone.js                # Маска +7 (___) ___-__-__
│   ├── validatePostForm.js           # Валидация формы объявления
│   ├── withAds.js                    # Рекламная обёртка
│   └── ymapsApi.js                   # Ожидание window.ymaps с таймаутом
│
├── pages/
│   ├── NakhodkaLanding.jsx           # Лендинг «/»
│   ├── MainFeed.tsx                  # Лента объявлений — главная страница
│   ├── MapModal.jsx, MapView.jsx     # Карта и полноэкранный режим
│   ├── ChatListPage.jsx, ChatPage.jsx
│   ├── ReviewsPage.jsx
│   ├── CreateUpdatePost.jsx          # Создание/редактирование (в т.ч. adminMode)
│   ├── PostOnFeed.jsx                # Карточка объявления
│   ├── RulesModal.jsx, SupportModal.jsx
│   └── AdminAdsPage.jsx              # Админ-панель
│
├── components/
│   ├── feed/        FeedTopBar.tsx, FeedContent, FeedSearchBar, PullToRefresh, RewardPopup
│   ├── nav/         BottomNav.jsx, Sidebar.jsx
│   ├── layout/      AppTopBar.jsx
│   ├── chat/        ChatHeader, ChatInputBar, ChatListItem, MessageBubble
│   ├── reviews/     RatingSummary, ReviewCard, ReviewForm, StarRating
│   ├── profile/     Profile, ProfileAvatar, ProfileFormFields, ProfileModal, profileValidation
│   ├── admin/       PostsTab, AdvertisingCreate/EditModal, CampaignCreate/EditModal, ...
│   ├── landing/     Hero, Navbar, StepsSection, RadarBeacon, FinalCta, Footer, Icon
│   ├── icons/       NavIcons + Calendar/Close/Globe/Menu/Moon/Phone/Pin/Sun/Trash
│   └── …            AuthModal, Toast, MapView, PostDetailModal, ImageViewer, AdCard, AdSlot, ...
│
├── styles/
│   ├── index.css                    # Точка входа CSS
│   ├── base.css                     # Переменные, сброс, keyframes
│   ├── layout.css, sidebar.css, card.css, modal.css, ui.css
│   └── chat.css, reviews.css, rules.css, admin.css, NakhodkaLanding.css
│
└── locales/
    ├── ru.json
    └── en.json

6. Маршруты

Путь Компонент Доступ
/ NakhodkaLanding публичный лендинг
/feed MainFeed лента (главная)
/reviews ReviewsPage отзывы о площадке
/chats ChatListPage список чатов
/chats/:chatId ChatPage чат
/admin/ads AdminAdsPage только isAdmin (иначе редирект на /feed)
* NakhodkaLanding 404-fallback

Провайдеры: ThemeProvider → AuthProvider → BrowserRouter.


7. Ключевые компоненты

MainFeed.tsx

Главная страница. Собирает топбар, поиск, фильтры, ленту, сайдбары, карту, модалки (объявление, профиль, вход, правила, поддержка, просмотр фото). Делегирует загрузку и мутации хукам useFeedPosts, useCityFeed, useSearchPosts, useMyPosts, useFeedActions, useModals. Ошибки удаления/отметки «найдено» показываются через Toast.

PostOnFeed.jsx

Карточка объявления: фото, бейджи категории/типа/статуса, заголовок, описание, адрес, дата, телефон, кнопка карты и «Написать». Для владельца — редактирование, удаление, отметка «найдено».

CreateUpdatePost.jsx

Модалка создания/редактирования. Переключатель Потеря/Находка, заголовок, описание, фото (сжатие на клиенте), категория, карта Яндекса с меткой и обратным геокодированием, адрес, телефон с маской, чекбокс правил. Валидация — validatePostForm. С пропом adminMode шлёт PUT /api/v1/admin/posts/{id}.

FeedContent.jsx / FeedTopBar.tsx / PullToRefresh.jsx

Лента, топбар со спрятанной панелью на скролле, pull-to-refresh с корректным cleanup класса активности.

MapView.jsx

Полноэкранная карта. Ждёт готовности API Яндекса, показывает плашку ошибки, если он не загрузился. Первая загрузка маркеров идёт по фактическим границам карты, а не по событию load (которого у ymaps.Map в 2.1 нет).

AdminAdsPage.jsx + components/admin/PostsTab.jsx

Админ-панель: вкладки «Рекламные кампании» и «Все объявления». Кампании, реклама, загрузка видео (presigned → S3 → confirm), фуллскрин-реклама. PostsTab — список всех объявлений с поиском, фильтрами, пагинацией, правкой, закрытием/возвратом в ленту и удалением.


8. Контексты

AuthContext

{
  user,              // object | null
  token,             // string | null — JWT
  isAuthenticated,   // !!token
  isAdmin,           // user.role === "ADMIN" — только скрытие UI, не защита
  login(user, token),    // пишет в localStorage + state
  logout(),              // чистит localStorage + state
  updateUser(userData),  // обновляет пользователя
}

Хранится в localStorage: user (JSON), token, refreshToken. Хук: useAuthContext().

ThemeContext

Светлая/тёмная тема; выбор сохраняется.

UnreadContext

Счётчики непрочитанных по чатам; сбрасываются при смене пользователя, данные приходят из WS-подписки /topic/user/{userId}/unread.


9. Хуки

Авторизация и API

Хук Назначение
apiClient.jsx → apiFetch fetch + Authorization + автоматический refresh по 401 (один refresh на параллельные запросы)
useProfile.js Загрузка профиля
useUpdateProfile.js Обновление профиля с аватаром

Объявления

Хук Назначение
useFeedPosts.jsx Лента с пагинацией, request-id guard, дедупликация
useCityFeed.jsx Лента по городу
useSearchPosts.js Поиск с дебаунсом
useMyPosts.jsx Мои объявления
useFeedActions.jsx Удаление и отметка «найдено» + error/clearError
usePostMutation.js Создание/обновление (обычное и adminMode)
useDeletePost.js, usePostStatus.js Точечные мутации
useImageUpload.js Выбор, сжатие, превью изображения
useModals.jsx Управление модалками
useInfiniteScroll.jsx Бесконечная прокрутка с cleanup IntersectionObserver
useCities.js, useClickOutside.jsx, useIsDesktop.js Вспомогательные

Реклама

Хук Назначение
useFullscreenAdTrigger.js Показ фуллскрин-рекламы
useAdClickTracking.js, useAdImpressionTracking.js Клики/показы
useTranscodingStatusPolling.js Статус транскодинга видео: сброс между роликами, fallback PROCESSING, лимит попыток

Чат

Хук Назначение
useStompClient.js Общий STOMP-клиент: reconnect, обработка close/ошибок, стабильные колбэки
useChatSocket.js Подписка на чат, отправка сообщений
useUnreadSocket.js Непрочитанные
useChats.js Список чатов, создание, сообщения (request-id guard)
useContactSeller.js Создание/получение чата с продавцом
useDeleteChat.js Удаление чата

Карта и локация

Хук Назначение
useGeolocation.js Геолокация пользователя со статусами
useYandexMap.js Инициализация карты, метка (drag), обратное геокодирование, ошибка загрузки API

Прочее

Хук Назначение
useReviews.jsx Отзывы: загрузка, отправка, средний рейтинг и распределение
useLanguageToggle.js Переключение ru/en

10. API-эндпоинты

Базовый URL — REACT_APP_API_URL. Защищённые эндпоинты вызываются через apiFetch, который сам добавляет Authorization: Bearer <JWT> и обновляет токен при 401.

Аутентификация

Метод Endpoint Тело Ответ
POST /api/v1/auth/login { login, password } { token, refreshToken, user }
POST /api/v1/auth/register { name, login, password } { token, refreshToken, user }
POST /api/v1/auth/refresh { refreshToken } { token, refreshToken }

Объявления

Метод Endpoint Назначение
GET /api/v1/posts?page=&size=&… Лента с пагинацией (content / items / массив)
GET /api/v1/posts/search?… Поиск
GET /api/v1/posts/cities Список городов
GET /api/v1/posts/map?minLat&maxLat&minLng&maxLng&… Маркеры в границах карты
GET /api/v1/posts/detail/:postId Карточка объявления
GET /api/v1/posts/:userId Объявления пользователя
POST /api/v1/posts Создать (FormData)
PUT /api/v1/posts/:id Обновить (FormData)
DELETE /api/v1/posts/:id Удалить
PATCH /api/v1/posts/:postId/status Статус ACTIVE / CLOSED

Профиль

Метод Endpoint Назначение
PUT /api/v1/users/:userId Обновить профиль (FormData)

Чаты

Метод Endpoint Назначение
GET /api/v1/chats Список чатов
POST /api/v1/chats/with/:otherUserId Создать/получить чат ({ postId })
GET /api/v1/chats/:chatId/messages Сообщения
POST /api/v1/chats/:chatId/messages Отправить
POST /api/v1/chats/:chatId/read Отметить прочитанным
DELETE /api/v1/chats/:chatId Удалить чат

Отзывы

Метод Endpoint Тело
GET /api/v1/reviews —
POST /api/v1/reviews { rating, text }

Файлы и реклама

Метод Endpoint Назначение
GET /api/v1/files/:filename Изображение по имени
GET /api/fullscreen-ad Текущая фуллскрин-реклама
GET /api/ads/next Следующая реклама
POST /api/ads/click, /api/ads/impression Трекинг

Значения категорий в запросах — стабильные русские строки (Вещь, Питомец, Другое): они хранятся в БД, поэтому не переводятся вместе с интерфейсом. Локализованы только подписи (feedConstants.js → CATEGORIES).


11. WebSocket

STOMP поверх SockJS: ${REACT_APP_API_URL}/ws (токен передаётся при подключении).

Назначение Путь
Подписка на чат /topic/chat/{chatId}
Подписка на непрочитанные /topic/user/{userId}/unread
Отправка сообщения /app/chat.send

Общий клиент — useStompClient: единый STOMP-клиент с reconnect, обработкой закрытия соединения и ошибок активации; колбэки хранятся в ref, поэтому переподписка не пересоздаёт подключение.


12. Админские эндпоинты

Админ-панель живёт на /admin/ads (только для isAdmin, только десктоп) и переключается вкладками:

  • /admin/ads — рекламные кампании;
  • /admin/ads?tab=posts — «Все объявления».

Общие требования

  • Префикс /api/v1/admin, все запросы с Authorization: Bearer <JWT>.
  • Роль ADMIN обязана проверяться на сервере по каждому эндпоинту. Клиентский isAdmin — только скрытие UI; подделка localStorage не должна давать доступ к чужим объявлениям и переписке.
  • Тело ошибок — JSON {"message": "..."}; фронт показывает этот текст.
  • Пагинация как у GET /api/v1/posts: { "content": [...], "last": bool, "totalElements": N }. Фронт принимает и content, и items, и голый массив.

Реклама (работает)

/api/v1/admin/campaigns, /api/v1/admin/ads, /api/v1/admin/fullscreen-ads.

Объявления

GET /api/v1/admin/posts

Параметры (все опциональны): page (с 0), size (по умолчанию 20, максимум 100), search, type (LOSS/FOUND), status.

Видны все статусы, включая MODERATION и REJECTED. Сортировка по умолчанию — новые сверху (createdAt DESC). search ищет без учёта регистра по названию, описанию, городу, району и данным владельца (login, имя, фамилия).

Элемент content — то же, что у публичного объявления, плюс данные автора (ownerId, ownerLogin, ownerName, ownerFirstName, ownerLastName). Фронт читает автора с запасом (ownerLogin / authorLogin / userLogin и т. п.).

PUT /api/v1/admin/posts/{postId}

multipart/form-data, как у пользовательского PUT /api/v1/posts/{id}: type, title, description, category, phone, reward, rewardText, images, existingImages, latitude, longitude, address. Ответ 200 — обновлённое объявление.

Отличия от пользовательской правки:

  • Статус сохраняется — админ чинит объявление, не убирая его из ленты (MODERATION отправил бы на повторную AI-модерацию, REJECTED скрыл бы навсегда). Смена статуса — отдельным PATCH.
  • Падение геокодинга не блокирует правку: город берётся из address.
  • Изображения, которых нет в existingImages, удаляются.

DELETE /api/v1/admin/posts/{postId}

Ответ 204 No Content. Вместе с объявлением чистятся изображения в хранилище, записи из favorites и обнуляется post_id в чатах.

PATCH /api/v1/admin/posts/{postId}/status

Тело { "status": "CLOSED" } — закрыть («найдено»), { "status": "ACTIVE" } — вернуть в ленту. Принимаются все PostStatus: ACTIVE, CLOSED, MODERATION, REJECTED (фронт отдаёт первые два). Переход в CLOSED увеличивает posts_found. Неизвестный статус — 400.

Чаты (вкладка не сделана)

Требуемые эндпоинты:

Метод Endpoint Назначение
GET /api/v1/admin/chats?page&size&search&postId Список чатов с превью
GET /api/v1/admin/chats/{chatId}/messages Сообщения чата
POST /api/v1/admin/chats/{chatId}/messages Написать в чат ({ "content": "…" })
DELETE /api/v1/admin/chats/{chatId} Удалить чат с перепиской

Важно: senderId должен браться из JWT, а не из тела запроса, иначе админ мог бы слать сообщения от имени пользователей. Авторизация — только ADMIN; это осознанное расширение прав по сравнению с пользовательским чатом (например, чтобы предупредить о мошенничестве).

Что сделано на клиенте

  • src/api/adminContentApi.js — getAdminPosts, deleteAdminPost, updateAdminPostStatus; пагинация принимает content / items / массив; type нормализуется в loss/found.
  • CreateUpdatePost с пропом adminMode → usePostMutation шлёт PUT /api/v1/admin/posts/{id} (форма и валидация общие с пользовательской).
  • src/components/admin/PostsTab.jsx — список, поиск с дебаунсом, фильтры, «Показать ещё», карточки с действиями.

Что осталось на клиенте

  • Вкладка «Все чаты» (ChatsTab.jsx не создана) — ждёт эндпоинтов чатов.
  • Проверка роли и скрытие UI: AuthContext.isAdmin, AdminRoute в App.js, isAdmin в сайдбаре. Это удобство, не защита.
  • Админка не постит сообщения через STOMP: используется REST, чтобы реплика не зависела от того, разрешена ли админу подписка /topic/chat/{id}.

Статус бэкенда

Репозиторий бэкенда: D:\reFound\ReFound.

Файл Назначение
controller/admin/PostAdminController.java Эндпоинты объявлений, у каждого @PreAuthorize("hasRole('ADMIN')")
service/admin/AdminPostService.java Логика: список, правка, удаление, статус
dto/admin/AdminPostResponse.java Объявление + данные владельца
dto/admin/AdminPostStatusUpdateRequest.java { "status": "..." }
util/PostSpecification.java adminSearch — поиск по полям объявления и владельца
repository/ImageRepository.java findByPostIdIn — картинки страницы одним запросом вместо N+1
repository/ChatRepository.java clearPostReference — обнуление post_id при удалении
test/.../AdminPostServiceTest.java 13 тестов: поиск, фильтры, статус, статистика, картинки, кэш

Защита дублируется: SecurityConfig требует ADMIN на /api/v1/admin/**, и методы контроллера дополнительно закрыты @PreAuthorize.


13. Стили и темы

Подключение через src/styles/index.css:

index.css → base.css → layout.css → sidebar.css → card.css → modal.css → ui.css

Дополнительно подключаются в компонентах: chat.css (App.js), reviews.css, rules.css, admin.css, NakhodkaLanding.css.

CSS-переменные в base.css:

Переменная Значение Описание
--teal #2de0c8 Основной акцент
--teal2 #1fc9b0 Тёмный оттенок
--border rgba(255,255,255,0.09) Границы
--muted / --muted2 rgba(255,255,255,.38) / .15 Второстепенный текст
--font Nunito, sans-serif Шрифт
--err #ff6b6b Ошибка

Тёмная тема — градиентные фоны, «стекло» (backdrop-filter), акцент #2de0c8; светлая переключается через ThemeContext.

Адаптивность:

Брейкпоинт Что меняется
≤ 900px Правый сайдбар скрывается, навигация внизу
≤ 768px Компактные отступы и кнопки
≤ 600px Модалки на всю ширину

14. Основные сценарии

Аутентификация. Гость нажимает «Разместить», «Мои объявления» или аватар → открывается AuthModal → POST /api/v1/auth/login → AuthContext.login() сохраняет user, token, refreshToken в localStorage. Просрочка access-токена обрабатывается прозрачно: apiFetch при 401 обновляет токен и повторяет запрос; при неудаче чистит сессию.

Создание объявления. Авторизованный пользователь → CreateUpdatePost → заполняет форму → клиентское сжатие фото → POST /api/v1/posts (FormData) → лента обновляется.

Поиск и чат. «Написать» → POST /api/v1/chats/with/:otherUserId → переход на /chats/:chatId → подписка на /topic/chat/{chatId} → сообщения в реальном времени.

Карта. useYandexMap ждёт window.ymaps (с таймаутом и ошибкой вместо вечного спиннера), клик по карте ставит метку, drag метки и клик переводят координаты в адрес через Яндекс.Геокодер; запрос на геокодинг защищён request-id от гонок.

Профиль. Аватар в топбаре → ProfileModal → PUT /api/v1/users/:id → данные обновляются в AuthContext и localStorage; «Выйти» — очистка сессии.


15. Известные проблемы и предупреждения

  • fs.F_OK is deprecated при сборке — предупреждение Node 24 из внутренностей react-scripts@5. Не влияет на результат; уходит при переходе на CRA 6 / Vite.
  • npm audit показывает уязвимости в транзитивных зависимостях CRA (webpack-dev-server и др.). Применять npm audit fix --force нельзя: это ломающие обновления в пределах react-scripts@5.
  • Ключи Яндекса попадали в историю git — их нужно перевыпустить.
  • Клиентская проверка роли (isAdmin, AdminRoute) — только скрытие UI. Реальная авторизация обязана быть на сервере по каждому эндпоинту.
  • Известная проблема бэкенда: GlobalExceptionHandler ловит Exception без исключений, поэтому битый JSON или неверный тип параметра дают 500, а не 400. Из-за этого type и status в админских эндпоинтах принимаются строками и разбираются вручную.
Description
No description provided
Readme 1.3 MiB
Languages
JavaScript 73.8%
CSS 19.9%
TypeScript 5.9%
HTML 0.4%