Create a Post Editor Similar to Twitter with a Responsive Images Grid with tailwindcss
Tags: tailwindcss, nextjs, webdev, javascript
Published: 2023-01-05 21:02:17

> Check my original blog post for better UI: [Create a Post Editor Similar to Twitter with a Responsive Images Grid with tailwindcss](https://devtomars.vercel.app/post/create-post-editor-similar-to-twitter-with-responsive-images-grid)

In this tutorial, we will create a React component for a social media post editor that allows the user to write posts and display a grid of selected images while preserving their aspect ratio. The component also allows the user to remove images from the grid.

We're going to use Tailwindcss and nextjs in this tutorial so make sure to install and configure  your project following [Tailwind css with Nextjs guide](https://tailwindcss.com/docs/guides/nextjs). 

To get started, let's import the necessary dependencies and create a functional component called PostEditorImages:

```javascript
import React, { useEffect, useState } from 'react';

const PostEditorImages = ({ images, removeImage }) => {
  // component code goes here
}
```
Our component will accept two props:

`images`: an array of images of type File, representing the images to be displayed in the grid.
`removeImage`: a function that removes an image from the images array and updates the state of the parent component.

Next, let's set up some state variables that we will use to control the layout of the image grid:

```javascript
const [imagesContainerGrid, setImagesContainerGrid] = useState("grid-cols-2 grid-rows-2");
const [aspectRatios, setAspectRatios] = useState({});

```
The `imagesContainerGrid` state variable will hold a string of CSS class names that define a grid template columns of 2 and rows of 2. The `aspectRatios` state variable will hold an object that maps the index of each image to its aspect ratio.

Now, let's write a `useEffect` hook that updates the `imagesContainerGrid` state variable based on the number of images:

```javascript
useEffect(() => {
  setImagesContainerGrid(images.length >= 3 ? "grid-cols-2 grid-rows-2 h-[300px]" : images.length > 1 ?"grid-cols-2" : "grid-cols-1");
}, [images]);

```

This `useEffect` hook will run every time the images prop changes. If there are three or more images, it will set the `imagesContainerGrid` state variable to a string that specifies a grid with two columns and two rows, with a fixed height of `300px`. If there are two or more images, it will set the `imagesContainerGrid` state variable to a string that specifies a grid with two columns. If there is only one image, it will set the `imagesContainerGrid` state variable to a string that specifies a grid with one column.

Now, let's write a `useEffect` hook that calculates the aspect ratio of each image and updates the `aspectRatios` state variable:

```javascript
useEffect(() => {
      images.forEach((image, index) => {
        const reader = new FileReader();
    
        reader.onload = function(event) {
          const data = event.target.result;
          const img = new Image();
          img.src = data;
          img.onload = function() {

            const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
            const numerator = img.width / gcd(img.width, img.height);
            const denominator = img.height / gcd(img.width, img.height);
            setAspectRatios(ratios => ({ ...ratios, [index]: `${numerator}/${denominator}` }));
          };
        };
    
        reader.readAsDataURL(image);
      });
}, [images]);

```

This code iterates over an array of images and calculates the aspect ratio of each image. It does this by:

Creating a `FileReader` object for each image, which allows the code to read the contents of the file as a data URL.
Setting an `onload` event handler for the `FileReader` object, which is called when the file has been successfully read. The event handler creates a new image object, sets its `src` property to the data URL, and sets an `onload` event handler for the image object.

The `onload` event handler for the image object calculates the aspect ratio of the image by dividing the width by the greatest common divisor of the width and height, and dividing the height by the greatest common divisor. It then updates the `aspectRatios` state variable by creating a new object that spreads the existing aspect ratios and adds a new key-value pair for the current image's index and aspect ratio.

Finally, the code calls the `readAsDataURL` method on the `FileReader` object, passing in the image file as an argument. This starts the process of reading the file as a data URL.

Finally, let's render the image grid in the component's JSX:

```javascript
return (
  <div className={` [grid-area:images] grid gap-1 ${imagesContainerGrid}  `}>
    {/* display the selected images */}
    {images.length > 0 &&
      [...images].map((image, index) => (
        <div
          key={image.name}
          className={`relative rounded-md`}
          style={{
            backgroundImage: `url(${URL.createObjectURL(image)})`,
            backgroundSize: 'cover',
            backgroundPosition: 'center center',
            backgroundRepeat: 'no-repeat',
            width: '100%',
            height: '100%',
            aspectRatio: aspectRatios[index]
          }}
        >
          <button
            className="absolute flex items-center justify-center top-0 text-sm right-0 p-2 text-white rounded-full w-7 h-7 shadow-md bg-black bg-opacity-60 hover:bg-opacity-100"
            onClick={() => removeImage(index)}
          >
            <span>X</span>
          </button>
        </div>
      ))
    }
  </div>
);
```

This JSX creates a div of `grid-area:images` that will be defined in the parent component , with the grid class and the `imagesContainerGrid` class, which defines the layout of the image grid. Inside the div, it maps over the images array and renders a div for each image. 
Each image div has a background image set to the image file, and the `aspectRatio` style property set to the aspect ratio of the image. The div also has a remove button that calls the `removeImage` function and passes in the index of the image to be removed.

That's it! With these simple steps, we have created a React component that displays a grid of images selected by the user, and allows the user to remove images from the grid.

To use the component, we need to create the `SocialMediaTextEditor` component which represents the text editor for the the social media app:

```javascript
const SocialMediaTextEditor = () => {
  const [text, setText] = useState('')// state to store the text;
  const [images, setImages] = useState([]); // state to store the images


  const imageContainerRef = useRef()

  const handleTextChange = (event) => {
    setText(event.target.value);
  };

  const handleImageChange = (event) => {
    // update the images state by concatenating the selected files with the existing ones
    setImages([...images, ...event.target.files]);
  };

  const removeImage = (index) => {
    // create a new array with the selected image removed
    const newImages = [...images];
    newImages.splice(index, 1);
    setImages(newImages);
  };

  return (...)
```

Here, we have the `handleTextChange`  to save the latest value of the text field, the `handleImageChange` and `removeImage` functions are responsible for adding and removing images from the images list.

Now lets define and style the post editor component using grid template areas with `tailwindcss`:

```javascript
return (
   <div
  className={` rounded-lg p-4
    grid gap-y-1
    grid-cols-[40px_5px_1fr]
    grid-rows-[40px_1fr_minmax(0,_max-content)_40px]
    [grid-template-areas:'profile_p1_post'_'empty2_p3_post'_'empty3_p3_images'_'empty4_p4_postActions']`}

>
{/*  */}
  </div>
)
```

The `SocialMediaTextEditor` component contains a profile area where you can add a profile image. The post area represents the text field where the user can write their post. The `p1, p3,` and `p4` represent a `5px` gap that is defined in the `grid-cols-[40px_5px_1fr]` layout. The `postActions` area holds the upload images button and the post button. The `emptyX` fills in the profile area in all grid rows except the first row, giving the component a similar style to the Twitter post editor. 

Now, let's define the JSX code for the profile, text field, and displayed images:

```javascript
...
⁠   {/* profile area */}
  <div className="[grid-area:profile] rounded-full bg-red-500" />
  {/* post area */}
  <textarea
    className="w-full [resize:none] [grid-area:post] h-fit p-2 rounded-lg text-gray-500  focus:outline-none"
    placeholder="Write your post here..."
    value={text}
    onChange={handleChange}
  />

  {/* display selected images */}
  <PostEditorImages images={images} removeImage={removeImage} />
⁠
...
```

Now add the jsx code that will allow us to select images from the local system :

```javascript
...
{/* display selected images */}
 <PostEditorImages images={images} removeImage={removeImage} />

  <div className="[grid-area:postActions] flex flex-row justify-between">
    <div className="justify-self-start flex flex-row gap-2">
      {/* add the input element to select images */}
      <label htmlFor="image-upload" className="cursor-pointer">
        <FaImage size={20} color="rgb(31 41 55)" />
      </label>
      <input
        id="image-upload"
        type="file"
        accept="image/*"
        multiple
        onChange={handleImageChange}
        className="hidden"
      />
    </div>
    <button
        className="bg-red-500 justify-self-end hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full"
    >
        Post
    </button>
 </div>
⁠
...


```

You can import the `FaImage` component from `react-icons` package.

That's it! You now have a fully functional social media post editor component with a responsive display images grid and a style similar to the Twitter post editor. Of course, you still need to create the logic for uploading the images to the server.

I hope this tutorial was helpful in showing you how to display and manage a grid of images in a React app. Happy coding!