Как создать редактор кода для 40+ языков с помощью React

от автора

Подробностями разработки онлайн-платформы выполнения и компиляции кода более чем на 40 языках делимся к старту курса по Frontend-разработке. Автор этого материала — основатель TailwindMasterKit.


Онлайн-платформа выполнения кода позволяет писать и сразу запускать код на любимом языке программирования. В идеале можно увидеть вывод программы, например двоичного поиска на JavaScript.

Демонстрации

Создадим функциональный редактор кода Monaco Editor. Вот его возможности:

  • поддержка VS Code;

  • компиляция в веб-приложении со стандартным вводом и выводом и поддержкой более чем 40 языков;

  • выбор темы редактора из списка доступных тем;

  • информация о коде (время выполнения, используемая память, статус и т. д.).

Технологический стек

  • React.js для фронтенда;

  • TailwindCSS для стилей;

  • Judge0 для компиляции и выполнения кода;

  • RapidAPI для быстрого развёртывания кода Judge0;

  • Monaco Editor — редактор кода для проекта.

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

Структура проекта проста:

  • сomponents: компоненты / сниппеты кода (например, CodeEditorWindow и Landing);

  • hooks: пользовательские хуки (и хуки нажатия клавиш — для компилирования кода с помощью событий клавиатуры);

  • lib: библиотечные функции (здесь создадим функцию определения темы);

  • constants: константы, такие как languageOptions и customStyles, для выпадающих списков;

  • utils: служебные функции для сопровождения кода.

Логика работы с приложением

Прежде чем переходить к коду, разберёмся в логике работы с приложением и в том, как писать для него код с нуля.

  • Пользователь попадает в веб-приложение и выбирает язык (по умолчанию — JavaScript).

  • После написания кода пользователь его компилирует, а выходные данные просматривает в окне вывода.

  • В окне вывода кода вы увидите вывод и статус кода.

  • Пользователь может добавлять к фрагментам кода свои входные данные, которые учитываются в judge (онлайн-компиляторе).

  • Пользователь может видеть информацию о выполненном коде (пример: на компиляцию и выполнение ушло 5 мс, использовано 2024 Кб памяти, выполнение кода завершено успешно).

Ознакомившись со структурой каталогов проекта и логикой работы с приложением, перейдём к коду и разберёмся, как тут всё организовано.

Как создать компонент редактора кода

Компонент редактора кода состоит из Monaco Editor, то есть настраиваемого NPM-пакета:

// CodeEditorWindow.js  import React, { useState } from "react";  import Editor from "@monaco-editor/react";  const CodeEditorWindow = ({ onChange, language, code, theme }) => {   const [value, setValue] = useState(code || "");    const handleEditorChange = (value) => {     setValue(value);     onChange("code", value);   };    return (     <div className="overlay rounded-md overflow-hidden w-full h-full shadow-4xl">       <Editor         height="85vh"         width={`100%`}         language={language || "javascript"}         value={value}         theme={theme}         defaultValue="// some comment"         onChange={handleEditorChange}       />     </div>   ); }; export default CodeEditorWindow;

Компоненты Editor берутся из пакета @monaco-editor/react, который позволяет развернуть редактор кода с соответствующей высотой области просмотра 85vh.

Компонент Editor принимает много свойств:

  • language: язык, для которого нужны подсветка синтаксиса и автодополнение ввода.

  • theme: цвета и фон фрагмента кода (настроим позже).

  • value: код, который вводится в редактор.

  • onChange: происходит при изменении value в редакторе. Изменившееся значение нужно сохранить в состоянии, чтобы позже для компиляции вызвать API Judge0.

Редактор получает свойства onChange, language, code и theme родительского компонента Landing.js. Когда в редакторе меняется свойство value, вызываем обработчик onChange из родительского компонента Landing.

Как создать компонент Landing

Компонент landing в состоит из трёх частей:

  • Actions Bar с компонентами выпадающих списков Languages и Themes.

  • Компонент Code Editor Window.

  • Компоненты Output и Custom Input.

