How to create a code editor for 40+ languages ​​with React


We share the details of developing an online platform for executing and compiling code in more than 40 languages ​​before the start Frontend development course. The author of this article is the founder of TailwindMasterKit.


The online code execution platform allows you to write and immediately run code in your favorite programming language. Ideally, you can see the output of a program such as a JavaScript binary search.

Demonstrations

Let’s create a functional code editor Monaco Editor. Here are its capabilities:

  • support for VS Code;

  • compilation in a web application with standard input and output and support for more than 40 languages;

  • selecting an editor theme from the list of available themes;

  • information about the code (execution time, memory used, status, etc.).

Technology stack

Project structure

The project structure is simple:

  • components: components / code snippets (for example, CodeEditorWindow and Landing);

  • hooks: custom hooks (and keystroke hooks for compiling code using keyboard events);

  • lib: library functions (here we will create a theme definition function);

  • constants: constants, such as languageOptions and customStyles, for dropdowns;

  • utils: utility functions for code maintenance.

Application logic

Before moving on to the code, let’s understand the logic of working with the application and how to write code for it from scratch.

  • The user enters the web application and selects a language (JavaScript by default).

  • After writing the code, the user compiles it, and views the output in the output window.

  • In the code output window, you will see the output and status of the code.

  • The user can add his input data to the code fragments, which are taken into account in the judge (online compiler).

  • The user can see information about the executed code (example: it took 5 ms to compile and execute, 2024 KB of memory was used, code execution completed successfully).

Having familiarized ourselves with the structure of the project directories and the logic of working with the application, let’s move on to the code and figure out how everything is organized here.

How to create a code editor component

The code editor component consists of Monaco Editor, which is a custom NPM package:

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

The Editor components come from the @monaco-editor/react package, which allows you to expand the code editor with an appropriate viewport height of 85vh.

The Editor component accepts many properties:

  • language: The language for which you want syntax highlighting and input completion.

  • theme: The colors and background of the code snippet (we’ll set it up later).

  • value: the code that is entered into the editor.

  • onChange: occurs when the value changes in the editor. The changed value needs to be saved in state so that the Judge0 API can be called later for compilation.

The editor gets the onChange, language, code, and theme properties of the parent Landing.js component. When the value property changes in the editor, we call the onChange handler from the parent Landing component.

How to create a Landing component

The landing component consists of three parts:

  • Actions Bar with Languages ​​and Themes drop-down list components.

  • Code Editor Window component.

  • Output and Custom Input components.

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

Let’s take a closer look at the basic Landing structure.

CodeEditorWindow Component

As we have already seen, the CodeEditorWindow component takes into account constantly changing code and the onChange method, which tracks changes to the code:.

// onChange method implementation

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

Set the code state and track changes.

The CodeEditorWindow component also respects the language property, which is the currently selected language for which syntax highlighting and input completion are required.

I created the languageOptions array to keep track of the language properties accepted in the Monaco Editor, as well as to work with compilation (we keep track of the languageId accepted in these judge0 APIs):

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

Each languageOptions object has id, name, label, and value properties. The languageOptions array is placed in the dropdown list and provided as options.

When the state of the dropdown list changes, the selected id is tracked in the onSelectChange method with the corresponding state change.

LanguageDropdown Component

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

For dropdown lists and their change handlers, the package is used react-select.

The main parameters of react-select are defaultValue and the options array (here we will pass languageOptions), with the help of which all these values ​​of the drop-down list are automatically displayed.

The defaultValue property is the default value specified in the component. The default language will be the first language in the array of languages ​​- JavaScript.

When the user changes language, it does so with onSelectChange:

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

ThemeDropdown Component

The ThemeDropdown component is very similar to LanguageDropdown (with UI and react-select package):

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

Here, we use the monacoThemes package to select beautiful themes from the list below available in Monaco Editor:

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

There are a lot of topics in monaco-themes, so the appearance of the future editor is not a problem.

Themes are selected by the defineTheme function, a promise is returned in it, through which the editor’s theme is set using the monaco.editor.defineTheme(theme, themeData) action. The very change of themes inside the Monaco Editor code window happens in this line of code.

The defineTheme function is called using the onChange callback we already saw in the ThemeDropdown.js component:

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

In the handleThemeChange() function, the theme is checked: light (light) or dark (dark). These themes are available in the MonacoEditor component by default – you don’t need to call the defineTheme() method.

If there are no themes in the list, we call the defineTheme() component and set the state of the selected theme.

How to compile code with Judge0

Let’s move on to the most “delicious” part of the application – compiling code in different languages, for which we use Judge0 – an interactive code execution system.

You can make an API call with arbitrary parameters (source code, language ID) and receive output data in response.

Setting up Judge0:

  • go to Judge0 and choose a basic plan;

  • actually Judge0 is hosted on RapidAPI (go ahead and subscribe to the basic plan);

  • after that, you can copy RAPIDAPI_HOST and RAPIDAPI_KEY (to make API calls to the code execution system).

The dashboard looks like this:

API calls require X-RapidAPI-Host and X-RapidAPI-Key parameters. Save them in .env files:

