Original article: How to Build a Code Editor with React that Compiles and Executes in 40+ Languages

Una plataforma de ejecución de código te permite escribir código en tu lenguaje de programación favorito y ejecutar ese código en la misma plataforma.

Puedes ver perfectamente una salida del programa que has escrito (por ejemplo, un programa de búsqueda binaria escrito en JavaScript).

Hoy, vamos a construir una plataforma de ejecución de código en línea llamada CodeRush el cual puede compilar y ejecutar código en más de 40+ lenguajes de programación diferentes.

¿Qué estaremos construyendo?

Screenshot-2022-05-18-at-9.05.14-PM

Código Fuente | Live Demo

Vamos a construir un rico editor de código que tiene las siguientes características:

  • Un Editor de Código (Monaco Editor) que alimenta a VS Code también.
  • Puede compilar código en una aplicación web con entrada y salida estándar con soporte a 40 lenguajes de programación.
  • Puedes cambiar el tema del editor desde una lista de temas disponibles.
  • Puedes obtener información del código ejecutado (tiempo tomado por el código, memoria usada, estado, y más).

Pila de Tecnologías

Para el proyecto, vamos a usar la siguiente pila de tecnologías:

  • React.js – Para el front-end
  • TailwindCSS – Para los estilos
  • Judge0 – Para compilar y ejecutar nuestro código
  • RapidAPI – Para desplegar código Judge0 rápidamente
  • Monaco Editor – El editor de código que alimenta el proyecto

Estructura del Proyecto

La estructura del proyecto bastante simple y fácil de entender:

  • Components: todos los componentes / porciones de código reusables están aquí (Ejemplo: CodeEditorWindow y Landing)
  • hooks: todos los custom hooks están aquí. (Vamos a usar hooks de presiones de teclas para compilar nuestro código usando los eventos del teclado)
  • lib: todas las funciones de librería están aquí. (Crearemos una función para definir nuestro tema aquí)
  • constants: todas las constantes como languageOptions y customStyles para los desplegables irán aquí.
  • utils: funciones de utilidad generales para ayudar a mantener el código van aquí.

Flujo de la aplicación:

Antes de que indaguemos en el código, entendamos el flujo de la aplicación y cómo deberíamos codificarlo desde cero.

  • Un usuario llega a la aplicación web y puede seleccionar sus lenguajes de programación preferidos (por defecto es JavaScript).
  • Una vez que el usuario termina de escribir su código, pueden compilar su código y ver la salida/resultados en la ventana de salida.
  • Verán éxito o fracaso de sus porciones de código. Todo es visible en la ventana de salida del código.
  • El usuario puede agregar entradas personalizadas para sus porciones de código, y el juez (nuestro compilador en línea) tomará en cuenta la entrada personalizada la cual el usuario provee.
  • El usuario puede ver detalles relevantes del código que fue ejecutado (Ejemplo: tomó 5ms para que el código se compile y se ejecute, 2024 kb de memoria fue usado, y el estado de tiempo de ejecución fue exitoso).

Ahora que estamos un poco más familiar con la estructura de carpetas y el flujo de la aplicación, indaguemos en el código y entendamos cómo funciona todo.

Cómo construir el componente editor de código

Screenshot-2022-05-18-at-9.26.48-PM

Primeramente, el componente editor de código está formado por el Editor Monaco, el cual es un paquete de NPM que podemos usar y personalizar.

// 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;

El componente Editor viene del paquete @monaco-editor/react que nos permite poner en marcha un editor de código con una altura de 85vh como se especifica.

El componente Editor toma un par de props:

  • language: el lenguaje por el cual necesitamos resaltado de sintaxis e intellisense
  • theme: Los colores y fondos de la porción del código (lo configuraremos en la última parte del tutorial)
  • value: El valor del código actual que va en el editor de código
  • onChange: esto se dispara cuando el valor en el editor de código cambia. Necesitamos guardar el valor modificado en el estado así podemos, más adelante, llamar a la API Judge0 para compilarlo

El editor recibe los props onChange, language, code, y theme de su componente padre, el cual es Landing.js. Cada vez que el value en el editor de código cambia, llamamos al manejador onChange que está presente en el componente padre Landing.

Cómo construir el componente landing