// Landing.js  import React, { useEffect, useState } from "react"; import CodeEditorWindow from "./CodeEditorWindow"; import axios from "axios"; import { classnames } from "../utils/general"; import { languageOptions } from "../constants/languageOptions";  import { ToastContainer, toast } from "react-toastify"; import "react-toastify/dist/ReactToastify.css";  import { defineTheme } from "../lib/defineTheme"; import useKeyPress from "../hooks/useKeyPress"; import Footer from "./Footer"; import OutputWindow from "./OutputWindow"; import CustomInput from "./CustomInput"; import OutputDetails from "./OutputDetails"; import ThemeDropdown from "./ThemeDropdown"; import LanguagesDropdown from "./LanguagesDropdown";  const javascriptDefault = `// some comment`;  const Landing = () => {   const [code, setCode] = useState(javascriptDefault);   const [customInput, setCustomInput] = useState("");   const [outputDetails, setOutputDetails] = useState(null);   const [processing, setProcessing] = useState(null);   const [theme, setTheme] = useState("cobalt");   const [language, setLanguage] = useState(languageOptions[0]);    const enterPress = useKeyPress("Enter");   const ctrlPress = useKeyPress("Control");    const onSelectChange = (sl) => {     console.log("selected Option...", sl);     setLanguage(sl);   };    useEffect(() => {     if (enterPress && ctrlPress) {       console.log("enterPress", enterPress);       console.log("ctrlPress", ctrlPress);       handleCompile();     }   }, [ctrlPress, enterPress]);   const onChange = (action, data) => {     switch (action) {       case "code": {         setCode(data);         break;       }       default: {         console.warn("case not handled!", action, data);       }     }   };   const handleCompile = () => {     // We will come to the implementation later in the code   };    const checkStatus = async (token) => {     // We will come to the implementation later in the code   };    function handleThemeChange(th) {     // We will come to the implementation later in the code   }   useEffect(() => {     defineTheme("oceanic-next").then((_) =>       setTheme({ value: "oceanic-next", label: "Oceanic Next" })     );   }, []);    const showSuccessToast = (msg) => {     toast.success(msg || `Compiled Successfully!`, {       position: "top-right",       autoClose: 1000,       hideProgressBar: false,       closeOnClick: true,       pauseOnHover: true,       draggable: true,       progress: undefined,     });   };   const showErrorToast = (msg) => {     toast.error(msg || `Something went wrong! Please try again.`, {       position: "top-right",       autoClose: 1000,       hideProgressBar: false,       closeOnClick: true,       pauseOnHover: true,       draggable: true,       progress: undefined,     });   };    return (     <>       <ToastContainer         position="top-right"         autoClose={2000}         hideProgressBar={false}         newestOnTop={false}         closeOnClick         rtl={false}         pauseOnFocusLoss         draggable         pauseOnHover       />       <div className="h-4 w-full bg-gradient-to-r from-pink-500 via-red-500 to-yellow-500"></div>       <div className="flex flex-row">         <div className="px-4 py-2">           <LanguagesDropdown onSelectChange={onSelectChange} />         </div>         <div className="px-4 py-2">           <ThemeDropdown handleThemeChange={handleThemeChange} theme={theme} />         </div>       </div>       <div className="flex flex-row space-x-4 items-start px-4 py-4">         <div className="flex flex-col w-full h-full justify-start items-end">           <CodeEditorWindow             code={code}             onChange={onChange}             language={language?.value}             theme={theme.value}           />         </div>          <div className="right-container flex flex-shrink-0 w-[30%] flex-col">           <OutputWindow outputDetails={outputDetails} />           <div className="flex flex-col items-end">             <CustomInput               customInput={customInput}               setCustomInput={setCustomInput}             />             <button               onClick={handleCompile}               disabled={!code}               className={classnames(                 "mt-4 border-2 border-black z-10 rounded-md shadow-[5px_5px_0px_0px_rgba(0,0,0)] px-4 py-2 hover:shadow transition duration-200 bg-white flex-shrink-0",                 !code ? "opacity-50" : ""               )}             >               {processing ? "Processing..." : "Compile and Execute"}             </button>           </div>           {outputDetails && <OutputDetails outputDetails={outputDetails} />}         </div>       </div>       <Footer />     </>   ); }; export default Landing;

