#react #ui Fairly simple widget cards with tab-able panes made with react typescript and bootstrap.

We start with a generalized widget card. I gave only a few options for fixed sizes, because I want them to be consistent if you are placing a bunch together on a page. Other than that it is basically a nice frame or wrapper around whatever children you give the component.

The style style={{ overflowY: "auto", overflowX: "hidden" }} is important if you want the widget to be scroll able from within the pane. Pretty cool.

You can also pass true to hideOnMobile, which will hide the widget on small screens.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { FC, ReactNode } from "react";

export const WidgetCard: FC<{
  children: ReactNode;
  cardTitle: string;
  path?: string;
  hideOnMobile?: boolean;
  size?: string;
}> = ({ children, cardTitle, path = "", hideOnMobile, size = "" }) => {
  const normalSize = { height: "600px", width: "400px" };
  const wideSize = { height: "600px", width: "800px" };
  const halfSize = { height: "300px", width: "400px" };
  var cardSize = setCardSize();

  return (
    <div
      className={
        "card m-1 p-0 shadow mb-2 " + (hideOnMobile ? "d-lg-flex d-none" : "")
      }
      style={cardSize}
    >
      <div className="card-header bg-primary text-light">
        <a className="text-decoration-none text-reset" href={path}>
          <p className="col m-0">
            {cardTitle} <i className="bi bi-box-arrow-up-right col-3"></i>
          </p>
        </a>
      </div>
      <div
        className="card-body p-2 pt-0"
        style={{ overflowY: "auto", overflowX: "hidden" }}
      >
        {children}
      </div>
    </div>
  );

  function setCardSize() {
    var cardSize = {};
    if (size === "wide") {
      cardSize = wideSize;
    } else if (size === "half") {
      cardSize = halfSize;
    } else {
      cardSize = normalSize;
    }
    return cardSize;
  }
};

Widget Tabs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { FC, useState } from "react";
import classes from "../../../components/NavBar.module.scss";

export const WidgetTabMenu: FC<{
  tabs: {
    key: string;
    name: string;
    component: JSX.Element;
  }[];
}> = ({ tabs }) => {
  const [selectedTab, setSelectedTab] = useState(
    tabs.length > 0 ? tabs[0].key : ""
  );

  const [hoverState, setHoverState] = useState(false);
  const toggleHoverState = () => {
    setHoverState(!hoverState);
  };

  return (
    <>
      <nav className="sticky-top">
        <div
          className="nav nav-tabs nav-justified bg-white "
          id="nav-tab"
          role="tablist"
        >
          {tabs.map((tab) => (
            <button
              key={tab.key + " button"}
              className={
                "nav-link  bg-white " +
                (selectedTab === tab.key
                  ? "active text-primary " + classes.active
                  : " text-secondary " + classes.link)
              }
              id={`nav-${tab.key}-tab`}
              data-bs-toggle="tab"
              data-bs-target={`#nav-${tab.key}`}
              type="button"
              role="tab"
              aria-controls={`nav-${tab.key}`}
              aria-selected={selectedTab === tab.key}
              onClick={() => setSelectedTab(tab.key)}
              onMouseEnter={toggleHoverState}
              onMouseLeave={toggleHoverState}
            >
              {tab.name}
            </button>
          ))}
        </div>
      </nav>
      <br />
      <div className="tab-content" id="nav-tabContent">
        {tabs.map(({ key, component }) => (
          <div
            key={key + "content"}
            className={`tab-pane fade ${
              selectedTab === key ? "active show " : ""
            }`}
            id={`nav-${key}`}
            role="tabpanel"
            aria-labelledby={`nav-${key}-tab`}
          >
            {component}
          </div>
        ))}
      </div>
    </>
  );
};

Dashboard Page

Here you can see some of the different sizes together on the dashboard page. The tabs at the top are all the default size, some have the tabs passed into them as a child component so it allows switching panes from within the card.

The link cards at the bottom use the same card, half size, and don’t have tabs passed as children. Pasted image 20221229124613.png

Here’s a closer look at how tabs with different paged components are passed to a widget.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export const MyFinances = () => {
  const tabs = [
    {
      key: "financesummary",
      name: "Summary", //Tab name
      component: <FinanceSummary />, //Component to show when tab is selected
    },
    {
      key: "financehistory",
      name: "History",
      component: <FinanceHistory />,
    },
    {
      key: "financenotifications",
      name: "Notifications",
      component: <FinanceNotifications />,
    },
  ];

  return (
    <WidgetCard cardTitle="My Finances" hideOnMobile={true}>
      <WidgetTabMenu tabs={tabs}></WidgetTabMenu>
    </WidgetCard>
  );
};

Pasted image 20221229124709.png