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?
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
ycustomStyles
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
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 intellisensetheme
: 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ódigoonChange
: 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 APIJudge0
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 componentesLanguages
y los desplegablesThemes
. - 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
// 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
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.
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 laRAPIDAPI_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í:
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étodohandleCompile()
es llamado. - La función
handleCompile()
llama a nuestro backendJudge0 RapidAPI
en el URLsubmissions
conlanguageId
,source_code
, ystdin
(customInput en nuestro caso) como parámetros del cuerpo. - Los
options
también toman elhost
y elsecret
como encabezados. base64_encoded
yfields
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ónpost
es untoken
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
:
O si hacemos un error en la sintaxis, obtendremos un error de compilación:
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
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
es6
– Tenemos un error de compilación. Para ello, la API regresa uncompile_output
que puede ser usado para mostrar el error. - Si
statusId
es3
– Tenemos un escenario de éxito, el cual esAccepted
. La API regresa unstdout
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
es5
– 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 de5
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étodoatob()
.
Aquí hay un escenario de éxito de un programa Binary Search
en JavaScript:
Componente Output Details
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.