DEV Community

Cover image for How to Build a Bookmark app using Tailwind, React and Strapi
Shada for Strapi

Posted on • Originally published at strapi.io

How to Build a Bookmark app using Tailwind, React and Strapi

This tutorial will build a Bookmarking app using React and style it with Tailwind CSS, and the backend will be built using Strapi.

What is Strapi Headless CMS?

Strapi is an open-source headless content management system (CMS) based on Nodejs used to develop APIs (RESTful and GraphQL APIs) and build the APIs content. The APIs can be built from the Strapi admin panel and also from Strapi's APIs. In addition, the content of the APIs can be added from the UI and through using the Strapi APIs.

These API endpoints can then be consumed from clients (mobile, desktop, web). The APIs are created in Strapi in the form of collection or single-type. A collection in Strapi contains the endpoints:

  • POST /endpoints: This endpoint adds new content.
  • GET /endpoints: Returns all data.
  • GET /endpoints/:id: Returns data by its id.
  • DELETE /endpoints/:id: Deletes a specific content.
  • PUT /endpoints/:id: Edits a specific content.

Strapi is both a server and a database. By default, it is powered by Koajs, a minimalistic web framework created by the team at Expressjs, and the database is SQLite.

What do we mean by Headless CMS?

A headless CMS is a CMS with only a backend. The CMS provides only the backend to the users, not the frontend. CMS like WordPress is "headful" because we can build our backend - APIs and their content on it and build the frontend on it. The frontend is the HTML page the displays content that users will see.

A CMS has:

  • A backend.
  • a UI editor to develop and build APIs and their content.
  • The frontend from which the APIs are consumed will be designed in the CMS, and this is what the users load to view the data in the APIs, e.g., blog or news website.

Headless CMS removes the last part. It does not provide any frontend for the users. It only provides the backend with the APIs for the users to use. So the "head" is chopped off, hence the term "headless."

So the "headless" is what Strapi gives us. It separates our content from the tech stack of our website and provides the content as data over an API. So we build the backend and the frontend separately without depending on each other.

Tailwind - Headless UI

Typically, during website design, CSS classes are created for each HTML element to style them. These CSS classes are mostly inline initially, but later on, they are extracted and held in CSS files. That's a general practice.
Now, this will ultimately lead to huge CSS files with our styling. So Tailwind does away with CSS files.

But what is Tailwind?

Tailwind is a utility-first CSS framework that lets us design websites without the need for CSS. Instead, the framework comes packed with CSS classes that we can bring together to design and style our HTML elements without leaving them there.

According to tailwindcss:
A utility-first CSS framework packed with classes like flex, pt-4, text-center, and rotate-90 that can be composed to build any design directly in your markup.

Unlike other CSS frameworks like Bootstrap, Foundation, Bulma, Material Design, etc. does not contain components like Buttons, Cards, BottomSheets, etc. Instead, Tailwind only has low-level CSS classes. You then use these classes to build your components.

We will build a bookmarking app to demonstrate how we can use Tailwind CSS framework and Reactjs to design and develop the app and learn how to consume our Strapi APIs from Reactjs.

The bookmark app will let users store links to websites, notes, save social media posts, etc., for easy retrieval. Just like the bookmark feature of most modern browsers, it allows users to store.

We will start by scaffolding a Strapi project.

Scaffolding a Strapi project

Create a central folder strapi-tailwind: mkdir strapi-tailwind. Move into the folder: cd strapi-tailwind.

This strapi-tailwind folder will contain our Strapi backend and our Reactjs frontend.

Now run the below command to generate a Strapi project:

    npx create-strapi-app strapi-api --quickstart
    # OR
    yarn create strapi-app strapi-api ---quickstart
Enter fullscreen mode Exit fullscreen mode

This command will create a Strapi project in strapi-app folder.
Strapi will serve the project on localhost:1337. It will launch the Strapi admin UI panel on localhost:1337/admin.

