- Published on
Promo Dashboard Feature Development
- Authors
- Name
- Jason R. Stevens, CFA
- linkedin@thinkjrs
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
- UNDRCVR ADS,
- SwayAd, and
- b00st.com.
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
whichCampaignList
holds a list ofCampaign
s.Pass the handler to
CampaignList
which will pass to eachCampaign
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 eachCampaign
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!
onClick
handler logic
Implementing 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 theCampaignList
, adding a comparison within the array map that renders eachCampaign
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!