[challenge from the course] Implementing the Issue status changing by improving the statusBadge component

Hi everyone, I am a rookie developer learning Nextjs from Mosh.
It is such a great course that not only allows me to follow and practice but also encourages me to challenge myself continuously.

So I challenged myself to implement the Issue Status switching by creating a reusable issueStatusBadge component using the “Badge” component from Radix UI along with implementing the tooltip when hovered.

It changes the Issue’s status both on the frontend and backend at an amazing speed whenever the user clicks on the status badge.

I would appreciate it if anyone could correct my code or improve it in any form of way!!!

"use client";
import { Spinner } from "@/app/components";
import { Issue, Status } from "@prisma/client";
import { Badge, Tooltip } from "@radix-ui/themes";
import axios from "axios";
import { useState } from "react";

const statusMap: Record<
  Status,
  {
    label: string;
    nextLabel: string;
    color: "red" | "violet" | "cyan" | "orange" | "green";
    variant: "soft" | "solid";
  }
> = {
  OPEN: {
    label: "Open",
    nextLabel: "In Progress",
    color: "red",
    variant: "soft",
  },
  CLOSED: {
    label: "Closed",
    nextLabel: "Open",
    color: "green",
    variant: "soft",
  },
  IN_PROGRESS: {
    label: "In Progress",
    nextLabel: "Closed",
    color: "violet",
    variant: "soft",
  },
};
type Props = {
  title?: Issue["title"];
  description?: Issue["description"];
  issueId?: Issue["id"];
  status: Issue["status"];
};

const IssueStatusBadge = ({ status, issueId, title, description }: Props) => {
  const [currentStatus, setCurrentStatus] = useState(status);
  const [isSettingStatus, setIsSettingStatus] = useState(false);

  const handleClick = async () => {
    let newStatus: Status;
    switch (currentStatus) {
      case "OPEN":
        newStatus = "IN_PROGRESS";

        break;
      case "IN_PROGRESS":
        newStatus = "CLOSED";

        break;
      case "CLOSED":
        newStatus = "OPEN";

        break;
      default:
        newStatus = currentStatus;
        break;
    }

    try {
      setIsSettingStatus(true);
      await axios.patch("/api/issues/" + issueId, {
        title: title,
        description: description,
        status: newStatus,
      });
      setCurrentStatus(newStatus);
    } catch (error) {
      setIsSettingStatus(false);
      console.error(error);
    }
    setIsSettingStatus(false);
  };

  return (
    <>
      <Tooltip
        className="!opacity-50"
        delayDuration={0}
        content={`Switch Status to: ${statusMap[currentStatus].nextLabel}`}
        disableHoverableContent={true}
      >
        <Badge
          className="transition-all duration-300 hover:cursor-pointer hover:shadow"
          variant={statusMap[currentStatus].variant}
          onClick={handleClick}
          radius="large"
          color={statusMap[currentStatus].color}
        >
          {statusMap[currentStatus].label}{" "}
          {isSettingStatus ? <Spinner /> : null}
        </Badge>
      </Tooltip>
    </>
  );
};

export default IssueStatusBadge;

Hi Felix,. I’m happy to do a code review.

Design Critiques

IssueStatusBadge Single Responsibility

The purpose of IssueStatusBadge is to display a status badge, but in this design it also knows about issue details and can make API calls that change issues. It is doing too much (SRP). This inhibits its reuse in places like the dashboard where we probably don’t want to make changes to data and should only show the badge.

Suggestion

Revert IssueStatusBadge to its simple form and wrap (decorate) it with a surrounding component that implements the additional functionality. Something like this:

const IssueStatusChooser = ({ status, issueId, title, description }: Props) => {
  const [currentStatus, setCurrentStatus] = useState(status);
  ...
  
  const changeStatus = async () => {
    const previousStatus = currentStatus;
    try {
      // call setCurrentStatus with new status (optimistic update)
      // make API PATCH call to update status    
    } catch (err) {
      // revert setCurrentStatus to previousStatus status if update fails
    }
  }
  return <Button onClick={changeStatus}><IssueStatusBadge status={currentStatus} /></Button>
}

