diff --git a/src/components/[guild]/Requirements/components/RequirementAccessIndicatorUI.tsx b/src/components/[guild]/Requirements/components/RequirementAccessIndicatorUI.tsx index 2dfcccc55b..35de839497 100644 --- a/src/components/[guild]/Requirements/components/RequirementAccessIndicatorUI.tsx +++ b/src/components/[guild]/Requirements/components/RequirementAccessIndicatorUI.tsx @@ -19,7 +19,8 @@ const CIRCLE_BG_CLASS = { gray: "bg-secondary", blue: "bg-info dark:bg-info-subtle-foreground", green: "bg-success dark:bg-success-subtle-foreground", - orange: "bg-warning dark:bg-warning-subtle-foreground", + orange: "bg-warning dark:bg-warning-subtle-foreground", + red: "bg-error dark:bg-error-subtle-foreground", } satisfies Record const RequirementAccessIndicatorUI = ({ diff --git a/src/v2/components/Account/components/PurchaseHistoryDrawer/PurchaseHistoryDrawer.tsx b/src/v2/components/Account/components/PurchaseHistoryDrawer/PurchaseHistoryDrawer.tsx index e1580b89d9..73c079f6f5 100644 --- a/src/v2/components/Account/components/PurchaseHistoryDrawer/PurchaseHistoryDrawer.tsx +++ b/src/v2/components/Account/components/PurchaseHistoryDrawer/PurchaseHistoryDrawer.tsx @@ -4,10 +4,16 @@ import { Button } from "@/components/ui/Button" import { Drawer, DrawerContent, - DrawerFooter, DrawerHeader, DrawerTitle, } from "@/components/ui/Drawer" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/DropdownMenu" +import { IconButton } from "@/components/ui/IconButton" import { Table, TableBody, @@ -16,122 +22,190 @@ import { TableHeader, TableRow, } from "@/components/ui/Table" -import { useBilling } from "@/hooks/useBilling" -import { DownloadSimple } from "@phosphor-icons/react" -import { useFetcherWithSign } from "hooks/useFetcherWithSign" -import useShowErrorToast from "hooks/useShowErrorToast" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/Tooltip" +import { Collapse } from "@chakra-ui/react" +import { + ClockClockwise, + DotsThreeVertical, + DownloadSimple, +} from "@phosphor-icons/react/dist/ssr" import { useAtom } from "jotai" -import { useEffect } from "react" -import { useAccount } from "wagmi" +import { useEffect, useRef } from "react" +import * as customChains from "static/customChains" +import * as viemChains from "viem/chains" +import OrderStatusBadge from "./components/OrderStatusBadge" +import useOrders from "./hooks/useOrders" -export const PurchaseHistoryDrawer = () => { - const [isOpen, setIsOpen] = useAtom(purchaseHistoryDrawerAtom) - const { address } = useAccount() +const prettyDate = (order: any) => { + return ( + order?.createdAt && + new Intl.DateTimeFormat("en-US", { + dateStyle: "short", + timeStyle: "short", + }).format(new Date(order.createdAt)) + ) +} - const { - receipts, - pagination, - mutate: refetch, - loadMore, - isLoading, - isValidating, - } = useBilling() +const getChainInfo = (chainId: number): { symbol: string; name: string } => { + for (const chain of Object.values(customChains)) { + if (chain.id === chainId) { + return { + symbol: chain.nativeCurrency.symbol, + name: chain.name, + } + } + } - useEffect(() => { - if (isOpen) refetch() - }, [isOpen, refetch]) + for (const chain of Object.values(viemChains)) { + if (chain.id === chainId) { + return { + symbol: chain.nativeCurrency.symbol, + name: chain.name, + } + } + } - const showLoadMore = - !!pagination && pagination.currentPage !== pagination.totalPages + throw new Error(`Chain with id ${chainId} not found`) +} - const fetcherWithSign = useFetcherWithSign() - const showErrorToast = useShowErrorToast() +const getTotal = (order: any) => { + return order.items.reduce( + (acc: number, item: any) => acc + item.pricePerUnit * item.quantity, + 0 + ) +} - const download = async (receiptId: string) => { - try { - const blob = await fetcherWithSign([ - `/v2/users/${address}/purchase-history/download/${receiptId}`, - { - method: "GET", - headers: { - Accept: "application/pdf", - "Content-Type": "application/pdf", - }, - }, - ]) +export const PurchaseHistoryDrawer = () => { + const isInitialMount = useRef(true) + const [isOpen, setIsOpen] = useAtom(purchaseHistoryDrawerAtom) + const { + orders, + isLoading: isLoadingMore, + isReachingEnd, + error, + loadMore, + mutate, + } = useOrders(isOpen) - const url = window.URL.createObjectURL(blob) - window.open(url) - window.URL.revokeObjectURL(url) - } catch (error) { - console.error("Error in submit function:", error) - showErrorToast( - "Failed to load receipt, please try again later or contact support" - ) + useEffect(() => { + if (isOpen) { + if (!isInitialMount.current) { + mutate() + } + isInitialMount.current = false } - } + }, [isOpen, mutate]) return ( - - + setIsOpen(false)}> + - Purchase History + + Purchase History{" "} + } + isLoading={isLoadingMore} + aria-label="Refresh" + className="ml-2 rounded-full" + onClick={() => mutate()} + /> + -
-
- - - - Receipt - Name - Amount - Date - Payment Address - - - - {receipts.map((receipt) => ( - - -
+ + + Date + Status + Items + Total + Chain + Payment Address + Actions + + + + {orders?.map((order: any) => ( + + {prettyDate(order)} + + + + +
+ {order.items[0].name} + {order.items[0].quantity > 1 && ( + + ×{order.items[0].quantity} + + )} +
+
+ + {getTotal(order)}{" "} + {getChainInfo(order.cryptoDetails.chainId).symbol} + + + {getChainInfo(order.cryptoDetails.chainId).name} + + + + + + + + } /> - - - {receipt.itemName} - - {receipt.totalPrice} USD - - - {new Date(receipt.createdAt).toLocaleDateString()} - - - - -
- ))} -
-
-
+ + + + + + Download Receipt + + + {order.receipt?.status !== "available" && ( + Receipt not available + )} + + + + + + ))} + {orders.length === 0 && !isLoadingMore && ( + + + No orders found + + + )} + +
- - {showLoadMore && ( - - )} - + + +
) diff --git a/src/v2/components/Account/components/PurchaseHistoryDrawer/components/OrderStatusBadge.tsx b/src/v2/components/Account/components/PurchaseHistoryDrawer/components/OrderStatusBadge.tsx new file mode 100644 index 0000000000..337c051714 --- /dev/null +++ b/src/v2/components/Account/components/PurchaseHistoryDrawer/components/OrderStatusBadge.tsx @@ -0,0 +1,32 @@ +import { Badge } from "@/components/ui/Badge" + +const OrderStatusBadge = ({ + status, + createdAt, +}: { status: string; createdAt: string }) => { + const pendingFailed = () => { + const timeDiff = new Date().getTime() - new Date(createdAt).getTime() + const hoursDiff = timeDiff / (1000 * 60 * 60) + + if (hoursDiff > 24) { + return true + } + return false + } + + if (status === "pending" && !pendingFailed()) { + return Pending + } + + if (status === "successful") { + return Successful + } + + if (status === "failed" || (status === "pending" && pendingFailed())) { + return Failed + } + + return Unknown +} + +export default OrderStatusBadge diff --git a/src/v2/components/Account/components/PurchaseHistoryDrawer/hooks/useOrders.ts b/src/v2/components/Account/components/PurchaseHistoryDrawer/hooks/useOrders.ts new file mode 100644 index 0000000000..5ba3a20fd8 --- /dev/null +++ b/src/v2/components/Account/components/PurchaseHistoryDrawer/hooks/useOrders.ts @@ -0,0 +1,77 @@ +import useUser from "components/[guild]/hooks/useUser" +import { useFetcherWithSign } from "hooks/useFetcherWithSign" +import { useGetKeyForSWRWithOptionalAuth } from "hooks/useGetKeyForSWRWithOptionalAuth" +import { useCallback } from "react" +import useSWRInfinite from "swr/infinite" + +const LIMIT = 8 + +type OrdersResponse = { + orders: any[] + pagination: { + total: number + page: number + limit: number + pages: number + } +} + +const useOrders = (shouldFetch: boolean) => { + const { id } = useUser() + + const fetcherWithSign = useFetcherWithSign() + const getKeyForSWRWithOptionalAuth = useGetKeyForSWRWithOptionalAuth() + + const getKey = useCallback( + (pageIndex: number, previousPageData: OrdersResponse | null) => { + if (!id || !shouldFetch) return null + + // If there's no previous page data, this is the first page + if (!previousPageData) { + return getKeyForSWRWithOptionalAuth( + `/v2/users/${id}/orders?page=1&limit=${LIMIT}` + ) + } + + // If we've reached the end, return null + if (pageIndex + 1 > previousPageData.pagination.pages) return null + + // Otherwise, return the next page key + return getKeyForSWRWithOptionalAuth( + `/v2/users/${id}/orders?page=${pageIndex + 1}&limit=${LIMIT}` + ) + }, + [id, getKeyForSWRWithOptionalAuth, shouldFetch] + ) + + const { data, size, setSize, isLoading, error, mutate, isValidating } = + useSWRInfinite(getKey, fetcherWithSign, { + revalidateFirstPage: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + keepPreviousData: true, + shouldRetryOnError: false, + }) + + const orders = data?.map((page) => page.orders).flat() ?? [] + const isLoadingMore = + isLoading || (size > 0 && data && typeof data[size - 1] === "undefined") + const isEmpty = data?.[0]?.orders?.length === 0 + const isReachingEnd = + isEmpty || + (data && + data[data.length - 1]?.pagination.page >= + data[data.length - 1]?.pagination.pages) + + return { + orders, + isLoading: isLoadingMore || isLoading || isValidating, + isReachingEnd, + error, + loadMore: () => setSize(size + 1), + mutate, + } +} + +export default useOrders diff --git a/src/v2/components/ui/Badge.tsx b/src/v2/components/ui/Badge.tsx index e538bcfd3a..cf1c8270f9 100644 --- a/src/v2/components/ui/Badge.tsx +++ b/src/v2/components/ui/Badge.tsx @@ -18,6 +18,7 @@ export const badgeVariants = cva( "[--badge-bg:var(--success-subtle)] [--badge-color:var(--success-subtle-foreground)]", orange: "[--badge-bg:var(--warning-subtle)] [--badge-color:var(--warning-subtle-foreground)]", + red: "[--badge-bg:var(--destructive-subtle)] [--badge-color:var(--destructive-subtle-foreground)]", gold: "[--badge-bg:var(--gold)] [--badge-color:var(--gold)]", }, size: { diff --git a/src/v2/components/ui/Table.stories.tsx b/src/v2/components/ui/Table.stories.tsx index 4f6b93a86e..519246e656 100644 --- a/src/v2/components/ui/Table.stories.tsx +++ b/src/v2/components/ui/Table.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react" import Link from "next/link" +import { NULL_ADDRESS } from "utils/guildCheckout/constants" import shortenHex from "utils/shortenHex" import { Table, @@ -28,7 +29,7 @@ const TableExample = () => ( INV001 Guild Pin - {shortenHex()} + {shortenHex(NULL_ADDRESS)} {new Date().toLocaleDateString()} $250.00 diff --git a/src/v2/components/ui/Table.tsx b/src/v2/components/ui/Table.tsx index 690236151f..35d5f1fec6 100755 --- a/src/v2/components/ui/Table.tsx +++ b/src/v2/components/ui/Table.tsx @@ -6,13 +6,13 @@ const Table = React.forwardRef< HTMLTableElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -
- - + //
+
+ // )) Table.displayName = "Table"