El componente landing consiste mayormente de 3 partes:

  • El Actions Bar el cual tiene los componentes Languages y los desplegables Themes.
  • El componente Code Editor Window
  • Los componentes Output and 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 = `// algún comentario`;

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("Opción seleccionada...", 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("caso no manejado!", action, data);
      }
    }
  };
  const handleCompile = () => {
    // Haremos la implementación luego en el código
  };

  const checkStatus = async (token) => {
    // Haremos la implementación luego en el código
  };

  function handleThemeChange(th) {
    // Haremos la implementación luego en el código
  }
  useEffect(() => {
    defineTheme("oceanic-next").then((_) =>
      setTheme({ value: "oceanic-next", label: "Oceanic Next" })
    );
  }, []);

  const showSuccessToast = (msg) => {
    toast.success(msg || `Compilado satisfactoriamente!`, {
      position: "top-right",
      autoClose: 1000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true,
      draggable: true,
      progress: undefined,
    });
  };
  const showErrorToast = (msg) => {
    toast.error(msg || `¡Algo salió mal! Por favor intenta otra vez.`, {
      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 ? "Procesando..." : "Compilar y ejecutar"}
            </button>
          </div>
          {outputDetails && <OutputDetails outputDetails={outputDetails} />}
        </div>
      </div>
      <Footer />
    </>
  );
};
export default Landing;

Entendamos la estructura básica de la página landing en más detalle.

Componente CodeEditorWindow

Como lo vimos antes, el componente CodeEditorWindow tomará en cuenta el código (el cual sigue cambiando) y un método onChange el cual mantendrá un registro de los cambios del código.

// implementación del método onChange

 const onChange = (action, data) => {
    switch (action) {
      case "code": {
        setCode(data);
        break;
      }
      default: {
        console.warn("caso no manejado!", action, data);
      }
    }
  };

Simplemente establecemos el estado del code y hacemos un registro de los cambios.

El componente CodeEditorWindow también toma en cuenta la prop language, el cual es el actual lenguaje seleccionado que necesitamos para el resaltado de sintaxis y para intellisense.

He creado un arreglo languageOptions el cual mantiene un registro de las props de lenguajes aceptados por el Editor Monaco y también maneja la compilación (mantenemos un registro del languageId que es aceptado por las APIs de 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",
  },
];

Cada objeto languageOptions contiene las claves id, name, label y value. El arreglo entero de languageOptions puede ser tomado y puesto dentro del despegable y suplirlos como opciones.

Cuando el estado del despegable cambie, el método onSelectChange mantiene un registro del id seleccionado y cambia el estado apropiadamente.

Componente LanguageDropdown

Screenshot-2022-05-19-at-10.42.43-PM
// 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;

Para el despegable, vamos a usar el paquete react-select que se encarga de los despegables y sus manejadores de cambio.

React select toma defaultValue y options como los parámetros mayores. options es un arreglo (y vamos a pasar languageOptions aquí) que automáticamente muestra todos los valores relevantes del despegable.

La prop defaultValue es el valor por defecto que es provisto al componente. Vamos a mantener a JavaScript como el lenguaje por defecto (el cual es el primero de nuestro arreglo de lenguajes).

Cuando sea que el usuario cambie el lenguaje, cambiamos el lenguaje con el callback onSelectChange:

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

Componente ThemeDropdown

Screenshot-2022-05-19-at-10.42.43-PM-1

El componente ThemeDropdown en realidad es muy similar al componente LanguageDropdown (con la UI y el paquete 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;

Aquí, vamos a usar el paquete monacoThemes para que nos ayude con los diferentes temas preciosos que están disponibles en el internet para el Editor Monaco.

Tenemos una lista de temas disponibles a nuestra disposición.

// 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 };

El paquete monaco-themes nos provee con un montón de temas que podemos usar para definir cómo va a lucir nuestro editor de código.

La función defineTheme se encarga de los diferentes temas de donde el usuario podría seleccionar. La función defineTheme regresa una promesa que en realidad establece el tema del editor Monaco usando la acción monaco.editor.defineTheme(theme, themeData). Esta línea de código es responsable en sí de cambiar los temas dentro de una ventana de código del Editor Monaco.

La función defineTheme es llamada con la ayuda del callback onChange que vimos anteriormente en el componente ThemeDropdown.js.

// Landing.js - función handleThemeChange()

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));
    }
  }
  

La función handleThemeChange() verifica si el tema es light o dark. Estos temas por defecto están disponibles en el componente MonacoEditor y no necesitamos llamar al método defineTheme() para esto.

Si no, simplemente llamamos al componente defineTheme() y ponemos el estado del tema seleccionado.

Screenshot-2022-05-19-at-10.46.34-PM

Cómo compilar el código con Judge0

Adentrémonos a la parte sustanciosa de la aplicación – el cual es compilar el código con diferentes lenguajes.

Para compilar nuestro código, vamos a usar Judge0. Judge0 es un sistema simple de ejecución de código, de código abierto, con el que podemos interactuar.

Podemos hacer una simple llamada API con algunos parámetros (código de fuente, ID de lenguaje) y obtenemos la salida como una respuesta.