IssueStatusBadge Change Cycle

Instead of wrapping the badge in a button and cycling through statuses on each click (which requires a DB update each time) maybe consider adding status badges to a Select list and updating on each new selection. This would also allow you to remove the switch statement logic. Something like this should work, but might require some tweaking of styles to remove select or reduce the select border outline.

import { Status } from "@prisma/client";

...

<Select.Root
  value={status}
  onValueChange={changeStatus}
>
  <Select.Trigger placeholder="Status" />
  <Select.Content>
    {Object.values(Status).map((status) => (
      <Select.Item key={status} value={status}>
        <IssueStatusBadge status={status} />
      </Select.Item>
    ))}
  </Select.Content>
</Select.Root>

Miscellaneous

  • Your color sum type includes unused strings “cyan” and “orange”
  • There’s no need to add variant to statusMap. Since the value is always “soft” you can pass it as a literal prop (variant="soft").

Hope this helps.

2 Likes

WOW! It’s the first time I’ve posted on this forum, and actually wasn’t expecting someone really reply.

Your advice is really helpful.

Thank you Mr.Programmist.

1 Like

Hi there,

I know it’s been 3 months now but I’d be happy if you can help me in this challenge.

I implemented the code Felix gave us, but it doesn’t change the database’s status. It does change the button tho. I’m using MongoDB instead.

Any idea why this is happening?

"use client"
import { Spinner } from "@/app/components"
import { Issue, Status } from "@prisma/client"
import { Badge, Tooltip } from "@radix-ui/themes"
import axios from "axios"
import { useState } from "react"

const statusMap: Record<
  Status,
  {
    label: string
    nextLabel: string
    color: "red" | "violet" | "cyan" | "orange" | "green"
    variant: "soft" | "solid"
  }
> = {
  OPEN: {
    label: "Open",
    nextLabel: "In Progress",
    color: "red",
    variant: "soft",
  },
  CLOSED: {
    label: "Closed",
    nextLabel: "Open",
    color: "green",
    variant: "soft",
  },
  IN_PROGRESS: {
    label: "In Progress",
    nextLabel: "Closed",
    color: "violet",
    variant: "soft",
  },
}

type Props = {
  title?: Issue["title"]
  description?: Issue["description"]
  issueId?: Issue["id"]
  status: Issue["status"]
}

const IssueStatusBadge = ({ status, issueId, title, description }: Props) => {
  const [currentStatus, setCurrentStatus] = useState(status)
  const [isSettingStatus, setIsSettingStatus] = useState(false)

  const handleClick = async () => {
    let newStatus: Status
    switch (currentStatus) {
      case "OPEN":
        newStatus = "IN_PROGRESS"

        break
      case "IN_PROGRESS":
        newStatus = "CLOSED"

        break
      case "CLOSED":
        newStatus = "OPEN"

        break
      default:
        newStatus = currentStatus
        break
    }

    try {
      setIsSettingStatus(true)
      await axios.patch(`/api/issues/${issueId}`, {
        title: title,
        description: description,
        status: newStatus,
      })
      setCurrentStatus(newStatus)
    } catch (error) {
      setIsSettingStatus(false)
      console.error("An error ocurred: ", error)
    }
    setIsSettingStatus(false)
  }

  return (
    <>
      <Tooltip
        className="!opacity-50"
        delayDuration={0}
        content={`Switch Status to: ${statusMap[currentStatus].nextLabel}`}
        disableHoverableContent={true}
      >
        <Badge
          className="transition-all duration-300 hover:cursor-pointer hover:shadow"
          variant={statusMap[currentStatus].variant}
          onClick={handleClick}
          radius="large"
          color={statusMap[currentStatus].color}
        >
          {statusMap[currentStatus].label}{" "}
          {isSettingStatus ? <Spinner /> : null}
        </Badge>
      </Tooltip>
    </>
  )
}

export default IssueStatusBadge

When I try implementing this it doesnt change in the database. any tips?