REACT_APP_RAPID_API_HOST = YOUR_HOST_URL
REACT_APP_RAPID_API_KEY = YOUR_SECRET_KEY
REACT_APP_RAPID_API_URL = YOUR_SUBMISSIONS_URL

In React, it is important to initialize environment variables with the REACT_APP prefix.

We will use the SUBMISSIONS_URL from the host and /submission route.

For example, https://judge0-ce.p.rapidapi.com/submissions would be the submissions URL in our case.

After setting the variables, we move on to the compilation logic.

Compilation Logic and Sequence

The compilation sequence is as follows:

  • Button press Compile and Execute calls the handleCompile() method.

  • The handleCompile() function calls the Judge0 RapidAPI backend at the submissions URL, specifying languageId, source_code, and stdin as request parameters – in our case, customInput.

  • Options also accepts host and secret as headers.

  • Additional parameters base64_encoded and fields can be passed.

  • When sending a submission POST request, our request is registered on the server and a process is created. The response to the POST request is a token required to check the execution status (Processing, Accepted, Time Limit Exceeded, Runtime Exceptions, etc.).

  • On return, you can test whether the results were successful using conditions, and then display the results in the output window.

Let’s analyze the handleCompile() method:

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

It accepts languageId, source_code and stdin. Notice the btoa before source_code and stdin. This is needed to encode strings in base64 format, because we have base64_encoded: true in our API request parameters.

If a successful response is received and there is a token, we call the checkStatus() method to poll the /submissions/${token} route:

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

To get the results of the code submitted earlier, we need to poll the submissions using the token from the response. To do this, we execute a GET request to the endpoint. After receiving the response, check statusId === 1 || statusId === 2. But what does that mean? We have 14 statuses associated with any piece of code sent to the 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",
  },
];

If statusId === 1 or statusId === 2, the code is processed and the API needs to be called again to check if the result is returned. Because of this, setTimeout() is written in if, where the checkStatus() function is called again, and inside it the API is called again and the status is checked.

If the status is not 2 or 3, code execution is complete and the result is successfully compiled code or code that exceeded the compile time limit. Or maybe code with a runtime exception; statusId represents all situations that can also be reproduced.

For example, while(true) throws a timeout error:

Or, if a syntax error is made, a compilation error will be returned:

One way or another, there is a result that is stored in the outputDetails state so that there is something to display in the output window, on the right side of the screen.

Output window component

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;

This is a simple component to display the success or failure of a compilation. The getOutput() method defines the output and text color.

  • If statusId is equal to 3, we have a successful scenario with the Accepted status. The API returns stdout – Standard Output. It is needed to display the data returned from the code sent to the API.

  • If statusId is 5, we have a timeout error. We simply show that the code has an infinite loop condition or the standard code execution time of 5 seconds has been exceeded.

  • If statusId is equal to 6, we have a compilation error. In this case, the API returns compile_output with the option to display an error.

  • For any other status, we get a standard stderr object for displaying errors.

  • Note that the atob() method is used because the output is a base64 string. The same method is needed to decode it.

Here is a successful JavaScript binary search script:

Detail output component

The OutputDetails component is a simple mapper for outputting data related to a natively compiled piece of code. The data is already set in the outputDetails state variable:

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 and status.description are read from the response from the API and then stored in outputDetails and displayed.

Keyboard events

And the last one is ctrl+enter to compile. To listen to keyboard events in a web application, a custom hook is created, which is cool and much cleaner:

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

Here, native JavaScript event listeners are needed to listen to the target key. The keydown and keyup events are listened for with a hook. The hook is initialized with the target key Enter and Control. The targetKey === key is checked and keyPressed is set accordingly, so the boolean return value of keyPressed – true or false – can be used.

Now we can listen to these events in the useEffect hook and make sure both keys are pressed at the same time:

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

The handleCompile() method is called when the user presses Ctrl and Enter either sequentially or simultaneously.

What to Consider

It was fun to work with, but Judge0’s basic plan is o.limited to, say, 100 requests per day. To get around the restrictions, you can set up your own server / droplet (on Digital Ocean) and host the open source project on your hosting, the documentation for this is excellent.

Conclusion

As a result, we got:

  • code editor capable of compiling over 40 languages;

  • theme switcher;

  • APIs – interactive and hosted on RapidAPI;

  • listening to keyboard events through custom React hooks;

  • and a lot of interesting things!

Want to work harder on a project? Consider implementing something like this:

  • Authorization and registration module – to save the code in your own dashboard.

  • A way to share code over the Internet.

  • Page and profile settings.

  • Working together on a single piece of code using socket programming and operational transformations.

  • Bookmarks for code snippets.

  • Custom dashboard with saving like CodePen.

I really enjoyed writing the code for this application from scratch. TailwindCSS is an absolute favorite and favorite resource for styling apps. If the article was helpful, leave a star in GitHub repositories. Have questions? Contact me at Twitter and/or on websiteI’ll be glad to help.

And we will help you to upgrade your skills or from the very beginning to master a profession that is in demand at any time:

Choose another in-demand profession.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *