diff --git a/components/HistoryView/HistoryFilter.tsx b/components/HistoryView/HistoryFilter.tsx new file mode 100644 index 00000000..b36d1b06 --- /dev/null +++ b/components/HistoryView/HistoryFilter.tsx @@ -0,0 +1,237 @@ +import FilterIcon from "@components/Icons/FilterIcon"; +import { + Badge, + Box, + Button, + Flex, + Popover, + PopoverContent, + PopoverTrigger, + Text, +} from "@livepeer/design-system"; + +interface HistoryFilterProps { + selectedEventTypes: string[]; + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onToggleEventType: (eventType: string) => void; + onClearFilters: () => void; + allEventTypes: string[]; + eventTypeLabels: Record; +} + +const HistoryFilter = ({ + selectedEventTypes, + isOpen, + onOpenChange, + onToggleEventType, + onClearFilters, + allEventTypes, + eventTypeLabels, +}: HistoryFilterProps) => { + return ( + + + + + + {/* Header - Sticky */} + + + Filters + + + + {/* Event type section - Scrollable */} + + + + Event Type + + + {allEventTypes.map((eventType) => { + const isChecked = selectedEventTypes.includes(eventType); + return ( + onToggleEventType(eventType)} + > + + {isChecked && ( + + + + )} + + + {eventTypeLabels[eventType]} + + + ); + })} + + + + + + ); +}; + +export default HistoryFilter; diff --git a/components/HistoryView/index.tsx b/components/HistoryView/index.tsx index cbc0ce70..28e28459 100644 --- a/components/HistoryView/index.tsx +++ b/components/HistoryView/index.tsx @@ -1,3 +1,4 @@ +import HistoryFilter from "@components/HistoryView/HistoryFilter"; import Spinner from "@components/Spinner"; import TransactionBadge from "@components/TransactionBadge"; import { Fm, parsePollIpfs } from "@lib/api/polls"; @@ -20,6 +21,7 @@ import { useTransactionsQuery, VoteEvent, } from "apollo"; +import { useHistoryFilter } from "hooks"; import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "lib/chains"; import { useRouter } from "next/router"; import numbro from "numbro"; @@ -176,6 +178,18 @@ const Index = () => { ] ); + // Filter events using history hook + const { + filteredEvents, + selectedEventTypes, + toggleEventType, + clearFilters, + isFilterOpen, + setIsFilterOpen, + allEventTypes, + eventTypeLabels, + } = useHistoryFilter(mergedEvents); + if (error) { console.error(error); } @@ -248,8 +262,31 @@ const Index = () => { position: "relative", }} > + + + - {mergedEvents.map((event, i: number) => renderSwitch(event, i))} + {filteredEvents.length > 0 ? ( + filteredEvents.map((event, i: number) => renderSwitch(event, i)) + ) : ( + + No events match the selected filters + + )} {loading && data.transactions.length >= 10 && ( ( + +); + +export default FilterIcon; diff --git a/hooks/filter/useHistoryFilter.ts b/hooks/filter/useHistoryFilter.ts new file mode 100644 index 00000000..1498c312 --- /dev/null +++ b/hooks/filter/useHistoryFilter.ts @@ -0,0 +1,104 @@ +import { useEffect, useMemo, useState } from "react"; + +type Event = { + __typename: string; + transaction?: { + timestamp?: number; + }; +}; + +// Event type labels mapping +export const EVENT_TYPE_LABELS: Record = { + BondEvent: "Bonded", + DepositFundedEvent: "Deposit Funded", + NewRoundEvent: "Initialize Round", + RebondEvent: "Rebond", + UnbondEvent: "Unbond", + RewardEvent: "Reward", + TranscoderUpdateEvent: "Transcoder Update", + WithdrawStakeEvent: "Withdraw Stake", + WithdrawFeesEvent: "Withdraw Fees", + WinningTicketRedeemedEvent: "Winning Ticket Redeemed", + ReserveFundedEvent: "Reserve Funded", +}; + +// All available event types +export const ALL_EVENT_TYPES = Object.keys(EVENT_TYPE_LABELS); + +export const useHistoryFilter = (mergedEvents: Event[]) => { + const [selectedEventTypes, setSelectedEventTypes] = useState([]); + const [isFilterOpen, setIsFilterOpen] = useState(false); + + const filteredEvents = useMemo(() => { + if (selectedEventTypes.length === 0) { + return mergedEvents; + } + return mergedEvents.filter((event) => + selectedEventTypes.includes(event?.__typename) + ); + }, [mergedEvents, selectedEventTypes]); + + const toggleEventType = (eventType: string) => { + setSelectedEventTypes((prev) => + prev.includes(eventType) + ? prev.filter((type) => type !== eventType) + : [...prev, eventType] + ); + }; + + const clearFilters = () => { + setSelectedEventTypes([]); + }; + + // Close filter when scrolling outside the filter area (page scroll) + useEffect(() => { + if (!isFilterOpen) return; + + const handleScroll = (event: globalThis.Event) => { + // Find the popover element by data attribute + const popoverElement = document.querySelector( + "[data-history-filter-popover]" + ); + + if (!popoverElement) { + // Popover not found, close it + setIsFilterOpen(false); + return; + } + + // Use composedPath to check if the scroll event originated from within the popover + const path = event.composedPath(); + const isScrollingInsidePopover = path.some( + (el) => + el === popoverElement || + (el instanceof Node && popoverElement.contains(el)) + ); + + if (isScrollingInsidePopover) { + // Scrolling inside popover, don't close + return; + } + + // Scrolling outside popover, close it + setIsFilterOpen(false); + }; + + // Listen to scroll events on document (captures all scroll events) + document.addEventListener("scroll", handleScroll, true); + + return () => { + document.removeEventListener("scroll", handleScroll, true); + }; + }, [isFilterOpen]); + + return { + filteredEvents, + selectedEventTypes, + toggleEventType, + clearFilters, + isFilterOpen, + setIsFilterOpen, + allEventTypes: ALL_EVENT_TYPES, + eventTypeLabels: EVENT_TYPE_LABELS, + }; +}; diff --git a/hooks/index.tsx b/hooks/index.tsx index 9d37f82c..ec78339f 100644 --- a/hooks/index.tsx +++ b/hooks/index.tsx @@ -1,6 +1,7 @@ import { useEffect } from "react"; // DO NOT IMPORT useHandleTransaction due to @rainbow-me/rainbowkit issues with SSR +export * from "./filter/useHistoryFilter"; export * from "./useExplorerStore"; export * from "./useSwr"; export * from "./wallet";