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

Kate

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

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

9eb5261734517a650b094fdd600865dd.png

Создадим функциональный редактор кода 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 Кб памяти, выполнение кода завершено успешно).
Ознакомившись со структурой каталогов проекта и логикой работы с приложением, перейдём к коду и разберёмся, как тут всё организовано.

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

5d0c165af33081f618e27fa1b8b29a00.png

Компонент редактора кода состоит из 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​

24c9c40ca264d229aa495d8f7957a014.png
// 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​

e6511aee591b890be6a398999c685525.png

Компонент 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() и задаём состояние выбранной темы.

fcbe8470de5dfea34659824ae55f2396.png

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

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

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

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

  • переходим к Judge0 и выбираем базовый план;
  • на самом деле Judge0 размещён на RapidAPI (идём дальше и подписываемся на базовый план);
  • после этого можно скопировать RAPIDAPI_HOST и RAPIDAPI_KEY (для выполнения вызовов API в систему выполнения кода).
Дашборд выглядит так:

b992c68de3a228b522b305a969741e3d.png

Для вызовов 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) выдаётся ошибка превышения предела времени:

5bc57a3bfa70cf310d594c6a7a8f17b9.png

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

84e2cc42c9310d86b4f53cd515d019de.png

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

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

6157b8e8380757530de7f55b316cd822.png
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:

ae1daf1e58992942f892ea33ae419436.png

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

2b307ef954a3f7f27f604ceca225221d.png

Компонент 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.
 
Сверху