{"id":19831468,"url":"https://github.com/notacat1/compass-3d-courses-web","last_synced_at":"2026-02-07T06:02:21.852Z","repository":{"id":219175040,"uuid":"748323913","full_name":"NotACat1/COMPASS-3D-courses-WEB","owner":"NotACat1","description":"Website for the Compass 3D courses","archived":false,"fork":false,"pushed_at":"2024-07-09T10:34:30.000Z","size":12368,"stargazers_count":0,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-29T22:31:33.146Z","etag":null,"topics":["eslint","htmlhint","husky","normalize-css","prettier","react","react-markdown","redux","scss","stylelint"],"latest_commit_sha":null,"homepage":"https://notacat1.github.io/COMPASS-3D-courses-WEB/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NotACat1.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-01-25T18:33:07.000Z","updated_at":"2024-07-09T10:34:34.000Z","dependencies_parsed_at":"2024-11-12T11:42:57.597Z","dependency_job_id":null,"html_url":"https://github.com/NotACat1/COMPASS-3D-courses-WEB","commit_stats":null,"previous_names":["notacat1/compass-3d-courses-web"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/NotACat1/COMPASS-3D-courses-WEB","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NotACat1%2FCOMPASS-3D-courses-WEB","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NotACat1%2FCOMPASS-3D-courses-WEB/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NotACat1%2FCOMPASS-3D-courses-WEB/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NotACat1%2FCOMPASS-3D-courses-WEB/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NotACat1","download_url":"https://codeload.github.com/NotACat1/COMPASS-3D-courses-WEB/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NotACat1%2FCOMPASS-3D-courses-WEB/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29187938,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-07T05:07:31.176Z","status":"ssl_error","status_checked_at":"2026-02-07T05:06:15.227Z","response_time":63,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["eslint","htmlhint","husky","normalize-css","prettier","react","react-markdown","redux","scss","stylelint"],"created_at":"2024-11-12T11:32:05.311Z","updated_at":"2026-02-07T06:02:21.824Z","avatar_url":"https://github.com/NotACat1.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Домашняя страница](https://github.com/NotACat1/COMPASS-3D-courses-WEB/assets/113008873/3717b89b-0cec-4f66-8f84-3a6a0d661b23)\n\n![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge\u0026logo=react\u0026logoColor=%2361DAFB)\n![Redux](https://img.shields.io/badge/redux-%23593d88.svg?style=for-the-badge\u0026logo=redux\u0026logoColor=white)\n![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge\u0026logo=SASS\u0026logoColor=white)\n![Markdown](https://img.shields.io/badge/markdown-%23000000.svg?style=for-the-badge\u0026logo=markdown\u0026logoColor=white)\n![ESLint](https://img.shields.io/badge/ESLint-4B3263?style=for-the-badge\u0026logo=eslint\u0026logoColor=white)\n![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge\u0026logo=javascript\u0026logoColor=%23F7DF1E)\n\n# Веб-сайт по курсам \"Компас 3D\"\n\nДобро пожаловать в веб-платформу по обучению использованию программы \"Компас 3D\". Наш проект призван предоставить пользователям полноценный доступ к образовательным курсам, материалам и ресурсам, специально разработанным для усвоения навыков работы с программой \"Компас 3D\".\n\n## Основная цель\n\n**Цель нашего веб-сайта - сделать процесс изучения \"Компас 3D\" удобным, эффективным и интересным**. Мы предоставляем структурированные курсы и обучающие материалы, которые позволяют пользователям освоить основы и продвинутые возможности этой мощной программы для 3D-моделирования.\n\n## Инструкция по установке и сборке\n\nЭта инструкция предназначена для разработчиков и технических специалистов, которые хотят установить и собрать проект \"Веб-сайт по курсам \"Компас 3D\"\" на своем локальном устройстве.\n\n### Предварительные требования\n\n1. **Node.js и npm:** убедитесь, что на вашем компьютере установлен `Node.js` и `npm`. Вы можете скачать их с [официального сайта Node.js](https://nodejs.org/).\n2. **Git:** установите `Git` для клонирования репозитория и управления версиями. Вы можете загрузить его с [официального сайта Git](https://git-scm.com/).\n\n### Установка\n\n1. **Клонирование репозитория:** в терминале выполните команду для клонирования репозитория на вашу локальную машину:\n\n```bash\ngit clone https://github.com/NotACat1/COMPASS-3D-courses.git\n```\n\n2. **Переход в директорию проекта:** перейдите в директорию проекта:\n\n```bash\ncd ваш-путь-к-репозиторию/COMPASS-3D-courses\n```\n\n3. **Установка зависимостей:** выполните команду для установки зависимостей проекта:\n\n```bash\nnpm install\n```\n\n### Сборка и Запуск\n\n1. **Запуск локального сервера:** для запуска локального сервера разработки выполните:\n\n```bash\nnpm start\n```\n\nЭто запустит приложение и откроет его в вашем браузере по адресу [http://localhost:3000](http://localhost:3000).\n\n2. **Сборка проекта:** если вы готовы развернуть проект на сервере, выполните команду для создания оптимизированной для продакшена сборки:\n\n```bash\nnpm run build\n```\n\nСборка будет размещена в папке `build`.\n\n### Дополнительные команды\n\n1. **Развертывание на GitHub Pages:** если вам нужно развернуть проект на `GitHub Pages`, используйте команду:\n\n```bash\nnpm run deploy\n```\n\nЭто предполагает, что у вас есть репозиторий на `GitHub` и вы настроили его для использования `GitHub Pages`.\n\n2. **Установка Git Hooks:** для установки `git hooks` (`Husky`) и необходимых прав выполните:\n\n```bash\nnpm run husky-inst\n```\n\nЭта команда устанавливает и настраивает `Husky` для автоматизации проверок при коммите.\n\nТеперь у вас должны быть удовлетворены все предварительные требования, и вы готовы устанавливать, разрабатывать и разворачивать проект по курсам \"Компас 3D\".\n\n## Файловая система проекта\n\n```css\n./\n  .husky/                    // Папка для настроек Husky (pre-commit и commit-msg hooks)\n    commit-msg               // Скрипт для проверки сообщения коммита\n    pre-commit               // Скрипт для запуска перед коммитом\n\n  build/                     // Папка для сборки проекта (появится после выполнения команды сборки)\n\n  public/                    // Папка для статических ресурсов, доступных напрямую из браузера\n    apple-touch-icon.png     // Иконка для устройств Apple с Retina-экранами\n    favicon.ico              // Иконка, отображаемая во вкладке браузера\n    icon-192.png             // Иконка размером 192x192 пикселя (используется для PWA)\n    icon-512.png             // Иконка размером 512x512 пикселей (используется для PWA)\n    icon.png                 // Основная иконка приложения\n    index.html               // Главная HTML-страница приложения\n    logo192.png              // Логотип размером 192x192 пикселя (используется для PWA)\n    logo512.png              // Логотип размером 512x512 пикселей (используется для PWA)\n    manifest.json            // Файл манифеста для Progressive Web App (PWA)\n    robots.txt               // Файл, используемый для указания правил поисковым роботам\n    tableau.png              // Пример изображения для закладок браузеров\n\n  src/                       // Исходный код проекта\n    assets/                  // Ресурсы проекта\n      fonts/                 // Шрифты проекта\n      images/                // Изображения и другие медиафайлы\n\n    components/              // Компоненты приложения\n      app/                   // Основной компонент приложения\n      block-image/           // Компонент блока с изображением\n      block-link/            // Компонент блока с ссылкой\n      block-table/           // Компонент блока с таблицей\n      error-display/         // Компонент отображения ошибок\n      footer/                // Компонент подвала страницы\n      header/                // Компонент шапки страницы\n      loader/                // Компонент загрузки данных\n      modal/                 // Компонент модального окна\n      modal-overlay/         // Компонент оверлея для модальных окон\n      module/                // Компонент модуля\n      module-info/           // Компонент информации о модуле\n      svg-close/             // Компонент SVG-иконки \"закрыть\"\n      svg-error/             // Компонент SVG-иконки \"ошибка\"\n      svg-home/              // Компонент SVG-иконки \"домой\"\n      svg-modules/           // Компонент SVG-иконки \"модули\"\n      svg-progress/          // Компонент SVG-иконки \"прогресс\"\n      svg-status/            // Компонент SVG-иконки \"статус урока\"\n      svg-theme/             // Компонент SVG-иконки \"тема\"\n      theme-switcher/        // Компонент переключателя темы\n\n    pages/                   // Страницы приложения\n      error/                 // Страница отображения ошибок\n      home/                  // Главная страница\n      lesson/                // Страница отображения урока\n      root/                  // Корневая страница\n\n    scss/                    // Файлы стилей проекта\n      mixins/                // SCSS-миксины\n      varibles/              // Переменные для стилей\n\n    services/                // Сервисы приложения\n      actions/               // Действия (actions) Redux\n      middleware/            // Middleware для Redux\n      reducers/              // Редукторы (reducers) Redux\n      types/                 // Типы данных для Redux\n      store.js               // Конфигурация Redux Store\n\n    utils/                   // Утилиты и вспомогательные функции\n      ApiService.js          // Класс для работы с API\n      constants.js           // Константы проекта\n      LocalStorageManager.js // Управление LocalStorage\n      ThemeManager.js        // Управление темой приложения\n      UserProgressManager.js // Управление прогрессией студентов\n\n    index.js                 // Точка входа в приложение\n    index.scss               // Главный файл стилей приложения\n    reportWebVitals.js       // Отчет о веб-виталах\n    setupTests.js            // Настройка тестов\n\n  .editorconfig              // Конфигурация редактора кода, обеспечивающая единообразие стиля кодирования\n  .eslintrc.json             // Конфигурация ESLint для статического анализа кода\n  .gitignore                 // Игнорируемые файлы и директории для Git\n  .prettierrc.json           // Конфигурация Prettier для форматирования кода\n  .stylelintrc.json          // Конфигурация Stylelint для проверки стилей CSS/SCSS\n  commitlint.config.js       // Конфигурация Commitlint для проверки сообщений коммитов\n  htmlhint.json              // Конфигурация HTMLHint для статической проверки HTML\n  package-lock.json          // Фиксированные версии зависимостей (генерируется автоматически)\n  package.json               // Файл с метаданными проекта и зависимостями\n  README.md                  // Файл с описанием проекта, инструкциями по установке и использованию\n```\n\n## Реализованный функционал\n\n### 1. Кнопка смены темы\n\nДобавлена интерактивная кнопка, позволяющая пользователям переключаться между светлой и темной темами. Реализован механизм сохранения выбранной темы в `LocalStorage` для сохранения пользовательских предпочтений даже после перезагрузки страницы.\n\n![Светлая и темная тема](https://github.com/NotACat1/COMPASS-3D-courses-WEB/assets/113008873/9a6c7e07-fe3d-4f74-8eeb-c98a890f69cc)\n\n### 2. Адаптивный дизайн\n\nВеб-сайт обладает адаптивным дизайном, что означает оптимальное отображение и удобство использования на устройствах различных размеров, включая компьютеры, планшеты и смартфоны.\n\n![Адаптивный дизайн](https://github.com/NotACat1/COMPASS-3D-courses-WEB/assets/113008873/0a0683f1-2e5f-4751-b7c1-54032fc0e9a4)\n\n### 3. Модальные окна\n\nИспользуются модальные окна для удобного отображения дополнительной информации. В частности, модальные окна открываются при просмотре списка уроков модуля и при просмотре изображений, обеспечивая удобное взаимодействие пользователя с контентом.\n\n![Модальные окна](https://github.com/NotACat1/COMPASS-3D-courses-WEB/assets/113008873/4b3f2251-9e9a-4d43-85d4-a4ca0680588b)\n\n### 4. Удобный объект по работе с LocalStorage\n\nСоздан удобный объект, который предоставляет абстракцию для взаимодействия с `LocalStorage`. Этот объект обеспечивает простой доступ к методам чтения, записи и удаления данных, упрощая работу с локальным хранилищем.\n\n```js\n// src/utils/LocalStorageManager.js\n\nconst LocalStorageManager = {\n  // Добавление данных в Local Storage\n  setItem: (key, value) =\u003e {\n    try {\n      localStorage.setItem(key, JSON.stringify(value));\n      return true; // Успешно добавлено\n    } catch (error) {\n      console.error('Error настройки элемента в Local Storage:', error);\n      return false; // Ошибка при добавлении\n    }\n  },\n\n  // Получение данных из Local Storage\n  getItem: key =\u003e {\n    try {\n      const item = localStorage.getItem(key);\n      return item ? JSON.parse(item) : null;\n    } catch (error) {\n      console.error('Error при получении элемента из Local Storage:', error);\n      return null;\n    }\n  },\n\n  // Удаление данных из Local Storage\n  removeItem: key =\u003e {\n    try {\n      localStorage.removeItem(key);\n      return true; // Успешно удалено\n    } catch (error) {\n      console.error('Error при удалении элемента из Local Storage:', error);\n      return false; // Ошибка при удалении\n    }\n  },\n};\n```\n\n#### Описание методов\n\n1. `setItem(key, value)`: добавляет данные в `Local Storage`. Принимает ключ (`key`) и значение (`value`). Данные конвертируются в `JSON-строку` перед сохранением. Возвращает `true` при успешном добавлении и `false` в случае ошибки.\n2. `getItem(key)`: получает данные из `Local Storage` по ключу. Если данные существуют, они распаковываются из `JSON`. Возвращает полученные данные или `null` в случае ошибки.\n3. `removeItem(key)`: удаляет данные из `Local Storage` по ключу. Возвращает `true` при успешном удалении и `false` в случае ошибки.\n\n### 5. Удобный класс по работе со сменой темы\n\nРазработан класс, предоставляющий удобные методы для смены темы оформления веб-сайта. Это включает в себя функционал изменения стилей, сохранение выбранной темы и мгновенное применение изменений.\n\n```js\n// src/utils/ThemeManager.js\n\nclass ThemeManager {\n  constructor(\n    localStorageManager = LocalStorageManager,\n    themes = THEMES,\n    storageKey = LOCAL_STORAGE.theme,\n    darkThemeClass = CLASS_DARK_THEME,\n  ) {\n    // Используем переданные аргументы или значения по умолчанию\n    this.localStorageManager = localStorageManager;\n    this.themes = themes;\n    this.storageKey = storageKey;\n    this.darkThemeClass = darkThemeClass;\n  }\n\n  // Инициализация темы при загрузке страницы\n  initTheme() {\n    // Получаем сохраненную тему из локального хранилища\n    const storedTheme = this.localStorageManager.getItem(this.storageKey);\n    // Определяем начальную тему или используем светлую тему по умолчанию\n    const initialTheme = this.themes[storedTheme] || this.themes.light;\n\n    // Устанавливаем тему\n    this.setTheme(initialTheme);\n\n    // Возвращаем начальную тему\n    return initialTheme;\n  }\n\n  // Переключение между светлой и темной темой\n  toggleTheme() {\n    // Определяем текущую тему\n    const currentTheme = document.body.classList.contains(this.darkThemeClass)\n      ? this.themes.dark\n      : this.themes.light;\n    // Определяем следующую тему для переключения\n    const nextTheme =\n      currentTheme === this.themes.light ? this.themes.dark : this.themes.light;\n\n    // Устанавливаем класс темы для body\n    document.body.className =\n      nextTheme === this.themes.dark ? this.darkThemeClass : '';\n    // Сохраняем тему в локальное хранилище\n    this.localStorageManager.setItem(this.storageKey, nextTheme);\n\n    // Возвращаем следующую тему\n    return nextTheme;\n  }\n\n  // Установка конкретной темы\n  setTheme(theme) {\n    // Устанавливаем класс темы для body\n    document.body.className =\n      theme === this.themes.dark ? this.darkThemeClass : '';\n    // Сохраняем тему в локальное хранилище\n    this.localStorageManager.setItem(this.storageKey, theme);\n  }\n\n  // Получение текущей темы\n  getTheme(defaultTheme = this.themes.light) {\n    // Получаем сохраненную тему из локального хранилища или используем тему по умолчанию\n    return this.localStorageManager.getItem(this.storageKey) || defaultTheme;\n  }\n}\n```\n\n#### Описание методов\n\n1. `initTheme()`: инициализирует тему при загрузке страницы. Получает сохраненную тему из локального хранилища, устанавливает начальную тему и возвращает ее.\n2. `toggleTheme()`: переключает между светлой и темной темой. Определяет текущую тему, определяет следующую тему для переключения, устанавливает соответствующий класс темы для `body` и сохраняет тему в локальное хранилище.\n3. `setTheme(theme)`: устанавливает конкретную тему. Устанавливает соответствующий класс темы для `body` и сохраняет тему в локальное хранилище.\n4. `getTheme(defaultTheme)`: получает текущую тему. Получает сохраненную тему из локального хранилища или использует тему по умолчанию, если сохраненная тема отсутствует.\n\n### 6. Хранение прогрессии студентов в LocalStorage\n\nРеализован механизм хранения прогрессии студентов в `LocalStorage`. Это позволяет сохранять достижения студентов, например, пройденные уроки или выполненные задания, даже после закрытия браузера.\n\n```js\n// src/utils/UserProgressManager.js\n\nclass UserProgressManager {\n  // Конструктор класса\n  constructor(\n    localStorageManager = LocalStorageManager,\n    storageKey = LOCAL_STORAGE.userProgress,\n  ) {\n    // Используем переданные аргументы или значения по умолчанию\n    this.localStorageManager = localStorageManager; // Менеджер локального хранилища\n    this.storageKey = storageKey; // Ключ хранения данных прогресса пользователя\n    this.userProgress = this.loadProgress(); // Загружаем прогресс пользователя из локального хранилища\n  }\n\n  // Метод для загрузки прогресса пользователя из локального хранилища\n  loadProgress() {\n    const progressData = this.localStorageManager.getItem(this.storageKey);\n    return progressData ? progressData : {}; // Возвращаем объект прогресса или пустой объект\n  }\n\n  // Метод для сохранения прогресса пользователя в локальное хранилище\n  saveProgress() {\n    this.localStorageManager.setItem(this.storageKey, this.userProgress);\n  }\n\n  // Метод для обновления прогресса пользователя при прохождении урока\n  updateProgress(moduleId, lessonId) {\n    // Если нет записи для модуля, создаем массив\n    if (!this.userProgress[moduleId]) {\n      this.userProgress[moduleId] = [];\n    }\n\n    // Если урок еще не отмечен как пройденный, добавляем его и сохраняем прогресс\n    if (!this.userProgress[moduleId].includes(lessonId)) {\n      this.userProgress[moduleId].push(lessonId);\n      this.saveProgress();\n    }\n  }\n\n  // Метод для получения прогресса пользователя по конкретному модулю\n  getProgressForModule(moduleId) {\n    return this.userProgress[moduleId] || []; // Возвращаем массив пройденных уроков для модуля\n  }\n}\n```\n\n#### Описание методов\n\n1. `loadProgress()`: метод для загрузки прогресса пользователя из локального хранилища. Возвращает объект прогресса, если он есть, или пустой объект.\n2. `saveProgress()` метод для сохранения текущего прогресса пользователя в локальное хранилище. Использует `JSON-сериализацию`.\n3. `updateProgress(moduleId, lessonId)`: метод для обновления прогресса пользователя при прохождении урока. Проверяет, есть ли запись для модуля, создает массив, если необходимо, и добавляет урок в массив, если его там еще нет. Затем вызывает `saveProgress()` для сохранения изменений.\n4. `getProgressForModule(moduleId)`: метод для получения прогресса пользователя по конкретному модулю. Возвращает массив пройденных уроков для данного модуля или пустой массив, если прогресса нет.\n\n### 7. Удобный класс по работе с API\n\nКласс `ApiService` предназначен для удобного взаимодействия с `API` и выполнения `HTTP-запросов`. Он содержит методы для получения данных по модулям и урокам в рамках курсов \"Компас 3D\".\n\n```js\n// src/utils/ApiService.js\n\nclass ApiService {\n  constructor(url) {\n    this._baseUrl = url;\n  }\n\n  // Обработка ответа от сервера\n  _checkResponseStatus(response) {\n    if (!response.ok) {\n      throw new Response('', { status: response.status });\n    }\n  }\n\n  // Получение данных по модулям\n  async getModules() {\n    try {\n      const response = await fetch(`${this._baseUrl}/index.json`);\n      this._checkResponseStatus(response);\n      const modulesData = await response.json();\n      return modulesData;\n    } catch (error) {\n      // Обработка ошибки при получении данных по модулям\n      throw new Response('', { status: error.status });\n    }\n  }\n\n  // Получение данных по уроку в рамках модуля\n  async getLesson(module, lesson) {\n    try {\n      const response = await fetch(`${this._baseUrl}/${module}/${lesson}`);\n      this._checkResponseStatus(response);\n      const lessonData = await response.text();\n      return lessonData;\n    } catch (error) {\n      // Обработка ошибки при получении данных по уроку\n      throw new Response('', { status: error.status });\n    }\n  }\n}\n```\n\n#### Описание методов\n\n1. `_checkResponseStatus(response)`: приватный метод для проверки статуса ответа от сервера. Если статус не является успешным (не в диапазоне 200-299), генерируется исключение.\n2. `getModules()`: метод для получения данных по модулям. Выполняет запрос к `API`, обрабатывает ответ и возвращает данные в формате `JSON`.\n   `getLesson(module, lesson)`: метод для получения данных по уроку в рамках модуля. Отправляет запрос к `API`, обрабатывает ответ и возвращает данные в виде текста.\n\n### 8. Использование React Router Dom v6\n\nДля организации навигации в веб-приложении используется библиотека `React Router Dom` в версии 6. Это обеспечивает удобство работы с маршрутами и обеспечивает плавные переходы между страницами.\n\n```js\n// src/components/app/app.utils.js\n\nconst router = createHashRouter([\n  {\n    path: '/',\n    loader: rootLoader,\n    element: \u003cRootPage /\u003e,\n    errorElement: \u003cErrorPage /\u003e,\n    children: [\n      {\n        index: true,\n        element: \u003cHomePage /\u003e,\n      },\n      {\n        path: 'lesson/:moduleId/:lessonId',\n        loader: lessonLoader,\n        element: \u003cLessonPage /\u003e,\n        errorElement: \u003cErrorPage /\u003e,\n      },\n    ],\n  },\n]);\n```\n\n### 9. Redux хранилище и связь с LocalStorage\n\n1. **Reducer для управления состоянием модулей**: `reducer` управляет состоянием, связанным с данными модулей. В данном случае, обрабатывает действие `UPDATE_MODULES`, обновляя список модулей в хранилище.\n\n```js\n// src/services/reducers/modules.js\n\n// Начальное состояние хранилища\nconst initialState = {\n  modules: [],\n};\n\n// Редуктор для управления состоянием хранилища\nconst modulesReducer = (state = initialState, { type, payload }) =\u003e {\n  // Обработка различных действий\n  switch (type) {\n    case UPDATE_MODULES: {\n      return {\n        ...state,\n        modules: [...payload],\n      };\n    }\n    // Если действие не определено, возвращаем текущее состояние\n    default: {\n      return state;\n    }\n  }\n};\n```\n\n2. **Reducer для управления состоянием пользователя**: `reducer` управляет состоянием пользователя. Обрабатывает действия обновления прогресса (`UPDATE_PROGRESS`) и темы (`UPDATE_THEME`). Инициализирует начальное состояние с данными, загруженными из `UserProgressManager` и `ThemeManager`.\n\n```js\n// src/services/reducers/user.js\n\n// Начальное состояние хранилища\nconst initialState = {\n  progress: UserProgressManager.loadProgress(),\n  theme: ThemeManager.initTheme(),\n};\n\n// Редуктор для управления состоянием хранилища\nconst userReducer = (state = initialState, { type, payload }) =\u003e {\n  // Обработка различных действий\n  switch (type) {\n    case UPDATE_PROGRESS: {\n      const { module, lesson } = payload;\n      return {\n        ...state,\n        progress: {\n          ...state.progress,\n          [module]: [...(state.progress[module] || []), lesson],\n        },\n      };\n    }\n    case UPDATE_THEME: {\n      return {\n        ...state,\n        theme: ThemeManager.getTheme(),\n      };\n    }\n    // Если действие не определено, возвращаем текущее состояние\n    default: {\n      return state;\n    }\n  }\n};\n```\n\n3. **Корневой Reducer**: корневой редюсер объединяет все редюсеры приложения - `modulesReducer` и `userReducer`.\n\n```js\n// src/services/reducers/index.js\n\n// Корневой редюсер, объединяющий все редюсеры\nexport const rootReducer = combineReducers({\n  modulesData: modulesReducer,\n  userData: userReducer,\n});\n```\n\n4. **Middleware для управления состоянием пользователя**: `middleware`, предназначенный для управления состоянием пользователя. Обрабатывает действия обновления прогресса и темы, вызывая соответствующие методы из `UserProgressManager` и `ThemeManager`.\n\n```js\n// src/services/middleware/userMiddleware.js\n\n// Middleware для управления состоянием пользователя\nconst userMiddleware = store =\u003e next =\u003e action =\u003e {\n  // Передача действия следующему обработчику в цепочке middleware\n  next(action);\n\n  // Обработка действия обновления прогресса\n  if (action.type === UPDATE_PROGRESS) {\n    // Вызываем метод updateProgress из UserProgressManager с передачей данных о модуле и уроке\n    UserProgressManager.updateProgress(\n      action.payload.module,\n      action.payload.lesson,\n    );\n  }\n\n  // Обработка действия обновления темы\n  if (action.type === UPDATE_THEME) {\n    // Вызываем метод toggleTheme из ThemeManager для переключения темы\n    ThemeManager.toggleTheme();\n  }\n};\n```\n\n#### Связь с LocalStorage\n\n- В редукторе `userReducer` происходит загрузка прогресса пользователя и темы из `LocalStorage` через `UserProgressManager.loadProgress()` и `ThemeManager.initTheme()` соответственно.\n- В `middleware` `userMiddleware` обновление прогресса пользователя вызывает метод `UserProgressManager.updateProgress`, который также обновляет данные в `LocalStorage`.\n- В `middleware` `userMiddleware` обновление темы вызывает метод `ThemeManager.toggleTheme`, который также обновляет тему в `LocalStorage`.\n\nТакая структура обеспечивает согласованность данных в хранилище `Redux` и `LocalStorage`, позволяя приложению сохранять данные пользователя между сеансами использования.\n\n### 10. Стильсистема с помощью SCSS\n\nСтруктура стилей в проекте основана на модульности и использовании `SCSS` для удобного оформления и поддержки. Файловая система `SCSS` разделена на несколько директорий:\n\n- **mixins**: содержит миксины, предназначенные для управления различными стилями, такими как ширина блока, медиазапросы, сброс свойств и другие.\n- **variables**: здесь определены переменные для использования в стилях. Это включает в себя переменные для радиуса скругления углов, контрольных точек медиазапросов, максимальной ширины контейнера, шрифтов и другие.\n\n```bash\n/src\n  /scss\n    /mixins\n      _container.scss   // Определение максимальной ширины блока в зависимости от контрольных точек\n      _media.scss       // Определение медиазапросов в зависимости от контрольных точек\n      _reset.scss       // Сброс CSS свойств\n      _scrollbar.scss   // Стилизация полосы прокрутки\n      _typography.scss  // Стили для текстового оформления\n    /variables\n      _border-radius.scss   // Определение переменных для радиуса скругления углов\n      _breakpoints.scss     // Определение контрольных точек для медиазапросов\n      _container.scss       // Определение максимальной ширины контейнера\n      _fonts.scss           // Определение переменных для шрифтов\n      _opacity.scss         // Определение переменных для прозрачности\n      _transitions.scss     // Определение переменных для переходов (анимаций)\n      _z-index.scss         // Определение переменных для уровней z-index\n```\n\nКомбинация миксинов и переменных обеспечивает гибкость и удобство в поддержке стилей приложения. Каждый файл имеет свою определенную роль, что облегчает чтение, понимание и модификацию стилей при необходимости.\n\nЭтот функционал создает приятное и интуитивно понятное взаимодействие пользователя с веб-сайтом, обеспечивая одновременно удобство, функциональность и персонализированный опыт.\n\n## Скрипты\n\nВ файле `package.json` определены следующие скрипты для управления различными аспектами проекта:\n\n```json\n\"scripts\": {\n  \"start\": \"react-scripts start\",\n  \"build\": \"react-scripts build\",\n  \"test\": \"react-scripts test\",\n  \"eject\": \"react-scripts eject\",\n  \"predeploy\": \"npm run build\",\n  \"deploy\": \"gh-pages -d build\",\n  \"husky-inst\": \"npx husky install \u0026\u0026 chmod +x .husky/pre-commit .husky/commit-msg\",\n  \"msg-commit\": \"npx commitlint -E HUSKY_GIT_PARAMS\",\n  \"pre-commit\": \"npx lint-staged\"\n}\n```\n\n1. **start:** запускает локальный сервер для разработки. Вам нужно выполнить `npm start`, чтобы начать разработку и просмотреть результат в браузере.\n2. **build:** создает оптимизированную для продакшена сборку проекта. Команда `npm run build` генерирует статические файлы, готовые к развертыванию на хостинге.\n3. **test:** запускает тесты. Эта команда (`npm test`) используется для выполнения тестов, которые обеспечивают стабильность функционала.\n4. **eject:** извлекает конфигурации и скрипты из Create React App, предоставляя полный контроль над конфигурацией проекта.\n5. **predeploy:** cкрипт, выполняющий предварительные действия перед развертыванием, в данном случае, выполняет сборку проекта.\n6. **deploy:** публикует сборку на GitHub Pages. Команда `npm run deploy` использует `gh-pages` для автоматической публикации содержимого папки `build` на GitHub Pages.\n7. **husky-inst:** устанавливает и настраивает `Husky`, инструмент для предотвращения плохих коммитов, и делает необходимые скрипты исполняемыми.\n8. **msg-commit:** проверяет сообщение коммита с использованием `commitlint`. Этот скрипт вызывается при фиксации изменений и обеспечивает соблюдение структуры коммит-сообщений.\n9. **pre-commit:** запускает предварительные проверки перед коммитом с использованием `lint-staged`. Этот скрипт автоматически проверяет измененные файлы на соответствие стандартам.\n\nЭти скрипты помогают автоматизировать и облегчить различные этапы разработки, обеспечивая при этом высокое качество и согласованность в проекте.\n\n## Страницы проекта\n\n### Страница ошибки\n\nСтраница ошибки (\"Error\") представляет собой компонент, отвечающий за отображение информации об ошибке. Эта страница может быть использована для информирования пользователя о различных ситуациях, когда что-то идет не так в приложении.\n\n![Страница ошибки](https://github.com/NotACat1/COMPASS-3D-courses-WEB/assets/113008873/8eeee1f4-5525-4c8f-bfd7-ae2bbdcd32d9)\n\n```jsx\n// src/pages/error/error.jsx\n\n// Компонент страницы ошибки\nexport default function ErrorPage() {\n  // Получаем информацию об ошибке из хука\n  const error = useRouteError();\n\n  // Создаем объект с данными об ошибке с использованием useMemo для оптимизации\n  const errorData = useMemo(() =\u003e {\n    // Проверяем наличие текста статуса ошибки\n    if (error.status) {\n      const data =\n        statusSpecificErrorMessages[error.status] ||\n        defaultErrorStatusMessages[Math.floor(error.status / 100) * 100];\n      return {\n        code: error.status,\n        ...data,\n      };\n    }\n    return null;\n  }, [error.statusText]);\n\n  // Рендер компонента отображения ошибки с переданными данными\n  return \u003cErrorDisplay data={errorData} /\u003e;\n}\n```\n\n_Комментарии_:\n\n- Хук `useRouteError`: используется для получения информации об ошибке из маршрута (роута) страницы.\n- Хук `useMemo`: применяется для оптимизации создания объекта с данными об ошибке. Предотвращает избыточные вычисления при изменении зависимостей.\n- Объект `errorData`: создается с использованием данных из хука об ошибке. Включает код ошибки (`code`) и текстовое описание ошибки, определенное в соответствии с `HTTP-статусом`.\n  Рендер компонента `ErrorDisplay`: используется компонент `ErrorDisplay` для отображения информации об ошибке. Передаются данные об ошибке для корректного отображения пользователю.\n\n### Домашняя страница\n\nДомашняя страница (\"Home\") предназначена для отображения информации об ошибках, которые могут возникнуть в процессе работы приложения. Она предоставляет пользователю информацию о возможных причинах ошибки и, при необходимости, указания по ее устранению.\n\n![Домашняя страница](https://github.com/NotACat1/COMPASS-3D-courses-WEB/assets/113008873/3717b89b-0cec-4f66-8f84-3a6a0d661b23)\n\n```jsx\n// Компонент главной страницы\nexport default function HomePage() {\n  const allModules = useSelector(state =\u003e state.modulesData.modules); // Получаем массив модулей из состояния Redux\n  const userProgress = useSelector(state =\u003e state.userData.progress); // Получаем прогресс пользователя из состояния Redux\n\n  // Мемоизированный прогресс для оптимизации рендеринга\n  const memoizedProgress = useMemo(\n    () =\u003e calculateProgress(allModules, userProgress),\n    [allModules, userProgress],\n  );\n\n  return (\n    \u003c\u003e\n      {/* Верхняя часть страницы */}\n      \u003cdiv className={styles.header}\u003e\n        \u003ch1 className={styles.title}\u003e\n          Основы КОМПАС-3D{' '}\n          {/* Компонент отображения прогресса с передачей стилей и значений */}\n          \u003cSvgProgress\n            extraClass={styles.progress}\n            extraTrackClass={styles.progress__track}\n            extraBarClass={styles.progress__bar}\n            progress={memoizedProgress}\n          /\u003e {memoizedProgress}% {/* Отображение числового значения прогресса */}\n        \u003c/h1\u003e\n      \u003c/div\u003e\n\n      {/* Секция с содержанием */}\n      \u003csection className={styles.content}\u003e\n        \u003ch2 className={styles.content__title}\u003eПрограмма курса\u003c/h2\u003e\n        {/* Отображение списка модулей */}\n        \u003cul className={styles.content__list}\u003e\n          {allModules.map(moduleData =\u003e (\n            \u003cModule key={moduleData.id} data={moduleData} /\u003e\n          ))}\n        \u003c/ul\u003e\n      \u003c/section\u003e\n    \u003c/\u003e\n  );\n}\n```\n\n### Страница урока\n\nСтраница урока (\"Lesson\") представляет собой страницу отображения конкретного урока в рамках выбранного модуля. Страница разработана для предоставления пользователю учебного материала и позволяет им взаимодействовать с контентом, а также обеспечивает возможность навигации между уроками и модулями.\n\n![Страница урока](https://github.com/NotACat1/COMPASS-3D-courses-WEB/assets/113008873/549c0083-1343-47b0-bd61-bad48e066ba4)\n\n#### Загрузчик данных (`loader`)\n\nОтвечает за загрузку данных урока с использованием `API`. Извлекает параметры модуля и урока из запроса, а затем возвращает данные урока, идентификатор модуля и урока.\n\n```jsx\n// src\\pages\\lesson\\lesson.jsx\n\nexport async function loader({ params }) {\n  try {\n    // Извлечение параметров модуля и урока из запроса\n    const moduleId = params.moduleId;\n    const lessonId = params.lessonId;\n\n    // Получение данных урока с использованием API\n    const lesson = await Api.getLesson(moduleId, lessonId);\n\n    // Возвращение данных урока, идентификатора модуля и урока\n    return { lesson, moduleId, lessonId };\n  } catch (error) {\n    // Обработка ошибки и создание кастомного объекта Response\n    throw new Response('', { status: error.status });\n  }\n}\n```\n\n#### Компонент страницы урока (`LessonPage`)\n\nОтображает содержимое урока, а также предоставляет пользователю возможность перейти к следующему уроку или модулю. Содержит следующие ключевые элементы:\n\n- `handleClick`: обработчик события клика, который отправляет действие обновления прогресса в `Redux`.\n- `useLoaderData`: хук, используемый для получения данных урока, идентификатора модуля и урока.\n- `getNextLink`: функция для определения следующей ссылки в зависимости от текущего состояния (является ли текущий модуль или урок последним).\n- `title` и `path`: данные следующей ссылки, используемые для отображения кнопки перехода.\n- `Markdown`: компонент для рендеринга текста урока с использованием синтаксиса `Markdown`.\n- `Link`: компонент для создания ссылки, обеспечивает навигацию на следующую страницу.\n- Дополнительные проверки и вычисления: определение текущего модуля, урока, индексов в массивах, а также определение, является ли текущий модуль или урок последним.\n\n```jsx\n// src\\pages\\lesson\\lesson.jsx\n\nexport default function LessonPage() {\n  const dispatch = useDispatch();\n\n  // Получение данных урока, идентификатора модуля и урока с использованием хука useLoaderData\n  const { lesson, moduleId, lessonId } = useLoaderData();\n\n  const handleClick = () =\u003e {\n    // Отправка действия обновления прогресса в редукс\n    dispatch(updateModules(moduleId, lessonId));\n  };\n\n  // Получение данных о модулях из состояния Redux\n  const modulesData = useSelector(state =\u003e state.modulesData.modules);\n\n  // Поиск текущего модуля по идентификатору\n  const currentModule = modulesData.find(module =\u003e module.id === moduleId);\n\n  // Получение массива уроков текущего модуля\n  const moduleLessons = currentModule?.lessons || [];\n\n  // Поиск текущего урока по идентификатору\n  const currentLesson = moduleLessons.find(lesson =\u003e lesson.id === lessonId);\n\n  // Получение индексов текущего модуля и урока в соответствующих массивах\n  const indexCurrentModule = modulesData.indexOf(currentModule);\n  const indexCurrentLesson = moduleLessons.indexOf(currentLesson);\n\n  // Определение является ли текущий модуль последним и является ли текущий урок последним в модуле\n  const isLastModule =\n    indexCurrentModule \u0026\u0026 indexCurrentModule === modulesData.length - 1;\n  const isLastLesson =\n    indexCurrentLesson \u0026\u0026 indexCurrentLesson === moduleLessons.length - 1;\n\n  // Функция для определения следующей ссылки в зависимости от текущего состояния\n  const getNextLink = () =\u003e {\n    if (isLastModule \u0026\u0026 isLastLesson) {\n      return { title: 'Вернуться на главную', path: '/' };\n    }\n\n    const nextModule = modulesData[indexCurrentModule + 1];\n\n    if (isLastLesson) {\n      // Если текущий урок последний, перейти к следующему модулю\n      return {\n        title: 'К следующему модулю',\n        path:\n          nextModule.id \u0026\u0026 nextModule.lessons[0].id\n            ? `../../${nextModule.id}/${nextModule.lessons[0].id}`\n            : '/',\n      };\n    }\n\n    const nextLesson = moduleLessons[indexCurrentLesson + 1];\n\n    // Если есть следующий урок в рамках текущего модуля, перейти к нему\n    return {\n      title: 'К следующему уроку',\n      path: nextLesson.id ? `../${nextLesson.id}` : '/',\n    };\n  };\n\n  // Получение данных следующей ссылки\n  const { title, path } = getNextLink();\n\n  // Возвращение разметки страницы урока\n  return (\n    \u003csection\u003e\n      {/* Отображение содержимого урока */}\n      \u003cMarkdown\n        className={styles.content}\n        remarkPlugins={[remarkGfm]}\n        components={{\n          img: BlockImage,\n          a: BlockLink,\n        }}\n      \u003e\n        {lesson}\n      \u003c/Markdown\u003e\n\n      {/* Ссылка для перехода на следующую страницу */}\n      \u003cLink\n        onClick={handleClick}\n        className={styles.link}\n        to={path}\n        relative=\"path\"\n      \u003e\n        {title}\n      \u003c/Link\u003e\n    \u003c/section\u003e\n  );\n}\n```\n\n### Корневая страница\n\nКорневая страница (\"Root\") представляет собой корневую страницу приложения, на которой отображается основное содержимое приложения. Она служит в качестве точки входа для пользователя и предоставляет навигацию к различным модулям и урокам.\n\n#### Загрузчик данных (`loader`)\n\n- Загрузчик данных (`loader`) представляет собой асинхронную функцию, которая выполняется при инициализации страницы.\n- В данном случае, загрузчик вызывает метод `Api.getModules()`, который получает данные по модулям из внешнего источника (например, с сервера).\n- Полученные данные по модулям передаются в объекте `{ modules }`.\n\n```jsx\n// src/pages/root/root.jsx\n\nexport async function loader() {\n  try {\n    const modules = await Api.getModules();\n    return { modules };\n  } catch (error) {\n    throw new Response('', { status: error.status });\n  }\n}\n```\n\n#### Компонент страницы урока (`RootPage`)\n\n- Компонент `RootPage` является функциональным компонентом `React`.\n- Используются хуки `useDispatch`, `useLoaderData`, `useNavigation`, и `useSelector` для управления состоянием и навигацией.\n- В компоненте реализовано обновление данных о модулях в `Redux Store` при монтировании компонента.\n- Используется маршрутизация через `Outlet` для внутреннего контента.\n- Динамически применяются стили (`styles.container` и `styles.container_lesson`) в зависимости от текущего пути.\n\n```jsx\n// src/pages/root/root.jsx\n\nexport default function RootPage() {\n  const dispatch = useDispatch();\n  const { modules } = useLoaderData();\n  const navigation = useNavigation();\n  const location = useLocation();\n\n  const allModules = useSelector(state =\u003e state.modulesData.modules);\n\n  // Обновление модулей в хранилище при монтировании компонента\n  useEffect(() =\u003e {\n    dispatch(updateModules(modules));\n  }, [dispatch]);\n\n  return (\n    \u003cdiv\n      className={`${styles.container} ${location.pathname !== '/' \u0026\u0026 styles.container_lesson}`}\n    \u003e\n      {/* Верхняя часть страницы: хедер */}\n      \u003cHeader /\u003e\n      {/* Основное содержимое страницы */}\n      \u003cmain className={styles.content}\u003e\n        {/* Маршрутизация внутреннего контента через Outlet */}\n        {(navigation.state === 'loading' || allModules.length === 0) \u0026\u0026 (\n          \u003cLoader /\u003e\n        )}\n        {navigation.state !== 'loading' \u0026\u0026 allModules.length \u003e 0 \u0026\u0026 \u003cOutlet /\u003e}\n      \u003c/main\u003e\n      {/* Нижняя часть страницы: футер */}\n      {location.pathname === '/' \u0026\u0026 \u003cFooter /\u003e}\n      \u003cThemeSwitcher /\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n## Технологии\n\nПроект \"Веб-сайт по курсам Компас 3D\" использует современные технологии для обеспечения эффективности, интерактивности и стильного дизайна. Вот подробное описание используемых технологий:\n\n### Основные библиотеки и фреймворки\n\n1. **React:**\n   - **Описание:** `React` - это `JavaScript-библиотека` для разработки пользовательских интерфейсов. Она позволяет создавать динамичные и отзывчивые веб-приложения.\n   - **Применение в проекте:** `React` используется для построения пользовательского интерфейса веб-сайта, обеспечивая быстрое реагирование на действия пользователя.\n2. **Redux:**\n   - **Описание:** `Redux` - это библиотека для управления состоянием приложения в `JavaScript`. Она обеспечивает предсказуемость и структурированность в управлении данными.\n   - **Применение в проекте:** `Redux` используется для эффективного управления состоянием приложения, особенно в контексте обучающих курсов.\n3. **SCSS:**\n   - **Описание:** `Sass` (Syntactically Awesome Stylesheets) - это препроцессор стилей, расширяющий возможности обычного `CSS`.\n   - **Применение в проекте:** `SCSS` применяется для удобного и структурированного написания стилей, обеспечивая легкость поддержки и модификации.\n4. **react-markdown:**\n   - **Описание:** Библиотека `react-markdown` предоставляет возможность встраивать и отображать `Markdown-разметку` в `React-приложениях`.\n   - **Применение в проекте:** `react-markdown` используется для визуализации контента в формате `Markdown`, что делает текстовую информацию более удобной и читаемой.\n\n### Инструменты для контроля качества кода\n\n1. **ESLint:**\n   - **Описание:** `ESLint` - это инструмент для статического анализа кода `JavaScript` с целью выявления и исправления паттернов программирования.\n   - **Применение в проекте:** `ESLint` используется для поддержания стандартов кодирования и выявления потенциальных проблем в `JavaScript-коде`.\n2. **Prettier:**\n   - **Описание:** `Prettier` - это инструмент форматирования кода, который автоматически приводит код к установленным стандартам.\n   - **Применение в проекте:** `Prettier` используется для автоматического форматирования кода, что обеспечивает единообразие и читаемость.\n3. **Stylelint:**\n   - **Описание:** `Stylelint` - это линтер для стилей, который проверяет `CSS/SCSS` на соответствие правилам написания кода.\n   - **Применение в проекте:** `Stylelint` используется для поддержания качества стилей и соблюдения стандартов написания.\n4. **HTMLHint:**\n   - **Описание:** `HTMLHint` - это инструмент для статического анализа `HTML-кода`, написанного на `JavaScript`.\n   - **Применение в проекте:** `HTMLHint` используется для выявления и исправления потенциальных проблем в `HTML-разметке`.\n\n### Инструменты для управления коммитами\n\n1. **Husky:**\n   - **Описание:** `Husky` - это инструмент для предотвращения плохих коммитов и предварительного выполнения скриптов перед коммитом.\n   - **Применение в проекте:** `Husky` используется для автоматизации процесса контроля качества кода перед фиксацией изменений.\n2. **Commitlint:**\n   - **Описание:** `Commitlint` - это инструмент для проверки соответствия сообщений коммитов заданным конвенциям.\n   - **Применение в проекте:** `Commitlint` используется для поддержания структурированности и соблюдения конвенций в сообщениях коммитов.\n\n### Нормализация стилей\n\n1. **Normalize.css:**\n   - **Описание:** `Normalize.css` - это небольшая библиотека, которая призвана предоставить браузерам единые стили по умолчанию.\n   - **Применение в проекте:** `Normalize.css` применяется для обеспечения однородного отображения элементов на различных браузерах.\n\nЭти технологии и инструменты совместно обеспечивают высокий стандарт качества кода, эффективность разработки и удобство сопровождения веб-сайта по курсам \"Компас 3D\".\n\n## Заключение\n\nПроект является результатом командного труда и открыт для улучшений. Если у вас есть предложения, замечания или желание внести вклад, не стесняйтесь создавать `issues` и `pull requests` в репозитории.\n\nСпасибо за интерес к проекту!\n\n```ts\nconst errorData = useMemo(() =\u003e {\n  // Проверяем наличие текста статуса ошибки\n  if (error.status) {\n    const data =\n      statusSpecificErrorMessages[error.status] ||\n      defaultErrorStatusMessages[Math.floor(error.status / 100) * 100];\n    return {\n      code: error.status,\n      ...data,\n    };\n  }\n  return undefined;\n}, [error]);\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnotacat1%2Fcompass-3d-courses-web","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnotacat1%2Fcompass-3d-courses-web","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnotacat1%2Fcompass-3d-courses-web/lists"}