Creating a code snippet generator in react js

Hello guys , in this article we will be building a simple version of a code snippet generator which can be extended further for more features, so without a further ado let's begin

Libraries

  1. react-simple-code-editor For having a simple code editor like text box which can will provide us a editor where we can write our code

  2. tailwind css For styling

  3. prism-js For code highlighting

  4. html-to-image For exporting our image , basically converting html dom elements into a image of various formats (i.e JPEG, PNG, SVG)

  5. downloadjs It will handle download of the image generated

We will be using vite for creating our react app , so let's get started

Starting with Initializing our project

Create your Vite React app and install tailwind css in it , It is very Simple you can do it on your own 🙂.

Since we will be having multiple components and data may be used between different components we will use React Context API for storing that data as context

StyleContext.tsx

import { createContext, RefObject, useRef, useState } from "react";

// Style interface to define the properties for our context
export interface Style {
    backgroundColor: string | null;
    backgroundGradient: string;
    codeEditorColor: string;
    primaryTextColor: string;
    primaryTextFont: string;
    width: number;
    rotation?: number;
    positionX: number;
    positionY: number;
    height: number;
    dropShadow: string;
    parentHeight: number;
    parentWidth: number;
}

// Props for the context
interface StyleContextProps {
    style: Style;
    setStyle: (style: Style) => void;
    childRef?: RefObject<HTMLDivElement>;
    parentRef?: RefObject<HTMLDivElement>;
}

// Default style values
const defaultStyle: Style = {
    backgroundColor: null,
    backgroundGradient: 'linear-gradient(to right, red, orange, yellow, green, blue)',
    codeEditorColor: '#282C34FF',
    primaryTextColor: 'white',
    primaryTextFont: '"Fira code", "Fira Mono", monospace',
    height: 300,
    dropShadow: 'rgb(38, 57, 77) 0px 20px 30px -10px',
    positionX: 100,
    positionY: 100,
    rotation: 0,
    width: 300,
    parentHeight: 700,
    parentWidth: 700,
};

// Create a context with default values
export const StyleContext = createContext<StyleContextProps>({
    style: defaultStyle,
    setStyle: (style: Style) => {
        console.warn("setStyle called outside of StyleContextProvider");
    },
});

// Context provider component
const StyleContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
    const [style, setStyle] = useState<Style>(defaultStyle);

    const childRef = useRef<HTMLDivElement>(null);
    const parentRef = useRef<HTMLDivElement>(null);

    return (
        <StyleContext.Provider value={{ style, setStyle, childRef, parentRef }}>
            {children}
        </StyleContext.Provider>
    );
};

export default StyleContextProvider;

Now we have to use this context and we will make all components where we will need the access of that context children of this provider

main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import StyleContextProvider from './context/StyleContext'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
  <StrictMode>
   <StyleContextProvider>
   <App/>
   </StyleContextProvider>
  </StrictMode>,
)

What we have done here is made our entire App Component children of the StyleContextProvider so the context will be accessible in the entire app , you can use it according to your use case.

Now the fun part begins let's code our editor

Container.tsx

import { useContext, useEffect, useRef, useState } from 'react';
import Editor from 'react-simple-code-editor';

//@ts-ignore
import { highlight, languages } from 'prismjs/components/prism-core';
import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-java';
import 'prismjs/themes/prism.css';
import '../App.css';

import { StyleContext } from '../context/StyleContext';

