├── public └── prev.png ├── .gitignore ├── package.json ├── README.md ├── questions ├── html_questions.json ├── react_questions.json ├── css_questions.json └── js_questions.json └── index.js /public/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FilimonovAlexey/tech-interview-trainer/HEAD/public/prev.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | leaderboard.db 26 | 27 | /logs -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tech-interview-trainer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "date-fns": "^3.6.0", 14 | "dotenv": "^16.4.5", 15 | "grammy": "^1.23.0", 16 | "nodemon": "^3.1.0", 17 | "sqlite": "^5.1.1", 18 | "sqlite3": "^5.1.7" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Бот для Подготовки к Собеседованию на Frontend Разработчика 2 | 3 | Этот Telegram бот разработан для помощи в подготовке к собеседованиям на позицию Frontend разработчика. Бот предлагает викторины по различным категориям, таким как HTML, CSS, JavaScript и React, и предоставляет рейтинговый режим для оценки навыков пользователя. 4 | 5 | ![](./public/prev.png) 6 | 7 | ## Функциональности бота 8 | - **Старт**: Пользователь начинает взаимодействие с ботом, используя команду `/start`. Бот приветствует пользователя и предлагает выбрать категорию вопросов или включить рейтинговый режим. 9 | - **Викторины**: Пользователь выбирает одну из категорий (HTML, CSS, JavaScript, React) и отвечает на вопросы. После каждого ответа бот сообщает, правильный ли ответ, и предлагает следующий вопрос. 10 | - **Рейтинговый режим**: Пользователь отвечает на вопросы из всех категорий до первой ошибки. Количество правильных ответов фиксируется как очки, которые сохраняются в таблице лидеров. 11 | - **Таблица лидеров**: Пользователь может посмотреть топ-10 игроков, набравших наибольшее количество очков в рейтинговом режиме. 12 | - **Профиль**: Команда `/profile` позволяет пользователю просмотреть информацию о количестве правильных ответов в каждой категории, а также последний результат в рейтинговом режиме. 13 | 14 | ## Используемые технологии 15 | - **Node.js**: Серверная платформа для выполнения JavaScript-кода. 16 | - **grammy**: Модуль для создания Telegram ботов. 17 | - **sqlite**: Встраиваемая база данных для хранения результатов пользователей. 18 | - **date-fns**: Библиотека для форматирования дат и времени. 19 | - **dotenv**: Модуль для загрузки переменных окружения из `.env` файла. 20 | 21 | ## Структура проекта 22 | - `index.js` - Главный файл с логикой бота. 23 | - `questions/` - Папка с файлами вопросов по HTML, CSS, JavaScript и React. 24 | - `leaderboard.db` - Файл базы данных SQLite для хранения таблицы лидеров. 25 | 26 | ## Демо бота 27 | Обзор возможностей бота и инструкция по настройке - [Смотреть на YouTube](https://youtu.be/fzgzOgq5_ho) 28 | Опробовать бота можно в Telegram по ссылке - [@Tehnomaniak_trainer_bot](https://t.me/Tehnomaniak_trainer_bot) 29 | 30 | ## Деплой бота на сервер 31 | Видео-гайд по деплою Telegram бота на сервер - [Смотреть на YouTube](https://youtu.be/vPqAYdjkm4o) 32 | 33 | * Установим Git и обновим компоненты системы 34 | ```bash 35 | sudo apt update 36 | sudo apt install git 37 | ``` 38 | 39 | * Клонируем репозиторий с ботом на сервер: 40 | ```bash 41 | git clone https://github.com/FilimonovAlexey/tech-interview-trainer.git 42 | ``` 43 | 44 | * Переходим в папку проекта: 45 | ```bash 46 | cd tech-interview-trainer 47 | ``` 48 | 49 | * Устанавливаем Node.js и пакетный менеджер npm 50 | ```bash 51 | sudo apt install nodejs 52 | sudo apt install npm 53 | ``` 54 | 55 | * Обновим Node js и npm, после выполняем перезапуск сервера 56 | ```bash 57 | sudo npm install -g n 58 | sudo n stable 59 | ``` 60 | * Устанавливаем все зависимости 61 | ```bash 62 | cd tech-interview-trainer 63 | npm i 64 | ``` 65 | 66 | * Создаем глобальную переменную 67 | ```bash 68 | nano .env 69 | ``` 70 | 71 | * Создаем внутри файлов .env две переменные 72 | ```bash 73 | BOT_API_KEY='' 74 | ADMIN_ID='' 75 | ``` 76 | 77 | * Устанавливаем pm2 для запуска бота 78 | ```bash 79 | npm i pm2 -g 80 | ``` 81 | 82 | * Запуск бота на сервере 83 | ```bash 84 | pm2 start index.js 85 | ``` 86 | 87 | ## Документация по grammy js 88 | 89 | [Документация grammy js](https://grammy.dev/guide/) 90 | 91 | 92 | ## Authors 93 | 94 | - [@FilimonovAlexey](https://github.com/FilimonovAlexey) -------------------------------------------------------------------------------- /questions/html_questions.json: -------------------------------------------------------------------------------- 1 | { 2 | "questions": [ 3 | { 4 | "question": "Что такое тег
?", 5 | "options": ["Определение раздела документа или области с похожим содержимым", "Определение ссылки", "Определение таблицы", "Определение изображения"], 6 | "correctOption": 0 7 | }, 8 | { 9 | "question": "Какой тег используется для создания ссылки?", 10 | "options": ["", "", "
", ""], 11 | "correctOption": 1 12 | }, 13 | { 14 | "question": "Какой тег используется для создания списка?", 15 | "options": ["", "
    ", "
      ", ""], 16 | "correctOption": 2 17 | }, 18 | { 19 | "question": "Какой тег используется для вставки изображения?", 20 | "options": ["", "", "", ""], 21 | "correctOption": 1 22 | }, 23 | { 24 | "question": "Какой атрибут используется для добавления альтернативного текста к изображению?", 25 | "options": ["alt", "title", "src", "href"], 26 | "correctOption": 0 27 | }, 28 | { 29 | "question": "Какой тег используется для создания заголовка самого высокого уровня?", 30 | "options": ["

      ", "

      ", "
      ", ""], 31 | "correctOption": 0 32 | }, 33 | { 34 | "question": "Какой тег используется для создания нумерованного списка?", 35 | "options": ["<ul>", "<ol>", "<li>", "<dl>"], 36 | "correctOption": 1 37 | }, 38 | { 39 | "question": "Какой тег используется для создания абзаца?", 40 | "options": ["<br>", "<span>", "<p>", "<article>"], 41 | "correctOption": 2 42 | }, 43 | { 44 | "question": "Какой атрибут используется для задания URL-адреса ссылки?", 45 | "options": ["src", "href", "link", "url"], 46 | "correctOption": 1 47 | }, 48 | { 49 | "question": "Какой тег используется для создания строки в таблице?", 50 | "options": ["<tr>", "<td>", "<th>", "<table>"], 51 | "correctOption": 0 52 | }, 53 | { 54 | "question": "Какой тег используется для создания ячейки таблицы?", 55 | "options": ["<tr>", "<td>", "<th>", "<table>"], 56 | "correctOption": 1 57 | }, 58 | { 59 | "question": "Какой тег используется для создания выделенного текста?", 60 | "options": ["<strong>", "<b>", "<em>", "<i>"], 61 | "correctOption": 0 62 | }, 63 | { 64 | "question": "Какой тег используется для создания курсивного текста?", 65 | "options": ["<em>", "<i>", "<strong>", "<b>"], 66 | "correctOption": 1 67 | }, 68 | { 69 | "question": "Какой тег используется для создания заголовка страницы в браузере?", 70 | "options": ["<header>", "<title>", "<meta>", "<head>"], 71 | "correctOption": 1 72 | }, 73 | { 74 | "question": "Какой тег используется для создания секции документа?", 75 | "options": ["<section>", "<div>", "<article>", "<span>"], 76 | "correctOption": 0 77 | }, 78 | { 79 | "question": "Какой тег используется для создания формы?", 80 | "options": ["<input>", "<form>", "<textarea>", "<button>"], 81 | "correctOption": 1 82 | }, 83 | { 84 | "question": "Какой атрибут используется для отправки данных формы?", 85 | "options": ["action", "method", "name", "type"], 86 | "correctOption": 0 87 | }, 88 | { 89 | "question": "Какой тег используется для создания ввода в форме?", 90 | "options": ["<input>", "<form>", "<textarea>", "<button>"], 91 | "correctOption": 0 92 | }, 93 | { 94 | "question": "Какой атрибут используется для указания URL-адреса, на который отправляются данные формы?", 95 | "options": ["action", "method", "name", "type"], 96 | "correctOption": 0 97 | }, 98 | { 99 | "question": "Какой тег используется для создания выпадающего списка?", 100 | "options": ["<select>", "<option>", "<list>", "<dropdown>"], 101 | "correctOption": 0 102 | }, 103 | { 104 | "question": "Какой тег используется для создания элемента выпадающего списка?", 105 | "options": ["<select>", "<option>", "<list>", "<dropdown>"], 106 | "correctOption": 1 107 | }, 108 | { 109 | "question": "Какой тег используется для создания кнопки?", 110 | "options": ["<button>", "<input>", "<form>", "<label>"], 111 | "correctOption": 0 112 | }, 113 | { 114 | "question": "Какой тег используется для создания контейнера со строгим форматированием текста?", 115 | "options": ["<code>", "<pre>", "<samp>", "<kbd>"], 116 | "correctOption": 1 117 | }, 118 | { 119 | "question": "Какой тег используется для создания горизонтальной линии?", 120 | "options": ["<line>", "<hr>", "<br>", "<div>"], 121 | "correctOption": 1 122 | }, 123 | { 124 | "question": "Какой тег используется для создания всплывающего окна с подсказкой?", 125 | "options": ["<title>", "<tooltip>", "<hint>", "<abbr>"], 126 | "correctOption": 3 127 | }, 128 | { 129 | "question": "Какой тег используется для определения заголовка секции или документа?", 130 | "options": ["<header>", "<h1>", "<title>", "<section>"], 131 | "correctOption": 0 132 | }, 133 | { 134 | "question": "Какой тег используется для создания метаинформации о документе?", 135 | "options": ["<meta>", "<head>", "<link>", "<style>"], 136 | "correctOption": 0 137 | }, 138 | { 139 | "question": "Какой тег используется для вставки аудиофайла на страницу?", 140 | "options": ["<audio>", "<sound>", "<media>", "<voice>"], 141 | "correctOption": 0 142 | }, 143 | { 144 | "question": "Какой тег используется для вставки видеофайла на страницу?", 145 | "options": ["<video>", "<movie>", "<media>", "<clip>"], 146 | "correctOption": 0 147 | }, 148 | { 149 | "question": "Какой тег используется для создания встроенного фрейма?", 150 | "options": ["<iframe>", "<frame>", "<embed>", "<object>"], 151 | "correctOption": 0 152 | } 153 | ] 154 | } 155 | -------------------------------------------------------------------------------- /questions/react_questions.json: -------------------------------------------------------------------------------- 1 | { 2 | "questions": [ 3 | { 4 | "question": "Что из перечисленного не является хуком в React?", 5 | "options": ["useEffect()", "useState()", "useReducer()", "useScript()"], 6 | "correctOption": 3 7 | }, 8 | { 9 | "question": "Какой метод классового компонента вызывается при монтировании компонента в DOM?", 10 | "options": ["componentDidMount()", "componentWillMount()", "componentDidUpdate()", "componentWillUnmount()"], 11 | "correctOption": 0 12 | }, 13 | { 14 | "question": "Каким образом можно оптимизировать производительность функциональных компонентов в React?", 15 | "options": ["Использовать React.PureComponent", "Использовать useMemo()", "Использовать shouldComponentUpdate()", "Использовать React.Component"], 16 | "correctOption": 1 17 | }, 18 | { 19 | "question": "Что такое JSX?", 20 | "options": ["Функция", "Стилизация компонентов", "Расширение синтаксиса JavaScript", "Библиотека для тестирования"], 21 | "correctOption": 2 22 | }, 23 | { 24 | "question": "Какой метод используется для преобразования списка компонентов?", 25 | "options": ["map()", "filter()", "reduce()", "forEach()"], 26 | "correctOption": 0 27 | }, 28 | { 29 | "question": "Как передать данные от родительского компонента к дочернему?", 30 | "options": ["С помощью состояния (state)", "С помощью props", "С помощью контекста (context)", "С помощью хуков"], 31 | "correctOption": 1 32 | }, 33 | { 34 | "question": "Какой хук используется для управления состоянием компонента?", 35 | "options": ["useState()", "useEffect()", "useContext()", "useReducer()"], 36 | "correctOption": 0 37 | }, 38 | { 39 | "question": "Что такое virtual DOM?", 40 | "options": ["Копия настоящего DOM", "Объектная модель браузера", "Виртуальное представление UI", "Абстракция над компонентами"], 41 | "correctOption": 2 42 | }, 43 | { 44 | "question": "Какой метод класса используется для обновления состояния в классовом компоненте?", 45 | "options": ["this.setState()", "this.updateState()", "this.changeState()", "this.modifyState()"], 46 | "correctOption": 0 47 | }, 48 | { 49 | "question": "Что такое React.Fragment?", 50 | "options": ["Компонент для группировки дочерних элементов", "Хук для управления состоянием", "Метод жизненного цикла", "Библиотека для работы с фрагментами"], 51 | "correctOption": 0 52 | }, 53 | { 54 | "question": "Каким образом можно избежать повторного рендеринга компонента?", 55 | "options": ["Использовать React.memo", "Использовать useState()", "Использовать componentDidMount()", "Использовать useEffect()"], 56 | "correctOption": 0 57 | }, 58 | { 59 | "question": "Какой метод используется для обработки событий в React?", 60 | "options": ["addEventListener()", "handleEvent()", "onEvent()", "eventHandler()"], 61 | "correctOption": 0 62 | }, 63 | { 64 | "question": "Какой метод жизненного цикла используется для выполнения действий после обновления компонента?", 65 | "options": ["componentDidMount()", "componentWillUpdate()", "componentDidUpdate()", "componentWillUnmount()"], 66 | "correctOption": 2 67 | }, 68 | { 69 | "question": "Какой хук позволяет управлять побочными эффектами в функциональных компонентах?", 70 | "options": ["useState()", "useEffect()", "useContext()", "useReducer()"], 71 | "correctOption": 1 72 | }, 73 | { 74 | "question": "Какой метод используется для принудительного обновления компонента?", 75 | "options": ["forceUpdate()", "refresh()", "reload()", "reRender()"], 76 | "correctOption": 0 77 | }, 78 | { 79 | "question": "Что из перечисленного не является типом данных в propTypes?", 80 | "options": ["string", "number", "boolean", "symbol"], 81 | "correctOption": 3 82 | }, 83 | { 84 | "question": "Что такое Context API в React?", 85 | "options": ["Метод для изменения состояния", "Способ передачи данных через дерево компонентов", "Инструмент для тестирования", "Библиотека для управления состоянием"], 86 | "correctOption": 1 87 | }, 88 | { 89 | "question": "Какой хук используется для создания ссылки на DOM-элемент?", 90 | "options": ["useState()", "useRef()", "useEffect()", "useCallback()"], 91 | "correctOption": 1 92 | }, 93 | { 94 | "question": "Что такое HOC (Higher-Order Component)?", 95 | "options": ["Компонент, который возвращает другой компонент", "Метод жизненного цикла", "Специальный хук", "Метод для оптимизации"], 96 | "correctOption": 0 97 | }, 98 | { 99 | "question": "Какой хук используется для мемоизации функции?", 100 | "options": ["useMemo()", "useCallback()", "useRef()", "useState()"], 101 | "correctOption": 1 102 | }, 103 | { 104 | "question": "Какой хук используется для управления редюсерами состояния?", 105 | "options": ["useState()", "useEffect()", "useContext()", "useReducer()"], 106 | "correctOption": 3 107 | }, 108 | { 109 | "question": "Как можно передать методы и свойства между компонентами без использования props?", 110 | "options": ["Использовать контекст (context)", "Использовать хуки", "Использовать состояния (state)", "Использовать методы жизненного цикла"], 111 | "correctOption": 0 112 | }, 113 | { 114 | "question": "Какой метод используется для преобразования JSX в элемент React?", 115 | "options": ["ReactDOM.render()", "React.createElement()", "React.cloneElement()", "React.Component()"], 116 | "correctOption": 1 117 | }, 118 | { 119 | "question": "Что делает метод React.cloneElement()?", 120 | "options": ["Создает копию элемента с новыми props", "Клонирует весь компонент", "Удаляет элемент", "Обновляет состояние элемента"], 121 | "correctOption": 0 122 | }, 123 | { 124 | "question": "Как можно объединить несколько редюсеров в React?", 125 | "options": ["combineReducers()", "mergeReducers()", "composeReducers()", "connectReducers()"], 126 | "correctOption": 0 127 | }, 128 | { 129 | "question": "Какой метод используется для выполнения кода перед размонтированием компонента?", 130 | "options": ["componentWillUnmount()", "componentWillMount()", "componentDidMount()", "componentDidUpdate()"], 131 | "correctOption": 0 132 | }, 133 | { 134 | "question": "Что из перечисленного не является методом жизненного цикла компонента?", 135 | "options": ["componentDidMount()", "componentWillUnmount()", "componentDidCatch()", "componentWillReceiveProps()"], 136 | "correctOption": 3 137 | }, 138 | { 139 | "question": "Какой метод используется для инициализации состояния в классовом компоненте?", 140 | "options": ["constructor()", "initialize()", "initState()", "setup()"], 141 | "correctOption": 0 142 | }, 143 | { 144 | "question": "Что делает метод shouldComponentUpdate()?", 145 | "options": ["Определяет, должен ли компонент обновляться", "Обновляет состояние компонента", "Удаляет компонент", "Создает новый компонент"], 146 | "correctOption": 0 147 | }, 148 | { 149 | "question": "Какой хук используется для работы с контекстом в функциональных компонентах?", 150 | "options": ["useContext()", "useState()", "useEffect()", "useReducer()"], 151 | "correctOption": 0 152 | }, 153 | { 154 | "question": "Какой хук позволяет выполнять побочные эффекты в функциональных компонентах?", 155 | "options": ["useState()", "useEffect()", "useContext()", "useReducer()"], 156 | "correctOption": 1 157 | }, 158 | { 159 | "question": "Что делает метод React.PureComponent?", 160 | "options": ["Оптимизирует рендеринг компонентов", "Создает новый элемент", "Обновляет состояние", "Удаляет компонент"], 161 | "correctOption": 0 162 | }, 163 | { 164 | "question": "Какой метод используется для создания рефов в классовом компоненте?", 165 | "options": ["React.createRef()", "React.useRef()", "React.newRef()", "React.ref()"], 166 | "correctOption": 0 167 | }, 168 | { 169 | "question": "Какой метод используется для доступа к элементу DOM в классовом компоненте?", 170 | "options": ["this.ref", "this.dom", "this.element", "this.node"], 171 | "correctOption": 0 172 | }, 173 | { 174 | "question": "Что такое ключи (keys) в React?", 175 | "options": ["Уникальные идентификаторы для элементов списка", "Идентификаторы состояния", "Методы компонента", "Свойства компонента"], 176 | "correctOption": 0 177 | } 178 | ] 179 | } -------------------------------------------------------------------------------- /questions/css_questions.json: -------------------------------------------------------------------------------- 1 | { 2 | "questions": [ 3 | { 4 | "question": "Что делает свойство CSS 'opacity'?", 5 | "options": ["Устанавливает непрозрачность элемента", "Изменяет цвет фона", "Добавляет фильтры к элементу", "Скрывает элемент без удаления его из документа"], 6 | "correctOption": 0 7 | }, 8 | { 9 | "question": "Какое свойство CSS используется для изменения текста элемента?", 10 | "options": ["font-style", "text-transform", "text-decoration", "font-weight"], 11 | "correctOption": 1 12 | }, 13 | { 14 | "question": "Что описывает 'z-index' в CSS?", 15 | "options": ["Горизонтальное выравнивание", "Вертикальное выравнивание", "Порядок наложения элементов", "Прозрачность элементов"], 16 | "correctOption": 2 17 | }, 18 | { 19 | "question": "Какое значение по умолчанию имеет свойство 'position' в CSS?", 20 | "options": ["absolute", "relative", "fixed", "static"], 21 | "correctOption": 3 22 | }, 23 | { 24 | "question": "Какое свойство CSS используется для задания внутренних отступов элемента?", 25 | "options": ["margin", "padding", "border", "outline"], 26 | "correctOption": 1 27 | }, 28 | { 29 | "question": "Как можно сделать текст полужирным в CSS?", 30 | "options": ["font-style: bold;", "text-decoration: bold;", "font-weight: bold;", "text-transform: bold;"], 31 | "correctOption": 2 32 | }, 33 | { 34 | "question": "Что делает свойство 'display: none;' в CSS?", 35 | "options": ["Скрывает элемент и освобождает занимаемое место", "Скрывает элемент, но оставляет занимаемое место", "Делает элемент прозрачным", "Удаляет элемент из документа"], 36 | "correctOption": 0 37 | }, 38 | { 39 | "question": "Какое свойство отвечает за отображение горизонтальной полосы прокрутки?", 40 | "options": ["overflow-y", "overflow-x", "scroll-x", "scroll-y"], 41 | "correctOption": 1 42 | }, 43 | { 44 | "question": "Какое значение свойства 'position' позволяет фиксировать элемент относительно окна браузера?", 45 | "options": ["absolute", "relative", "fixed", "static"], 46 | "correctOption": 2 47 | }, 48 | { 49 | "question": "Какое значение свойства 'flex-direction' располагает элементы в столбец?", 50 | "options": ["row", "column", "wrap", "nowrap"], 51 | "correctOption": 1 52 | }, 53 | { 54 | "question": "Какое свойство CSS используется для изменения цвета текста?", 55 | "options": ["background-color", "color", "text-color", "font-color"], 56 | "correctOption": 1 57 | }, 58 | { 59 | "question": "Что делает свойство 'border-radius'?", 60 | "options": ["Задает радиус бордюра", "Изменяет ширину бордюра", "Скругляет углы элемента", "Добавляет тень к бордюру"], 61 | "correctOption": 2 62 | }, 63 | { 64 | "question": "Какое свойство CSS используется для задания внешних отступов элемента?", 65 | "options": ["margin", "padding", "border", "outline"], 66 | "correctOption": 0 67 | }, 68 | { 69 | "question": "Что делает свойство 'text-align: center;'?", 70 | "options": ["Выравнивает текст по левому краю", "Выравнивает текст по правому краю", "Выравнивает текст по центру", "Выравнивает текст по обеим сторонам"], 71 | "correctOption": 2 72 | }, 73 | { 74 | "question": "Какое значение свойства 'display' превращает элемент в строчный блок?", 75 | "options": ["inline", "block", "inline-block", "flex"], 76 | "correctOption": 2 77 | }, 78 | { 79 | "question": "Какое свойство CSS используется для добавления тени к тексту?", 80 | "options": ["box-shadow", "text-shadow", "shadow", "filter"], 81 | "correctOption": 1 82 | }, 83 | { 84 | "question": "Какое свойство отвечает за расположение фона элемента?", 85 | "options": ["background-position", "background-size", "background-repeat", "background-attachment"], 86 | "correctOption": 0 87 | }, 88 | { 89 | "question": "Что делает свойство 'float' в CSS?", 90 | "options": ["Открывает всплывающее окно", "Позволяет элементу 'плавать' по экрану", "Устанавливает обтекание текста вокруг элемента", "Добавляет анимацию к элементу"], 91 | "correctOption": 2 92 | }, 93 | { 94 | "question": "Какое свойство CSS используется для задания интервала между строками текста?", 95 | "options": ["letter-spacing", "line-height", "word-spacing", "text-indent"], 96 | "correctOption": 1 97 | }, 98 | { 99 | "question": "Что делает свойство 'overflow' в CSS?", 100 | "options": ["Определяет, как будет обрабатываться содержимое, выходящее за пределы элемента", "Определяет, как будет отображаться текст", "Определяет, как будут обработаны события мыши", "Определяет, как будет изменяться размер элемента"], 101 | "correctOption": 0 102 | }, 103 | { 104 | "question": "Какое значение свойства 'position' позволяет элементу 'плавать' над другими элементами?", 105 | "options": ["absolute", "relative", "fixed", "static"], 106 | "correctOption": 0 107 | }, 108 | { 109 | "question": "Что делает свойство 'text-decoration' в CSS?", 110 | "options": ["Изменяет цвет текста", "Добавляет стили к тексту, такие как подчеркивание", "Изменяет размер текста", "Изменяет шрифт текста"], 111 | "correctOption": 1 112 | }, 113 | { 114 | "question": "Какое свойство CSS используется для задания ширины элемента?", 115 | "options": ["width", "height", "size", "length"], 116 | "correctOption": 0 117 | }, 118 | { 119 | "question": "Какое свойство CSS используется для задания высоты элемента?", 120 | "options": ["width", "height", "size", "length"], 121 | "correctOption": 1 122 | }, 123 | { 124 | "question": "Какое значение свойства 'display' превращает элемент в блочный элемент?", 125 | "options": ["inline", "block", "inline-block", "flex"], 126 | "correctOption": 1 127 | }, 128 | { 129 | "question": "Какое значение свойства 'flex-wrap' позволяет элементам переноситься на следующую строку?", 130 | "options": ["nowrap", "wrap", "wrap-reverse", "inherit"], 131 | "correctOption": 1 132 | }, 133 | { 134 | "question": "Какое свойство CSS используется для задания внешней тени элемента?", 135 | "options": ["box-shadow", "text-shadow", "shadow", "filter"], 136 | "correctOption": 0 137 | }, 138 | { 139 | "question": "Что делает свойство 'visibility: hidden;' в CSS?", 140 | "options": ["Скрывает элемент и освобождает занимаемое место", "Скрывает элемент, но оставляет занимаемое место", "Делает элемент прозрачным", "Удаляет элемент из документа"], 141 | "correctOption": 1 142 | }, 143 | { 144 | "question": "Какое свойство CSS используется для задания цвета фона элемента?", 145 | "options": ["background-color", "color", "text-color", "font-color"], 146 | "correctOption": 0 147 | }, 148 | { 149 | "question": "Какое свойство CSS используется для задания размеров фона?", 150 | "options": ["background-position", "background-size", "background-repeat", "background-attachment"], 151 | "correctOption": 1 152 | }, 153 | { 154 | "question": "Что делает свойство 'outline' в CSS?", 155 | "options": ["Добавляет внутренний отступ", "Добавляет внешнюю тень", "Добавляет контур вокруг элемента", "Добавляет границу вокруг элемента"], 156 | "correctOption": 2 157 | }, 158 | { 159 | "question": "Какое свойство CSS используется для задания шрифта текста?", 160 | "options": ["font-family", "font-style", "font-weight", "font-size"], 161 | "correctOption": 0 162 | }, 163 | { 164 | "question": "Какое свойство CSS используется для изменения размера шрифта?", 165 | "options": ["font-family", "font-style", "font-weight", "font-size"], 166 | "correctOption": 3 167 | }, 168 | { 169 | "question": "Какое значение свойства 'flex-direction' располагает элементы в строку?", 170 | "options": ["row", "column", "wrap", "nowrap"], 171 | "correctOption": 0 172 | }, 173 | { 174 | "question": "Что делает свойство 'text-indent' в CSS?", 175 | "options": ["Изменяет межбуквенный интервал", "Изменяет межстрочный интервал", "Изменяет интервал между словами", "Задает отступ для первой строки текста"], 176 | "correctOption": 3 177 | }, 178 | { 179 | "question": "Какое свойство CSS используется для задания стиля шрифта?", 180 | "options": ["font-family", "font-style", "font-weight", "font-size"], 181 | "correctOption": 1 182 | }, 183 | { 184 | "question": "Какое свойство CSS используется для задания цвета границы элемента?", 185 | "options": ["border-color", "border-style", "border-width", "border-radius"], 186 | "correctOption": 0 187 | }, 188 | { 189 | "question": "Что делает свойство 'white-space' в CSS?", 190 | "options": ["Изменяет цвет пробелов", "Изменяет размер пробелов", "Управляет отображением пробелов и переносов строк", "Добавляет пробелы вокруг элемента"], 191 | "correctOption": 2 192 | } 193 | ] 194 | } -------------------------------------------------------------------------------- /questions/js_questions.json: -------------------------------------------------------------------------------- 1 | { 2 | "questions": [ 3 | { 4 | "question": "Что возвращает оператор typeof для массивов в JavaScript?", 5 | "options": ["'array'", "'object'", "'string'", "'undefined'"], 6 | "correctOption": 1 7 | }, 8 | { 9 | "question": "Какой метод создает новый массив с результатом вызова указанной функции для каждого элемента массива?", 10 | "options": ["map()", "filter()", "reduce()", "forEach()"], 11 | "correctOption": 0 12 | }, 13 | { 14 | "question": "Какой оператор используется для того, чтобы проверить, как тип, так и значение?", 15 | "options": ["==", "===", "equals", "=="], 16 | "correctOption": 1 17 | }, 18 | { 19 | "question": "Что делает метод push() в JavaScript?", 20 | "options": ["Добавляет один или более элементов в конец массива", "Удаляет последний элемент из массива", "Добавляет элемент в начало массива", "Удаляет первый элемент из массива"], 21 | "correctOption": 0 22 | }, 23 | { 24 | "question": "Как объявить функцию в JavaScript?", 25 | "options": ["function myFunction() {}", "def myFunction() {}", "function:myFunction() {}", "func myFunction() {}"], 26 | "correctOption": 0 27 | }, 28 | { 29 | "question": "Что возвращает метод Array.prototype.pop()?", 30 | "options": ["Удаленный последний элемент массива", "Длину массива", "Новый массив без последнего элемента", "Первый элемент массива"], 31 | "correctOption": 0 32 | }, 33 | { 34 | "question": "Какой метод используется для сортировки элементов массива?", 35 | "options": ["sort()", "order()", "arrange()", "organize()"], 36 | "correctOption": 0 37 | }, 38 | { 39 | "question": "Какой метод объединяет все элементы массива в строку?", 40 | "options": ["concat()", "join()", "merge()", "glue()"], 41 | "correctOption": 1 42 | }, 43 | { 44 | "question": "Как преобразовать строку в число в JavaScript?", 45 | "options": ["Number()", "parseInt()", "parseFloat()", "Все выше перечисленное"], 46 | "correctOption": 3 47 | }, 48 | { 49 | "question": "Что делает метод Array.prototype.shift()?", 50 | "options": ["Удаляет первый элемент массива", "Удаляет последний элемент массива", "Добавляет элемент в начало массива", "Добавляет элемент в конец массива"], 51 | "correctOption": 0 52 | }, 53 | { 54 | "question": "Какой оператор используется для проверки нестрогого равенства в JavaScript?", 55 | "options": ["==", "===", "equals", "!="], 56 | "correctOption": 0 57 | }, 58 | { 59 | "question": "Какой метод используется для поиска подстроки в строке?", 60 | "options": ["indexOf()", "search()", "includes()", "Все выше перечисленное"], 61 | "correctOption": 3 62 | }, 63 | { 64 | "question": "Какой из следующих методов НЕ изменяет исходный массив?", 65 | "options": ["map()", "splice()", "push()", "pop()"], 66 | "correctOption": 0 67 | }, 68 | { 69 | "question": "Что делает метод Array.prototype.slice()?", 70 | "options": ["Создает новый массив, содержащий копию части исходного массива", "Удаляет часть массива", "Изменяет элементы массива на месте", "Объединяет два или более массивов"], 71 | "correctOption": 0 72 | }, 73 | { 74 | "question": "Какой метод используется для проверки, содержит ли массив определенный элемент?", 75 | "options": ["includes()", "contains()", "has()", "exists()"], 76 | "correctOption": 0 77 | }, 78 | { 79 | "question": "Что делает метод Array.prototype.filter()?", 80 | "options": ["Создает новый массив с элементами, прошедшими проверку", "Создает новый массив с результатом вызова функции для каждого элемента", "Изменяет элементы массива на месте", "Объединяет два или более массивов"], 81 | "correctOption": 0 82 | }, 83 | { 84 | "question": "Что такое замыкание в JavaScript?", 85 | "options": ["Функция, которая имеет доступ к переменным из своей внешней области", "Объект, содержащий набор методов", "Функция, вызывающая саму себя", "Механизм наследования в JavaScript"], 86 | "correctOption": 0 87 | }, 88 | { 89 | "question": "Какой метод используется для объединения двух или более массивов?", 90 | "options": ["concat()", "merge()", "combine()", "join()"], 91 | "correctOption": 0 92 | }, 93 | { 94 | "question": "Что возвращает метод Array.prototype.reduce()?", 95 | "options": ["Один итоговый результат", "Новый массив с элементами, прошедшими проверку", "Длину массива", "Первый элемент массива"], 96 | "correctOption": 0 97 | }, 98 | { 99 | "question": "Как проверить, является ли значение массивом в JavaScript?", 100 | "options": ["Array.isArray()", "typeof", "isArray()", "instanceof Array"], 101 | "correctOption": 0 102 | }, 103 | { 104 | "question": "Что такое промис в JavaScript?", 105 | "options": ["Объект, представляющий результат асинхронной операции", "Функция, вызывающая саму себя", "Переменная, доступная во всей программе", "Метод объекта"], 106 | "correctOption": 0 107 | }, 108 | { 109 | "question": "Какой метод используется для выполнения функции для каждого элемента массива?", 110 | "options": ["forEach()", "map()", "filter()", "reduce()"], 111 | "correctOption": 0 112 | }, 113 | { 114 | "question": "Что возвращает метод Array.prototype.find()?", 115 | "options": ["Первый элемент, прошедший проверку", "Все элементы, прошедшие проверку", "Новый массив с элементами, прошедшими проверку", "Индекс первого элемента, прошедшего проверку"], 116 | "correctOption": 0 117 | }, 118 | { 119 | "question": "Как преобразовать объект в JSON строку?", 120 | "options": ["JSON.stringify()", "JSON.parse()", "toString()", "toJSON()"], 121 | "correctOption": 0 122 | }, 123 | { 124 | "question": "Что делает оператор '...' (spread) в JavaScript?", 125 | "options": ["Расширяет выражение в местах, где ожидается несколько аргументов", "Соединяет строки", "Клонирует объект", "Сортирует массив"], 126 | "correctOption": 0 127 | }, 128 | { 129 | "question": "Что возвращает метод Array.prototype.includes()?", 130 | "options": ["true, если массив содержит указанный элемент, иначе false", "Индекс элемента, если он найден", "Все элементы, содержащие указанный элемент", "Длину массива"], 131 | "correctOption": 0 132 | }, 133 | { 134 | "question": "Как объявить переменную, значение которой нельзя изменить?", 135 | "options": ["const", "let", "var", "static"], 136 | "correctOption": 0 137 | }, 138 | { 139 | "question": "Какой из методов удаляет последний элемент массива?", 140 | "options": ["pop()", "push()", "shift()", "unshift()"], 141 | "correctOption": 0 142 | }, 143 | { 144 | "question": "Что такое функция обратного вызова (callback function) в JavaScript?", 145 | "options": ["Функция, переданная другой функции в качестве аргумента", "Функция, вызывающая саму себя", "Функция, которая возвращает другую функцию", "Метод объекта"], 146 | "correctOption": 0 147 | }, 148 | { 149 | "question": "Какой метод проверяет, удовлетворяет ли хотя бы один элемент массива условию?", 150 | "options": ["some()", "every()", "filter()", "find()"], 151 | "correctOption": 0 152 | }, 153 | { 154 | "question": "Что возвращает метод Array.prototype.every()?", 155 | "options": ["true, если все элементы массива удовлетворяют условию", "false, если хотя бы один элемент массива удовлетворяет условию", "Массив элементов, удовлетворяющих условию", "Индекс первого элемента, удовлетворяющего условию"], 156 | "correctOption": 0 157 | }, 158 | { 159 | "question": "Как преобразовать JSON строку в объект JavaScript?", 160 | "options": ["JSON.parse()", "JSON.stringify()", "toObject()", "parseJSON()"], 161 | "correctOption": 0 162 | }, 163 | { 164 | "question": "Как создать новый объект в JavaScript?", 165 | "options": ["new Object()", "Object.create()", "{}", "Все выше перечисленное"], 166 | "correctOption": 3 167 | }, 168 | { 169 | "question": "Что делает метод Array.prototype.reverse()?", 170 | "options": ["Изменяет порядок элементов массива на обратный", "Создает новый массив с элементами в обратном порядке", "Возвращает индекс последнего элемента массива", "Удаляет последний элемент массива"], 171 | "correctOption": 0 172 | }, 173 | { 174 | "question": "Как проверить, является ли значение функцией в JavaScript?", 175 | "options": ["typeof value === 'function'", "typeof value === 'object'", "value instanceof Function", "value === 'function'"], 176 | "correctOption": 0 177 | }, 178 | { 179 | "question": "Что такое строгий режим ('use strict') в JavaScript?", 180 | "options": ["Режим, который помогает выявить ошибки в коде", "Режим, который делает код быстрее", "Режим, который включает новые возможности языка", "Режим, который запрещает использование определенных синтаксических конструкций"], 181 | "correctOption": 0 182 | }, 183 | { 184 | "question": "Какой из методов объединяет все элементы массива в строку, разделенную запятыми?", 185 | "options": ["toString()", "join()", "concat()", "split()"], 186 | "correctOption": 1 187 | }, 188 | { 189 | "question": "Что делает оператор '===' в JavaScript?", 190 | "options": ["Сравнивает как тип, так и значение", "Сравнивает только тип", "Сравнивает только значение", "Присваивает значение"], 191 | "correctOption": 0 192 | }, 193 | { 194 | "question": "Какой метод используется для удаления пробелов с обеих сторон строки?", 195 | "options": ["trim()", "slice()", "replace()", "split()"], 196 | "correctOption": 0 197 | }, 198 | { 199 | "question": "Что возвращает метод Array.prototype.concat()?", 200 | "options": ["Новый массив, состоящий из объединенных массивов", "Количество элементов в объединенных массивах", "Первый элемент объединенного массива", "Массив объектов, каждый из которых содержит элементы из обоих массивов"], 201 | "correctOption": 0 202 | } 203 | ] 204 | } 205 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { Bot, GrammyError, HttpError, Keyboard, InlineKeyboard, session } = require('grammy'); 3 | const fs = require('fs').promises; 4 | const sqlite3 = require('sqlite3').verbose(); 5 | const { open } = require('sqlite'); 6 | const { format } = require('date-fns'); 7 | const { ru } = require('date-fns/locale'); 8 | 9 | const bot = new Bot(process.env.BOT_API_KEY); 10 | 11 | bot.use(session({ 12 | initial: () => ({ 13 | correctAnswers: { 14 | html: 0, 15 | css: 0, 16 | js: 0, 17 | react: 0 18 | }, 19 | hasStartedRatingMode: false 20 | }) 21 | })); 22 | 23 | let questionsData = {}; 24 | let db; 25 | 26 | async function loadQuestions() { 27 | const categories = { 28 | html: 'html_questions.json', 29 | css: 'css_questions.json', 30 | js: 'js_questions.json', 31 | react: 'react_questions.json' 32 | }; 33 | for (const [category, file] of Object.entries(categories)) { 34 | try { 35 | const data = await fs.readFile(`questions/${file}`, 'utf8'); 36 | questionsData[category] = JSON.parse(data).questions; 37 | } catch (error) { 38 | console.error(`Ошибка при загрузке вопросов из файла ${file}:`, error); 39 | } 40 | } 41 | } 42 | 43 | async function initDatabase() { 44 | db = await open({ 45 | filename: 'leaderboard.db', 46 | driver: sqlite3.Database 47 | }); 48 | 49 | await db.exec(` 50 | CREATE TABLE IF NOT EXISTS leaderboard ( 51 | id INTEGER PRIMARY KEY AUTOINCREMENT, 52 | username TEXT NOT NULL, 53 | score INTEGER NOT NULL, 54 | last_played TEXT NOT NULL 55 | ) 56 | `); 57 | } 58 | 59 | async function createProfile(username) { 60 | const existingEntry = await db.get('SELECT * FROM leaderboard WHERE username = ?', username); 61 | if (!existingEntry) { 62 | await db.run('INSERT INTO leaderboard (username, score, last_played) VALUES (?, ?, ?)', username, 0, 'Еще не играл'); 63 | } 64 | } 65 | 66 | async function updateLeaderboard(username, score) { 67 | const now = new Date().toISOString(); 68 | const existingEntry = await db.get('SELECT * FROM leaderboard WHERE username = ?', username); 69 | if (existingEntry) { 70 | if (existingEntry.score < score) { 71 | await db.run('UPDATE leaderboard SET score = ?, last_played = ? WHERE username = ?', score, now, username); 72 | } else { 73 | await db.run('UPDATE leaderboard SET last_played = ? WHERE username = ?', now, username); 74 | } 75 | } else { 76 | await db.run('INSERT INTO leaderboard (username, score, last_played) VALUES (?, ?, ?)', username, score, now); 77 | } 78 | } 79 | 80 | async function getLeaderboard() { 81 | return await db.all('SELECT username, score FROM leaderboard ORDER BY score DESC LIMIT 10'); 82 | } 83 | 84 | async function getTotalUsers() { 85 | const result = await db.get('SELECT COUNT(*) AS count FROM leaderboard'); 86 | return result.count; 87 | } 88 | 89 | function initializeQuizState(ctx, category) { 90 | if (!ctx.session.askedQuestions) { 91 | ctx.session.askedQuestions = {}; 92 | } 93 | if (!ctx.session.askedQuestions[category]) { 94 | ctx.session.askedQuestions[category] = []; 95 | } 96 | if (ctx.session.firstAttempt === undefined) { 97 | ctx.session.firstAttempt = true; 98 | } 99 | } 100 | 101 | function initializeRatingMode(ctx) { 102 | ctx.session.ratingMode = true; 103 | ctx.session.score = 0; 104 | ctx.session.askedQuestions = {}; 105 | ctx.session.currentCategory = null; 106 | } 107 | 108 | function getStartKeyboard() { 109 | return new Keyboard() 110 | .text('HTML') 111 | .text('CSS') 112 | .row() 113 | .text('JavaScript') 114 | .text('React') 115 | .row() 116 | .text('🏆Рейтинговый режим') 117 | .row() 118 | .text('📣Таблица лидеров') 119 | .row(); 120 | } 121 | 122 | bot.command('start', async (ctx) => { 123 | const username = ctx.from.username || ctx.from.first_name; 124 | await createProfile(username); 125 | 126 | const startKeyboard = getStartKeyboard(); 127 | 128 | await ctx.reply( 129 | 'Привет! Я помогу тебе подготовиться к собеседованию. Используй команды ниже для взаимодействия с ботом:\n' + 130 | '/start - Начать использование бота\n' + 131 | '/profile - Просмотр вашего профиля', 132 | { reply_markup: startKeyboard } 133 | ); 134 | await ctx.reply('С чего начнем? Выбирай тему👇', { 135 | reply_markup: startKeyboard, 136 | }); 137 | }); 138 | 139 | bot.command('profile', async (ctx) => { 140 | const username = ctx.from.username || ctx.from.first_name; 141 | const result = await db.get('SELECT * FROM leaderboard WHERE username = ?', username); 142 | 143 | const htmlQuestionsTotal = questionsData.html.length; 144 | const cssQuestionsTotal = questionsData.css.length; 145 | const jsQuestionsTotal = questionsData.js.length; 146 | const reactQuestionsTotal = questionsData.react.length; 147 | 148 | const htmlCorrect = ctx.session.correctAnswers.html; 149 | const cssCorrect = ctx.session.correctAnswers.css; 150 | const jsCorrect = ctx.session.correctAnswers.js; 151 | const reactCorrect = ctx.session.correctAnswers.react; 152 | 153 | if (result) { 154 | const formattedDate = result.last_played === 'Еще не играл' ? result.last_played : format(new Date(result.last_played), 'dd MMMM yyyy, HH:mm', { locale: ru }); 155 | const profileMessage = `👤 Профиль пользователя ${username}:\n` + 156 | `🏆 Счет в рейтинговой игре: ${result.score} очков\n` + 157 | `📅 Дата последней игры: ${formattedDate}\n` + 158 | `📚 Вопросы по HTML: решено верно ${htmlCorrect} из ${htmlQuestionsTotal}\n` + 159 | `📚 Вопросы по CSS: решено верно ${cssCorrect} из ${cssQuestionsTotal}\n` + 160 | `📚 Вопросы по JavaScript: решено верно ${jsCorrect} из ${jsQuestionsTotal}\n` + 161 | `📚 Вопросы по React: решено верно ${reactCorrect} из ${reactQuestionsTotal}`; 162 | 163 | await ctx.reply(profileMessage); 164 | } else { 165 | await ctx.reply('Профиль не найден. Начните игру в рейтинговом режиме, чтобы создать профиль.'); 166 | } 167 | }); 168 | 169 | bot.command('admin', async (ctx) => { 170 | const userId = ctx.from.id; 171 | const adminId = parseInt(process.env.ADMIN_ID, 10); 172 | 173 | if (userId === adminId) { 174 | const totalUsers = await getTotalUsers(); 175 | await ctx.reply(`Общее количество пользователей: ${totalUsers}`); 176 | } else { 177 | await ctx.reply('У вас нет прав для использования этой команды.'); 178 | } 179 | }); 180 | 181 | bot.on('message', async (ctx) => { 182 | const { text } = ctx.message; 183 | 184 | if (ctx.session.awaitingRetryConfirmation) { 185 | if (text === 'Да') { 186 | const category = ctx.session.awaitingRetryConfirmation; 187 | ctx.session.askedQuestions[category] = []; 188 | ctx.session.firstAttempt = false; 189 | ctx.session.awaitingRetryConfirmation = null; 190 | await startQuiz(ctx, category); 191 | } else if (text === 'Нет') { 192 | ctx.session.awaitingRetryConfirmation = null; 193 | const startKeyboard = getStartKeyboard(); 194 | await ctx.reply('Выберите категорию:', { reply_markup: startKeyboard }); 195 | } 196 | return; 197 | } 198 | 199 | if (text === 'Назад ↩️') { 200 | const startKeyboard = getStartKeyboard(); 201 | await ctx.reply('Выберите категорию:', { reply_markup: startKeyboard }); 202 | } else { 203 | switch (text) { 204 | case 'HTML': 205 | ctx.session.firstAttempt = true; 206 | await startQuiz(ctx, 'html'); 207 | break; 208 | case 'CSS': 209 | ctx.session.firstAttempt = true; 210 | await startQuiz(ctx, 'css'); 211 | break; 212 | case 'JavaScript': 213 | ctx.session.firstAttempt = true; 214 | await startQuiz(ctx, 'js'); 215 | break; 216 | case 'React': 217 | ctx.session.firstAttempt = true; 218 | await startQuiz(ctx, 'react'); 219 | break; 220 | case '🏆Рейтинговый режим': 221 | if (!ctx.session.hasStartedRatingMode) { 222 | ctx.session.hasStartedRatingMode = true; 223 | await ctx.reply( 224 | 'Рейтинговый режим содержит вопросы из всех категорий. За каждый правильный ответ дается балл, а при неверном ответе игра прекращается. Таблица лидеров выводит топ 10 игроков в рейтинге.' 225 | ); 226 | } 227 | initializeRatingMode(ctx); 228 | await startRatingQuiz(ctx); 229 | break; 230 | case '📣Таблица лидеров': 231 | await showLeaderboard(ctx); 232 | break; 233 | default: 234 | handleQuizAnswer(ctx, text); 235 | } 236 | } 237 | }); 238 | 239 | async function handleQuizAnswer(ctx, answer) { 240 | try { 241 | if (!ctx.session.currentQuestion) { 242 | await ctx.reply('Кажется, я забыл вопрос. Давай начнем заново.'); 243 | return; 244 | } 245 | 246 | const correctAnswer = ctx.session.currentQuestion.options[ctx.session.currentQuestion.correctOption]; 247 | 248 | if (answer === correctAnswer) { 249 | await ctx.reply('Верно!'); 250 | if (ctx.session.firstAttempt) { 251 | ctx.session.correctAnswers[ctx.session.currentCategory]++; 252 | } 253 | if (ctx.session.ratingMode) { 254 | ctx.session.score += 1; 255 | await startRatingQuiz(ctx); 256 | } else { 257 | await startQuiz(ctx, ctx.session.currentCategory); 258 | } 259 | } else { 260 | if (ctx.session.ratingMode) { 261 | const username = ctx.from.username || ctx.from.first_name; 262 | await updateLeaderboard(username, ctx.session.score); 263 | ctx.session.ratingMode = false; 264 | const startKeyboard = getStartKeyboard(); 265 | await ctx.reply(`Ошибка! Вы набрали ${ctx.session.score} очков.`, { reply_markup: startKeyboard }); 266 | ctx.session.score = 0; 267 | } else { 268 | await ctx.reply('Неправильно. Попробуйте еще раз.'); 269 | } 270 | } 271 | } catch (error) { 272 | console.error('Ошибка обработки ответа на вопрос:', error); 273 | await ctx.reply('Произошла ошибка при обработке ответа на вопрос. Попробуйте еще раз позже.'); 274 | } 275 | } 276 | 277 | function getRandomQuestion(questions, asked) { 278 | const availableQuestions = questions.filter((_, index) => !asked.includes(index)); 279 | if (availableQuestions.length === 0) { 280 | return null; 281 | } 282 | const randomIndex = Math.floor(Math.random() * availableQuestions.length); 283 | return availableQuestions[randomIndex]; 284 | } 285 | 286 | async function startQuiz(ctx, category) { 287 | initializeQuizState(ctx, category); 288 | 289 | const questions = questionsData[category]; 290 | if (!questions) { 291 | await ctx.reply(`Не удалось загрузить вопросы для категории ${category.toUpperCase()}. Проверьте файл: questions/${category}_questions.json`); 292 | return; 293 | } 294 | 295 | const questionData = getRandomQuestion(questions, ctx.session.askedQuestions[category]); 296 | if (!questionData) { 297 | const retryKeyboard = new Keyboard() 298 | .text('Да').row() 299 | .text('Нет').row(); 300 | 301 | await ctx.reply(`Вы ответили на все вопросы по ${category.toUpperCase()}! Желаете повторить?`, { 302 | reply_markup: retryKeyboard, 303 | }); 304 | 305 | ctx.session.awaitingRetryConfirmation = category; 306 | return; 307 | } 308 | 309 | const questionIndex = questions.indexOf(questionData); 310 | ctx.session.askedQuestions[category].push(questionIndex); 311 | ctx.session.currentQuestion = questionData; 312 | ctx.session.currentCategory = category; 313 | 314 | const keyboard = new Keyboard(); 315 | questionData.options.forEach(option => keyboard.text(option).row()); 316 | keyboard.text('Назад ↩️').row(); 317 | 318 | await ctx.reply(questionData.question, { reply_markup: keyboard }); 319 | } 320 | 321 | async function startRatingQuiz(ctx) { 322 | const categories = ['html', 'css', 'js', 'react']; 323 | const randomCategory = categories[Math.floor(Math.random() * categories.length)]; 324 | 325 | initializeQuizState(ctx, randomCategory); 326 | 327 | const questions = questionsData[randomCategory]; 328 | if (!questions) { 329 | await ctx.reply(`Не удалось загрузить вопросы для категории ${randomCategory.toUpperCase()}. Проверьте файл: questions/${randomCategory}_questions.json`); 330 | return; 331 | } 332 | 333 | const questionData = getRandomQuestion(questions, ctx.session.askedQuestions[randomCategory]); 334 | if (!questionData) { 335 | await ctx.reply(`Вы ответили на все вопросы по ${randomCategory.toUpperCase()}!`); 336 | return; 337 | } 338 | 339 | const questionIndex = questions.indexOf(questionData); 340 | ctx.session.askedQuestions[randomCategory].push(questionIndex); 341 | ctx.session.currentQuestion = questionData; 342 | ctx.session.currentCategory = randomCategory; 343 | 344 | const keyboard = new Keyboard(); 345 | questionData.options.forEach(option => keyboard.text(option).row()); 346 | keyboard.text('Назад ↩️').row(); 347 | 348 | await ctx.reply(questionData.question, { reply_markup: keyboard }); 349 | } 350 | 351 | async function showLeaderboard(ctx) { 352 | const topPlayers = await getLeaderboard(); 353 | if (topPlayers.length === 0) { 354 | await ctx.reply('Таблица лидеров пока пуста.'); 355 | return; 356 | } 357 | 358 | let leaderboardMessage = 'Таблица лидеров:\n'; 359 | topPlayers.forEach(({ username, score }, index) => { 360 | leaderboardMessage += `${index + 1}. ${username}: ${score} очков\n`; 361 | }); 362 | 363 | await ctx.reply(leaderboardMessage); 364 | } 365 | 366 | bot.api.setMyCommands([ 367 | { command: 'start', description: 'Запуск бота' }, 368 | { command: 'profile', description: 'Просмотр вашего профиля' } 369 | ]); 370 | 371 | (async () => { 372 | await loadQuestions(); 373 | await initDatabase(); 374 | bot.start(); 375 | })(); 376 | --------------------------------------------------------------------------------