Подробностями разработки онлайн-платформы выполнения и компиляции кода более чем на 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 и/или на сайте, буду рад помочь.

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

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

Комментарии (2)


  1. raamid
    11.06.2022 15:50
    +1

    Очень круто, спасибо! Наконец-то на Реакте не просто какой-то TODO list а что-то серьезное. А то на мелком проекте не очень хорошо чувствуется философия React, а для меня, например это очень важно.


  1. raamid
    12.06.2022 18:08
    +2

    За что минусы, господа? Я в настоящее время самостоятельно изучаю Реакт. Туториалов много, но все они крутятся вокруг простенького интерфейса. Я эти туториалы могу сделать без всякого Реакта, гораздо быстрее и с меньшим количеством кода. Т.е. для моей существующей практики Реакт вроде бы не нужен. Вместе с тем, огромное количество вакансий требуют этот самый Реакт и значит это для меня важно. Но я не чувствую его нужность с точки зрения программной архитектуры и это мешает учиться.

    И вот я вижу большой проект на Реакте с детальными объяснениями. Я признателен автору и написал об этом.