Configuremos Judge0 y sigamos con los siguientes pasos:

  • Dirígete a Judge0 y elige el Plan Básico
  • Judge0 está hospedado en realidad en RapidAPI. Ve y suscríbete al plan básico.
  • Una vez que estés suscrito, puedes copiar el RAPIDAPI_HOST y la RAPIDAPI_KEY los cuales son necesarios para hacer las llamadas a la API para nuestro sistema de ejecución de código.

El panel luce algo así:

Untitled-design

Los parámetros X-RapidAPI-Host y X-RapidAPI-Key serán requeridos para nuestras llamadas a la API. Guárdalo para un uso futuro en el archivo .env así:

REACT_APP_RAPID_API_HOST = YOUR_HOST_URL
REACT_APP_RAPID_API_KEY = YOUR_SECRET_KEY
REACT_APP_RAPID_API_URL = YOUR_SUBMISSIONS_URL

Es importante en React que inicialicemos nuestras variables de entorno con el prefijo REACT_APP.

La SUBMISSIONS_URL es la URL que vamos a usar. Básicamente consiste de tu host , seguido de una ruta /submission.

Por ejemplo: https://judge0-ce.p.rapidapi.com/submissions será la URL submissions en nuestro caso.

Una vez que tenemos las variables configuradas correctamente podemos continuar adelante y manejar la lógica de compilation.

Flujo de Compilación y Lógica

El flujo de compilación es como sigue:

  • Cuando se hace clic en el botón Compile and Execute, el método handleCompile() es llamado.
  • La función handleCompile() llama a nuestro backend Judge0 RapidAPI en el URL submissions con languageId , source_code, y stdin (customInput en nuestro caso) como parámetros del cuerpo.
  • Los options también toman el host y el secret como encabezados.
  • base64_encoded y fields son parámetros opcionales que pueden ser pasados.
  • La petición POST submission registra nuestra petición en el servidor y crea un proceso. La respuesta de la petición post es un token que puede ser usado más adelante para verificar el estado de nuestra ejecución. (Hay varios estados – Procesando, Aceptado, Límite de Tiempo Excedido, Excepciones de Tiempo de Ejecución y más.)
  • Una vez que nuestros resultados son regresados, podemos verificar condicionalmente si los resultados son exitosos o no y mostrar los resultados en nuestras pantallas.

Adentrémonos en el código y entendamos el método handleCompile().

const handleCompile = () => {
    setProcessing(true);
    const formData = {
      language_id: language.id,
      // codifica el código fuente en 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);
      });
  };

Como se ve arriba, el método handleCompile() toma languageId, source_code y stdin. Nota el btoa antes de source_code y stdin. Esto es para codificar nuestras cadenas a base64 ya que estamos usando base64_encoded: true en nuestros parámetros para la API.

Cuando hay una respuesta exitosa y tenemos un token, llamamos al método checkStatus() para sondear la ruta /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;

      // Procesado - tenemos un resultado
      if (statusId === 1 || statusId === 2) {
        // todavía procesando
        setTimeout(() => {
          checkStatus(token)
        }, 2000)
        return
      } else {
        setProcessing(false)
        setOutputDetails(response.data)
        showSuccessToast(`Compilado satisfactoriamente!`)
        console.log('response.data', response.data)
        return
      }
    } catch (err) {
      console.log("err", err);
      setProcessing(false);
      showErrorToast();
    }
  };

Para obtener los resultados de nuestro código que enviamos anteriormente, necesitamos sondear a la API submissions con un token que recibimos como respuesta.

Como vimos arriba, vamos a hacer una petición GET al endpoint. Una vez que tengamos una respuesta, vamos a verificar  statusId === 1 || statusId === 2. Pero ¿Qué significa?

Tenemos un total de 14 estados asociados con cualquier porción de código que enviamos a la API, y esos son:

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",
  },
];

Así que, si  statusId ===1 o statusId ===2 significa que nuestro código todavía está procesando y necesitamos llamar a la API de nuevo para verificar si obtenemos algún resultado o no.

Por esto, tenemos un setTimeout() en la condición if que llama a la función checkStatus() otra vez, el cual internamente llama a la API nuevamente y verifica el estado.

Si el estado es algo más que 2 o 3 significa que la ejecución de nuestro código ha sido completada y tenemos un resultado. Sea un código successfully compiled o un código Time Limit Exceeded – o tal vez sea un Runtime Exception. El statusId representa cada escenario y podemos replicarlos también.

Por ejemplo, while(true) nos dará un error time limit exceeded:

Screenshot-2022-05-20-at-1.33.08-AM

O si hacemos un error en la sintaxis, obtendremos un error de compilación:

Screenshot-2022-05-20-at-1.34.42-AM