Рассмотрим базовую структуру Landing подробнее.

Компонент CodeEditorWindow

Как мы уже видели, в компоненте CodeEditorWindow учитываются постоянно меняющийся код и метод onChange, с помощью которого отслеживаются изменения в код:.

// onChange method implementation   const onChange = (action, data) => {     switch (action) {       case "code": {         setCode(data);         break;       }       default: {         console.warn("case not handled!", action, data);       }     }   };

Задаём состояние code и отслеживаем изменения.

В компоненте CodeEditorWindow также учитывается свойство language — выбранный в данный момент язык, для которого нужны подсветка синтаксиса и автодополнение ввода.

Массив languageOptions я создал для отслеживания принятых в Monaco Editor свойств языка, а также для работы с компиляцией (отслеживаем languageId, принимаемый в этих API judge0):

// constants/languageOptions.js  export const languageOptions = [   {     id: 63,     name: "JavaScript (Node.js 12.14.0)",     label: "JavaScript (Node.js 12.14.0)",     value: "javascript",   },   {     id: 45,     name: "Assembly (NASM 2.14.02)",     label: "Assembly (NASM 2.14.02)",     value: "assembly",   },     ...     ...     ...     ...     ...     ...        {     id: 84,     name: "Visual Basic.Net (vbnc 0.0.0.5943)",     label: "Visual Basic.Net (vbnc 0.0.0.5943)",     value: "vbnet",   }, ];

В каждом объекте languageOptions есть свойства id, name, label и value. Массив languageOptions помещается в выпадающий список и предоставляются как его варианты.

Когда состояние выпадающего списка меняется, в методе onSelectChange отслеживается выбранный id с соответствующим изменением состояния.

Компонент LanguageDropdown

// LanguageDropdown.js  import React from "react"; import Select from "react-select"; import { customStyles } from "../constants/customStyles"; import { languageOptions } from "../constants/languageOptions";  const LanguagesDropdown = ({ onSelectChange }) => {   return (     <Select       placeholder={`Filter By Category`}       options={languageOptions}       styles={customStyles}       defaultValue={languageOptions[0]}       onChange={(selectedOption) => onSelectChange(selectedOption)}     />   ); };  export default LanguagesDropdown;

Для выпадающих списков и их обработчиков изменений используется пакет react-select.

Основные параметры react-select — defaultValue и массив options (здесь будем передавать languageOptions), с помощью которого автоматически отображаются все эти значения выпадающего списка.

Свойство defaultValue — это указываемое в компоненте значение по умолчанию. Языком по умолчанию оставим первый язык в массиве языков — JavaScript.

Когда пользователь меняет язык, это происходит с помощью onSelectChange:

const onSelectChange = (sl) => {     setLanguage(sl); };

Компонент ThemeDropdown

Компонент ThemeDropdown очень похож на LanguageDropdown (с пользовательским интерфейсом и пакетом react-select):

// ThemeDropdown.js  import React from "react"; import Select from "react-select"; import monacoThemes from "monaco-themes/themes/themelist"; import { customStyles } from "../constants/customStyles";  const ThemeDropdown = ({ handleThemeChange, theme }) => {   return (     <Select       placeholder={`Select Theme`}       // options={languageOptions}       options={Object.entries(monacoThemes).map(([themeId, themeName]) => ({         label: themeName,         value: themeId,         key: themeId,       }))}       value={theme}       styles={customStyles}       onChange={handleThemeChange}     />   ); };  export default ThemeDropdown;

Здесь для выбора красивых тем из списка ниже, доступных Monaco Editor, используем пакет monacoThemes:

// lib/defineTheme.js  import { loader } from "@monaco-editor/react";  const monacoThemes = {   active4d: "Active4D",   "all-hallows-eve": "All Hallows Eve",   amy: "Amy",   "birds-of-paradise": "Birds of Paradise",   blackboard: "Blackboard",   "brilliance-black": "Brilliance Black",   "brilliance-dull": "Brilliance Dull",   "chrome-devtools": "Chrome DevTools",   "clouds-midnight": "Clouds Midnight",   clouds: "Clouds",   cobalt: "Cobalt",   dawn: "Dawn",   dreamweaver: "Dreamweaver",   eiffel: "Eiffel",   "espresso-libre": "Espresso Libre",   github: "GitHub",   idle: "IDLE",   katzenmilch: "Katzenmilch",   "kuroir-theme": "Kuroir Theme",   lazy: "LAZY",   "magicwb--amiga-": "MagicWB (Amiga)",   "merbivore-soft": "Merbivore Soft",   merbivore: "Merbivore",   "monokai-bright": "Monokai Bright",   monokai: "Monokai",   "night-owl": "Night Owl",   "oceanic-next": "Oceanic Next",   "pastels-on-dark": "Pastels on Dark",   "slush-and-poppies": "Slush and Poppies",   "solarized-dark": "Solarized-dark",   "solarized-light": "Solarized-light",   spacecadet: "SpaceCadet",   sunburst: "Sunburst",   "textmate--mac-classic-": "Textmate (Mac Classic)",   "tomorrow-night-blue": "Tomorrow-Night-Blue",   "tomorrow-night-bright": "Tomorrow-Night-Bright",   "tomorrow-night-eighties": "Tomorrow-Night-Eighties",   "tomorrow-night": "Tomorrow-Night",   tomorrow: "Tomorrow",   twilight: "Twilight",   "upstream-sunburst": "Upstream Sunburst",   "vibrant-ink": "Vibrant Ink",   "xcode-default": "Xcode_default",   zenburnesque: "Zenburnesque",   iplastic: "iPlastic",   idlefingers: "idleFingers",   krtheme: "krTheme",   monoindustrial: "monoindustrial", };  const defineTheme = (theme) => {   return new Promise((res) => {     Promise.all([       loader.init(),       import(`monaco-themes/themes/${monacoThemes[theme]}.json`),     ]).then(([monaco, themeData]) => {       monaco.editor.defineTheme(theme, themeData);       res();     });   }); };  export { defineTheme };

В monaco-themes тем много, так что внешний вид будущего редактора — не проблема.

Темы выбирает функция defineTheme, в ней возвращается промис, посредством которого с помощью экшена monaco.editor.defineTheme(theme, themeData) задаётся тема редактора. Само изменение тем внутри окна кода Monaco Editor происходит в этой строке кода.

Функция defineTheme вызывается с помощью обратного вызова onChange, который мы уже видели в компоненте ThemeDropdown.js:

// Landing.js - handleThemeChange() function  function handleThemeChange(th) {     const theme = th;     console.log("theme...", theme);      if (["light", "vs-dark"].includes(theme.value)) {       setTheme(theme);     } else {       defineTheme(theme.value).then((_) => setTheme(theme));     }   }   

В функции handleThemeChange() проверяется тема: light (светлая) или dark (тёмная). Эти темы по умолчанию доступны в компоненте MonacoEditor — вызывать метод defineTheme() не нужно.

Если тем в списке нет, вызываем компонент defineTheme() и задаём состояние выбранной темы.

Как компилировать код с помощью Judge0

Перейдём к самой «вкусной» части приложения — компиляции кода на разных языках, для которой используем Judge0 — интерактивную систему выполнения кода.

Выполнить вызов API можно с произвольными параметрами (исходный код, идентификатор языка) и получить в ответ выходные данные.

Настраиваем Judge0:

  • переходим к Judge0 и выбираем базовый план;

  • на самом деле Judge0 размещён на RapidAPI (идём дальше и подписываемся на базовый план);

  • после этого можно скопировать RAPIDAPI_HOST и RAPIDAPI_KEY (для выполнения вызовов API в систему выполнения кода).

Дашборд выглядит так:

Для вызовов API нужны параметры X-RapidAPI-Host и X-RapidAPI-Key. Сохраните их в файлах .env:

REACT_APP_RAPID_API_HOST = YOUR_HOST_URL REACT_APP_RAPID_API_KEY = YOUR_SECRET_KEY REACT_APP_RAPID_API_URL = YOUR_SUBMISSIONS_URL

В React важно инициализировать переменные окружения с префиксом REACT_APP.

Будем использовать URL-адрес SUBMISSIONS_URL из хоста и маршрута /submission.

Например, https://judge0-ce.p.rapidapi.com/submissions будет URL-адресом submissions в нашем случае.

После настройки переменных переходим к логике компиляции.

Логика и последовательность компиляции

Последовательность компиляции следующая:

  • Нажатие кнопки Compile and Execute вызывает метод handleCompile().

  • В функции handleCompile() вызывается бэкенд Judge0 RapidAPI по URL-адресу submissions с указанием в качестве параметров запроса — languageId, source_code и stdin — в нашем случае customInput.

  • В options как заголовки также принимаются host и secret.

  • Могут передаваться дополнительные параметры base64_encoded и fields.

  • При отправке POST-запроса submission наш запрос регистрируется на сервере, и создаётся процесс. Ответ на POST-запрос — token, необходимый для проверки статуса выполнения (Processing, Accepted, Time Limit Exceeded, Runtime Exceptions и др.).

  • По возвращении успешность результатов можно проверить с помощью условий, а затем показать результаты в окне вывода.

Разберём метод handleCompile():

