diff --git a/.env b/.env deleted file mode 100644 index b5cd69a..0000000 --- a/.env +++ /dev/null @@ -1,3 +0,0 @@ -REACT_APP_YANDEX_GEOCODER_KEY=c2d3c717-a109-4494-8863-7edbc32044e3 -REACT_APP_YANDEX_MAPS_KEY=d4696cc8-6a1f-4356-9219-108ac474093b -REACT_APP_API_URL=http://192.168.1.76:8080 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4d29575..fcb3e48 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ # misc .DS_Store + +# env — реальные ключи и адреса бэкенда не коммитим. +# Образец переменных держим в .env.example +.env .env.local .env.development.local .env.test.local diff --git a/README.md b/README.md index 58beeac..1a410ae 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,614 @@ -# Getting Started with Create React App +# Находка -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +Платформа для поиска потерянных вещей и питомцев. SPA на React 18 с JWT-аутентификацией +и автоматическим обновлением токена, чатом на WebSocket (STOMP/SockJS), картами Яндекса, +загрузкой изображений, админ-панелью рекламы и модерации объявлений, i18n (ru/en) и +светлой/тёмной темой. -## Available Scripts +--- -In the project directory, you can run: +## Содержание -### `npm start` +1. [Быстрый старт](#1-быстрый-старт) +2. [Переменные окружения](#2-переменные-окружения) +3. [Стек и зависимости](#3-стек-и-зависимости) +4. [Скрипты](#4-скрипты) +5. [Структура проекта](#5-структура-проекта) +6. [Маршруты](#6-маршруты) +7. [Ключевые компоненты](#7-ключевые-компоненты) +8. [Контексты](#8-контексты) +9. [Хуки](#9-хуки) +10. [API-эндпоинты](#10-api-эндпоинты) +11. [WebSocket](#11-websocket) +12. [Админские эндпоинты](#12-админские-эндпоинты) +13. [Стили и темы](#13-стили-и-темы) +14. [Основные сценарии](#14-основные-сценарии) +15. [Известные проблемы и предупреждения](#15-известные-проблемы-и-предупреждения) -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. +--- -The page will reload when you make changes.\ -You may also see any lint errors in the console. +## 1. Быстрый старт -### `npm test` +Требуется Node.js 18+ (проверено на Node 24). -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +```bash +npm install # зависимости (см. раздел о .npmrc) +npm start # dev-сервер на http://localhost:3000 +npm run build # production-сборка в build/ +``` -### `npm run build` +Фронтенд ожидает работающий бэкенд (по умолчанию `http://localhost:8080`). +Если нужно обращаться к другому адресу — скопируйте `.env.example` в `.env` +и задайте `REACT_APP_API_URL`. -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +--- -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +## 2. Переменные окружения -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +Файл `.env` **не** коммитится (см. `.gitignore`), шаблон — `.env.example`. -### `npm run eject` +| Переменная | Обязательна | По умолчанию | Описание | +|------------|-------------|--------------|----------| +| `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` | нет | есть значение по умолчанию | Почта поддержки в модалке «Поддержка» | -**Note: this is a one-way operation. Once you `eject`, you can't go back!** +> **Безопасность.** Ключи Яндекса — публичные, но попадание реального `.env` в git +> означает утечку в историю коммитов. Если файл уже коммитился, перевыпустите ключи: +> добавление `.env` в `.gitignore` не удаляет его из истории. -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +--- -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. +## 3. Стек и зависимости -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. +| Пакет | Назначение | +|-------|-----------| +| `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) | -## Learn More +Карты Яндекса подключаются напрямую через глобальный `window.ymaps` +(см. `src/utils/ymapsApi.js`), без обёртки — она ждёт готовности API +с таймаутом и отдаёт ошибку вместо вечного спиннера. -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +Dev-зависимости: `typescript` 5.9, `@types/react` 18, `@types/react-dom`, +`@types/node`, `@testing-library/*`. -To learn React, check out the [React documentation](https://reactjs.org/). +### `.npmrc` и конфликт peer-зависимостей -### Code Splitting +В репозитории лежит `.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`. -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) +Побочный эффект `legacy-peer-deps`: npm не ставит peer-зависимости автоматически, +поэтому `emoji-mart` объявлен в `dependencies` явно (его требует `@emoji-mart/react`). -### Analyzing the Bundle Size +--- -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) +## 4. Скрипты -### Making a Progressive Web App +| Скрипт | Что делает | +|--------|-----------| +| `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` | -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) +Перед коммитом стоит прогнать все четыре проверки: `lint`, `typecheck`, `test:ci`, `build`. -### Advanced Configuration +--- -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) +## 5. Структура проекта -### Deployment +``` +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 +``` -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) +--- -### `npm run build` fails to minify +## 6. Маршруты -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) +| Путь | Компонент | Доступ | +|------|-----------|--------| +| `/` | `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` + +```js +{ + 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 ` и обновляет +токен при 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 `. +- **Роль `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` в админских эндпоинтах принимаются + строками и разбираются вручную. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e351eff..a7d83eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,22 +11,14 @@ "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@hookform/resolvers": "^5.2.2", - "@pbe/react-yandex-maps": "^1.2.5", "@stomp/stompjs": "^7.3.0", - "@tailwindcss/vite": "^4.2.4", - "@tanstack/react-query": "^5.100.5", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^13.5.0", - "autoprefixer": "^10.5.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", "emoji-mart": "^5.6.0", "framer-motion": "^12.38.0", "i18next": "^22.5.1", - "lucide-react": "^1.11.0", - "postcss": "^8.5.12", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.74.0", @@ -34,17 +26,14 @@ "react-router-dom": "^7.14.2", "react-scripts": "5.0.1", "sockjs-client": "^1.6.1", - "tailwind-merge": "^3.5.0", - "tailwindcss": "^4.2.4", "web-vitals": "^2.1.4", - "zod": "^4.3.6", - "zustand": "^5.0.12" + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "^25.6.0", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.7", - "typescript": "^4.9.5" + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "typescript": "^5.9.3" } }, "node_modules/@adobe/css-tools": { @@ -2408,37 +2397,6 @@ "postcss-selector-parser": "^6.0.10" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emoji-mart/data": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", @@ -2959,24 +2917,6 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "license": "MIT" }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -3043,31 +2983,6 @@ "node": ">= 8" } }, - "node_modules/@oxc-project/types": { - "version": "0.149.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", - "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/oxc-project" - } - }, - "node_modules/@pbe/react-yandex-maps": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@pbe/react-yandex-maps/-/react-yandex-maps-1.2.5.tgz", - "integrity": "sha512-cBojin5e1fPx9XVCAqHQJsCnHGMeBNsP0TrNfpWCrPFfxb30ye+JgcGr2mn767Gbr1d+RufBLRiUcX2kaiAwjQ==", - "license": "MIT", - "dependencies": { - "@types/yandex-maps": "2.1.29" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "react": "^16.x || ^17.x || ^18.x" - } - }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz", @@ -3116,286 +3031,6 @@ } } }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", - "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", - "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", - "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", - "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", - "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", - "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", - "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", - "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", - "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", - "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", - "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", - "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", - "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", - "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", - "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "license": "MIT", - "peer": true - }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3756,307 +3391,6 @@ "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@tailwindcss/node": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", - "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.2.4" - } - }, - "node_modules/@tailwindcss/node/node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/@tailwindcss/node/node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", - "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-x64": "4.2.4", - "@tailwindcss/oxide-freebsd-x64": "4.2.4", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-x64-musl": "4.2.4", - "@tailwindcss/oxide-wasm32-wasi": "4.2.4", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", - "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", - "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", - "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", - "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", - "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", - "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", - "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", - "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", - "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", - "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", - "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", - "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", - "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.2.4", - "@tailwindcss/oxide": "4.2.4", - "tailwindcss": "4.2.4" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.100.5", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.5.tgz", - "integrity": "sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.100.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.5.tgz", - "integrity": "sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.100.5" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -4162,16 +3496,6 @@ "node": ">= 6" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -4421,6 +3745,13 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "license": "MIT" }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/q": { "version": "1.5.8", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", @@ -4440,23 +3771,24 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", - "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "devOptional": true, + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, "license": "MIT", "dependencies": { + "@types/prop-types": "*", "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==", - "devOptional": true, + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.2.0" + "@types/react": "^18.0.0" } }, "node_modules/@types/resolve": { @@ -4549,12 +3881,6 @@ "@types/node": "*" } }, - "node_modules/@types/yandex-maps": { - "version": "2.1.29", - "resolved": "https://registry.npmjs.org/@types/yandex-maps/-/yandex-maps-2.1.29.tgz", - "integrity": "sha512-nuibRWj3RU/9KXlCzTrRtDE+n6V9l7NbT9JakicqZ5OXIdwyb6blvV2Uwn6lB58WYm3DSUDP2I2AWlnWMc8z2w==", - "license": "MIT" - }, "node_modules/@types/yargs": { "version": "16.0.11", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz", @@ -6172,9 +5498,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001791", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", - "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", "funding": [ { "type": "opencollective", @@ -6297,18 +5623,6 @@ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "license": "MIT" }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -6341,15 +5655,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -7102,7 +6407,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -7312,15 +6617,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -11918,255 +11214,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -12287,15 +11334,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lucide-react": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.11.0.tgz", - "integrity": "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -15570,40 +14608,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rolldown": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", - "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@oxc-project/types": "=0.149.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.8", - "@rolldown/binding-android-arm64": "1.2.8", - "@rolldown/binding-darwin-arm64": "1.2.8", - "@rolldown/binding-darwin-x64": "1.2.8", - "@rolldown/binding-freebsd-x64": "1.2.8", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", - "@rolldown/binding-linux-arm64-gnu": "1.2.8", - "@rolldown/binding-linux-arm64-musl": "1.2.8", - "@rolldown/binding-linux-ppc64-gnu": "1.2.8", - "@rolldown/binding-linux-s390x-gnu": "1.2.8", - "@rolldown/binding-linux-x64-gnu": "1.2.8", - "@rolldown/binding-linux-x64-musl": "1.2.8", - "@rolldown/binding-openharmony-arm64": "1.2.8", - "@rolldown/binding-win32-arm64-msvc": "1.2.8", - "@rolldown/binding-win32-x64-msvc": "1.2.8" - } - }, "node_modules/rollup": { "version": "2.80.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", @@ -16945,22 +15949,6 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, - "node_modules/tailwind-merge": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", - "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", - "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", - "license": "MIT" - }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", @@ -17449,16 +16437,17 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -17700,370 +16689,6 @@ "node": ">= 0.8" } }, - "node_modules/vite": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", - "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.26", - "rolldown": "~1.2.4", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0 || ^0.5.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "license": "MPL-2.0", - "peer": true, - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/vite/node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -19002,35 +17627,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zustand": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", - "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } } } } diff --git a/package.json b/package.json index 6cd8488..5366ea0 100644 --- a/package.json +++ b/package.json @@ -6,22 +6,14 @@ "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@hookform/resolvers": "^5.2.2", - "@pbe/react-yandex-maps": "^1.2.5", "@stomp/stompjs": "^7.3.0", - "@tailwindcss/vite": "^4.2.4", - "@tanstack/react-query": "^5.100.5", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^13.5.0", - "autoprefixer": "^10.5.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", "emoji-mart": "^5.6.0", "framer-motion": "^12.38.0", "i18next": "^22.5.1", - "lucide-react": "^1.11.0", - "postcss": "^8.5.12", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.74.0", @@ -29,16 +21,16 @@ "react-router-dom": "^7.14.2", "react-scripts": "5.0.1", "sockjs-client": "^1.6.1", - "tailwind-merge": "^3.5.0", - "tailwindcss": "^4.2.4", "web-vitals": "^2.1.4", - "zod": "^4.3.6", - "zustand": "^5.0.12" + "zod": "^4.3.6" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", + "test:ci": "react-scripts test --watchAll=false", + "lint": "eslint src --ext .js,.jsx,.ts,.tsx --max-warnings=0", + "typecheck": "tsc --noEmit", "eject": "react-scripts eject" }, "eslintConfig": { @@ -61,8 +53,11 @@ }, "devDependencies": { "@types/node": "^25.6.0", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.7", - "typescript": "^4.9.5" + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "typescript": "^5.9.3" + }, + "overrides": { + "caniuse-lite": "^1.0.30001814" } } diff --git a/react_front/index.html b/react_front/index.html deleted file mode 100644 index 0ba6111..0000000 --- a/react_front/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - react_front - - -
- - - diff --git a/react_front/public/favicon.svg b/react_front/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/react_front/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/react_front/src/App.css b/react_front/src/App.css deleted file mode 100644 index f90339d..0000000 --- a/react_front/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/react_front/src/App.tsx b/react_front/src/App.tsx deleted file mode 100644 index a66b5ef..0000000 --- a/react_front/src/App.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from './assets/vite.svg' -import heroImg from './assets/hero.png' -import './App.css' - -function App() { - const [count, setCount] = useState(0) - - return ( - <> -
-
- - React logo - Vite logo -
-
-

Get started

-

- Edit src/App.tsx and save to test HMR -

-
- -
- -
- -
-
- -

Documentation

-

Your questions, answered

- -
-
- -

Connect with us

-

Join the Vite community

- -
-
- -
-
- - ) -} - -export default App diff --git a/react_front/src/index.css b/react_front/src/index.css deleted file mode 100644 index 5fb3313..0000000 --- a/react_front/src/index.css +++ /dev/null @@ -1,111 +0,0 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } -} - -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -body { - margin: 0; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); -} diff --git a/react_front/src/main.tsx b/react_front/src/main.tsx deleted file mode 100644 index bef5202..0000000 --- a/react_front/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' - -createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/api/adsApi.js b/src/api/adsApi.js index 71e4893..5a3ce52 100644 --- a/src/api/adsApi.js +++ b/src/api/adsApi.js @@ -63,30 +63,3 @@ export function putWithProgress(url, file, onProgress) { xhr.send(file); }); } - -export async function uploadAdVideo(advertisingId, file, onProgress) { - const presignRes = await apiFetch( - `${API_URL}/api/v1/admin/ads/${advertisingId}/video/presign-upload`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ contentType: file.type }), - } - ); - if (!presignRes.ok) throw new Error(`Presign failed (${presignRes.status})`); - const { uploadUrl, sourceKey } = await presignRes.json(); - - await putWithProgress(uploadUrl, file, onProgress); - - const confirmRes = await apiFetch( - `${API_URL}/api/v1/admin/ads/${advertisingId}/video/confirm-upload`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ sourceKey }), - } - ); - if (!confirmRes.ok) throw new Error(`Confirm failed (${confirmRes.status})`); - - return { sourceKey }; -} \ No newline at end of file diff --git a/src/components/AuthModal.jsx b/src/components/AuthModal.jsx index cc2f4d8..e01f837 100644 --- a/src/components/AuthModal.jsx +++ b/src/components/AuthModal.jsx @@ -106,7 +106,7 @@ function LoginFormInline({ saveAuth, onClose, onSwitch }) {