De cualquier forma, vamos a obtener un resultado. Y vamos a almacenar este resultado en nuestro estado outputDetails. Esto es para asegurar que tenemos algo para mostrar en la parte derecha de la pantalla (el cual es la Pantalla de Salida).

Componente Output Window

Screenshot-2022-05-20-at-1.37.39-AM
import React from "react";

const OutputWindow = ({ outputDetails }) => {
  const getOutput = () => {
    let statusId = outputDetails?.status?.id;

    if (statusId === 6) {
      // error de compilación
      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;

Este es un componente directo que solamente muestra los escenarios apropiados de éxito o falla.

El método getOutput() determinará como se verá el color del texto y qué debería ser impreso.

  • Si statusId es 6 – Tenemos un error de compilación. Para ello, la API regresa un compile_output que puede ser usado para mostrar el error.
  • Si statusId es 3 – Tenemos un escenario de éxito, el cual es Accepted. La API regresa un stdout el cual significa Salida Estándar (Standard Output). Esto es usado para mostrar los datos el cual es devuelto (returned) del código que suplimos a la API.
  • Si statusId es 5 – Tenemos un error Time Limit Exceeded. Simplemente mostramos que hay una condición de bucle infinito en el código o si excede el tiempo estándar de 5 segundos para la ejecución del código.
  • Para cualquier otro estado, vamos a obtener un objeto estándar stderr que podemos usar para mostrar los errores.
  • Nota el método atob() en uso. Esto es porque obtenemos la salida como una cadena base64. Para decodificarlo, usamos el método atob().

Aquí hay un escenario de éxito de un programa Binary Search en JavaScript:

Screenshot-2022-05-20-at-1.42.55-AM

Componente Output Details

Screenshot-2022-05-20-at-1.44.01-AM

El componente OutputDetails es un "mapeador" simple para mostrar los detalles asociados con un pedazo de código que inicialmente compilamos. Los datos ya están puestos en la variable de estado 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;

Los atributos time, memory y status.description todos son recibidos de la respuesta de la API los cuales luego son almacenados en outputDetails y mostrados.

Eventos del Teclado

Lo último en la aplicación es usar ctrl+enter para compilar nuestro código. Para esto, vamos a crear un hook personalizado (porque son increíbles y más limpios) para escuchar varios eventos del teclado en nuestra aplicación web.

// 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");
   ...
   ...
}

Aquí, estamos usando los eventos nativos de JavaScript Event Listeners para escuchar nuestra clave target el cual vamos a usar.

El Hook escucha a los eventos keydown y keyup. Inicializamos nuestro hook con una clave target de Enter y Control.

Ya que estamos verificando si el targetKey === key y estableciendo keyPressed respectivamente, podemos usar el booleano keyPressed regresado, el cual será true o false.

Ahora, podemos escuchar a estos eventos en el hook useEffect para asegurarnos de que ambos fueron presionados al mismo tiempo:

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

Así que cuando sea que el usuario apriete control y enter uno después del otro o al mismo tiempo, el método handleCompile() será llamado.

Algunas cosas a tener en mente

Este fue un proyecto divertido con el cual trabajar. Pero el plan básico de Judeg0 tiene algunas limitaciones, a saber 100 peticiones por día.

Para solucionar esto, podrías crear tu propio servidor / droplet (en Digital Ocean) y auto alojar el proyecto de código abierto (tienen una documentación excelente para eso).

Conclusión

Al final, tenemos:

  • Un editor de código que puede compilar a más de 40+ lenguajes
  • Un conmutador de temas para cambiar la apariencia de nuestro editor de código
  • Interactuar y Alojar APIs en RapidAPI
  • Usar eventos del teclado en React usando hooks personalizados
  • ¡Mucha y mucha diversión! ;)

Finalmente, si quieres profundizar en el proyecto, aquí hay algunas de las características que podrías considerar para implementar:

  • Módulos de inicio de sesión y registro – así puedes guardar tu código en tu propio panel personal.
  • Una forma de compartir el código con otras personas por internet.
  • Página de perfil y personalizaciones.
  • Programación en pareja en una porción de código único usando programación de sockets y transformaciones operacionales.
  • Marcar porciones de código como favoritos.
  • Panel personalizado de tus pedazos de código (que están guardados) - justo como CodePen.

Me encantó realmente codificar esta aplicación desde cero, y TailwindCSS es mi recurso favorito absoluto para estilar mis aplicaciones.

Si este artículo te fue de utilidad, deja un ⭐️ en el repositorio de GitHub.
Si tienes alguna pregunta, por favor contáctame por mi Twitter y/o Sitio web y me encantaría ayudarte.

Código Fuente | Demo en vivo