Fill in your details and click on the "LET'S START" button. Then, we will begin to build our bookmark collection.

Build the Bookmark collections

In our app, a bookmark will have a title, content, and synopsis. The Bookmark model will be this:

    Bookmark {
        title
        content
        synopsis
    }
Enter fullscreen mode Exit fullscreen mode

The title field will be the title of the bookmark. The content will be the content of the bookmark, and it can be links, notes, etc. Finally, the synopsis holds a preview of the bookmark's content.

On the Strapi admin UI, click on the"> CREATE YOUR FIRST CONTENT-TYPE" button.

A modal will show up, type in bookmark and click on the Continue button.

Insert the fields:

  • Select Text field, type in "title".
  • Select Text field, type in "content" and select "Long text".
  • Select Text field, type in "synopsis"

Click on the Finish button. On the page that appears click on the "Save" button on the top-right section of the page.

We will now have the below endpoints in our Bookmark collection:

  • POST /bookmarks: Create a new bookmark
  • GET /bookmarks: Get all bookmarks
  • GET /bookmarks/:id: Get a bookmark
  • DELETE /bookmarks/:id: Delete a bookmark
  • PUT /bookmarks/:id: Update a bookmark.

Seed the database

Let's insert some content in our Bookmark collection. First, click on the Bookmarks item on the sidebar, click on the + Add New Bookmarks button on the top-right page.

Type in the below data:

    "title" -> Become a qualified dev

    "content" -> https://raddevon.com/5-projects-to-become-qualified-as-a-web-developer/?ck_subscriber_id=1287376433

    "synopsis" -> https://raddevon.com/5-projects-to-become-qua...
Enter fullscreen mode Exit fullscreen mode

See that we are bookmarking a link so we can retrieve it easily.

Click on the "Save" button on the top-right section of the page, then click on the "Publish" button.

Let's add another data:

    "title" -> A Shadaya post

    "content" -> When they hit their 30s, the pressure won't be about iphones, clothes, cars, it will be about who got a lovely home, a stable marriage & a happy family. Jealous, bitterness & regrets for the "woke" ones & happiness, joy & fulfilment for the "lame" ones.

    "synopsis" -> When they hit their 30s, the pressure won't be about iphones...
Enter fullscreen mode Exit fullscreen mode

This is a post from social media we want to bookmark.

Click on the "Save" button and then on the "Publish" button. This time let's bookmark a Twitter post link:

    "title" -> Twitter post

    "content" -> https://twitter.com/Drwhales_/status/1388404654342610944

    "synopsis" -> https://twitter.com/Drwhales_/status/138...
Enter fullscreen mode Exit fullscreen mode

Click on "Save" and on "Publish".

Allow access

Like this, only authenticated users can access our bookmark endpoints, but we want everybody to access them without restriction.

To do that, click on the Settings item on the sidebar. Then on Roles on the right and Public.
Scroll down to "Permissions," and on the BOOKMARK section, check the Select all
Click on the Save button on the top-right. This will save these changes.
Our Bookmark collection endpoints are now openly accessible by the Public.

Now we build our frontend.

Building the React app

First, we scaffold a Reactjs app. Run the below command to do that:

    npx create-react-app strapi-tailwind
Enter fullscreen mode Exit fullscreen mode

Move into the folder cd strapi-tailwind.

Install the dependencies:

  • axios: HTTP library, we will use it to make HTTP requests to the collection's endpoints.
  • react-router-dom: React library for adding routing system to React apps.
    yarn add axios react-router-dom
Enter fullscreen mode Exit fullscreen mode

Setup Tailwind

Let's set up Tailwind in our project.

Install the dependencies:

    yarn add --dev tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
Enter fullscreen mode Exit fullscreen mode

We installed the PostCSS because create-react-app does not support PostCSS 8 yet.

Next, we install Craco:

    yarn add @craco/craco
Enter fullscreen mode Exit fullscreen mode

This dependency helps us configure Tailwind.

Next, we will update the scripts in our package.json to use craco instead of react-scripts.