{t('auth.noAccount')}{" "} - diff --git a/src/components/MapView.jsx b/src/components/MapView.jsx index a0a8b1a..73aa444 100644 --- a/src/components/MapView.jsx +++ b/src/components/MapView.jsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { API_URL, getImageUrl } from "../config/authConstants"; import { useGeolocation, GEO_STATUS } from "../hooks/useGeolocation"; +import { whenYmapsReady } from "../utils/ymapsApi"; const MARKER_COLORS = { LOST: "#ff3b30", @@ -118,6 +119,10 @@ export default function MapView({ onBack, }) { const { t } = useTranslation(); + // tRef нужен, чтобы читать перевод из эффектов, не добавляя t в их deps: + // иначе смена языка пересоздавала бы карту и все маркеры с нуля. + const tRef = useRef(t); + tRef.current = t; const mapRef = useRef(null); const ymapRef = useRef(null); const userMarkerRef = useRef(null); @@ -319,9 +324,20 @@ export default function MapView({ } }; - if (window.ymaps?.ready) { - window.ymaps.ready(init); - } + whenYmapsReady() + .then((ymaps) => { + if (destroyed) return; + ymaps.ready(init); + }) + .catch((e) => { + // API карт не подгрузился (заблокирован/офлайн/медленный CDN). + // Без этой ветки пользователь смотрел бы в вечный спиннер без ошибки. + if (!destroyed) { + setError(tRef.current("post.mapLoadFailed")); + setLoading(false); + } + console.warn("Yandex Maps API failed to load:", e); + }); return () => { destroyed = true; diff --git a/src/components/Toast.jsx b/src/components/Toast.jsx index d1e8cd6..f0b1c89 100644 --- a/src/components/Toast.jsx +++ b/src/components/Toast.jsx @@ -1,5 +1,5 @@ // src/components/Toast.jsx -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; /** * Toast — всплывающее сообщение об ошибке/успехе, автоматически скрывается. @@ -10,10 +10,16 @@ import { useEffect } from "react"; * @param {number} [duration=4000] — время жизни в мс */ export default function Toast({ message, type = "error", onDismiss, duration = 4000 }) { + // onDismiss почти всегда — свежий инлайн-стрелки. Если держать его в + // зависимостях useEffect, любой ре-рендер родителя (сообщения чата, поллинг + // рекламы) перезапускал бы отсчёт и тост висел бы бесконечно. + const dismissRef = useRef(onDismiss); + dismissRef.current = onDismiss; + useEffect(() => { - const timer = setTimeout(onDismiss, duration); + const timer = setTimeout(() => dismissRef.current?.(), duration); return () => clearTimeout(timer); - }, [onDismiss, duration]); + }, [duration]); if (!message) return null; diff --git a/src/components/feed/FeedBlurMask.jsx b/src/components/feed/FeedBlurMask.jsx deleted file mode 100644 index d3530c7..0000000 --- a/src/components/feed/FeedBlurMask.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect, useState } from "react"; - -/** - * Отдельный блюр-слой поверх ленты, независим от FeedTopBar. - * Прижат к верху общего контейнера, высота подстраивается под - * реальную высоту topBar (включая safe-area на iOS). - */ -export default function FeedBlurMask({ topBarRef }) { - const [height, setHeight] = useState(56); - - useEffect(() => { - const el = topBarRef?.current; - if (!el) return; - const update = () => setHeight(el.getBoundingClientRect().height || 56); - update(); - const ro = new ResizeObserver(update); - ro.observe(el); - return () => ro.disconnect(); - }, [topBarRef]); - - return ( -

@@ -317,6 +319,7 @@ export default function CreateUpdatePost({ onClose, onSuccess, initialData, admi {form.address ? `📍 ${form.address}` : t('post.mapPickLabel')} {!coords &&

{t('post.mapHint')}

} + {mapLoadError &&

{mapLoadError}

} {errors.map &&

{errors.map}

} diff --git a/src/pages/MainFeed.tsx b/src/pages/MainFeed.tsx index 7407f6d..06f2720 100644 --- a/src/pages/MainFeed.tsx +++ b/src/pages/MainFeed.tsx @@ -27,9 +27,11 @@ import { useInfiniteScroll } from "../hooks/useInfiniteScroll"; import { useModals } from "../hooks/useModals"; import { useFeedActions } from "../hooks/useFeedActions"; import { useTranslation } from "react-i18next"; +import { CATEGORY_ITEM, CATEGORY_OTHER, CATEGORY_PET } from "../config/feedConstants"; import { useProfile } from "../hooks/useProfile"; import { useFullscreenAdTrigger } from "../hooks/useFullscreenAdTrigger"; import FullscreenAdOverlay from "../components/FullscreenAdOverlay"; +import Toast from "../components/Toast"; import type { ComponentType } from "react"; // Legacy .jsx-модули: TS инферирует их типы из JS неточно (бывают «странные» @@ -328,10 +330,13 @@ export default function MainFeed() { result = feed.ads; } + // Сравниваем с теми же значениями, что уходят на бэкенд (CATEGORIES), + // а не с русскими литералами: интерфейс-то может быть на английском. if (activeNav === "items") { - result = result.filter(ad => ["Вещь", "Другое"].includes(ad.category)); + result = result.filter((ad: AnyAds[number]) => + [CATEGORY_ITEM, CATEGORY_OTHER].includes(ad.category)); } else if (activeNav === "pets") { - result = result.filter(ad => ad.category === "Питомец"); + result = result.filter((ad: AnyAds[number]) => ad.category === CATEGORY_PET); } if (typeFilter) { @@ -514,6 +519,14 @@ export default function MainFeed() { {fullscreenAd && } + {/* Ошибки удаления/отметки «найдено»: без этого тоста неудачный запрос + выглядел бы как «ничего не произошло». */} + + setAuthModalOpen(true), authorFallbackName: profileData.firstName || t('reviews.you'), diff --git a/src/stores/chatPostStore.js b/src/stores/chatPostStore.js index dec316e..8ae3ddf 100644 --- a/src/stores/chatPostStore.js +++ b/src/stores/chatPostStore.js @@ -2,6 +2,13 @@ const STORAGE_KEY = "nakhodka:chatPosts"; const postByChatId = new Map(); +/** + * Map не приводит ключи к строке, а chat.id из JSON приходит числом, тогда как + * из useParams() — всегда строкой. Без нормализации запись просто не + * находилась (а после перезагрузки страницы в кеше жили оба варианта ключа). + */ +const key = (chatId) => String(chatId); + /** Восстанавливает кеш постов из localStorage (выживает при перезагрузке страницы). */ function loadFromStorage() { try { @@ -32,18 +39,18 @@ function persist() { loadFromStorage(); export function setPostForChat(chatId, post) { - postByChatId.set(chatId, post); + postByChatId.set(key(chatId), post); persist(); } export function getPostForChat(chatId) { - return postByChatId.get(chatId); + return postByChatId.get(key(chatId)); } export function cachePostsFromChats(chats) { for (const chat of chats) { if (chat.post) { - postByChatId.set(chat.id, chat.post); + postByChatId.set(key(chat.id), chat.post); } } if (chats.length) persist(); @@ -51,16 +58,6 @@ export function cachePostsFromChats(chats) { /** Удаляет запись при удалении конкретного чата. */ export function removePostForChat(chatId) { - postByChatId.delete(chatId); + postByChatId.delete(key(chatId)); persist(); } - -/** Полностью очищает кеш — вызывать при logout(), чтобы не хранить данные предыдущей сессии. */ -export function clearPostCache() { - postByChatId.clear(); - try { - localStorage.removeItem(STORAGE_KEY); - } catch (e) { - // ignore - } -} diff --git a/src/styles/auth.css b/src/styles/auth.css deleted file mode 100644 index f28ee60..0000000 --- a/src/styles/auth.css +++ /dev/null @@ -1,308 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Onest:wght@400;500;600;700;800;900&display=swap'); - -/* ─── Page ──────────────────────────────────────────────────────────────────── */ -.auth-page { - font-family: 'Onest', sans-serif; - min-height: 100vh; - display: flex; align-items: center; justify-content: center; - background: radial-gradient(ellipse at 30% 40%, #1a2340 0%, #0d1018 60%); - overflow: hidden; position: relative; -} - -.bg-glow { - position: fixed; top: 40%; right: 0; - width: 500px; height: 400px; - background: radial-gradient(ellipse, #0f2a22 0%, transparent 70%); - pointer-events: none; opacity: 0.7; -} - -/* ─── Floating icons ────────────────────────────────────────────────────────── */ -.fi { - position: fixed; - opacity: 0.13; - pointer-events: none; - user-select: none; - animation: float 7s ease-in-out infinite; -} -@keyframes float { - 0%, 100% { transform: translateY(0) rotate(-6deg); } - 50% { transform: translateY(-18px) rotate(6deg); } -} - -/* ─── Card ──────────────────────────────────────────────────────────────────── */ -.auth-card { - position: relative; z-index: 10; width: 375px; - background: linear-gradient(155deg, #1d2436 0%, #131720 100%); - border: 1px solid rgba(255,255,255,0.08); - border-radius: 30px; padding: 36px 38px 30px; - box-shadow: 0 40px 100px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.03); - /* height transition — карточка плавно меняет размер при смене формы */ - transition: height 0.35s ease; - overflow: hidden; -} - -/* Появление карточки / контента */ -.card-in { - animation: cardIn .45s cubic-bezier(.34,1.56,.64,1) both; -} -@keyframes cardIn { - from { opacity: 0; transform: scale(.92) translateY(30px); } - to { opacity: 1; transform: scale(1) translateY(0); } -} - -/* Уход карточки при переключении */ -.card-out { - animation: cardOut .3s cubic-bezier(.4,0,.6,1) both; -} -@keyframes cardOut { - from { opacity: 1; transform: scale(1) translateY(0); } - to { opacity: 0; transform: scale(.94) translateY(-20px); } -} - -/* Контент формы — появляется снизу при смене */ -.form-body { - animation: formIn .4s cubic-bezier(.34,1.56,.64,1) both; -} -@keyframes formIn { - from { opacity: 0; transform: translateY(22px); } - to { opacity: 1; transform: translateY(0); } -} - -/* Переход между шагами регистрации */ -@keyframes stepIn { - from { opacity: 0; transform: translateX(18px) scale(.98); } - to { opacity: 1; transform: translateX(0) scale(1); } -} - -/* ─── Logo ──────────────────────────────────────────────────────────────────── */ -.logo-wrap { text-align: center; margin-bottom: 2px; } - -@keyframes logoPulse { - 0%, 100% { filter: drop-shadow(0 0 18px rgba(45,224,200,.45)); } - 50% { filter: drop-shadow(0 0 38px rgba(45,224,200,.8)); } -} -/* Apply this class to your MagnifierLogo svg if needed */ -.logo-pulse { animation: logoPulse 3s ease-in-out infinite; } - -/* ─── Typography ────────────────────────────────────────────────────────────── */ -.auth-title { - text-align: center; color: #fff; - font-size: 2rem; font-weight: 900; - letter-spacing: -0.5px; margin: 12px 0 0; -} -.form-subtitle { - text-align: center; color: rgba(255,255,255,0.35); - font-size: 0.83rem; margin: 4px 0 20px; -} - -/* ─── Step dots ─────────────────────────────────────────────────────────────── */ -.step-dots { - display: flex; align-items: center; justify-content: center; - gap: 6px; margin-bottom: 18px; -} -.dot { - width: 8px; height: 8px; border-radius: 50%; - background: rgba(255,255,255,0.15); - transition: all 0.35s cubic-bezier(.34,1.56,.64,1); -} -.dot-active { - width: 26px; border-radius: 4px; - background: #2de0c8; -} -.dot-done { - background: rgba(45,224,200,0.4); -} - -/* ─── Fields ────────────────────────────────────────────────────────────────── */ -.field-wrap { margin-bottom: 12px; } - -.field-label { - display: block; color: rgba(255,255,255,0.45); - font-size: 0.73rem; font-weight: 700; - letter-spacing: 0.6px; margin-bottom: 5px; - text-transform: uppercase; font-family: 'Onest', sans-serif; -} - -.field { - width: 100%; box-sizing: border-box; - background: rgba(255,255,255,0.055); - border: 1.5px solid rgba(255,255,255,0.09); - border-radius: 14px; padding: 13px 18px; - color: #fff; font-family: 'Onest', sans-serif; - font-size: 0.93rem; outline: none; - transition: border-color .2s, background .2s, box-shadow .2s; -} -.field:focus { - border-color: #2de0c8; - background: rgba(45,224,200,.07); - box-shadow: 0 0 0 3px rgba(45,224,200,.12); -} -.field::placeholder { color: rgba(255,255,255,.32); } - -.field-err { border-color: rgba(255,107,107,0.55) !important; box-shadow: 0 0 0 3px rgba(255,107,107,0.09) !important; } -.field-ok { border-color: rgba(45,224,200,0.45) !important; } - -.eye-btn { - position: absolute; right: 13px; top: 50%; - transform: translateY(-50%); - background: none; border: none; cursor: pointer; - color: rgba(255,255,255,0.35); font-size: 1.05rem; line-height: 1; - padding: 0; -} - -.input-wrap { - position: relative; -} - -.input-wrap .eye-btn { - position: absolute; - right: 13px; - top: 50%; - transform: translateY(-50%); -} - -.input-wrap .field { - padding-right: 44px; -} - -/* ─── Password strength ──────────────────────────────────────────────────────── */ -.strength-bar { display: flex; gap: 4px; margin-top: 7px; } -.seg { - flex: 1; height: 3px; border-radius: 2px; - background: rgba(255,255,255,0.1); - transition: background 0.3s; -} -.sw { background: #ff6b6b; } -.sm { background: #ffd166; } -.ss { background: #2de0c8; } -.strength-label { font-size: 0.72rem; text-align: right; margin-top: 3px; } - -/* ─── Messages ──────────────────────────────────────────────────────────────── */ -.err-msg { - color: #ff6b6b; font-size: 0.8rem; - margin-top: 5px; padding-left: 2px; - font-family: 'Onest', sans-serif; -} - -/* ─── Confirm box ───────────────────────────────────────────────────────────── */ -.confirm-box { - background: rgba(255,255,255,0.04); - border: 1px solid rgba(255,255,255,0.08); - border-radius: 18px; padding: 16px 20px; margin-bottom: 14px; -} -.confirm-row { display: flex; justify-content: space-between; align-items: center; } -.confirm-label { color: rgba(255,255,255,0.4); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.5px; } -.confirm-val { color: #fff; font-weight: 700; } -.confirm-val.accent { color: #2de0c8; } -.confirm-divider { height: 1px; background: rgba(255,255,255,0.06); margin: 10px 0; } - -/* ─── Buttons ───────────────────────────────────────────────────────────────── */ -.btn-primary { - width: 100%; - background: linear-gradient(90deg, #1fc9b0, #2de0c8, #1fc9b0); - background-size: 200% 100%; - border: none; border-radius: 14px; padding: 15px; - color: #082320; font-family: 'Onest', sans-serif; - font-size: 1.05rem; font-weight: 900; cursor: pointer; - margin-top: 6px; - box-shadow: 0 6px 24px rgba(45,224,200,0.32); - animation: shimmer 3s linear infinite; - transition: opacity .15s, transform .15s; -} -.btn-primary:hover:not(:disabled) { opacity: .88; transform: translateY(-2px); } -.btn-primary:active:not(:disabled) { transform: translateY(0); } -.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } - -@keyframes shimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } -} - -.ghost-btn { - display: block; width: 100%; text-align: center; - color: #2de0c8; font-size: 0.87rem; margin-top: 13px; - background: none; border: none; cursor: pointer; - font-family: 'Onest', sans-serif; - transition: opacity .15s; -} -.ghost-btn:hover { opacity: 0.75; } - -.back-btn { - display: block; width: 100%; text-align: center; - color: rgba(255,255,255,0.35); font-size: 0.85rem; margin-top: 11px; - background: none; border: none; cursor: pointer; - font-family: 'Onest', sans-serif; - transition: color .15s; -} -.back-btn:hover { color: rgba(255,255,255,0.6); } - -/* ─── Switch row ────────────────────────────────────────────────────────────── */ -.switch-row { - text-align: center; color: rgba(255,255,255,0.4); - font-size: 0.87rem; margin-top: 13px; -} -.link-btn { - color: #2de0c8; background: none; border: none; - cursor: pointer; font-family: 'Onest', sans-serif; - font-size: inherit; font-weight: 700; padding: 0; - transition: opacity .15s; -} -.link-btn:hover { opacity: 0.75; text-decoration: underline; } - -/* ─── Dots btn ──────────────────────────────────────────────────────────────── */ -.dots-btn { - position: absolute; top: 18px; right: 20px; - background: none; border: none; - color: rgba(255,255,255,0.25); font-size: 1.3rem; - cursor: pointer; letter-spacing: 3px; -} - -/* ─── Success ───────────────────────────────────────────────────────────────── */ -.success-wrap { text-align: center; padding: 16px 0 8px; } -.success-icon { - font-size: 3.8rem; display: block; margin-bottom: 14px; - animation: pop .55s cubic-bezier(.34,1.56,.64,1) both; -} -@keyframes pop { - from { transform: scale(0) rotate(-20deg); opacity: 0; } - to { transform: scale(1) rotate(0deg); opacity: 1; } -} -.success-title { color: #2de0c8; font-size: 1.45rem; font-weight: 900; margin-bottom: 6px; } -.success-sub { color: rgba(255,255,255,0.4); font-size: 0.88rem; line-height: 1.5; } - -.success-wrap { - display: flex; - flex-direction: column; - align-items: center; -} - -.success-icon { - background-color: #2de0c8; - -webkit-mask: url("../assets/success.png") no-repeat center; - mask: url("../assets/success.png") no-repeat center; -} - -.logoSvg { - width: 130px; - height: 130px; - animation: logoPulse 3s ease-in-out infinite; -} - -.auth-modal { - width: 420px; -} - -.auth-form-error { - margin-bottom: 10px; -} - -.auth-submit-btn { - margin-top: 6px; -} - -.auth-row { - text-align: center; - margin-top: 14px; - color: rgba(255,255,255,.45); - font-size: .85rem -} \ No newline at end of file diff --git a/src/styles/global.css b/src/styles/global.css deleted file mode 100644 index 20c0103..0000000 --- a/src/styles/global.css +++ /dev/null @@ -1,54 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Onest:wght@400;500;600;700;800;900&display=swap'); - -.fi { - position: fixed; - opacity: 0.15; - filter: grayscale(1) brightness(2); - pointer-events: none; - user-select: none; - animation: float 7s ease-in-out infinite; -} -@keyframes float { - 0%, 100% { transform: translateY(0) rotate(-6deg); } - 50% { transform: translateY(-18px) rotate(6deg); } -} - -.card-in { - animation: cardIn .7s cubic-bezier(.34,1.56,.64,1) both; -} -@keyframes cardIn { - from { opacity: 0; transform: scale(.86) translateY(40px); } - to { opacity: 1; transform: scale(1) translateY(0); } -} - -@keyframes logoPulse { - 0%, 100% { filter: drop-shadow(0 0 18px rgba(var(--teal-rgb), .45)); } - 50% { filter: drop-shadow(0 0 38px rgba(var(--teal-rgb), .8)); } -} - -@keyframes shimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } -} - -.li:focus { - border-color: var(--teal) !important; - background: rgba(var(--teal-rgb), .07) !important; - box-shadow: 0 0 0 3px rgba(var(--teal-rgb), .12) !important; -} -.li::placeholder { color: rgba(255,255,255,.32); } - -.btn-l:hover { opacity: .88; transform: translateY(-2px); transition: all .15s; } -.btn-l:active { transform: translateY(0); } - -.soc:hover { - background: rgba(255,255,255,.1) !important; - border-color: rgba(255,255,255,.22) !important; - transform: translateY(-1px); - transition: all .2s; -} - -.div-line::before, .div-line::after { - content: ''; flex: 1; - height: 1px; background: rgba(255,255,255,.08); -} \ No newline at end of file diff --git a/src/utils/adSession.js b/src/utils/adSession.js index 5346fc0..41ca323 100644 --- a/src/utils/adSession.js +++ b/src/utils/adSession.js @@ -12,10 +12,23 @@ function generateAdSessionId() { } export function getAdSessionId() { - let sessionId = localStorage.getItem(AD_SESSION_KEY); - if (!sessionId) { - sessionId = generateAdSessionId(); + // localStorage бросает в приватном режиме Safari и при отключённом + // хранилище. Здесь это ломало не только трекинг, но и сам клик по рекламе + // (id читается до window.open), поэтому падение деградируем до временного id. + let persisted = null; + try { + persisted = localStorage.getItem(AD_SESSION_KEY); + } catch { + persisted = null; + } + + if (persisted) return persisted; + + const sessionId = generateAdSessionId(); + try { localStorage.setItem(AD_SESSION_KEY, sessionId); + } catch { + // хранилище недоступно — отдаём id без persist } return sessionId; } \ No newline at end of file diff --git a/src/utils/formatPhone.js b/src/utils/formatPhone.js index d7f102b..1ef0689 100644 --- a/src/utils/formatPhone.js +++ b/src/utils/formatPhone.js @@ -14,7 +14,10 @@ export function formatPhone(val) { : digits; let result = "+7"; if (local.length > 0) result += " (" + local.slice(0, 3); + // Скобку закрываем сразу же: иначе при 1–2 цифрах поле показывало + // «+7 (9» / «+7 (91» и каретка прыгала при каждом нажатии. if (local.length >= 3) result += ") " + local.slice(3, 6); + else if (local.length > 0) result += ")"; if (local.length >= 6) result += "-" + local.slice(6, 8); if (local.length >= 8) result += "-" + local.slice(8, 10); return result; diff --git a/src/utils/isMobileDevice.js b/src/utils/isMobileDevice.js deleted file mode 100644 index dc0d926..0000000 --- a/src/utils/isMobileDevice.js +++ /dev/null @@ -1,3 +0,0 @@ -export function isMobileDevice() { - return /Mobi|Android|iPhone/i.test(navigator.userAgent); -} \ No newline at end of file diff --git a/src/utils/validatePostForm.js b/src/utils/validatePostForm.js index 599d145..3329c78 100644 --- a/src/utils/validatePostForm.js +++ b/src/utils/validatePostForm.js @@ -2,14 +2,20 @@ import i18n from '../i18n'; export function validatePostForm(form) { const errors = {}; - if (!form.title.trim()) errors.title = i18n.t('validation.titleRequired'); - if (!form.description.trim()) errors.description = i18n.t('validation.descriptionRequired'); - if (!form.category) errors.category = i18n.t('validation.categoryRequired'); + // Поля нормализуем через ??: форма могла прийти без ключа (например, из + // нового вызывающего кода), и .trim() на undefined уронил бы форму + // с TypeError вместо нормального сообщения валидации. + const title = (form.title ?? "").trim(); + const description = (form.description ?? "").trim(); + + if (!title) errors.title = i18n.t('validation.titleRequired'); + if (!description) errors.description = i18n.t('validation.descriptionRequired'); + if (!form.category) errors.category = i18n.t('validation.categoryRequired'); // Телефон обязателен для объявления (в отличие от профиля) — проверяем // количество цифр, а не просто пустоту, т.к. form.phone форматируется // через formatPhone и всегда содержит минимум "+7". - if (form.phone.replace(/\D/g, "").length < 11) { + if ((form.phone ?? "").replace(/\D/g, "").length < 11) { errors.phone = i18n.t('validation.phoneRequired'); }