/ packages / frontend / src / components / Navigation.tsx
Navigation.tsx
  1  // SPDX-FileCopyrightText: 2024 Mass Labs
  2  //
  3  // SPDX-License-Identifier: GPL-3.0-or-later
  4  
  5  import { useEffect, useState } from "react";
  6  import { Link, useNavigate } from "@tanstack/react-router";
  7  import { useDisconnect } from "wagmi";
  8  
  9  import { Order, OrderedItem } from "@massmarket/schema";
 10  import { CodecValue } from "@massmarket/utils/codec";
 11  
 12  import { KeycardRole } from "../types.ts";
 13  import Cart from "./cart/Cart.tsx";
 14  import { useStateManager } from "../hooks/useStateManager.ts";
 15  import { useShopDetails } from "../hooks/useShopDetails.ts";
 16  import { useKeycard } from "../hooks/useKeycard.ts";
 17  import { useCurrentOrder } from "../hooks/useCurrentOrder.ts";
 18  
 19  const merchantMenu = [
 20    {
 21      title: "Dashboard",
 22      img: "menu-dashboard.svg",
 23      href: "/merchant-dashboard",
 24    },
 25    //TODO: href for orders, contact, share.
 26    { title: "Manage Products", img: "menu-products.svg", href: "/listings" },
 27    { title: "Manage Orders", img: "menu-order.svg", href: "/orders" },
 28    { title: "Shop Settings", img: "menu-settings.svg", href: "/settings" },
 29    { title: "Disconnect", img: "menu-disconnect.svg" },
 30  ];
 31  
 32  const customerMenu = [
 33    { title: "Shop", img: "menu-products.svg", href: "/listings" },
 34    {
 35      title: "Cart",
 36      img: "menu-cart.svg",
 37      href: `/cart`,
 38    },
 39    {
 40      title: "Contact",
 41      img: "menu-contact.svg",
 42      href: `/contact`,
 43    },
 44    {
 45      title: "Share",
 46      img: "menu-share.svg",
 47      href: `/share`,
 48    },
 49  ];
 50  
 51  function Navigation() {
 52    const [menuOpen, setMenuOpen] = useState<boolean>(false);
 53    const [cartVisible, setCartVisible] = useState<boolean>(false);
 54    const [cartSize, setCartSize] = useState<number>(0);
 55  
 56    const navigate = useNavigate();
 57    const { shopDetails } = useShopDetails();
 58    const { stateManager } = useStateManager();
 59    const { currentOrder } = useCurrentOrder();
 60    const [keycard] = useKeycard();
 61    const { disconnect } = useDisconnect();
 62    const isMerchantView = keycard.role === KeycardRole.MERCHANT;
 63  
 64    useEffect(() => {
 65      // in the hook `useCurrentOrder`, we "reset" currentOrder for the states OrderState.Canceled and OrderState.Paid.
 66      // this is done by setting it to null.
 67      // TODO (@alp 2025-04-17): raise the question of using a sentinel value for "reset" orderIDs e.g. currentOrder = SENTINEL_ORDER
 68      //
 69      // NOTE(@alp 2025-04-17): this same bug (setting currentOrder = null) *might* be afflicting ListingDetail.tsx and
 70      // the call to cancelAndRecreateOrder
 71      if (!currentOrder) {
 72        setCartSize(0);
 73        return;
 74      }
 75      stateManager?.get(["Orders", currentOrder.ID])
 76        .then((o: CodecValue | undefined) => {
 77          if (!o) {
 78            throw new Error("No order found");
 79          }
 80          const order = Order.fromCBOR(o);
 81          // Getting number of items in order.
 82          let cartSize = 0;
 83          order.Items.forEach((item: OrderedItem) => (cartSize += item.Quantity));
 84          setCartSize(cartSize);
 85        });
 86    }, [currentOrder]);
 87  
 88    function onDisconnect() {
 89      setMenuOpen(false);
 90      localStorage.clear();
 91      disconnect();
 92      navigate({
 93        to: "/merchant-connect",
 94      });
 95    }
 96  
 97    function menuSwitch() {
 98      setMenuOpen(!menuOpen);
 99      cartVisible && setCartVisible(false);
100    }
101  
102    function onCheckout() {
103      setCartVisible(false);
104      navigate({
105        to: "/shipping",
106        search: (prev: Record<string, string>) => ({
107          shopId: prev.shopId,
108        }),
109      });
110    }
111  
112    function renderMenuItems() {
113      const menuItems = isMerchantView ? merchantMenu : customerMenu;
114      return menuItems.map((opt, i) => {
115        if (opt.title === "Disconnect") {
116          return (
117            <button
118              type="button"
119              style={{ backgroundColor: "transparent", padding: 0 }}
120              className="cursor-pointer"
121              key={i}
122              onClick={onDisconnect}
123            >
124              <div className="flex gap-3 items-center">
125                <img
126                  src={`/icons/${opt.img}`}
127                  width={20}
128                  height={20}
129                  alt="menu-item"
130                  className="w-5 h-5"
131                />
132                <h2 className="font-normal">{opt.title}</h2>
133                <img
134                  src="/icons/chevron-right.svg"
135                  width={12}
136                  height={12}
137                  alt="chevron-right"
138                  className="ml-auto w-3 h-3"
139                />
140              </div>
141            </button>
142          );
143        }
144  
145        return (
146          <div
147            data-testid={`menu-button-${opt.title}`}
148            key={i}
149            onClick={() => setMenuOpen(false)}
150          >
151            <Link
152              to={opt.href!}
153              key={opt.title}
154              search={(prev: Record<string, string>) => ({
155                shopId: prev.shopId,
156              })}
157            >
158              <div className="flex gap-3 items-center">
159                <img
160                  src={`/icons/${opt.img}`}
161                  width={20}
162                  height={20}
163                  alt="menu-item"
164                  className="w-5 h-5"
165                />
166                <h2 className="font-normal text-black">{opt.title}</h2>
167                <img
168                  src="/icons/chevron-right.svg"
169                  width={12}
170                  height={12}
171                  alt="chevron-right"
172                  className="ml-auto w-3 h-3"
173                />
174              </div>
175            </Link>
176          </div>
177        );
178      });
179    }
180  
181    return (
182      <section>
183        {(cartVisible || menuOpen) && (
184          <span
185            className="fixed bg-black w-full h-full opacity-60 z-5"
186            onClick={() => {
187              cartVisible && setCartVisible(false);
188              menuOpen && setMenuOpen(false);
189            }}
190          />
191        )}
192        <section
193          className={`bg-white flex justify-center z-10 relative`}
194          data-testid="navigation"
195        >
196          <section className="relative w-full text-base flex justify-between md:w-[800px] h-[56px] mr-3">
197            <div
198              id="logo"
199              className="flex gap-2 cursor-pointer m-2"
200              onClick={() => {
201                navigate({
202                  to: isMerchantView ? "/merchant-dashboard" : "/listings",
203                  search: (prev: Record<string, string>) => ({
204                    shopId: prev.shopId,
205                  }),
206                });
207                setMenuOpen(false);
208                setCartVisible(false);
209              }}
210            >
211              {shopDetails.profilePictureUrl
212                ? (
213                  <div className="overflow-hidden rounded-full w-10 h-10">
214                    <img
215                      src={shopDetails.profilePictureUrl}
216                      width={40}
217                      height={40}
218                      alt="profile-avatar"
219                      className="w-10 h-10"
220                    />
221                  </div>
222                )
223                : (
224                  <img
225                    src={`/icons/mass-labs-logo.svg`}
226                    width={40}
227                    height={40}
228                    alt="mass-labs-logo"
229                    className="w-10 h-10"
230                  />
231                )}
232  
233              <h2 className="flex items-center">{shopDetails.name}</h2>
234            </div>
235            <section className="absolute right-0 flex">
236              <div
237                id="menu"
238                className={`${
239                  cartVisible ? "invisible" : "visible"
240                } flex flex-col items-end`}
241              >
242                <button
243                  onClick={menuSwitch}
244                  style={{
245                    backgroundColor: menuOpen ? "#F3F3F3" : "transparent",
246                    paddingLeft: 15,
247                    paddingRight: 15,
248                  }}
249                  type="button"
250                  className="self-end h-[56px] cursor-pointer"
251                >
252                  <img
253                    src={menuOpen
254                      ? "/icons/close-icon.svg"
255                      : "/icons/hamburger.svg"}
256                    width={20}
257                    height={20}
258                    alt="menu-icon"
259                    className="w-5 h-5"
260                  />
261                </button>
262                <div
263                  className={`${menuOpen ? "hidden md:block z-10" : "hidden"}`}
264                >
265                  <div className="fixed bg-background-gray w-full flex flex-col gap-5 rounded-b-lg p-5 w-fit static">
266                    {renderMenuItems()}
267                  </div>
268                </div>
269              </div>
270              <div
271                id="cart"
272                className={`${
273                  menuOpen ? "invisible" : "visible"
274                } flex flex-col items-end relative`}
275              >
276                <button
277                  type="button"
278                  data-testid="cart-toggle"
279                  className={`${
280                    isMerchantView ? "hidden" : ""
281                  } self-end h-[56px]`}
282                  style={{
283                    backgroundColor: cartVisible ? "#F3F3F3" : "transparent",
284                    paddingLeft: 15,
285                    paddingRight: 15,
286                  }}
287                  onClick={() => {
288                    setCartVisible(!cartVisible);
289                    menuOpen && setMenuOpen(false);
290                  }}
291                >
292                  <img
293                    src={cartVisible
294                      ? "/icons/close-icon.svg"
295                      : "/icons/menu-cart.svg"}
296                    width={20}
297                    height={20}
298                    alt="cart-icon"
299                    className="w-5 h-5"
300                  />
301                  <div
302                    className={`${
303                      (!cartSize || cartVisible) ? "hidden" : ""
304                    } bg-red-700 rounded-full absolute top-[10px] right-[7px] w-4 h-4 flex justify-center items-center`}
305                  >
306                    <p className="text-white text-[10px]">{cartSize}</p>
307                  </div>
308                </button>
309                <div
310                  className={`${cartVisible ? "hidden md:block z-10" : "hidden"}`}
311                >
312                  <div
313                    data-testid="desktop-cart"
314                    className="fixed bg-background-gray w-full flex flex-col gap-5 rounded-b-lg p-5 static"
315                  >
316                    <h1>Cart</h1>
317                    <Cart
318                      onCheckout={onCheckout}
319                      closeCart={() => setCartVisible(false)}
320                    />
321                  </div>
322                </div>
323              </div>
324            </section>
325          </section>
326        </section>
327        <section id="mobile-menu" className="md:hidden absolute z-10">
328          {menuOpen
329            ? (
330              <section>
331                <div className="fixed bg-background-gray w-full flex flex-col gap-5 rounded-b-lg p-5">
332                  {renderMenuItems()}
333                </div>
334              </section>
335            )
336            : null}
337          {cartVisible
338            ? (
339              <section>
340                <div className="fixed bg-background-gray w-full flex flex-col gap-5 rounded-b-lg p-5">
341                  <h1>Cart</h1>
342                  <Cart
343                    onCheckout={onCheckout}
344                    closeCart={() => setCartVisible(false)}
345                  />
346                </div>
347              </section>
348            )
349            : null}
350        </section>
351      </section>
352    );
353  }
354  
355  export default Navigation;