diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2f404fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +dist +.git +.gitignore +npm-debug.log* +yarn-error.log* +Dockerfile +docker-compose.yml +docs diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1b8ac88 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +# Ignore artifacts: +build +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4b88918 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{"printWidth":150,"tabWidth":2,"useTabs":false,"semi":true,"singleQuote":true,"trailingComma":"es5","bracketSpacing":true} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4e74300 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,23 @@ +# Repository Guidelines + +## Project Structure & Module Organization +The Vite client lives here; runtime code sits under `src`, separated by domain: `src/app` for routing/layout, `src/modules` for feature bundles, `src/providers` for cross-cutting contexts, `src/shared` for reusable UI/logic, `src/api` and `src/stores` for data access and state. Global styles and fonts live in `src/index.css` and `src/assets`. Static files go in `public`. Keep feature-specific assets colocated with their module to ease tree shaking. + +## Build, Test, and Development Commands +- `npm install` installs all workspace dependencies; rerun after lockfile updates. +- `npm run dev` starts Vite with fast refresh at http://localhost:5173 for interactive work. +- `npm run build` performs a type-check (`tsc -b`) and production bundle; fail the PR if this fails. +- `npm run lint` applies the shared ESLint config; fix all warnings before review. +- `npm run preview` serves the built `dist` bundle to mirror production. + +## Coding Style & Naming Conventions +Use TypeScript with ES modules and React 19. Prefer functional components and hooks; move shared hooks/utilities into `src/shared`. Adopt 2-space indentation, single quotes in TS/TSX, and keep components under 200 lines. Name components in PascalCase (`UserPanel`), hooks in camelCase with a `use` prefix, and files that match their default export (`src/modules/auth/LoginForm.tsx`). Format via Prettier 3.6 before committing. + +## Testing Guidelines +Automated tests are not wired yet; add Vitest + React Testing Library as coverage grows. Place specs next to implementation under `__tests__` folders and name them `*.spec.tsx`. Target >80% coverage for new modules and capture edge-case scenarios (loading, error boundaries). Until automation exists, record manual QA steps and verify critical flows via `npm run preview`. + +## Commit & Pull Request Guidelines +Use imperative subjects in the form `type(scope): summary`, e.g., `feat(profile): add avatar upload`. Reference issues/Jira IDs in the body and call out breaking changes explicitly. Pull requests should describe intent, list validation commands or screenshots, and link any related backend work. Keep diffs focused (<400 LOC) and ensure `build` and `lint` pass before requesting review. + +## Security & Configuration Tips +Do not commit `.env*` files; rely on `.env.example` to document required keys and load values through `import.meta.env`. Treat API endpoints and tokens as secrets managed by the backend. Run `npm audit` monthly and upgrade high-risk dependencies immediately. Avoid embedding credentials or irreversible IDs in client bundles. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2172aa3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS deps +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +FROM deps AS build +WORKDIR /app +COPY . . + +# Build-time API endpoint override; defaults handled in code. +ARG VITE_API_URL +ENV VITE_API_URL=${VITE_API_URL} + +RUN npm run build + +FROM nginx:1.27-alpine AS runtime +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d0fa4a0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + web: + build: + context: . + dockerfile: Dockerfile + args: + # Override at build time: VITE_API_URL=https://api.example.com docker compose up --build + VITE_API_URL: ${VITE_API_URL:-http://localhost:8080} + ports: + - '8000:80' + restart: unless-stopped diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..63e3200 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,97 @@ +# Архитектура клиентского приложения + +Документ описывает устройство React‑клиента из `src/` и взаимосвязь его модулей, чтобы упростить навигацию по проекту и подключение новых разработчиков. + +## Технологический стек + +- **React 19 + TypeScript** — основной UI‑фреймворк (`src/main.tsx`, `src/app/App.tsx`), компоненты пишутся как функции с хуками. +- **Vite** — сборка, локальный дев‑сервер и предпросмотр (`package.json` скрипты `dev`, `build`, `preview`). +- **React Router v7** — маршрутизация клиентского приложения (`src/app/router/routes.tsx`). +- **MobX + mobx-react-lite** — управление состоянием через сторы и модели (`src/stores/*`), привязка к компонентам через `observer`. +- **Material UI 7** — дизайн‑система и тема (`src/shared/theme`), включает Emotion для стилизации. +- **Zod** — валидация сетевых ответов и входных данных (`src/api/**/*.ts`). + +## Организация каталогов + +``` +src/ +├─ app/ # точка входа UI, глобальные маршруты и обёртка приложения +├─ modules/ # фичевые модули (публичный сайт, админка, разделы) +├─ shared/ # переиспользуемые UI‑компоненты, хуки, тема +├─ providers/ # React‑провайдеры, контексты +├─ stores/ # состояние MobX и доменные модели +├─ api/ # HTTP‑клиент, DTO и SDK для бэкенда +├─ assets/ # картинки и брендинговые ассеты +├─ index.css # глобальные стили +└─ main.tsx # инициализация React‑приложения +``` + +`public/` хранит статические файлы Vite, а `docs/` отведён под документацию (включая текущий файл). + +## Жизненный цикл приложения + +1. `src/main.tsx` монтирует `` в StrictMode. +2. `src/app/App.tsx` собирает корневое дерево: `` → `` → `` → ``. +3. `src/app/router/routes.tsx` объявляет все маршруты, разделяя публичные страницы (`/`, `/news`, `/services`, `/about`) и защищённую зону `/admin/dashboard` с вложенными разделами. + +Благодаря `` внутри `AdminDashboardLayout` (`src/modules/admin/components/dashboard-layout.tsx`) дочерние страницы админки получают общий chrome (AppBar, Drawer, выход из аккаунта). + +## Управление состоянием + +- `src/stores/root.tsx` инициализирует `RootStore` с коллекциями новостей, услуг и категорий услуг. Экземпляр передаётся в React‑контекст через `StoreProvider` (`src/providers/store.tsx`), а хук `useStore` (`src/shared/hooks/useStore.ts`) упрощает доступ к нему. +- Каждый доменный объект описан MobX‑моделью (например, `NewsModel` и `ServiceModel`), которая инкапсулирует нормализацию данных, вычисляемые геттеры (`href`) и методы `update` / `toJSON`. +- Коллекции (`NewsCollectionModel`, `ServicesCollectionModel`, `ServiceCategoryCollectionModel`) отвечают за загрузку данных из API, трекинг `isLoading`/`error`, пагинацию и дедупликацию через `Map`. +- В компонентах состояние наблюдается через `observer` (см. `src/modules/main/components/services.tsx`, `src/modules/services/pages/service-details.tsx`, `src/modules/admin/components/news/dashboard-news.tsx`), что гарантирует реактивность без ручных `useState`. + +## Сетевой слой и API + +- `src/api/httpClient.ts` содержит унифицированный `send` (GET/POST/PUT/DELETE), сборщик query‑параметров и работу с токеном администратора в `localStorage`. Ответы проходят через Zod‑схемы, а при ошибках выбрасывается `ApiError`. +- В `src/api/*` реализованы специализированные SDK: + - `newsApi`, `servicesApi`, `leadsApi` — публичные чтения. + - `adminApi` — все CRUD‑операции админки (авторизация, категории, услуги, новости, администрирование). +- Типы DTO и схемы (`src/api/**/types.ts`) документируют поля и позволяют IDE/TypeScript подсказывать структуры данных по всему приложению. + +## UI-слой и тема + +- Тема MUI собирается в `src/shared/theme/theme.tsx`: кастомные `colorSchemes`, `typography`, `shadows` и набор component overrides (`shared/theme/customizations/*`). Цветовая схема подключается через CSS‑переменные и переключатель режима (`ColorModeSelect`, `ColorModeIconDropdown`). +- Глобальные стили (`src/index.css`) подключают шрифты и базовые переменные. +- Переиспользуемые шапка и подвал (`src/shared/components/header.tsx`, `src/shared/components/footer.tsx`) формируют каркас публичных страниц. + +## Фичевые модули + +- **Main (`src/modules/main`)** — лендинг: блоки `Hero`, `Services`, `RecentNews`, `Features`, `Partners`, `Feedback`. Использует сторы для вывода последних услуг и новостей, а `usePageTitle` (`src/shared/hooks/usePageTitle.ts`) обновляет `document.title`. +- **Services (`src/modules/services`)** — список и детальная страница услуг. `ServiceDetailsPage` грузит данные по `slug`, отображает карточку, хлебные крошки и форматирует цену через `formatPrice`. +- **News (`src/modules/news`)** — лента с пагинацией, просмотр новости (`NewsDetailsPage`), утилита форматирования дат `formatDate`. +- **About (`src/modules/about`)** — информационная статическая страница. +- **Admin (`src/modules/admin`)** — полноценная панель управления: + - маршруты `/admin` (логин) и `/admin/dashboard/*`; + - `dashboard-layout` отвечает за адаптивную навигацию; + - разделы: новости, услуги, категории, лиды, администраторы; + - формы создания/редактирования (`components/services/service-form.tsx`, `components/news/news-create-form.tsx`, `components/service-categories/*`) используют MUI `TextField`, локальную валидацию и обращения к `adminApi`. + - раздел лидов (`components/leads/dashboard-leads.tsx`) общается с `leadsApi` напрямую и хранит состояние в локальных хуках, поскольку данных немного. + +## Поток данных и валидации + +1. Компонент фичи (например, `Services` или `AdminDashboardNews`) вызывает методы стора (`services.fetch`, `news.fetchAdmin`). +2. Стор делегирует запрос соответствующему SDK (`servicesApi`, `adminApi`) и обновляет MobX‑модели. +3. Компоненты, обёрнутые в `observer`, автоматически перерисовываются и показывают лоадеры/ошибки. +4. Формы в админке валидируют ввод на клиенте, а серверные ошибки ловятся как `ApiError` и выводятся в UI. + +Такая цепочка (компонент → стор → API → стор → компонент) делает логику прозрачной и облегчает внедрение новых сущностей. + +## Ассеты и стили + +- Брендовые изображения и логотипы лежат в `src/assets` и импортируются напрямую в компоненты (например, хедер использует `src/assets/logo.png`). +- Файлы в `public/` доступны по прямым URL и могут использоваться для метатегов или фавиконок. +- Изображения партнёров (`src/assets/partners`) подключаются только внутри лендинга, что упрощает tree-shaking. + +## Расширение проекта + +Чтобы добавить новую сущность: + +1. Создайте Zod‑схемы и API‑клиент в `src/api/`. +2. Опишите MobX‑модель + коллекцию в `src/stores/`, инициализируйте её в `RootStore`. +3. Подготовьте UI в отдельном модуле внутри `src/modules/` и подключите маршруты через `src/app/router/routes.tsx`. +4. При необходимости добавьте переиспользуемые компоненты/хуки в `src/shared`. + +Следование существующим паттернам гарантирует единообразие и упрощает поддержку сервиса. diff --git a/docs/part1-2-brief.md b/docs/part1-2-brief.md new file mode 100644 index 0000000..7c57922 --- /dev/null +++ b/docs/part1-2-brief.md @@ -0,0 +1,387 @@ +# 📘 Бриф по Главе 1 + +## Тема: Рекламно-информационный сайт предприятия + +--- + +## 🎯 Цель главы + +Определить значение рекламно-информационного сайта в деятельности организации, проанализировать предметную область и существующие решения, сформулировать требования к создаваемой системе и обосновать выбор инструментов и технологий для её реализации. + +--- + +## 🧩 Структура и содержание + +### **1.1. Введение в тему** + +Рекламно-информационные сайты играют ключевую роль в цифровой деятельности организаций. Они обеспечивают представление компании в сети, выполняют функции информирования, продвижения и коммуникации с клиентами. Основная задача таких систем — создание единого центра актуальной информации, доступного пользователям в круглосуточном режиме. + +--- + +### **1.2. Роль рекламно-информационного сайта в деятельности компании** + +* Сайт выступает инструментом маркетингового взаимодействия, каналом распространения информации и формирования имиджа. +* Обеспечивает комплексное продвижение товаров и услуг, снижает зависимость от традиционных каналов рекламы. +* Выполняет функции оперативного обновления информации, аналитики пользовательской активности и обратной связи. +* Повышает уровень доверия к компании, способствует укреплению бренда и конкурентоспособности. + +--- + +### **1.3. Анализ предметной области и существующих решений** + +* Предметная область охватывает процессы разработки веб-ресурсов, организации хранения и обработки данных, а также взаимодействие с пользователем. +* Типичные функции сайтов-аналогов: главная страница, разделы «О компании», каталог услуг, новости, формы обратной связи, адаптивная верстка. +* Современные тенденции: использование систем управления контентом (CMS), интеграция с социальными сетями, мультимедийный контент, адаптивный дизайн и персонализированные сервисы. +* Вывод: сайт должен быть информативным, интуитивно понятным и обеспечивать оперативное обновление данных при высокой производительности. + +--- + +### **1.4. Постановка задачи** + +#### **Функциональные требования:** + +* публикация сведений об организации, услугах и новостях; +* поиск по содержимому сайта; +* формы обратной связи и администрирование контента; +* централизованное хранение данных в базе. + +#### **Нефункциональные требования:** + +* корректное отображение на различных устройствах; +* безопасность и защита данных; +* высокая скорость работы и возможность масштабирования; +* удобство сопровождения и расширяемость архитектуры. + +Задача разработки заключается в создании системы, обеспечивающей удобный доступ к информации и эффективное управление содержимым со стороны организации. + +--- + +### **1.5. Обоснование выбора инструментов и технологий** + +| Компонент | Технология | Аргументация выбора | +| --------------- | --------------------------- | ----------------------------------------------------------------------------------------------- | +| **Фронтенд** | React | Модульная структура, высокая производительность, повторное использование компонентов. | +| **Бэкенд** | Kotlin (Ktor / Spring Boot) | Строгая типизация, устойчивость, поддержка асинхронных операций, интеграция с Java-экосистемой. | +| **База данных** | PostgreSQL | Реляционная модель, транзакционная целостность, масштабируемость, надёжность. | +| **Веб-сервер** | NGINX | Высокая производительность, стабильность, возможности балансировки нагрузки. | + +📌 Совокупность данных технологий обеспечивает создание надёжной, безопасной и масштабируемой системы, соответствующей современным требованиям веб-разработки. + +--- + +## 🧠 Ключевые выводы по главе + +1. Рекламно-информационный сайт является стратегически значимым элементом цифрового присутствия организации. +2. Анализ аналогичных решений позволил определить актуальные тенденции и стандарты для разработки. +3. Сформулированные требования отражают как пользовательские, так и внутренние потребности компании. +4. Выбор инструментов (React, Kotlin, PostgreSQL, NGINX) обоснован с позиций эффективности, надёжности и дальнейшего развития проекта. +5. Итогом первой главы является определение концептуальной базы и технологического фундамента для последующего проектирования системы. + +--- + +## 📄 Итоговая роль главы + +Первая глава закладывает методологическую и техническую основу дипломного проекта. +На её основе формируется архитектура системы, разрабатывается структура базы данных и описываются механизмы взаимодействия клиентской и серверной частей, которые раскрываются в последующих разделах работы. + + +Brief: Проектирование и разработка веб-приложения «BankInfo» + +(суммаризация второй главы) + +1. Архитектурная концепция + +Веб-приложение реализовано как трёхуровневая система: + +Frontend — React SPA (React 19, TypeScript, MobX, MUI, React Router, Zod) + +Backend — Ktor (Kotlin), Ktorm ORM, HikariCP, JWT + +Database — PostgreSQL 14 (схема public) + +Ключевые принципы проектирования: + +модульность и слабая связанность; + +разделение ответственности по слоям; + +REST-взаимодействие между клиентом и сервером; +* статeless-авторизация через JWT; +* строгая типизация DTO и валидируемых данных. + +--- + +## **2. Архитектура приложения** + +### **2.1. Общая схема взаимодействия** + +Поток данных: + +``` +Client (React SPA) + → REST API /api/v1/* + → Ktor Server (Routing → Controllers → Services → Repositories) + → PostgreSQL (SQL через Ktorm) + ← JSON Response +``` + +В приложении предусмотрено место под: + +* **Рисунок 2.1** — Architecture / Deployment diagram +* **Приложение А** — увеличенная схема + +--- + +### **2.2. Backend** + +Структура серверной части: + +``` +app/ +├─ Database (HikariCP) +├─ HTTP plugins +├─ Serialization (kotlinx) +├─ Security (JWT) +└─ Routing (подключение модулей) + +modules/ +├─ aНиже — **брив (brief-doc) по всей второй главе**, оформленный в Markdown. +Это краткая, структурированная, сжатая версия всей главы 2 — идеально подходит: + +* для презентации, +* для защиты диплома, +* для приложений, +* для рабочего черновика. + +Готов к копированию в отдельный файл `brief-chapter-2.md`. + +--- + +# **Brief: Проектирование и разработка веб-приложения «BankInfo»** + +*(суммаризация второй главы)* + +--- + +## **1. Архитектурная концепция** + +Веб-приложение реализовано как трёхуровневая система: + +* **Frontend** — React SPA (React 19, TypeScript, MobX, MUI, React Router, Zod) +* **Backend** — Ktor (Kotlin), Ktorm ORM, HikariCP, JWT +* **Database** — PostgreSQL 14 (схема `public`) + +Ключевые принципы проектирования: + +* модульность и слабая связанность; +* раздdmin +├─ news +├─ services +├─ serviceCategory +└─ lead +``` + +Используемые паттерны: + +* **Layered Architecture** +* **Controller → Service → Repository** +* **DTO Boundary** +* **Repository Pattern (DDD)** +* **Exception Mapping** через StatusPages +* **Dependency Injection (ручной)** +* **Stateless Auth** через JWT + +Основные технические особенности: + +* валидация slug/username/email реализована сервисами; +* eager-loading связей в услугах (JOIN в Ktorm); +* SQL-пагинация через `LIMIT/OFFSET`; +* единый формат ошибок; +* отсутствие уникальных ограничений на уровне БД по slug/email (осознанное решение). + +--- + +### **2.3. Frontend** + +Стек: + +* React 19 +* TypeScript +* MobX (`observer`, observable state tree) +* React Router v7 (nested routing) +* MUI (design system) +* Zod (schema validation) +* Vite (bundler) + +Структура каталогов: + +``` +src/ +├─ app/ (маршруты, App) +├─ modules/ (публичные страницы + админка) +├─ stores/ (MobX) +├─ api/ (httpClient + SDK + DTO + Zod) +├─ shared/ (UI-компоненты, тема) +├─ providers/ (StoreProvider, ThemeProvider) +└─ assets/ +``` + +Паттерны: + +* **Component-based architecture** +* **Separation of concerns** +* **Observer Pattern (MobX)** +* **Gateway/API Client** +* **DTO Boundary для фронтенда** +* **Layout Composition** для админки +* **Design System Architecture** (MUI) + +Поток данных: + +``` +Component → Store → API → Store → UI +``` + +Вставляется как: + +* **Рисунок 2.3** — Data Flow Diagram +* **Приложение В** — полная версия + +--- + +## **3. REST API** + +Основная структура: `/api/v1/*` + +Ключевые маршруты: + +| Endpoint | Метод | Назначение | +| ------------- | ------------------- | -------------------------- | +| `/auth/login` | POST | Авторизация администратора | +| `/news` | GET/POST/PUT/DELETE | Управление новостями | +| `/services` | GET/POST/PUT/DELETE | Услуги | +| `/categories` | GET/POST/PUT/DELETE | Категории | +| `/leads` | GET/POST | Лиды | +| `/users` | GET/POST/PUT/DELETE | Администраторы | + +Все административные endpoints защищены JWT-middleware. + +--- + +## **4. Модель данных** + +### **Основные сущности:** + +* **Admin** +* **News** +* **ServiceCategory** +* **Service** +* **Lead** + +### **Особенности БД:** + +* PostgreSQL 14.19 +* схема: `public` +* `timestamptz` для временных полей +* `varchar` дефолтной длины +* единственный FK: `services.category_id → categories.id` +* уникальность slug/username/email **не** enforced в БД (реализуется сервисами) + +### **Реализация через Ktorm:** + +* интерфейсы Entity +* объекты Table +* `Database.sequenceOf(Table)` +* сущности загружаются с категориями через JOIN (eager) +* DTO разделены на публичные и административные + +ER-диаграмма: + +* **Рисунок 2.4** — ER Diagram +* **Приложение Д** — масштабируемая версия + +--- + +## **5. UI / UX проектирование** + +Публичные страницы: + +* Главная +* Услуги +* Услуга +* Новости +* Новость +* О компании +* Контакты / форма лида + +Административная панель: + +* Dashboard +* Администраторы +* Услуги +* Категории +* Новости +* Лиды + +`sitemap`: + +* **диаграмма (Рисунок 2.5)** +* **Приложение Е** + +--- + +## **6. Реализация** + +### **Backend:** + +* безопасная авторизация JWT +* унифицированные CRUD-модули +* SQL-запросы через Ktorm +* централизованная обработка ошибок +* слабая связанность сущностей +* ручная структура БД (без миграций) + +### **Frontend:** + +* реактивность MobX +* строгая типизация API через Zod +* модульная структура pages/features/components +* единая тема оформления +* локальная валидация форм +* защищённые маршруты для админки + +--- + +## **7. Тестирование** + +### **Проверялось:** + +* корректность REST API +* авторизация и работа с JWT +* маршруты и вложенная навигация +* формы и валидация +* реактивность MobX +* стабильность CRUD-операций +* отображение данных на публичных страницах +* стабильность админ-панели + +--- + +## **8. Общий вывод** + +Система имеет следующую совокупность свойств: + +* надёжная архитектурная основа; +* модульность и расширяемость; +* современный стек технологий; +* строгие границы ответственности между слоями; +* безопасная модель аутентификации; +* предсказуемые, универсальные CRUD-процессы; +* реактивность клиентской части; +* типобезопасный сетевой слой; +* минимальная связанность базы данных; +* удобная административная панель; +* чистая архитектура backend и frontend. + +С точки зрения инженерии ПО, проект демонстрирует высокий уровень проработки, следование современным практикам и архитектурным принципам, а также готовность к дальнейшему развитию и эксплуатационному использованию + diff --git a/docs/sitemap.svg b/docs/sitemap.svg new file mode 100644 index 0000000..2ea0520 --- /dev/null +++ b/docs/sitemap.svg @@ -0,0 +1,133 @@ + + + + + + + + + Карта сайта веб-приложения + + + + + + + + + / + Главная + (основная) + + + + + + /services + Список услуг + + + + + /news + Лента новостей + + + + + /about + О компании + + + + + /contacts + Контакты / заявка + + + + + + /services/:slug + Детали услуги + + + + + /news/:slug + Новостная статья + + + + + + + + + + + + + + + + + + /admin + Логин + + + + + /admin/dashboard + Панель управления + + + + + /admin/dashboard/news + Управление новостями + + + + + /admin/dashboard/services + Управление услугами + + + + + /admin/dashboard/service-categories + Категории услуг + + + + + /admin/dashboard/leads + Заявки + + + + + /admin/dashboard/users + Администраторы + + + + + + + + + + diff --git a/docs/structure.svg b/docs/structure.svg new file mode 100644 index 0000000..a9d534c --- /dev/null +++ b/docs/structure.svg @@ -0,0 +1,57 @@ + + + + + + + + + Поток данных в клиентском приложении (Компонент → Store → + API → Store → UI) + + + + Компонент React + (UI + обработчики) + + + + MobX Store + (управление + состоянием) + + + + API Client + (fetch + Zod + валидация) + + + + Store Update + (наблюдаемые + изменения) + + + + UI Re-render + (observer + компоненты) + + + действие / намерение + + fetch-запрос + + валидированный ответ + + событие изменения + + следующее взаимодействие + \ No newline at end of file diff --git a/index.html b/index.html index a16cb6b..82a382e 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + client diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..dc0c388 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,14 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /assets/ { + expires 1y; + add_header Cache-Control "public"; + } +} diff --git a/package-lock.json b/package-lock.json index 27d2c87..04f6aea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,15 @@ "name": "client", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@fontsource/roboto": "^5.2.8", + "@mui/icons-material": "^7.3.4", + "@mui/material": "^7.3.4", + "autoprefixer": "^10.4.21", + "mobx": "^6.13.4", + "mobx-react-lite": "^4.0.7", + "postcss-nested": "^7.0.2", "react": "^19.1.1", "react-dom": "^19.1.1" }, @@ -21,16 +30,18 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", "globals": "^16.4.0", + "prettier": "3.6.2", + "react-router-dom": "^7.9.4", "typescript": "~5.9.3", "typescript-eslint": "^8.45.0", - "vite": "^7.1.7" + "vite": "^7.1.7", + "zod": "^4.1.12" } }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -87,7 +98,6 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.3", @@ -121,7 +131,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -131,7 +140,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -173,7 +181,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -183,7 +190,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -217,7 +223,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -261,11 +266,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -280,7 +293,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -299,7 +311,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -309,6 +320,160 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", @@ -908,6 +1073,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fontsource/roboto": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.8.tgz", + "integrity": "sha512-oh9g4Cg3loVMz9MWeKWfDI+ooxxG1aRVetkiKIb2ESS2rrryGecQ/y4pAj4z5A5ebyw450dYRi/c4k/I3UBhHA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -964,7 +1138,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -986,7 +1159,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -996,20 +1168,252 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.4.tgz", + "integrity": "sha512-BIktMapG3r4iXwIhYNpvk97ZfYWTreBBQTWjQKbNbzI64+ULHfYavQEX2w99aSWHS58DvXESWIgbD9adKcUOBw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.4.tgz", + "integrity": "sha512-9n6Xcq7molXWYb680N2Qx+FRW8oT6j/LXF5PZFH3ph9X/Rct0B/BlLAsFI7iL9ySI6LVLuQIVtrLiPT82R7OZw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.3.4", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz", + "integrity": "sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/core-downloads-tracker": "^7.3.4", + "@mui/system": "^7.3.3", + "@mui/types": "^7.4.7", + "@mui/utils": "^7.3.3", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.1.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.3", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.3.tgz", + "integrity": "sha512-OJM+9nj5JIyPUvsZ5ZjaeC9PfktmK+W5YaVLToLR8L0lB/DGmv1gcKE43ssNLSvpoW71Hct0necfade6+kW3zQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.3.tgz", + "integrity": "sha512-CmFxvRJIBCEaWdilhXMw/5wFJ1+FT9f3xt+m2pPXhHPeVIbBg9MnMvNSJjdALvnQJMPw8jLhrUtXmN7QAZV2fw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.3.tgz", + "integrity": "sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/private-theming": "^7.3.3", + "@mui/styled-engine": "^7.3.3", + "@mui/types": "^7.4.7", + "@mui/utils": "^7.3.3", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.7.tgz", + "integrity": "sha512-8vVje9rdEr1rY8oIkYgP+Su5Kwl6ik7O3jQ0wl78JGSmiZhRHV+vkjooGdKD8pbtZbutXFVTWQYshu2b3sG9zw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.3.tgz", + "integrity": "sha512-kwNAUh7bLZ7mRz9JZ+6qfRnnxbE4Zuc+RzXnhSpRSxjTlSTj7b4JxRLXpG+MVtPVtqks5k/XC8No1Vs3x4Z2gg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.7", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1048,6 +1452,16 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.38", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", @@ -1433,11 +1847,22 @@ "undici-types": "~7.14.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "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==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -1454,6 +1879,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", @@ -1811,6 +2245,58 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1822,7 +2308,6 @@ "version": "2.8.16", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", - "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -1856,7 +2341,6 @@ "version": "4.26.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -1891,7 +2375,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1901,7 +2384,6 @@ "version": "1.0.30001750", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -1935,6 +2417,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "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/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1969,6 +2460,41 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1984,18 +2510,28 @@ "node": ">= 8" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2016,13 +2552,31 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.235", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz", "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==", - "dev": true, "license": "ISC" }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", @@ -2069,7 +2623,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2079,7 +2632,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2354,6 +2906,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2392,6 +2950,19 @@ "dev": true, "license": "ISC" }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2407,6 +2978,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2460,6 +3040,33 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2474,7 +3081,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2497,6 +3103,27 @@ "node": ">=0.8.19" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2541,7 +3168,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2561,7 +3187,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2577,6 +3202,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2628,6 +3259,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2651,6 +3288,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2698,18 +3347,52 @@ "node": "*" } }, + "node_modules/mobx": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.15.0.tgz", + "integrity": "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + } + }, + "node_modules/mobx-react-lite": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.1.1.tgz", + "integrity": "sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2735,9 +3418,26 @@ "version": "2.0.23", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", - "dev": true, "license": "MIT" }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2792,7 +3492,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -2801,6 +3500,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2821,11 +3538,25 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2845,7 +3576,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2861,6 +3591,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2870,6 +3601,50 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-nested": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-7.0.2.tgz", + "integrity": "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2880,6 +3655,39 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2926,6 +3734,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2933,6 +3742,12 @@ "react": "^19.2.0" } }, + "node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2943,11 +3758,86 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", + "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", + "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-router": "7.9.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -3046,6 +3936,13 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true, + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3069,11 +3966,19 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -3092,6 +3997,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3105,6 +4016,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3243,7 +4166,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3280,6 +4202,21 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vite": { "version": "7.1.10", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz", @@ -3433,6 +4370,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 69eaa2c..748abf0 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,15 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@fontsource/roboto": "^5.2.8", + "@mui/icons-material": "^7.3.4", + "@mui/material": "^7.3.4", + "autoprefixer": "^10.4.21", + "mobx": "^6.13.4", + "mobx-react-lite": "^4.0.7", + "postcss-nested": "^7.0.2", "react": "^19.1.1", "react-dom": "^19.1.1" }, @@ -23,8 +32,11 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", "globals": "^16.4.0", + "prettier": "3.6.2", + "react-router-dom": "^7.9.4", "typescript": "~5.9.3", "typescript-eslint": "^8.45.0", - "vite": "^7.1.7" + "vite": "^7.1.7", + "zod": "^4.1.12" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..ea64612 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,14 @@ +/** @type {import('postcss-load-config').Config} */ +import prefix from 'autoprefixer' +import nest from 'postcss-nested' + + + +const config = { + plugins: [ + prefix, + nest + ] +} + +export default config \ No newline at end of file diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..8e045a2 Binary files /dev/null and b/public/logo.png differ diff --git a/src/api/admin/index.ts b/src/api/admin/index.ts new file mode 100644 index 0000000..a563863 --- /dev/null +++ b/src/api/admin/index.ts @@ -0,0 +1,175 @@ +import { del, post, put, request, setAdminAuthToken } from '../httpClient.ts'; +import { + adminLoginPayloadSchema, + adminLoginResponseSchema, + adminProfileSchema, + adminServiceCategoryListSchema, + adminServiceCategoryCreatePayloadSchema, + adminServiceCategoryCreateResponseSchema, + adminServiceCategoryUpdatePayloadSchema, + adminServiceCategoryUpdateResponseSchema, + adminNewsUpdatePayloadSchema, + adminNewsUpdateResponseSchema, + adminNewsCreatePayloadSchema, + adminNewsCreateResponseSchema, + adminRegisterPayloadSchema, + adminRegisterResponseSchema, + adminPasswordChangePayloadSchema, + adminPasswordChangeResponseSchema, + adminDeleteResponseSchema, + adminServiceCreatePayloadSchema, + adminServiceCreateResponseSchema, + adminServiceUpdatePayloadSchema, + adminServiceUpdateResponseSchema, + type AdminLoginPayload, + type AdminLoginResponse, + type AdminProfile, + type AdminServiceCategoryList, + type AdminServiceCategoryCreatePayload, + type AdminServiceCategoryCreateResponse, + type AdminServiceCategoryUpdatePayload, + type AdminServiceCategoryUpdateResponse, + type AdminNewsUpdatePayload, + type AdminNewsUpdateResponse, + type AdminNewsCreatePayload, + type AdminNewsCreateResponse, + type AdminRegisterPayload, + type AdminRegisterResponse, + type AdminPasswordChangePayload, + type AdminPasswordChangeResponse, + type AdminDeleteResponse, + type AdminServiceCreatePayload, + type AdminServiceCreateResponse, + type AdminServiceUpdatePayload, + type AdminServiceUpdateResponse, +} from './types.ts'; +import { type NewsPageResponse, newsPageSchema } from '../news/types.ts'; +import type { ListNewsParams } from '../news'; +import { serviceItemSchema, servicePageSchema } from '../services/types.ts'; +import type { ServiceItem } from '../services/types.ts'; +import type { ListServicesParams } from '../services'; + +const ADMIN_LOGIN_ENDPOINT = '/api/v1/admin/login'; +const ADMIN_PROFILE_ENDPOINT = '/api/v1/admin'; +const ADMIN_SERVICE_CATEGORIES_ENDPOINT = '/api/v1/admin/service-categories'; +const ADMIN_SERVICES_ENDPOINT = '/api/v1/admin/services'; +const ADMIN_NEWS_ENDPOINT = '/api/v1/admin/news'; + +const buildNewsQuery = (params?: ListNewsParams) => { + if (!params) { + return undefined; + } + const { page, limit, search, tags } = params; + return { page, limit, search, tags }; +}; + +const buildServicesQuery = (params?: ListServicesParams) => { + if (!params) { + return undefined; + } + const { page, limit, q, category, minPrice, maxPrice } = params; + return { page, limit, q, category, minPrice, maxPrice }; +}; + +export const adminApi = { + async login(payload: AdminLoginPayload): Promise { + adminLoginPayloadSchema.parse(payload); + const response = await post(ADMIN_LOGIN_ENDPOINT, adminLoginResponseSchema, { + body: payload, + }); + + setAdminAuthToken(response.token); + return response; + }, + + async profile(): Promise { + return request(ADMIN_PROFILE_ENDPOINT, adminProfileSchema); + }, + + async registerAdmin(payload: AdminRegisterPayload): Promise { + adminRegisterPayloadSchema.parse(payload); + return post(ADMIN_PROFILE_ENDPOINT, adminRegisterResponseSchema, { + body: payload, + }); + }, + + async changePassword(adminId: string | number, payload: AdminPasswordChangePayload): Promise { + adminPasswordChangePayloadSchema.parse(payload); + return put(`${ADMIN_PROFILE_ENDPOINT}/${adminId}/password`, adminPasswordChangeResponseSchema, { + body: payload, + }); + }, + + async deleteAdmin(adminId: string | number): Promise { + return del(`${ADMIN_PROFILE_ENDPOINT}/${adminId}`, adminDeleteResponseSchema); + }, + + async listServiceCategories(): Promise { + return request(ADMIN_SERVICE_CATEGORIES_ENDPOINT, adminServiceCategoryListSchema); + }, + + async createServiceCategory(payload: AdminServiceCategoryCreatePayload): Promise { + const normalized = adminServiceCategoryCreatePayloadSchema.parse(payload); + return post(ADMIN_SERVICE_CATEGORIES_ENDPOINT, adminServiceCategoryCreateResponseSchema, { + body: normalized, + }); + }, + + async updateServiceCategory( + identifier: string | number, + payload: AdminServiceCategoryUpdatePayload, + ): Promise { + const normalized = adminServiceCategoryUpdatePayloadSchema.parse(payload); + return put(`${ADMIN_SERVICE_CATEGORIES_ENDPOINT}/${identifier}`, adminServiceCategoryUpdateResponseSchema, { + body: normalized, + }); + }, + + async listAdminServices(params?: ListServicesParams) { + return request(ADMIN_SERVICES_ENDPOINT, servicePageSchema, { + query: buildServicesQuery(params), + }); + }, + + async createService(payload: AdminServiceCreatePayload): Promise { + const normalized = adminServiceCreatePayloadSchema.parse(payload); + return post(ADMIN_SERVICES_ENDPOINT, adminServiceCreateResponseSchema, { + body: normalized, + }); + }, + + async updateService(identifier: string | number, payload: AdminServiceUpdatePayload): Promise { + const normalized = adminServiceUpdatePayloadSchema.parse(payload); + return put(`${ADMIN_SERVICES_ENDPOINT}/${identifier}`, adminServiceUpdateResponseSchema, { + body: normalized, + }); + }, + + async getAdminService(identifier: string | number): Promise { + return request(`${ADMIN_SERVICES_ENDPOINT}/${identifier}`, serviceItemSchema); + }, + + async listNews(params?: ListNewsParams): Promise { + return request(ADMIN_NEWS_ENDPOINT, newsPageSchema, { + query: buildNewsQuery(params), + }); + }, + + async createNews(payload: AdminNewsCreatePayload): Promise { + adminNewsCreatePayloadSchema.parse(payload); + return post(ADMIN_NEWS_ENDPOINT, adminNewsCreateResponseSchema, { + body: payload, + }); + }, + + async updateNews(identifier: string, payload: AdminNewsUpdatePayload): Promise { + adminNewsUpdatePayloadSchema.parse(payload); + return put(`${ADMIN_NEWS_ENDPOINT}/${identifier}`, adminNewsUpdateResponseSchema, { + body: payload, + }); + }, + + logout() { + setAdminAuthToken(null); + }, +}; diff --git a/src/api/admin/types.ts b/src/api/admin/types.ts new file mode 100644 index 0000000..21b4ca9 --- /dev/null +++ b/src/api/admin/types.ts @@ -0,0 +1,181 @@ +import { z } from 'zod'; + +export const adminLoginPayloadSchema = z.object({ + username: z.string().min(1), + password: z.string().min(1), +}); + +export type AdminLoginPayload = z.infer; + +export const adminLoginResponseSchema = z.object({ + token: z.string(), +}); + +export type AdminLoginResponse = z.infer; + +export const adminProfileSchema = z.object({ + id: z.union([z.string(), z.number()]), + username: z.string(), + createdAt: z.string(), +}); + +export type AdminProfile = z.infer; + +export const adminRegisterPayloadSchema = z.object({ + username: z.string().min(3), + password: z.string().min(8), +}); + +export type AdminRegisterPayload = z.infer; + +export const adminRegisterResponseSchema = z.object({ + id: z.union([z.string(), z.number()]), + username: z.string(), +}); + +export type AdminRegisterResponse = z.infer; + +export const adminPasswordChangePayloadSchema = z.object({ + currentPassword: z.string().min(1), + newPassword: z.string().min(8), +}); + +export type AdminPasswordChangePayload = z.infer; + +export const adminPasswordChangeResponseSchema = z.object({ + updated: z.boolean(), +}); + +export type AdminPasswordChangeResponse = z.infer; + +export const adminDeleteResponseSchema = z.object({ + deleted: z.boolean(), +}); + +export type AdminDeleteResponse = z.infer; + +const optionalString = z + .string() + .trim() + .transform((value) => (value.length === 0 ? undefined : value)) + .optional(); + +const optionalNullableString = optionalString.nullable(); + +const optionalNullableNumberId = z + .union([z.string(), z.number()]) + .transform((value) => { + const numeric = Number(value); + return Number.isNaN(numeric) ? undefined : numeric; + }) + .optional() + .nullable(); + +export const adminServiceCreatePayloadSchema = z.object({ + title: z.string().min(1), + slug: z.string().min(1), + description: z.string().min(1), + priceFrom: optionalNullableString, + imageUrl: optionalNullableString, + status: optionalString, + categoryId: optionalNullableNumberId, +}); + +export type AdminServiceCreatePayload = z.infer; + +export const adminServiceCreateResponseSchema = z.object({ + id: z.union([z.string(), z.number()]), +}); + +export type AdminServiceCreateResponse = z.infer; + +export const adminServiceUpdatePayloadSchema = z.object({ + title: z.string().min(1).optional(), + slug: z.string().min(1).optional(), + description: z.string().min(1).optional(), + priceFrom: optionalNullableString, + imageUrl: optionalNullableString, + status: optionalString, + categoryId: optionalNullableNumberId, +}); + +export type AdminServiceUpdatePayload = z.infer; + +export const adminServiceUpdateResponseSchema = z.object({ + updated: z.boolean(), +}); + +export type AdminServiceUpdateResponse = z.infer; + +export const adminServiceCategorySchema = z.object({ + id: z.union([z.string(), z.number()]), + name: z.string(), + slug: z.string(), +}); + +export type AdminServiceCategory = z.infer; + +export const adminServiceCategoryListSchema = z.array(adminServiceCategorySchema); + +export type AdminServiceCategoryList = z.infer; + +export const adminServiceCategoryCreatePayloadSchema = z.object({ + name: z.string().min(1), + slug: z.string().min(1), +}); + +export type AdminServiceCategoryCreatePayload = z.infer; + +export const adminServiceCategoryCreateResponseSchema = z.object({ + id: z.union([z.string(), z.number()]), +}); + +export type AdminServiceCategoryCreateResponse = z.infer; + +export const adminServiceCategoryUpdatePayloadSchema = z.object({ + name: z.string().min(1).optional(), + slug: z.string().min(1).optional(), +}); + +export type AdminServiceCategoryUpdatePayload = z.infer; + +export const adminServiceCategoryUpdateResponseSchema = z.object({ + updated: z.boolean(), +}); + +export type AdminServiceCategoryUpdateResponse = z.infer; + +export const adminNewsUpdatePayloadSchema = z.object({ + title: z.string().min(1), + slug: z.string().min(1), + summary: z.string().min(1), + content: z.string().min(1), + imageUrl: z.string().url().optional().or(z.null()), + status: z.enum(['draft', 'published', 'archived']), +}); + +export type AdminNewsUpdatePayload = z.infer; + +export const adminNewsCreateResponseSchema = z.object({ + id: z.union([z.string(), z.number()]), +}); + +export type AdminNewsCreateResponse = z.infer; + +export const adminNewsCreatePayloadSchema = adminNewsUpdatePayloadSchema.extend({ + status: adminNewsUpdatePayloadSchema.shape.status.default('draft'), +}); + +export type AdminNewsCreatePayload = z.infer; + +export const adminNewsPublishResponseSchema = z.object({ + published: z.boolean(), +}); + +export type AdminNewsPublishResponse = z.infer; + +export const adminNewsUpdateResponseSchema = z.object({ + updated: z.boolean(), +}); + +export type AdminNewsUpdateResponse = z.infer; diff --git a/src/api/httpClient.ts b/src/api/httpClient.ts new file mode 100644 index 0000000..490bf09 --- /dev/null +++ b/src/api/httpClient.ts @@ -0,0 +1,199 @@ +import { z } from 'zod'; + +type QueryValue = string | number | boolean | undefined | null; + +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; + +export type RequestQuery = Record; + +export interface HttpRequestOptions extends Omit { + query?: RequestQuery; + body?: Record | FormData; +} + +const API_BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8080'; +const ADMIN_TOKEN_STORAGE_KEY = 'admin_token'; +const ADMIN_API_PREFIX = '/api/v1/admin'; +const isBrowserEnvironment = typeof window !== 'undefined'; + +let adminAuthToken: string | null = null; + +const loadAdminTokenFromStorage = () => { + if (!isBrowserEnvironment) { + return null; + } + + const storedToken = window.localStorage.getItem(ADMIN_TOKEN_STORAGE_KEY); + adminAuthToken = storedToken; + return storedToken; +}; + +loadAdminTokenFromStorage(); + +export const setAdminAuthToken = (token: string | null) => { + adminAuthToken = token; + + if (!isBrowserEnvironment) { + return; + } + + if (token) { + window.localStorage.setItem(ADMIN_TOKEN_STORAGE_KEY, token); + } else { + window.localStorage.removeItem(ADMIN_TOKEN_STORAGE_KEY); + } +}; + +export const getAdminAuthToken = () => adminAuthToken; + +export class ApiError extends Error { + status: number; + details?: unknown; + + constructor(message: string, status: number, details?: unknown) { + super(message); + this.name = 'ApiError'; + this.status = status; + this.details = details; + } +} + +const buildQueryString = (query?: RequestQuery) => { + if (!query) { + return ''; + } + + const params = new URLSearchParams(); + + Object.entries(query).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + + if (Array.isArray(value)) { + value.forEach((item) => { + if (item === undefined || item === null) { + return; + } + params.append(key, String(item)); + }); + return; + } + + params.append(key, String(value)); + }); + + const serialized = params.toString(); + return serialized ? `?${serialized}` : ''; +}; + +const buildUrl = (path: string, query?: RequestQuery) => { + const normalizedPath = path.startsWith('/') ? path : `/${path}`; + return `${API_BASE_URL}${normalizedPath}${buildQueryString(query)}`; +}; + +const shouldAttachAdminAuth = (path: string) => path.startsWith(ADMIN_API_PREFIX); + +const buildHeaders = (base: Record, extra?: HeadersInit) => { + const headers = new Headers(base); + + if (extra) { + const additional = new Headers(extra); + additional.forEach((value, key) => { + headers.set(key, value); + }); + } + + return headers; +}; + +const send = async ( + method: HttpMethod, + path: string, + schema: z.ZodType, + options?: HttpRequestOptions, +): Promise => { + const { query, body, headers, ...rest } = options ?? {}; + const url = buildUrl(path, query); + const hasJsonBody = body && !(body instanceof FormData); + const shouldSendBody = method !== 'GET' && method !== 'DELETE' ? true : Boolean(body); + const baseHeaders: Record = { + Accept: 'application/json', + ...(hasJsonBody ? { 'Content-Type': 'application/json' } : {}), + }; + + const requestHeaders = buildHeaders(baseHeaders, headers); + + if (shouldAttachAdminAuth(path) && adminAuthToken) { + requestHeaders.set('Authorization', `Bearer ${adminAuthToken}`); + } + + const response = await fetch(url, { + method, + ...rest, + headers: requestHeaders, + body: shouldSendBody ? (hasJsonBody ? JSON.stringify(body) : (body ?? null)) : undefined, + }); + + const contentType = response.headers.get('content-type'); + const contentLength = response.headers.get('content-length'); + const isNoContentStatus = response.status === 204 || response.status === 205; + const payloadHasBody = !isNoContentStatus && contentLength !== '0'; + + let payload: unknown = undefined; + + if (payloadHasBody) { + const rawBody = await response.text(); + if (rawBody.length > 0) { + if (contentType && contentType.includes('application/json')) { + try { + payload = JSON.parse(rawBody); + } catch { + payload = rawBody; + } + } else { + payload = rawBody; + } + } + } + + if (!response.ok) { + const serverMessage = + typeof payload === 'object' && payload !== null && 'error' in payload && typeof (payload as Record).error === 'string' + ? (payload as { error: string }).error + : null; + if (response.status === 401) { + setAdminAuthToken(null); + if (isBrowserEnvironment) { + window.location.replace('/admin'); + } + } + throw new ApiError(serverMessage ?? 'Request failed', response.status, payload); + } + + return schema.parse(payload); +}; + +export const request = async ( + path: string, + schema: z.ZodType, + options?: HttpRequestOptions, +) => send('GET', path, schema, options); + +export const post = async ( + path: string, + schema: z.ZodType, + options?: HttpRequestOptions, +) => send('POST', path, schema, options); + +export const put = async ( + path: string, + schema: z.ZodType, + options?: HttpRequestOptions, +) => send('PUT', path, schema, options); + +export const del = async ( + path: string, + schema: z.ZodType, + options?: HttpRequestOptions, +) => send('DELETE', path, schema, options); diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..12955b8 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,39 @@ +export { newsApi } from './news/index.ts'; +export type { NewsItem, NewsListResponse } from './news/types.ts'; +export { servicesApi } from './services/index.ts'; +export type { ServiceCategory, ServiceItem, ServiceListResponse } from './services/types.ts'; +export { adminApi } from './admin/index.ts'; +export type { + AdminLoginPayload, + AdminLoginResponse, + AdminProfile, + AdminServiceCategory, + AdminServiceCategoryList, + AdminServiceCategoryCreatePayload, + AdminServiceCategoryCreateResponse, + AdminServiceCategoryUpdatePayload, + AdminServiceCategoryUpdateResponse, + AdminRegisterPayload, + AdminRegisterResponse, + AdminPasswordChangePayload, + AdminPasswordChangeResponse, + AdminDeleteResponse, + AdminServiceCreatePayload, + AdminServiceCreateResponse, + AdminServiceUpdatePayload, + AdminServiceUpdateResponse, + AdminNewsUpdatePayload, + AdminNewsCreateResponse, + AdminNewsCreatePayload, + AdminNewsPublishResponse, + AdminNewsUpdateResponse, +} from './admin/types.ts'; +export { leadsApi } from './leads/index.ts'; +export type { + Lead, + LeadCreatePayload, + LeadCreateResponse, + LeadDeleteResponse, + LeadListItem, + LeadPageResponse, +} from './leads/types.ts'; diff --git a/src/api/leads/index.ts b/src/api/leads/index.ts new file mode 100644 index 0000000..888eefb --- /dev/null +++ b/src/api/leads/index.ts @@ -0,0 +1,54 @@ +import { del, post, request } from '../httpClient.ts'; +import { + leadCreatePayloadSchema, + leadCreateResponseSchema, + leadDeleteResponseSchema, + leadPageSchema, + leadSchema, + type Lead, + type LeadCreatePayload, + type LeadCreateResponse, + type LeadDeleteResponse, + type LeadPageResponse, +} from './types.ts'; + +export interface ListLeadsParams { + limit?: number; + page?: number; + q?: string; +} + +const PUBLIC_LEADS_ENDPOINT = '/api/v1/leads'; +const ADMIN_LEADS_ENDPOINT = '/api/v1/admin/leads'; + +const buildListQuery = (params?: ListLeadsParams) => { + if (!params) { + return undefined; + } + + const { limit, page, q } = params; + return { limit, page, q }; +}; + +export const leadsApi = { + async create(payload: LeadCreatePayload): Promise { + const normalized = leadCreatePayloadSchema.parse(payload); + return post(PUBLIC_LEADS_ENDPOINT, leadCreateResponseSchema, { + body: normalized, + }); + }, + + async list(params?: ListLeadsParams): Promise { + return request(ADMIN_LEADS_ENDPOINT, leadPageSchema, { + query: buildListQuery(params), + }); + }, + + async get(id: number): Promise { + return request(`${ADMIN_LEADS_ENDPOINT}/${id}`, leadSchema); + }, + + async remove(id: number): Promise { + return del(`${ADMIN_LEADS_ENDPOINT}/${id}`, leadDeleteResponseSchema); + }, +}; diff --git a/src/api/leads/types.ts b/src/api/leads/types.ts new file mode 100644 index 0000000..e1f623d --- /dev/null +++ b/src/api/leads/types.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +export const leadListItemSchema = z.object({ + id: z.number(), + fullName: z.string(), + email: z.string().email(), + phone: z.string().nullable().optional(), + createdAt: z.string().optional(), +}); + +export type LeadListItem = z.infer; + +export const leadSchema = leadListItemSchema.extend({ + createdAt: z.string(), +}); + +export type Lead = z.infer; + +export const leadPageSchema = z.object({ + items: z.array(leadListItemSchema), + total: z.number(), + limit: z.number(), + offset: z.number(), +}); + +export type LeadPageResponse = z.infer; + +export const leadCreatePayloadSchema = z.object({ + fullName: z.string().min(1), + email: z.string().email(), + phone: z.string().min(5).max(32).optional(), +}); + +export type LeadCreatePayload = z.infer; + +export const leadCreateResponseSchema = z.object({ + id: z.number(), +}); + +export type LeadCreateResponse = z.infer; + +export const leadDeleteResponseSchema = z.object({ + deleted: z.boolean(), +}); + +export type LeadDeleteResponse = z.infer; diff --git a/src/api/news/index.ts b/src/api/news/index.ts new file mode 100644 index 0000000..69c00df --- /dev/null +++ b/src/api/news/index.ts @@ -0,0 +1,36 @@ +import { request } from '../httpClient.ts'; +import { newsItemSchema, newsPageSchema, type NewsItem, type NewsPageResponse } from './types.ts'; + +export interface ListNewsParams { + page?: number; + limit?: number; + search?: string; + tags?: string[]; +} + +const NEWS_ENDPOINT = '/api/v1/news'; + +const buildQuery = (params?: ListNewsParams) => { + if (!params) { + return undefined; + } + + const { page, limit, search, tags } = params; + return { + page, + limit, + search, + tags, + }; +}; + +export const newsApi = { + async list(params?: ListNewsParams): Promise { + return request(NEWS_ENDPOINT, newsPageSchema, { + query: buildQuery(params), + }); + }, + async get(slug: string): Promise { + return request(`${NEWS_ENDPOINT}/${slug}`, newsItemSchema); + }, +}; diff --git a/src/api/news/types.ts b/src/api/news/types.ts new file mode 100644 index 0000000..4b6d30a --- /dev/null +++ b/src/api/news/types.ts @@ -0,0 +1,27 @@ +import { z } from 'zod'; + +export const newsItemSchema = z.object({ + id: z.union([z.string(), z.number()]), + title: z.string(), + slug: z.string(), + summary: z.string(), + content: z.string(), + imageUrl: z.string().url().optional().nullable(), + publishedAt: z.string().optional().nullable(), + status: z.enum(['draft', 'published', 'archived']).default('draft'), +}); + +export type NewsItem = z.infer; + +export const newsListSchema = z.array(newsItemSchema); + +export type NewsListResponse = z.infer; + +export const newsPageSchema = z.object({ + items: newsListSchema, + total: z.number(), + limit: z.number(), + offset: z.number(), +}); + +export type NewsPageResponse = z.infer; diff --git a/src/api/services/index.ts b/src/api/services/index.ts new file mode 100644 index 0000000..63c35f4 --- /dev/null +++ b/src/api/services/index.ts @@ -0,0 +1,41 @@ +import { request } from '../httpClient.ts'; +import { serviceItemSchema, servicePageSchema, type ServiceItem, type ServicePageResponse } from './types.ts'; + +export interface ListServicesParams { + limit?: number; + page?: number; + q?: string; + category?: string; + minPrice?: number; + maxPrice?: number; +} + +const SERVICES_ENDPOINT = '/api/v1/services'; + +const buildQuery = (params?: ListServicesParams) => { + if (!params) { + return undefined; + } + + const { limit, page, q, category, minPrice, maxPrice } = params; + + return { + limit, + page, + q, + category, + minPrice, + maxPrice, + }; +}; + +export const servicesApi = { + async list(params?: ListServicesParams): Promise { + return request(SERVICES_ENDPOINT, servicePageSchema, { + query: buildQuery(params), + }); + }, + async get(slug: string | number): Promise { + return request(`${SERVICES_ENDPOINT}/${slug}`, serviceItemSchema); + }, +}; diff --git a/src/api/services/types.ts b/src/api/services/types.ts new file mode 100644 index 0000000..d75afc9 --- /dev/null +++ b/src/api/services/types.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; + +export const serviceCategorySchema = z + .object({ + id: z.union([z.string(), z.number()]), + name: z.string(), + slug: z.string(), + description: z.string().optional(), + }) + .passthrough(); + +export const serviceItemSchema = z.object({ + id: z.union([z.string(), z.number()]), + title: z.string(), + slug: z.string(), + description: z.string(), + priceFrom: z.number().nullable().optional(), + imageUrl: z.string().nullable().optional(), + status: z.string(), + category: serviceCategorySchema.nullish(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +export type ServiceItem = z.infer; +export type ServiceCategory = z.infer; + +export const serviceListSchema = z.array(serviceItemSchema); + +export type ServiceListResponse = z.infer; + +export const servicePageSchema = z.object({ + items: serviceListSchema, + total: z.number(), + limit: z.number(), + offset: z.number(), +}); + +export type ServicePageResponse = z.infer; diff --git a/src/app/App.css b/src/app/App.css index b9d355d..e69de29 100644 --- a/src/app/App.css +++ b/src/app/App.css @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/app/App.tsx b/src/app/App.tsx index ce97400..429c0c8 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,35 +1,19 @@ -import { useState } from 'react' -import reactLogo from '../assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { RouterProvider } from 'react-router'; +import './App.css'; +import { router } from './router/routes.tsx'; +import { StoreProvider } from '../providers/store.tsx'; +import CssBaseline from '@mui/material/CssBaseline'; +import { AppTheme } from '../shared/theme/theme.tsx'; function App() { - const [count, setCount] = useState(0) - return ( - <> - -