const CodeContainer = ({ onMouseDown }: { onMouseDown: any }) => {
  const [code, setCode] = useState(`function add(a, b) {\n  return a + b;\n}`);

  // Function to set the color of the header to a lighter version of the selected background color of the code editor
  function lightenColor(hex: string, percent: number) {
    // Convert hex to RGB
    let r = parseInt(hex.slice(1, 3), 16);
    let g = parseInt(hex.slice(3, 5), 16);
    let b = parseInt(hex.slice(5, 7), 16);

    // Increase each color component by the percentage
    r = Math.min(255, Math.floor(r + (255 - r) * (percent / 100)));
    g = Math.min(255, Math.floor(g + (255 - g) * (percent / 100)));
    b = Math.min(255, Math.floor(b + (255 - b) * (percent / 100)));

    // Convert back to hex
    const newHex = `#${r.toString(16).padStart(2, "0")}${g
      .toString(16)
      .padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;

    return newHex;
  }

  const fileNameRef = useRef<HTMLDivElement>(null);
  const [fileName, setFileName] = useState('untitled');
  const [isEditingFileName, setIsEditingFileName] = useState(false);

  // Managing the line numbers
  const [lines, setLines] = useState(1);
  const { style } = useContext(StyleContext);

  // Updating the line numbers on change in code
  useEffect(() => {
    setLines(code.split('\n').length);
  }, [code]);

  return (
    <div
      id="main"
      className="h-full w-full grid overflow-y-clip"
      style={{ gridTemplateRows: 'auto 1fr' }}
    >
      <div
        className="head row-span-1 max-h-[150px]"
        style={{ backgroundColor: lightenColor(style.codeEditorColor, 10) }}
        onMouseDown={fileNameRef.current !== document.activeElement ? onMouseDown : () => {}}
      >
        <div
          className="file-name"
          style={{ backgroundColor: style.codeEditorColor }}
          onMouseDown={(e) => e.stopPropagation()}
          ref={fileNameRef}
        >
          <input
            type="text"
            className="bg-transparent w-auto font-bold text-sm px-2 focus:outline-none"
            value={fileName}
            onChange={(e) => setFileName(e.target.value)}
          />
        </div>
        <div className="plus-icon">+</div>
        <div className="t-dots">
          <div className="red-dot"></div>
          <div className="yellow-dot"></div>
          <div className="green-dot"></div>
        </div>
      </div>

      <div className="code-container flex-grow">
        <div
          className="line-numbers"
          style={{
            backgroundColor: style.codeEditorColor,
            textAlign: 'left',
            paddingRight: '10px',
            paddingLeft: '10px',
            color: '#888',
            userSelect: 'none',
            fontFamily: 'monospace',
          }}
        >
          {Array.from({ length: lines }, (v: any, index) => (
            <div key={index} className="mt-2">
              <div
                style={{
                  fontSize: '17.5px',
                  maxHeight: '20px',
                  paddingTop: '1px',
                  paddingBottom: '1px',
                }}
              >
                {index + 1}
              </div>
            </div>
          ))}
        </div>

        <div className="editor" style={{ backgroundColor: style.codeEditorColor }}>
          <Editor
            value={code}
            maxLength={200}
            onValueChange={(code) => setCode(code)}
            highlight={(code) => highlight(code, languages.java)}
            padding={5}
            style={{
              backgroundColor: style.codeEditorColor,
              fontFamily: style.primaryTextFont,
              color: style.primaryTextColor,
            }}
          />
        </div>
      </div>
    </div>
  );
};

export default CodeContainer;

CodeContainer.tsx

import { useContext, useEffect, useRef, useState } from 'react'

import Editor from 'react-simple-code-editor';

//@ts-ignore
import { highlight, languages } from 'prismjs/components/prism-core';

import 'prismjs/components/prism-clike';

import 'prismjs/components/prism-java';

import 'prismjs/themes/prism.css';

import '../App.css'

import { StyleContext } from '../context/StyleContext';

const CodeContainer = ({onMouseDown}:{onMouseDown:any}) => {

    const [code, setCode] = useState(

        `function add(a, b) {\n  return a + b;\n}`

      );
/* function to set the color of the header to a lighter version of the selected background color of the code editor */

      function lightenColor(hex:string, percent:number) {

        // Convert hex to RGB

        let r = parseInt(hex.slice(1, 3), 16);

        let g = parseInt(hex.slice(3, 5), 16);

        let b = parseInt(hex.slice(5, 7), 16);

        // Increase each color component by the percentage

        r = Math.min(255, Math.floor(r + (255 - r) * (percent / 100)));

        g = Math.min(255, Math.floor(g + (255 - g) * (percent / 100)));

        b = Math.min(255, Math.floor(b + (255 - b) * (percent / 100)));

        // Convert back to hex

        const newHex = `#${r.toString(16).padStart(2, "0")}${g

          .toString(16)

          .padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;

        return newHex;

      }

    const fileNameRef = useRef<HTMLDivElement>(null)

    const [fileName,setFileName] = useState('untitled');

    const [isEditingFileName,setIsEditingFileName] = useState(false);

// managing the line numbers
      const [lines,setLines] = useState(1);
     const {style} = useContext(StyleContext);

// updating the line numbers on change in code
      useEffect(()=>{
        setLines(code.split('\n').length);
      },[code])

  return (
    <div id='main' className='h-full w-full grid overflow-y-clip'style={{ gridTemplateRows: "auto 1fr" }} >
    <div className='head row-span-1 max-h-[150px]' style={{backgroundColor:lightenColor(style.codeEditorColor,10)}} onMouseDown={fileNameRef.current!==document.activeElement?onMouseDown:()=>{}}>

  <div className='file-name' style={{backgroundColor:style.codeEditorColor}} onMouseDown={(e)=>{e.stopPropagation()}} ref={fileNameRef} >

{ <input type='text' className='bg-transparent w-auto font-bold text-sm px-2 focus:outline-none' value={fileName} onChange={(e)=>{setFileName(e.target.value)}}></input>}

  </div>

  <div className='plus-icon'>+</div>

  <div className='t-dots'>

    <div className='red-dot'></div>

    <div className='yellow-dot'></div>

    <div className='green-dot'></div>

  </div>

</div>

<div className='code-container flex-grow'>

  <div className='line-numbers ' style={

    {  backgroundColor:style.codeEditorColor,

        textAlign: "left",

        paddingRight: "10px",

        paddingLeft:"10px",

        color: "#888",

        userSelect: "none",

        fontFamily: "monospace",

    }

  }>

{

 Array.from({length:lines},(v:any,index)=>(

  <div key={index}className='mt-2' ><div  style={{fontSize:"17.5px",maxHeight:'20px',paddingTop:'1px',paddingBottom:'1px'}}>{index+1}</div></div>

 ))



}

  </div>

  <div className='editor' style={{backgroundColor:style.codeEditorColor}}>

  <Editor

      value={code}

      maxLength={200}

      onValueChange={code => {setCode(code)}}

      highlight={code => highlight(code, languages.java)}

      padding={5}

      style={{

        backgroundColor:style.codeEditorColor,

        fontFamily:style.primaryTextFont,

        color:style.primaryTextColor,

      }}

    />

  </div>



</div>

    </div>



  )

}



export default CodeContainer

One final component is our Style Editor where all editing tools will be

StyleEditor.tsx

import {
  AlignCenterHorizontal,
  AlignCenterVertical,
  AlignHorizontalJustifyEnd,
  AlignHorizontalJustifyStart,
  RotateCw,
  Pipette,
} from "lucide-react";
import React, { useContext, useState } from "react";
import { Style, StyleContext } from "../context/StyleContext";
import { toPng } from "html-to-image";

const StyleEditor = () => {
  const { style, setStyle, childRef, parentRef } = useContext(StyleContext);

  const alignmentOptions = [
    { type: "left", icon: <AlignHorizontalJustifyStart /> },
    { type: "right", icon: <AlignHorizontalJustifyEnd /> },
    { type: "vertical-center", icon: <AlignCenterVertical /> },
    { type: "horizontal-center", icon: <AlignCenterHorizontal /> },
  ];

  const [codeBackgroundColorPickerOpen, setCodeBackgroundColorPickerOpen] =
    useState(false);

  const clamp = (value: number, min: number, max: number): number =>
    Math.min(Math.max(value, min), max);

  const handleResize = (
    e: React.ChangeEvent<HTMLInputElement>,
    id: string
  ) => {
    const value = parseFloat(e.target.value);

    switch (id) {
      case "edit-H":
        setStyle((prev: Style) => ({
          ...prev,
          height: clamp(value, 0, style.parentHeight - 20),
        }));
        break;

      case "edit-W":
        setStyle((prev: Style) => ({
          ...prev,
          width: clamp(value, 0, style.parentWidth - 20),
        }));
        break;

      case "edit-A":
        setStyle((prev: Style) => ({
          ...prev,
          rotation: clamp(value, -360, 360),
        }));
        break;

      default:
        break;
    }
  };

  const handleAlignment = (type: string) => {
    let positionX = style.positionX;
    let positionY = style.positionY;

    if (parentRef?.current && childRef?.current) {
      switch (type) {
        case "left":
          positionX = 0;
          break;

        case "right":
          positionX = style.parentWidth - style.width;
          break;

        case "vertical-center":
          positionY = (style.parentHeight - style.height) / 2;
          break;

        case "horizontal-center":
          positionX = (style.parentWidth - style.width) / 2;
          break;

        default:
          break;
      }
    }

    setStyle((prev: Style) => ({
      ...prev,
      positionX,
      positionY,
    }));
  };

  const downloadImage = async () => {
    if (parentRef?.current) {
      try {
        const dataUrl = await toPng(document.getElementById("container")!, {
          cacheBust: false,
        });
        const link = document.createElement("a");
        link.download = "my-image-name.png";
        link.href = dataUrl;
        link.click();
      } catch (err) {
        console.error(err);
      }
    }
  };

  return (
    <div className="bg-white rounded-md drop-shadow-lg shadow-black min-h-screen p-4">
      {/* Alignment Editors */}
      <div className="flex gap-2 justify-center items-center mt-4">
        {alignmentOptions.map((option) => (
          <button
            key={option.type}
            title={option.type}
            onClick={() => handleAlignment(option.type)}
            className="bg-inherit shadow-sm border border-black p-2 rounded-md hover:bg-slate-200 hover:scale-95"
          >
            {option.icon}
          </button>
        ))}
      </div>

      {/* Position Editors */}
      <div className="grid grid-cols-2 mt-4 gap-3">
        {["X", "Y", "W", "H", "A"].map((id, index) => (
          <div
            key={index}
            className="flex gap-2 items-center justify-center"
          >
            <p className="cursor-col-resize">{id}</p>
            <input
              id={`edit-${id}`}
              type="number"
              onChange={(e) => handleResize(e, `edit-${id}`)}
              value={
                id === "X"
                  ? style.positionX
                  : id === "Y"
                  ? style.positionY
                  : id === "W"
                  ? style.width
                  : id === "H"
                  ? style.height
                  : style.rotation
              }
              className="rounded-md border max-w-24 border-blue-300 focus:outline-none font-bold antialiased"
            />
          </div>
        ))}
      </div>

      {/* Background Color Picker */}
      <div className="grid grid-cols-2 mt-4 gap-3">
        <div className="flex gap-2">
          <Pipette />
          {codeBackgroundColorPickerOpen ? (
            <input
              type="color"
              value={style.codeEditorColor}
              onChange={(e) =>
                setStyle((prev: Style) => ({
                  ...prev,
                  codeEditorColor: e.target.value,
                }))
              }
            />
          ) : (
            <div
              onClick={() =>
                setCodeBackgroundColorPickerOpen(!codeBackgroundColorPickerOpen)
              }
              className="min-w-[100px]"
              style={{ backgroundColor: style.codeEditorColor }}
            >
              f
            </div>
          )}
        </div>
      </div>

      {/* Download Button */}
      <div className="mt-24">
        <button
          className="bg-red-400 text-white p-3 rounded-md"
          onClick={downloadImage}
        >
          Download
        </button>
      </div>
    </div>
  );
};

export default StyleEditor;

Finally Connecting all the components in our App.tsx

import React from 'react';

import StyleContextProvider from './context/StyleContext';
import Container from '../components/Container';
import StyleEditor from '../components/StyleEditor';

function App() {
  return (
    <StyleContextProvider>
      <div className="flex gap-12">
        <StyleEditor />
        <div id="container" className="w-full flex justify-center">
          <Container />
        </div>
      </div>
    </StyleContextProvider>
  );
}

export default App;

Thanks for reading hope you liked it and the code worked if any suggestion or question please drop in the comments.