diff --git a/components/HistoryView/HistoryFilter.tsx b/components/HistoryView/HistoryFilter.tsx new file mode 100644 index 00000000..5f65fe45 --- /dev/null +++ b/components/HistoryView/HistoryFilter.tsx @@ -0,0 +1,254 @@ +import FilterIcon from "@components/Icons/FilterIcon"; +import { + Badge, + Box, + Button, + Flex, + Popover, + PopoverContent, + PopoverTrigger, + Text, +} from "@livepeer/design-system"; +import { CheckIcon } from "@modulz/radix-icons"; + +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) => { + const hasActiveFilters = selectedEventTypes.length > 0; + + return ( + + + + + + + + + Filters + + + + + + + + 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 e409aa64..f10a44d8 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"; @@ -188,6 +190,52 @@ const Index = () => { ] ); + // Filter events using history hook + const { + filteredEvents, + selectedEventTypes, + toggleEventType, + clearFilters, + isFilterOpen, + setIsFilterOpen, + allEventTypes, + eventTypeLabels, + } = useHistoryFilter(mergedEvents); + const hasActiveFilters = selectedEventTypes.length > 0; + + const isHydratingFilteredEvents = useMemo(() => { + const extendedVoteEventIds = new Set( + extendedVoteEventsData.map((event) => event.id) + ); + const extendedTreasuryVoteEventIds = new Set( + extendedTreasuryVoteEventsData.map((event) => event.id) + ); + + return events.some((event) => { + if (hasActiveFilters && !selectedEventTypes.includes(event.__typename)) { + return false; + } + + if (isVoteEvent(event)) { + return !extendedVoteEventIds.has(event.id); + } + + if (isTreasuryVoteEvent(event)) { + return !extendedTreasuryVoteEventIds.has(event.id); + } + + return false; + }); + }, [ + events, + extendedTreasuryVoteEventsData, + extendedVoteEventsData, + hasActiveFilters, + isTreasuryVoteEvent, + isVoteEvent, + selectedEventTypes, + ]); + if (error) { console.error(error); } @@ -268,8 +316,44 @@ const Index = () => { position: "relative", }} > + + + - {mergedEvents.map((event, i: number) => renderSwitch(event, i))} + {filteredEvents.length > 0 ? ( + filteredEvents.map((event, i: number) => renderSwitch(event, i)) + ) : isHydratingFilteredEvents ? ( + + + + ) : ( + + {hasActiveFilters + ? "No events match the selected filters" + : "No history"} + + )} {loading && totalLoaded >= 10 && ( ( + +); + +export default FilterIcon; diff --git a/hooks/filter/useHistoryFilter.ts b/hooks/filter/useHistoryFilter.ts new file mode 100644 index 00000000..71d33939 --- /dev/null +++ b/hooks/filter/useHistoryFilter.ts @@ -0,0 +1,110 @@ +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", + VoteEvent: "Poll Vote", + TreasuryVoteEvent: "Treasury Vote", +}; + +// 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 popoverSelector = "[data-history-filter-popover]"; + let popoverElement = document.querySelector(popoverSelector); + + const handleScroll = (event: globalThis.Event) => { + if (!popoverElement || !popoverElement.isConnected) { + popoverElement = document.querySelector(popoverSelector); + } + + if (!popoverElement) { + // Popover not found, close it + setIsFilterOpen(false); + return; + } + + const currentPopoverElement = popoverElement; + + // Use composedPath to check if the scroll event originated from within the popover + const path = event.composedPath(); + const isScrollingInsidePopover = path.some( + (el) => + el === currentPopoverElement || + (el instanceof Node && currentPopoverElement.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";