Next create a craco.config.js file in the root folder and paste the below code in it:

    // craco.config.js
    module.exports = {
      style: {
        postcss: {
          plugins: [require("tailwindcss"), require("autoprefixer")],
        },
      },
    };
Enter fullscreen mode Exit fullscreen mode

Initialize Tailwind in our project by running the command:

    npx tailwind init
Enter fullscreen mode Exit fullscreen mode

This command will create a tailwind.config.js in our root folder. Open it and paste the below code:

    module.exports = {
      purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
      darkMode: false, // or 'media' or 'class'
      theme: {
        extend: {},
      },
      variants: {
        extend: {},
      },
      plugins: [],
    };
Enter fullscreen mode Exit fullscreen mode

Now, we include Tailwind in our CSS. Paste the below CSS code in your src/index.css file:

    @tailwind base;
    @tailwind components;
    @tailwind utilities;

    ...
Enter fullscreen mode Exit fullscreen mode

We are good to go.

Build components

Our app will have a single route /, this index route will render the bookmarks in the system.
The page will look like this:

List of bookmarks

Adding a new bookmark

Editing a bookmark

Viewing a bookmark

It will have a

  • copy icon to copy the content of the bookmark when pressed.
  • delete icon to delete the bookmark.
  • view icon to view the bookmark. This will pop up a dialog that displays the title and content of the bookmark.
  • edit icon to edit a bookmark. This icon will popup a dialog with input boxes filled with

We will have the components:

  • Header: This will contain the header of our app and will display on every page.
  • BookmarkCard: This component will display a bookmark title and synopsis on the index page.
  • AddBookmarkDialog: This is a dialog where new bookmarks are added to the system.
  • EditBookmarkDialog: This is a dialog where an existing bookmark will be edited.
  • ViewBookmarkDialog: This is a dialog that will display a bookmark's content.
  • BookmarkList: This component displays all the bookmarks in our system.

The above components will reside in a components folder. Let's begin to create them:

    mkdir src/components
    cd src/components

    mkdir Header
    touch Header/index.js

    mkdir BookmarkCard
    touch BookmarkCard/index.js

    mkdir AddBookmarkDialog
    touch AddBookmarkDialog/index.js

    mkdir EditBookmarkDialog
    touch EditBookmarkDialog/index.js

    mkdir ViewBookmarkDialog
    touch ViewBookmarkDialog/index.js

    mkdir BookmarkList
    touch BookmarkList/index.js

    mkdir CloseIcon
    touch CloseIcon/index.js
Enter fullscreen mode Exit fullscreen mode

Let's start with the Header component:

Header
This component will hold a simple UI of the header section. Paste the below code to src/components/Header/index.js:

    export default function Header() {
      return (
        <section class="p-4 text-2xl font-bold bg-red-600 text-white mb-4">
          <div>Bookmarks</div>
        </section>
      );
    }
Enter fullscreen mode Exit fullscreen mode

See how we styled our header by applying Tailwind utility classes. We didn't use React's own className, everything is done in the class attribute.

The p-4 applies padding of 4px to all the corners(top, bottom, left, and right). The text-2xl gives it a font size of 1.5rem. The font-bold sets the text to be bold. The bg-red-600 sets the background color of the header to be red in color with a darker opacity. The text-white sets the text color to be white. The mb-4 sets the bottom margin of the header to be 4px.

CopyIcon
We will add a CopyIcon component. This component will render an svg icon representation of the copy symbol.

Paste the below code to src/components/CloseIcon/index.js:

    export default function CloseIcon() {
      return (
        <svg
          width="12px"
          height="10px"
          xmlns="http://www.w3.org/2000/svg"
          style={{ cursor: "pointer" }}
          fill="white"
        >
          <path
            d="M10.0719417,0.127226812 C10.1612888,0.127226812 10.2403266,0.161591074 10.3090551,0.230319596 L10.3090551,0.230319596 L10.8245191,0.745783513 C10.8932476,0.814512036 10.9276118,0.893549837 10.9276118,0.982896916 C10.9276118,1.07224399 10.8932476,1.1512818 10.8245191,1.22001032 L10.8245191,1.22001032 L6.77297267,5.27155671 L10.8245191,9.3231031 C10.8932476,9.39183162 10.9276118,9.47086942 10.9276118,9.5602165 C10.9276118,9.64956358 10.8932476,9.72860138 10.8245191,9.79732991 L10.8245191,9.79732991 L10.3090551,10.3127938 C10.2403266,10.3815223 10.1612888,10.4158866 10.0719417,10.4158866 C9.98259466,10.4158866 9.90355686,10.3815223 9.83482834,10.3127938 L9.83482834,10.3127938 L5.92809485,6.40509433 C4.98802554,7.34516364 3.68545904,8.64773014 2.02039535,10.3127938 C1.95166683,10.3815223 1.87262903,10.4158866 1.78328195,10.4158866 C1.69393487,10.4158866 1.61489707,10.3815223 1.54616855,10.3127938 L1.03070463,9.79732991 C0.961976106,9.72860138 0.927611845,9.64956358 0.927611845,9.5602165 C0.927611845,9.47086942 0.961976106,9.39183162 1.03070463,9.3231031 L5.08225102,5.27155671 L1.03070463,1.22001032 C0.961976106,1.1512818 0.927611845,1.07224399 0.927611845,0.982896916 C0.927611845,0.893549837 0.961976106,0.814512036 1.03070463,0.745783513 L1.54616855,0.230319596 C1.61489707,0.161591074 1.69393487,0.127226812 1.78328195,0.127226812 C1.87262903,0.127226812 1.95166683,0.161591074 2.02039535,0.230319596 L5.92761184,4.13822681 L9.83482834,0.230319596 C9.88637473,0.178773204 9.94372009,0.146556709 10.0068644,0.133670111 Z"
            fillRule="nonzero"
          ></path>
        </svg>
      );
    }
Enter fullscreen mode Exit fullscreen mode

We render an svg code of the copy symbol above. We made the icon to be 12px wide and 10px high. Also, we used the styling style={{ cursor: "pointer" }} to make the cursor transform to a hand icon when the mouse cursor hovers above the copy icon, it gives the users hint that the copy icon is clickable.

BookmarkCard
This component will receive a bookmark in its props and then proceed to display its title and synopsis.