const handleCompile = () => {     setProcessing(true);     const formData = {       language_id: language.id,       // encode source code in base64       source_code: btoa(code),       stdin: btoa(customInput),     };     const options = {       method: "POST",       url: process.env.REACT_APP_RAPID_API_URL,       params: { base64_encoded: "true", fields: "*" },       headers: {         "content-type": "application/json",         "Content-Type": "application/json",         "X-RapidAPI-Host": process.env.REACT_APP_RAPID_API_HOST,         "X-RapidAPI-Key": process.env.REACT_APP_RAPID_API_KEY,       },       data: formData,     };      axios       .request(options)       .then(function (response) {         console.log("res.data", response.data);         const token = response.data.token;         checkStatus(token);       })       .catch((err) => {         let error = err.response ? err.response.data : err;         setProcessing(false);         console.log(error);       });   };

Он принимает languageId, source_code и stdin. Обратите внимание на btoa перед source_code и stdin. Это нужно для кодирования строк в формате base64, потому что у нас в параметрах запроса к API есть base64_encoded: true.

Если получен успешный ответ и есть token, вызываем метод checkStatus() для опроса маршрута /submissions/${token}:

const checkStatus = async (token) => {     const options = {       method: "GET",       url: process.env.REACT_APP_RAPID_API_URL + "/" + token,       params: { base64_encoded: "true", fields: "*" },       headers: {         "X-RapidAPI-Host": process.env.REACT_APP_RAPID_API_HOST,         "X-RapidAPI-Key": process.env.REACT_APP_RAPID_API_KEY,       },     };     try {       let response = await axios.request(options);       let statusId = response.data.status?.id;        // Processed - we have a result       if (statusId === 1 || statusId === 2) {         // still processing         setTimeout(() => {           checkStatus(token)         }, 2000)         return       } else {         setProcessing(false)         setOutputDetails(response.data)         showSuccessToast(`Compiled Successfully!`)         console.log('response.data', response.data)         return       }     } catch (err) {       console.log("err", err);       setProcessing(false);       showErrorToast();     }   };

Чтобы получить результаты отправленного ранее кода, нужно опросить submissions с помощью token из ответа. Для этого выполняем GET-запрос к конечной точке. После получения ответа проверяем statusId === 1 || statusId === 2. Но что это значит? У нас 14 статусов, связанных с любой отправляемой в API частью кода:

export const statuses = [   {     id: 1,     description: "In Queue",   },   {     id: 2,     description: "Processing",   },   {     id: 3,     description: "Accepted",   },   {     id: 4,     description: "Wrong Answer",   },   {     id: 5,     description: "Time Limit Exceeded",   },   {     id: 6,     description: "Compilation Error",   },   {     id: 7,     description: "Runtime Error (SIGSEGV)",   },   {     id: 8,     description: "Runtime Error (SIGXFSZ)",   },   {     id: 9,     description: "Runtime Error (SIGFPE)",   },   {     id: 10,     description: "Runtime Error (SIGABRT)",   },   {     id: 11,     description: "Runtime Error (NZEC)",   },   {     id: 12,     description: "Runtime Error (Other)",   },   {     id: 13,     description: "Internal Error",   },   {     id: 14,     description: "Exec Format Error",   }, ];

Если statusId === 1 или statusId === 2, код обрабатывается, и нужно снова вызвать API и проверить, получен ли результат. Из-за этого в if прописан setTimeout(), где снова вызывается функция checkStatus(), а внутри неё снова вызывается API и проверяется статус.

Если статус не 2 или 3, выполнение кода завершено и есть результат — успешно скомпилированный код или код с превышением предела времени компиляции. А может, код с исключением времени выполнения; statusId представляет все ситуации, которые тоже можно воспроизвести.

Например, в while(true) выдаётся ошибка превышения предела времени:

Или, если допущена ошибка синтаксиса, вернётся ошибка компиляции:

Так или иначе, есть результат, который сохраняется в состоянии outputDetails, чтобы было что отображать в окне вывода, в правой части экрана.

Компонент окна вывода

import React from "react";  const OutputWindow = ({ outputDetails }) => {   const getOutput = () => {     let statusId = outputDetails?.status?.id;      if (statusId === 6) {       // compilation error       return (         <pre className="px-2 py-1 font-normal text-xs text-red-500">           {atob(outputDetails?.compile_output)}         </pre>       );     } else if (statusId === 3) {       return (         <pre className="px-2 py-1 font-normal text-xs text-green-500">           {atob(outputDetails.stdout) !== null             ? `${atob(outputDetails.stdout)}`             : null}         </pre>       );     } else if (statusId === 5) {       return (         <pre className="px-2 py-1 font-normal text-xs text-red-500">           {`Time Limit Exceeded`}         </pre>       );     } else {       return (         <pre className="px-2 py-1 font-normal text-xs text-red-500">           {atob(outputDetails?.stderr)}         </pre>       );     }   };   return (     <>       <h1 className="font-bold text-xl bg-clip-text text-transparent bg-gradient-to-r from-slate-900 to-slate-700 mb-2">         Output       </h1>       <div className="w-full h-56 bg-[#1e293b] rounded-md text-white font-normal text-sm overflow-y-auto">         {outputDetails ? <>{getOutput()}</> : null}       </div>     </>   ); };  export default OutputWindow;

Это простой компонент для отображения успеха или неуспеха компиляции. В методе getOutput() определяются вывод и цвет текста.

  • Если statusId равен 3, имеем успешный сценарий со статусом Accepted. От API возвращается stdout — Standard Output («Стандартный вывод»). Он нужен для отображения данных, возвращаемых из отправленного в API кода.

  • Если statusId равен 5, имеем ошибку превышения предела времени. Просто показываем, что в коде есть условие бесконечного цикла или превышено стандартное время выполнения кода 5 секунд.

  • Если statusId равен 6, имеем ошибку компиляции. В этом случае API возвращает compile_output с возможностью отображения ошибки.

  • При любом другом статусе получаем стандартный объект stderr для отображения ошибок.

  • Обратите внимание: используется метод atob(), потому что выходные данные — это строка в base64. Тот же метод нужен, чтобы декодировать её.

Вот успешный сценарий программы двоичного поиска на JavaScript:

Компонент вывода подробностей

Компонент OutputDetails — это простой модуль сопоставления для вывода данных, связанных с изначально скомпилированным фрагментом кода. Данные уже заданы в переменной состояния outputDetails:

import React from "react";  const OutputDetails = ({ outputDetails }) => {   return (     <div className="metrics-container mt-4 flex flex-col space-y-3">       <p className="text-sm">         Status:{" "}         <span className="font-semibold px-2 py-1 rounded-md bg-gray-100">           {outputDetails?.status?.description}         </span>       </p>       <p className="text-sm">         Memory:{" "}         <span className="font-semibold px-2 py-1 rounded-md bg-gray-100">           {outputDetails?.memory}         </span>       </p>       <p className="text-sm">         Time:{" "}         <span className="font-semibold px-2 py-1 rounded-md bg-gray-100">           {outputDetails?.time}         </span>       </p>     </div>   ); };  export default OutputDetails;

time, memory и status.description читаются из ответа от API, а затем сохраняются в outputDetails и отображаются.

События клавиатуры

И последнее — ctrl+enter для компиляции. Чтобы прослушивать в веб-приложении события клавиатуры, создаётся пользовательский хук, крутой и намного чище:

// useKeyPress.js  import React, { useState } from "react";  const useKeyPress = function (targetKey) {   const [keyPressed, setKeyPressed] = useState(false);    function downHandler({ key }) {     if (key === targetKey) {       setKeyPressed(true);     }   }    const upHandler = ({ key }) => {     if (key === targetKey) {       setKeyPressed(false);     }   };    React.useEffect(() => {     document.addEventListener("keydown", downHandler);     document.addEventListener("keyup", upHandler);      return () => {       document.removeEventListener("keydown", downHandler);       document.removeEventListener("keyup", upHandler);     };   });    return keyPressed; };  export default useKeyPress;
// Landing.js  ... ... ... const Landing = () => {     ...     ...       const enterPress = useKeyPress("Enter");       const ctrlPress = useKeyPress("Control");    ...    ... }

Здесь для прослушивания целевой клавиши нужны нативные прослушиватели событий JavaScript. События keydown и keyup прослушиваются с помощью хука. Хук инициализируется целевой клавишей Enter и Control. Проверяется targetKey === key и, соответственно, задаётся keyPressed, поэтому можно использовать возвращаемое логическое значение keyPressed — true или false.

Теперь можно прослушать эти события в хуке useEffect и убедиться, что обе клавиши нажаты одновременно:

useEffect(() => {     if (enterPress && ctrlPress) {       console.log("enterPress", enterPress);       console.log("ctrlPress", ctrlPress);       handleCompile();     }   }, [ctrlPress, enterPress]);

Метод handleCompile() вызывается, когда пользователь нажимает Ctrl и Enter последовательно или одновременно.

Что нужно учитывать

Работать было интересно, но базовый план Judge0 о.ограничен, например, сотней запросов в день. Чтобы обойти ограничения, можно поднять собственный сервер/дроплет (на Digital Ocean) и разместить проект с открытым исходным кодом на своём хостинге, документация для этого отличная.

Заключение

В итоге у нас появился:

  • редактор кода, способный компилировать более 40 языков;

  • переключатель тем;

  • API — интерактивные и размещаемые на RapidAPI;

  • прослушивание событий клавиатуры через кастомные хуки React;

  • и много всего интересного!

Хотите поработать над проектом плотнее? Подумайте над реализацией такого функционала:

  • Модуль авторизации и регистрации — для сохранения кода в собственном дашборде.

  • Способ совместного использования кода через Интернет.

  • Страница и настройки профиля.

  • Работа вдвоём над одним фрагментом кода с использованием программирования сокетов и операционных преобразований.

  • Закладки для фрагментов кода.

  • Пользовательский дашборд с сохранением, как CodePen.

Мне очень понравилось писать код этого приложения с нуля. TailwindCSS — абсолютный фаворит и любимый ресурс для стилизации приложений. Если статья оказалась полезной, оставьте звезду в репозитории GitHub. Есть вопросы? Свяжитесь со мной в Twitter и/или на сайте, буду рад помочь.

А мы поможем вам прокачать навыки или с самого начала освоить профессию, востребованную в любое время:

Выбрать другую востребованную профессию.


ссылка на оригинал статьи https://habr.com/ru/company/skillfactory/blog/670380/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *