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
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
tailwind css For styling
prism-js For code highlighting
html-to-image For exporting our image , basically converting html dom elements into a image of various formats (i.e JPEG, PNG, SVG)
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.