OWolf

2024-09-30 web, development, javascript

Blog Post Creation Interface

By O. Wolfson

I built a simple user interface for my blog post creation script , using Next.js and TypeScript. This browser based UI allows the user to enter the title, author, categories, published date, description, and body of a blog post. The user can also preview the blog post in Markdown format. The user interface allows the user to save the blog post to a JSON file. Find code for Next.js project here .

jsx
//These imports are all hooks that allow you to manage state, perform side effects, handle events, and manipulate the DOM in a React component.
import React, { useState, useEffect, ChangeEvent, useRef } from "react";
//ReactMarkdown is used for rendering Markdown syntax as HTML in a React component.
import ReactMarkdown from "react-markdown";
// rehypeRaw is used to parse HTML elements in Markdown.
import rehypeRaw from "rehype-raw";
// DatePicker is used for a user-friendly calendar component for selecting dates.
import DatePicker from "react-datepicker";
// This is the CSS for the DatePicker component.
import "react-datepicker/dist/react-datepicker.css";
// moment is used for manipulating dates and times in JavaScript.
import moment from "moment";

// This code defines an interface in TypeScript called BlogPostData. Interfaces in TypeScript are used to define the shape of an object, meaning they specify the names and types of the object's properties.
// The BlogPostData interface defines the shape of the blogPostData state variable in the Write component. It specifies that the blogPostData state variable is an object with the following properties:
interface BlogPostData {
  title: string;
  author: string;
  categories: string[];
  publishedDate: string | Date;
  description: string;
  excerpt: string;
  body: string;
}

// This is the Write component. It is used to render a form for writing a blog post.
function Write() {
  const [blogPostData, setBlogPostData] = useState < Partial < BlogPostData >> {};
  const [markdown, setMarkdown] = useState("");
  const [publishedDate, setPublishedDate] = (useState < Date) | (null > null);
  const inputRefs = useRef < any > {};

// This function is used to set the blogPostData state variable to the value of the input field that the user is typing in. It also sets the property of the blogPostData state variable that corresponds to the name of the input field that the user is typing in to the value of the input field that the user is typing in.
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target;
    setBlogPostData((prevState) => ({ ...prevState, [name]: value }));
  };

// This function is used to set the blogPostData state variable to the value of the textarea that the user is typing in. It also sets the property of the blogPostData state variable that corresponds to the name of the textarea that the user is typing in to the value of the textarea that the user is typing in. It also sets the markdown state variable to the value of the textarea that the user is typing in if the name of the textarea that the user is typing in is "body" and the value of the textarea that the user is typing in is greater than 0.
  function handleTextAreaChange(event: ChangeEvent<HTMLTextAreaElement>) {
    const { name, value } = event.target;
    setBlogPostData((prevState) => ({ ...prevState, [name]: value }));
    if (event.target.name === "body" && event.target.value.length > 0) {
      setMarkdown(value);
    }
  }


// This function is used to set the publishedDate state variable to the date selected by the user in the DatePicker component. It also sets the publishedDate property of the blogPostData state variable to the date selected by the user in the DatePicker component.
  function handleDateChange(date: Date) {
    setPublishedDate(date);
    setBlogPostData((prevState) => ({
      ...prevState,
      publishedDate: moment(date).format("YYYY-MM-DD"),
    }));
  }

  const handleSaveBlogPost = async () => {

     // This code defines a variable called data that is of type BlogPostData. It is used to store the data that will be sent to the server when the user clicks the "Save Blog Post" button.
    const data: BlogPostData = {
      title: blogPostData.title ?? "",
      author: blogPostData.author ?? "",
      categories: blogPostData.categories ?? [],
      publishedDate: blogPostData.publishedDate
        ? new Date(blogPostData.publishedDate).toISOString()
        : "",
      description: blogPostData.description ?? "",
      excerpt: blogPostData.excerpt ?? "",
      body: blogPostData.body ?? "",
    };

    // This code sends a POST request to the server with the data that the user entered in the form. If the request is successful, the user is notified that the blog post was saved and the form is cleared. If the request is unsuccessful, the user is notified that the blog post was not saved.
    const response = await fetch("/api/save-blog-post", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      console.error("Failed to save blog post");
    } else {
      console.log("Blog post saved");
      setBlogPostData({});
      setPublishedDate(null);
      setMarkdown("");
      var toast = document.getElementById("toast");
      // Show the toast message using Tailwind classes
      toast?.classList.remove("hidden");
      // Hide the toast message after 3 seconds using Tailwind classes
      setTimeout(function () {
        toast?.classList.add("hidden");
      }, 3000);
      // Clear the input fields
      inputRefs.current.title.value = "";
      inputRefs.current.author.value = "";
      inputRefs.current.categories.value = "";
      inputRefs.current.description.value = "";
      inputRefs.current.body.value = "";
    }
  };
html
<!-- This is the JSX code for the Write component. It is used to render a form for writing a blog post. -->
return (
  <div className="flex flex-col p-20 space-y-6">
    <input
      className="h-10 px-2 border-4 border-blue-500 rounded"
      type="text"
      id="title"
      name="title"
      ref={(el) => (inputRefs.current.title = el)}
      placeholder="Title"
      onChange={handleInputChange}
      value={blogPostData.title}
    />
    <input
      className="h-10 px-2 border-4 border-blue-500 rounded"
      type="text"
      id="author"
      name="author"
      ref={(el) => (inputRefs.current.author = el)}
      placeholder="Author"
      onChange={handleInputChange}
      value={blogPostData.author}
    />
    <input
      className="h-10 px-2 border-4 border-blue-500 rounded"
      type="text"
      id="categories"
      name="categories"
      ref={(el) => (inputRefs.current.categories = el)}
      placeholder="Categories. Separate with commas"
      onChange={handleInputChange}
      value={blogPostData.categories}
    />
    <div className="date-picker-container">
      <DatePicker
        //popperPlacement="top-end"
        className="h-10 px-2 border-4 border-blue-500 rounded"
        id="publishedDate"
        name="publishedDate"
        selected={publishedDate}
        onChange={handleDateChange}
        dateFormat="yyyy-MM-dd"
        placeholderText="Published Date"
      />
    </div>
    <textarea
      name="description"
      id="description"
      ref={(el) => (inputRefs.current.description = el)}
      placeholder="Please enter a blog post description"
      cols={30}
      rows={3}
      className="p-2 border-4 border-blue-500 rounded"
      onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
        handleTextAreaChange(event)
      }
      value={blogPostData.description}
    ></textarea>
    <textarea
      name="body"
      id="body"
      ref={(el) => (inputRefs.current.body = el)}
      placeholder="Please enter the body of the blog post. You can use markdown."
      cols={30}
      rows={10}
      className="p-2 border-4 border-blue-500 rounded"
      onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
        handleTextAreaChange(event)
      }
      value={blogPostData.body}
    ></textarea>
    <div>
      {markdown.length > 0 && (
        <h4 className="mb-4 text-xl font-bold text-gray-400">Body Preview</h4>
      )}
    </div>
    <ReactMarkdown
      rehypePlugins={[rehypeRaw]}
      skipHtml={false}
      children={markdown}
      components={{
        h1: ({ children }) => (
          <h2 className="mb-4 text-5xl font-bold">{children}</h2>
        ),
        h2: ({ children }) => (
          <h2 className="mb-4 text-3xl font-bold">{children}</h2>
        ),
        h3: ({ children }) => (
          <h3 className="mb-4 text-2xl font-bold">{children}</h3>
        ),
        // and so on for other heading levels
      }}
    />
    <button
      className="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700 "
      onClick={handleSaveBlogPost}
    >
      Save Blog Post
    </button>
    <div
      id="toast"
      className="fixed z-10 hidden px-4 py-2 text-xl text-white transform -translate-x-1/2 bg-gray-800 rounded-md bottom-5 left-1/2 opacity-70"
    >
      Post saved!
    </div>
  </div>
);

export default Write;