Paste the below code on src/components/BookmarkCard/index.js:

    import axios from "axios";
    import ViewBookmarkDialog from "./../ViewBookmarkDialog";
    import EditBookmarkDialog from "./../EditBookmarkDialog";
    import { useState } from "react";

    export default function BookmarkCard({ bookmark }) {
      const { id, title, content, synopsis } = bookmark;
      const [edit, setEdit] = useState(false);
      const [view, setView] = useState(false);
      const [showCopy, setShowCopy] = useState(false);
      var timeout;

      function copyBookmark() {
        navigator.clipboard.writeText(content).then(
          function () {
            /* clipboard successfully set */
            setShowCopy(true);
            clearTimeout(timeout);
            timeout = setTimeout(() => {
              setShowCopy(false);
            }, 1000);
          },
          function () {
            /* clipboard write failed */
            setShowCopy(false);
          }
        );
      }

      function viewBookmark() {
        setView(true);
      }
      function editBookmark() {
        setEdit(true);
      }

      async function deleteBookmark() {
        if (window.confirm("Do you want to delete this bookmark?")) {
          await axios.delete("http://localhost:1337/bookmarks/" + id);
          window.location.reload();
        }
      }

      return (
        <div
          style={{ width: "600px" }}
          class="border border-gray-200 rounded-md m-3 p-4 shadow-md bg-white hover:shadow-xl"
        >
          {showCopy ? <Message /> : null}
          <div class="py-2">
            <h4 class="text-xl font-bold">{title}</h4>
          </div>
          <div>{synopsis}</div>
          <div class="py-2 my-3 flex">
            <span
              class="cursor-pointer inline mx-1 text-white font-bold py-2 px-4 rounded"
              onClick={copyBookmark}
            >
              <CopyIcon />
            </span>
            <span
              class="cursor-pointer inline mx-1 text-white font-bold py-2 px-4 rounded"
              onClick={deleteBookmark}
            >
              <DeleteIcon />
            </span>
            <span
              class="cursor-pointer inline mx-1 text-white font-bold py-2 px-4 rounded"
              onClick={viewBookmark}
            >
              <ViewIcon />
            </span>
            <span
              class="cursor-pointer inline mx-1 text-white font-bold py-2 px-4 rounded"
              onClick={editBookmark}
            >
              <EditIcon />
            </span>
          </div>
          {view ? (
            <ViewBookmarkDialog
              bookmark={bookmark}
              closeModal={() => setView(false)}
            />
          ) : null}
          {edit ? (
            <EditBookmarkDialog
              bookmark={bookmark}
              closeModal={() => setEdit(false)}
            />
          ) : null}
        </div>
      );
    }

    function DeleteIcon() {
      return (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="#e73d52"
        >
          <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
        </svg>
      );
    }

    function CopyIcon() {
      return (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="#e73d52"
        >
          <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
        </svg>
      );
    }

    function ViewIcon() {
      return (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="#e73d52"
        >
          <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
        </svg>
      );
    }

    function EditIcon() {
      return (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="#e73d52"
        >
          <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
        </svg>
      );
    }

    function Message() {
      return (
        <div class="z-50 fixed flex p-3 bg-blue-200 rounded-md border-2 border-blue-600 font-bold opacity-90">
          <div class="mr-2">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="24"
              height="24"
              viewBox="0 0 24 24"
              fill="#e73d52"
            >
              <path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
            </svg>
          </div>
          <div>
            <span class="text-red-600">Copied!</span>
          </div>
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

See the component destructures the passed in bookmark object from its props.
Next, we destructured the bookmark's id, title, content, and synopsis from the bookmark object.

We have states edit, view and showCopy,the edit and view states toggles the EditBookmarkDialog and ViewBookmarkDialog visibility respectively. The showCopy toggles a message component when a bookmark's content is copied.

The timeout will hold a setTimeout id, we will use this to clear out timeouts.
The UI of this component displays the title and the synopsis of the bookmark. We used several Tailwind classes to design this component.

  • cursor-pointer: This makes the mouse take the shape of a hand.
  • rounded-md: This makes the element's border-radius to be 0.25rem.
  • inline: This makes the element to be an inline element.
  • flex: This sets display:flex; on the element.
  • hover: shadow-xl: This sets the box-shadow to be deeper when the element is hovered with a mouse.
  • border: The border width is 1px
  • border-gray-200: The border color is darker gray.
  • py-2: This sets the top and bottom padding of the element to be 2px.
  • m-3: This sets the margin of the element to be 3px.
  • shadow-md: This sets the box-shadow of the element to be 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);.
  • z-50: The element has a z-index of 50. This controls the stack order of elements.
  • fixed: Makes the element a positioned element, in this case, a fixed element.
  • opacity-90: Makes the element have an opacity of 0.9

Also, we have icons to copy, delete, edit, and view the bookmark. We used svg code to display the icons. Each icon has its component:

  • DeleteIcon - has svg code that renders a delete icon.
  • ViewIcon - has svg code that renders an eye icon that denotes viewing an item.
  • CopyIcon - this renders an svg code that renders a copy icon.
  • EditIcon - renders svg code that renders an edit icon.

We enclosed each icon in span element with an onClick attribute. The copyBookmark function triggered by the copy icon, uses the Clipboard API to copy the contents of the bookmark.

The viewBookmark sets the view state to true which causes the ViewBookmarkDialog to show up. The bookmark and a closeModal function are sent to the component so the component can respectively access the bookmark and close itself using the closeModal function.

The editBookmark function sets the edit state to true which displays the EditBookmarkDialog component.

The deleteBookmark function deletes the current bookmark from the db. It makes a DELETE HTTP request to localhost:1337/bookmarks/+id. The id will be the id of the bookmark, after the request the page is reloaded.

AddBookmarkDialog
This component is where we add new bookmarks. Paste the below code to src/components/AddBookmarkDialog/index.js:

    import axios from "axios";
    import { useRef } from "react";
    import CloseIcon from "./../CloseIcon";

    export default function AddBookmarkDialog({ closeModal }) {
      const formRef = useRef();

      async function addBookmark() {
        var { title, content } = formRef.current;
        title = title.value;
        content = content.value;

        await axios.post("http://localhost:1337/bookmarks", {
          title,
          content,
          synopsis: content.slice(0, 100) + "...",
        });
        window.location.reload();
      }

      return (
        <div class="modal fixed -top-0 left-0 w-full h-full flex flex-col z-0 items-center">
          <div
            class="modal-backdrop opacity-70 bg-gray-50 fixed w-full h-full z-10"
            onClick={closeModal}
          ></div>
          <div class="modal-content z-20 w-2/5 mt-5 bg-white shadow-md">
            <div class="modal-header flex justify-between items-center bg-red-600 p-3 text-white">
              <h3 class="text-white font-bold">Add Bookmark</h3>
              <span
                style={{ padding: "10px", cursor: "pointer" }}
                onClick={closeModal}
              >
                <CloseIcon />
              </span>
            </div>
            <div className="modal-body content m-2 p-5 z-50">
              <form ref={formRef}>
                <div class="w-full">
                  <div class="pl-2">
                    <span>TITLE</span>
                  </div>

                  <input
                    type="text"
                    class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
                    placeholder="Type in title.."
                    name="title"
                  />
                </div>
                <div class="w-full">
                  <div class="pl-2 mt-3">
                    <span>CONTENT</span>
                  </div>
                  <textarea
                    type="text"
                    class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
                    placeholder="Type in content.."
                    name="content"
                  ></textarea>
                </div>
              </form>
            </div>
            <div className="modal-footer flex justify-between p-4 bg-gray-200">
              <button
                class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-3 rounded"
                onClick={closeModal}
              >
                Cancel
              </button>
              <button
                class="bg-red-600 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
                onClick={addBookmark}
              >
                Add
              </button>
            </div>
          </div>
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

See it has input boxes where the title and content of the bookmark are to be typed. The Add button calls the addBookmark function, this function retrieves the values of the bookmark's title and content from the input boxes.

Then it makes a POST HTTP request to localhost:1337/bookmarks with the synopsis, title, and content retrieved from the UI as payload. The synopsis is generated by slicing off 100 words from the content to get a preview of the content. This request adds the bookmark to our Strapi backend. The page is reloaded which displays the newly added bookmark on the UI.

BookmarkList

This component gets the list of bookmarks from the Strapi endpoint localhost:1337/bookmarks and displays them.

Add the below code to src/components/BookmarkList/index.js:

    import BookmarkCard from "./../BookmarkCard";
    import axios from "axios";
    import { useEffect, useState } from "react";
    import AddBookmarkDialog from "./../AddBookmarkDialog";

    export default function BookmarkList(params) {
      const [bookmarks, setBookmarks] = useState([]);
      const [showAddBookmarkDialog, setShowAddBookmarkDialog] = useState(false);

      useEffect(async () => {
        const data = await axios.get("http://localhost:1337/bookmarks");
        setBookmarks(data?.data);
      }, []);

      return (
        <div class="flex flex-col flex-wrap justify-center">
          <div class="m-2 p-2">
            <button
              onClick={() => setShowAddBookmarkDialog(true)}
              class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
            >
              Add Bookmark
            </button>
          </div>
          <div>
            {bookmarks
              ?.sort((a, b) => b.created_at.localeCompare(a.created_at))
              .map((bookmark, i) => (
                <BookmarkCard bookmark={bookmark} key={i} />
              ))}
          </div>
          {showAddBookmarkDialog ? (
            <AddBookmarkDialog closeModal={() => setShowAddBookmarkDialog(false)} />
          ) : null}
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

We have a state, bookmarks. This is an array state that will hold the bookmarks fetched from our Strapi backend.

We use the useEffect hook to call the localhost:1337/bookmarks endpoint. The returned bookmarks are set to the bookmarks state. The bookmarks are then rendered using the Array#map method.

The Add Bookmark button displays the AddBookmarkDialog component when clicked.

EditBookmarkDialog
This component is where we can edit a selected bookmark. Paste the below code in src/components/EditBookmarkDialog/index.js:

    import axios from "axios";
    import { useRef } from "react";
    import CloseIcon from "../CloseIcon";

    export default function EditBookmarkDialog({ closeModal, bookmark }) {
      const formRef = useRef();

      async function editBookmark() {
        var { title, content } = formRef.current;
        title = title.value;
        content = content.value;

        await axios.put("http://localhost:1337/bookmarks/" + bookmark?.id, {
          title,
          content,
          synopsis: content.slice(0, 100) + "...",
        });
        window.location.reload();
      }

      return (
        <div class="modal fixed -top-0 left-0 w-full h-full flex flex-col z-0 items-center">
          <div
            class="modal-backdrop opacity-70 bg-gray-50 fixed w-full h-full z-10"
            onClick={closeModal}
          ></div>
          <div class="modal-content z-20 w-2/5 mt-5 bg-white shadow-md">
            <div class="modal-header flex justify-between items-center bg-red-600 p-3 text-white">
              <h3 class="text-white font-bold">Edit Bookmark</h3>
              <span
                style={{ padding: "10px", cursor: "pointer" }}
                onClick={closeModal}
              >
                <CloseIcon />
              </span>
            </div>
            <div className="modal-body content m-2 p-5 z-50">
              <form ref={formRef}>
                <div class="w-full">
                  <div class="pl-2">
                    <span>TITLE</span>
                  </div>
                  <input
                    type="text"
                    class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
                    placeholder="Type in title..."
                    defaultValue={bookmark?.title}
                    name="title"
                  />
                </div>
                <div class="w-full">
                  <div class="pl-2 mt-3">
                    <span>CONTENT</span>
                  </div>
                  <textarea
                    type="text"
                    class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
                    placeholder="Type in content..."
                    defaultValue={bookmark?.content}
                    name="content"
                  ></textarea>
                </div>
              </form>
            </div>
            <div className="modal-footer flex justify-between p-4 bg-gray-200">
              <button
                class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-3 rounded"
                onClick={closeModal}
              >
                Cancel
              </button>
              <button
                class="bg-red-600 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
                onClick={editBookmark}
              >
                Save
              </button>
            </div>
          </div>
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

The selected bookmark to edit is sent via the component props. It is destructured to bookmark.
The title and content of the bookmark are set in the input boxes. Here the user can then modify their contents.

The Save button calls the editBookmark function, this function collects the values of the bookmark's from the input boxes. It generates a synopsis from the content, then makes an HTTP PUT request to localhost:1337/bookmarks/+id.

The synopsis, title, and content are sent as payload. The id will be the id of the bookmark, this enables Strapi to edit the bookmark with the sent payload.

Finally, the page is reloaded so we see the updated bookmark.

ViewBookmarkDialog
The component is used to view a bookmark. Paste the below code in the src/components/ViewBookmarkDialog/index.js:

    import CloseIcon from "./../CloseIcon";

    export default function ViewBookmarkDialog({ closeModal, bookmark }) {
      return (
        <div class="modal fixed -top-0 left-0 w-full h-full flex flex-col z-0 items-center">
          <div
            class="modal-backdrop opacity-70 bg-gray-50 fixed w-full h-full z-10"
            onClick={closeModal}
          ></div>
          <div class="modal-content z-20 w-2/5 mt-5 bg-white shadow-md">
            <div class="modal-header flex justify-between items-center bg-red-600 p-3 text-white">
              <h3 class="text-white font-bold">View Bookmark</h3>
              <span
                style={{ padding: "10px", cursor: "pointer" }}
                onClick={closeModal}
              >
                <CloseIcon />
              </span>
            </div>
            <div className="modal-body content m-2 p-5 z-50">
              <div class="w-full">
                <div class="pl-2">
                  <span>TITLE</span>
                </div>

                <input
                  type="text"
                  class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
                  placeholder="Type in title.."
                  defaultValue={bookmark?.title}
                  disabled={true}
                />
              </div>
              <div class="w-full">
                <div class="pl-2 mt-3">
                  <span>CONTENT</span>
                </div>

                <textarea
                  type="text"
                  class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
                  placeholder="Type in content.."
                  disabled={true}
                  defaultValue={bookmark?.content}
                ></textarea>
              </div>
            </div>
            <div className="modal-footer flex justify-between p-4 bg-gray-200">
              <button
                class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-3 rounded"
                onClick={closeModal}
              >
                Close
              </button>
            </div>
          </div>
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

This component destructures the bookmark object and closeModal function from its props. It displays the title and the content from the bookmark object. The closeModal function closes the component.

App.js
We will add routing to our App component, paste the below code to src/App.js:

    import "./App.css";
    import { BrowserRouter, Route, Switch } from "react-router-dom";
    import Header from "./components/Header";
    import BookmarkList from "./components/BookmarkList";

    function App() {
      return (
        <>
          <Header />
          <div class="container bg-gray-100">
            <head>
              <title>Bookmark</title>
              <link rel="icon" href="/favicon.ico" />
            </head>

            <main class="flex justify-center mx-86">
              <BrowserRouter>
                <Switch>
                  <Route exact path="/">
                    <BookmarkList />
                  </Route>
                  <Route path="*">
                    <BookmarkList />
                  </Route>{" "}
                </Switch>
              </BrowserRouter>
            </main>
          </div>
        </>
      );
    }

    export default App;
Enter fullscreen mode Exit fullscreen mode

We have a single route which is the / index route. We used the Route component from react-touter-dom to render the BookmarkList component when the index route / is navigated.

We see that the Header component is outside the BrowserRouter, this makes it render on every page in our routing system.

See that the div element below the Header is set to have container in its class attribute. We want the div element to center its content. To do that we have to go to the tailwind.config.js file and make an addition to the theme.extend object.

Add the below code to tailwind.config.js:

    module.exports = {
      purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
      darkMode: false, // or 'media' or 'class'
      theme: {
        extend: {
          container: {
            center: true,
          },
        },
      },
      variants: {
        extend: {},
      },
      plugins: [],
    };
Enter fullscreen mode Exit fullscreen mode

The div.container will center its content.

Testing the app

Make sure your Strapi backend is running and your Reactjs is running too if not, start your Reactjs server with the below command:

    yarn start
Enter fullscreen mode Exit fullscreen mode

To start the Strapi server run the below command:

    yarn develop
Enter fullscreen mode Exit fullscreen mode

Add bookmark

View bookmark

Copy bookmark

Edit bookmark

Delete bookmark

You can get the source code of the backend and frontend of this app below.

Source code

References

Border Radius
Tailwind CSS For Absolute Beginners
The Sexiness of Headless UI Components

Conclusion

This was my very first time using Tailwind CSS, and I must say it blew my mind. It made my job very fast and easy. The fantastic thing is that I built this app without writing a single line of CSS code !!

Let's recap, we started by learning about Headless CMS and headless UI, then we proceeded to learn about Strapi and Tailwind CSS. They are examples of Headless CMS and Headless UI, respectively. Later on, we demonstrated how to use both Strapi and Tailwind CSS by building a Bookmarking app.

Discussion (0)