Vite + React

-
- -

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

-
-

- Click on the Vite and React logos to learn more -

- - ) + + + + + + + ); } -export default App +export default App; diff --git a/src/app/router/routes.ts b/src/app/router/routes.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/router/routes.tsx b/src/app/router/routes.tsx new file mode 100644 index 0000000..afa3cae --- /dev/null +++ b/src/app/router/routes.tsx @@ -0,0 +1,67 @@ +import { createBrowserRouter, Navigate } from 'react-router-dom'; +import { MainPage } from '../../modules/main/pages'; +import { NewsDetailsPage, NewsFeedPage } from '../../modules/news/pages'; +import { ServiceDetailsPage, ServicesListPage } from '../../modules/services/pages'; +import { AdminLoginPage } from '../../modules/admin/pages/login.tsx'; +import { AdminDashboardPage } from '../../modules/admin/pages/dashboard.tsx'; +import { AdminNewsEditPage } from '../../modules/admin/pages/news-edit.tsx'; +import { AdminNewsCreatePage } from '../../modules/admin/pages/news-create.tsx'; +import { AdminDashboardNews } from '../../modules/admin/components/news/dashboard-news.tsx'; +import { AdminDashboardServices } from '../../modules/admin/components/services/dashboard-services.tsx'; +import { AdminDashboardLeads } from '../../modules/admin/components/leads/dashboard-leads.tsx'; +import { AdminDashboardAdmins } from '../../modules/admin/components/admins/dashboard-admins.tsx'; +import { AdminDashboardServiceCategories } from '../../modules/admin/components/service-categories/dashboard-service-categories.tsx'; +import { AdminServiceCreatePage } from '../../modules/admin/pages/service-create.tsx'; +import { AdminServiceEditPage } from '../../modules/admin/pages/service-edit.tsx'; +import { AdminServiceCategoryCreatePage } from '../../modules/admin/pages/service-category-create.tsx'; +import { AdminServiceCategoryEditPage } from '../../modules/admin/pages/service-category-edit.tsx'; + +export const router = createBrowserRouter([ + { + path: '/', + element: , + }, + { + path: '/news', + element: , + }, + { + path: '/news/:slug', + element: , + }, + { + path: '/about', + element: , + }, + { + path: '/services', + element: , + }, + { + path: '/services/:slug', + element: , + }, + { + path: '/admin', + element: , + }, + { + path: '/admin/dashboard', + element: , + children: [ + { index: true, element: }, + { path: 'news/new', element: }, + { path: 'news', element: }, + { path: 'news/:slug/edit', element: }, + { path: 'services/new', element: }, + { path: 'services', element: }, + { path: 'services/:serviceId/edit', element: }, + { path: 'categories/new', element: }, + { path: 'categories', element: }, + { path: 'categories/:categoryId/edit', element: }, + { path: 'leads', element: }, + { path: 'admins', element: }, + ], + }, +]); +import { AboutPage } from '../../modules/about/pages/about.tsx'; diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..8e045a2 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/assets/partners/logo1.png b/src/assets/partners/logo1.png new file mode 100644 index 0000000..6ef3a82 Binary files /dev/null and b/src/assets/partners/logo1.png differ diff --git a/src/assets/partners/logo10.png b/src/assets/partners/logo10.png new file mode 100644 index 0000000..8442c56 Binary files /dev/null and b/src/assets/partners/logo10.png differ diff --git a/src/assets/partners/logo2.png b/src/assets/partners/logo2.png new file mode 100644 index 0000000..86244f4 Binary files /dev/null and b/src/assets/partners/logo2.png differ diff --git a/src/assets/partners/logo3.png b/src/assets/partners/logo3.png new file mode 100644 index 0000000..6ab88a9 Binary files /dev/null and b/src/assets/partners/logo3.png differ diff --git a/src/assets/partners/logo4.png b/src/assets/partners/logo4.png new file mode 100644 index 0000000..aa83a04 Binary files /dev/null and b/src/assets/partners/logo4.png differ diff --git a/src/assets/partners/logo5.png b/src/assets/partners/logo5.png new file mode 100644 index 0000000..944a6a8 Binary files /dev/null and b/src/assets/partners/logo5.png differ diff --git a/src/assets/partners/logo6.png b/src/assets/partners/logo6.png new file mode 100644 index 0000000..43d29a1 Binary files /dev/null and b/src/assets/partners/logo6.png differ diff --git a/src/assets/partners/logo7.png b/src/assets/partners/logo7.png new file mode 100644 index 0000000..2c5e3b5 Binary files /dev/null and b/src/assets/partners/logo7.png differ diff --git a/src/assets/partners/logo8.png b/src/assets/partners/logo8.png new file mode 100644 index 0000000..1549dc7 Binary files /dev/null and b/src/assets/partners/logo8.png differ diff --git a/src/assets/partners/logo9.png b/src/assets/partners/logo9.png new file mode 100644 index 0000000..830e25b Binary files /dev/null and b/src/assets/partners/logo9.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/index.css b/src/index.css index 08a3ac9..0b34c58 100644 --- a/src/index.css +++ b/src/index.css @@ -1,68 +1,4 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} +@import '@fontsource/roboto/300.css'; +@import '@fontsource/roboto/400.css'; +@import '@fontsource/roboto/500.css'; +@import '@fontsource/roboto/700.css'; \ No newline at end of file diff --git a/src/modules/about/pages/about.tsx b/src/modules/about/pages/about.tsx new file mode 100644 index 0000000..d2751b7 --- /dev/null +++ b/src/modules/about/pages/about.tsx @@ -0,0 +1,135 @@ +import { Fragment } from 'react'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Container from '@mui/material/Container'; +import Grid from '@mui/material/Grid'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +import { Header } from '../../../shared/components/header.tsx'; +import { Footer } from '../../../shared/components/footer.tsx'; +import { usePageTitle } from '../../../shared/hooks/usePageTitle.ts'; + +const aboutFacts = [ + { + title: 'Год основания', + value: '1841', + description: 'Сбербанк ведёт свою историю от Сберегательных касс Российской империи.', + }, + { + title: 'Генеральный директор', + value: 'Герман Греф', + description: 'Глава ПАО «Сбербанк» с 2007 года, проводит масштабную цифровую трансформацию.', + }, + { + title: 'Офисы и представительства', + value: 'Крупнейшая сеть в России и СНГ', + description: 'Более 14 тысяч отделений и присутствие в большинстве регионов страны.', + }, +]; + +const performanceMetrics = [ + { label: 'Совокупные активы', value: '≈ 45 трлн ₽', note: 'по МСФО за 2023 год' }, + { label: 'Чистая прибыль', value: '≈ 1,5 трлн ₽', note: 'рекордный результат 2023 года' }, + { label: 'Доля безналичных операций', value: '> 95%', note: 'в розничных транзакциях клиентов' }, +]; + +const strategicDirections = [ + 'Развитие экосистемы сервисов: финансы, e-commerce, телемедицина, образование и логистика.', + 'Лидерство в технологиях ИИ и машинного обучения для обслуживания клиентов и внутренней автоматизации.', + 'Ответственное развитие: снижение углеродного следа, «зелёные» ипотека и облигации, благотворительные программы.', +]; + +export const AboutPage: React.FC = () => { + usePageTitle('О компании'); + return ( + +
+ + + + + + О компании «Сбербанк» + + + Крупнейший финансовый институт России и Восточной Европы, формирующий экосистему цифровых сервисов для миллионов клиентов. + + + + + {aboutFacts.map((fact) => ( + + + + + {fact.title} + + + {fact.value} + + + {fact.description} + + + + + ))} + + + + + + Финансовые показатели + + + {performanceMetrics.map((metric) => ( + + + {metric.label} + + {metric.value} + + {metric.note} + + + ))} + + + + + + + + Стратегия и направления роста + + + {strategicDirections.map((item) => ( + + {item} + + ))} + + + + + + + + Социальная роль + + + Сбербанк активно поддерживает предпринимателей и частных клиентов, инвестирует в инфраструктуру, образование и социальные проекты. + Особое внимание уделяется доступности финансовых услуг, цифровой безопасности и развитию регионов присутствия. + + + + + + +