Slightly Sharpe
Published on

Promo Dashboard Feature Development

Authors
  • avatar
    Name
    Jason R. Stevens, CFA
    LinkedIn
    linkedin@thinkjrs
The fictional AI-generated open-source software laboratory at Tincre.
Tincre's epic, imaginary AI-generated open-source software lab.

This piece outlines adding the ability to delete inactive and unpaid campaigns in the Promo Dashboard, the dashboard that powers Tincre Promo integrations such as

Think of this as a thought-trace that will traverse through how we develop features for our libraries and applications at Tincre.

Full disclaimer, none of the below will work as a "copy-pasta" project.
It covers only the main portions of our feature development
for the delete functionality.

Follow along on Github between commits 291870d4 - v0.8.10 through 067eb68b - v0.8.11.

General overview

Our task is to add a delete button, functionality, and styling to the dashboard for campaigns that are inactive and unpaid.

That means we need to interact with the components that make up the campaign card, add a new delete button component, and add some default handler so that the delete button behaves as expected when a user clicks "delete".

We'll modify the component that lists all of the Campaign components users see on their dashboards, modify that Campaign component to render an "X" for delete and passthrough an onClick handler to perform some logic for deletion.

Lastly, we'll give this a little styling and add an example to the library docs.

Adding delete buttons to the dashboard

A few of the items we'll tackle include the following.

  • Examine CampaignList component and map out this todo list.

  • We'll need a handler to delete a Campaign which CampaignList holds a list of Campaigns.

  • Pass the handler to CampaignList which will pass to each Campaign which will pass to each delete button rendered on each campaign.

  • Add the appropriate type to the handler using React's MouseEvent<HTMLButtonElement> type in component props, i.e.

    handleDeleteButtonOnClick: (
      event: MouseEvent<HTMLButtonElement>,
      data: CampaignData,
    ) => void;
    
  • We'll need a small "X" component that shows on hover of inactive and unpaid campaigns that takes the handler above. This will need absolute CSS positioning within each Campaign and will accept the passed handler, a rendered "X" for for each Campaign component that's inactive and unpaid.

    Here's what the final component looks like (sans imports and license boilerplate):

    export function CampaignDeleteButton({
      data,
      handleDeleteButtonOnClick,
      id,
    }: {
      data: CampaignData;
      handleDeleteButtonOnClick?: (event: MouseEvent<HTMLButtonElement>, data: CampaignData) => void;
      id?: string;
    }) {
      return (
        <button
          aria-label={`campaign-delete-${data?.pid || 'default'}-button`}
          type="button"
          onClick={(event: MouseEvent<HTMLButtonElement>) => {
            return typeof handleDeleteButtonOnClick !== 'undefined'
              ? handleDeleteButtonOnClick(event, data)
              : console.warn(
                  `promo-dashboard::CampaignDeleteButton::Undefined handleDeleteButtonOnClick. Please contact the developer of this application and report this error.`,
                );
          }}
          id={id}
          className="promo-dashboard-campaign-delete-button absolute inset-x-0 bottom-2 z-10 mx-auto sm:bottom-10"
        >
          <XCircleIcon
            className="h-8 w-8 text-red-500 hover:text-red-800 group-hover:rounded-full group-hover:bg-slate-800 group-hover:text-red-200 sm:h-6 sm:w-6"
            id="promo-dashboard-campaign-delete-button-x-circle-icon"
            aria-hidden="true"
          />
        </button>
      );
    }
    

    Obviously you can catch the full code for this in the repository!

Implementing onClick handler logic

On a high-level, we don't want to actually handle deleting a campaign, as CampaignData[] are passed into the Promo Dashboard from the parent component.

Furthermore, we've exposed the handler to the parent for customization, such that the rendering application can completely handle state and other logic from outside the Promo Dashboard. We use these customization features extensively at Tincre.

With Tincre's Promo integrations we handle core campaign data via the Promo API, not within client applications. This separates management of campaigns from their views and client features. Think of this as a distributed, cloud-based MVC architecture.

So what should we do?

When a human clicks a delete button they'll expect that the campaign is deleted. Because this is a front-end library, let's simply track an array of deletes locally. Client callers (apps) are responsible for feeding in data, and therefore, responsible for calling the Promo API.

As long as the Promo API's DELETE /campaigns HTTP method is called correctly, this data will update and pass down to the Promo Dashboard component when re-rendered.

For now, we simply need to track an array of deletes and keep from displaying those.

  • Let's add some state to track this in the top-level component:

    const [deletedCampaigns, setDeletedCampaigns] = useState<string[]>([]);
    

    We type this as a string array that will hold Promo IDs and will check against it when rendering the campaign list in the CampaignList component.

    Next we'll add a prop to accept deletedCampaigns from the CampaignList, adding a comparison within the array map that renders each Campaign component.

    // inside the array map
    if (!deletedCampaigns.includes(pid)) {
      // render Campaign component
    }
    
  • We'll need logic in our handler to add the campaign id that's deleted if available for deletion.

    Remember that we don't want to allow deletion of campaigns that are active and paid. That's another task.

    To accomplish this, we need to simply add to the array above via setDeletedCampaigns, i.e.

    // inside the handler
    setDeletedCampaigns((current) => [...current, data.pid]);
    

In all we should now have a handler that looks like this:

const handleDeleteButtonOnClick = (event: MouseEvent<HTMLButtonElement>, data: CampaignData) => {
  event.preventDefault();
  setDeletedCampaigns((current) => [...current, `${data.pid}`]);
};

And the CampaignList component now takes a string array deletedCampaigns and compares campaign IDs to that deletedCampaigns array before rendering them.

When the Promo API backend updates user data on login deletedCampaigns will be an empty array, however, the campaigns deleted in the next portion via the API should not show because they've been removed in the API itself.

Default backend logic

  • We'll need some demo backend code to show how to use the Promo API /campaigns DELETE method in TypeScript.

  • The default logic should actually call the standard /api/promo route with a DELETE HTTP verb and the correct Promo API payload, using the above "demo" code.

The delete call should look something like the following:

const response = await fetch('/api/promo', {
  body: JSON.stringify([data.pid]),
  headers: {
    'Content-Type': 'application/json',
  },
  method: 'DELETE',
});

Obviously we need to catch and handle errors so our actual implementation adds some try-catch blocks and error messages.

Add user notifications

We use React Hot Toast for notifications within the Promo Dashboard.

Now that we have a working delete mechanism and backend call, let's add delete notifications on success and failure of the backend call.

We should add a success and failure notifications from our lib/notifications.ts module:

  • success:

    successToast(`${data.pid} successfully deleted.`);
    
  • failure:

    failureToast(`${data.pid} was not deleted. Try again.`);
    

In conclusion

All-in-all adding full deletion functionality wasn't all that difficult, though it involved quite a few parts.

In particular, we created a new component CampaignDeleteButton, rendered that in Campaign components with truthy values for CampaignData.isReceipt and created onClick handler infrastructure to perform necessary logic.

Want to dig into the Promo Dashboard and add your own feature? File an issue letting us know and we'll be thrilled to walk you through the process.

Thanks for reading. We ❤️ you people!

👋 We use cookies to enhance your experience. In using this site you agree to the storing of cookies on your device.