From bcb62d490e7bf0fae4cd793fa6f2fb0b591c5c4b Mon Sep 17 00:00:00 2001 From: roaring30s Date: Tue, 27 Jan 2026 19:24:41 -0500 Subject: [PATCH 1/6] feat: add history filter to user account --- components/HistoryView/HistoryFilter.tsx | 222 +++++++++++++++++++++++ components/HistoryView/index.tsx | 47 ++++- components/Icons/FilterIcon.tsx | 28 +++ hooks/index.tsx | 1 + hooks/useHistoryFilter.ts | 104 +++++++++++ 5 files changed, 394 insertions(+), 8 deletions(-) create mode 100644 components/HistoryView/HistoryFilter.tsx create mode 100644 components/Icons/FilterIcon.tsx create mode 100644 hooks/useHistoryFilter.ts diff --git a/components/HistoryView/HistoryFilter.tsx b/components/HistoryView/HistoryFilter.tsx new file mode 100644 index 00000000..8438f52f --- /dev/null +++ b/components/HistoryView/HistoryFilter.tsx @@ -0,0 +1,222 @@ +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 */} + + + Filters + + + + {/* Event type section */} + + + 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 1bfd9c97..10050fdd 100644 --- a/components/HistoryView/index.tsx +++ b/components/HistoryView/index.tsx @@ -1,14 +1,10 @@ +import HistoryFilter from "@components/HistoryView/HistoryFilter"; import Spinner from "@components/Spinner"; import dayjs from "@lib/dayjs"; import { formatAddress, formatTransactionHash } from "@lib/utils"; -import { - Box, - Card as CardBase, - Flex, - Link as A, - styled, -} from "@livepeer/design-system"; +import { Box, Card as CardBase, Flex, Link as A, styled } from "@livepeer/design-system"; import { ExternalLinkIcon } from "@modulz/radix-icons"; +import { useHistoryFilter } from "hooks"; import { useTransactionsQuery } from "apollo"; import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "lib/chains"; import { useRouter } from "next/router"; @@ -72,6 +68,18 @@ const Index = () => { [events, data, lastEventTimestamp] ); + // Filter events using history hook + const { + filteredEvents, + selectedEventTypes, + toggleEventType, + clearFilters, + isFilterOpen, + setIsFilterOpen, + allEventTypes, + eventTypeLabels, + } = useHistoryFilter(mergedEvents); + if (error) { console.error(error); } @@ -143,8 +151,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/index.tsx b/hooks/index.tsx index 9d37f82c..8bafb0e9 100644 --- a/hooks/index.tsx +++ b/hooks/index.tsx @@ -2,6 +2,7 @@ import { useEffect } from "react"; // DO NOT IMPORT useHandleTransaction due to @rainbow-me/rainbowkit issues with SSR export * from "./useExplorerStore"; +export * from "./useHistoryFilter"; export * from "./useSwr"; export * from "./wallet"; diff --git a/hooks/useHistoryFilter.ts b/hooks/useHistoryFilter.ts new file mode 100644 index 00000000..c98ed7b2 --- /dev/null +++ b/hooks/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, + }; +}; From 413be2fb1de8ecd201465c42c8265beb1472c471 Mon Sep 17 00:00:00 2001 From: roaring30s Date: Tue, 27 Jan 2026 19:27:08 -0500 Subject: [PATCH 2/6] fix: apply linter and prettier --- components/HistoryView/index.tsx | 10 ++++++++-- hooks/useHistoryFilter.ts | 10 +++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/components/HistoryView/index.tsx b/components/HistoryView/index.tsx index 10050fdd..fe018101 100644 --- a/components/HistoryView/index.tsx +++ b/components/HistoryView/index.tsx @@ -2,10 +2,16 @@ import HistoryFilter from "@components/HistoryView/HistoryFilter"; import Spinner from "@components/Spinner"; import dayjs from "@lib/dayjs"; import { formatAddress, formatTransactionHash } from "@lib/utils"; -import { Box, Card as CardBase, Flex, Link as A, styled } from "@livepeer/design-system"; +import { + Box, + Card as CardBase, + Flex, + Link as A, + styled, +} from "@livepeer/design-system"; import { ExternalLinkIcon } from "@modulz/radix-icons"; -import { useHistoryFilter } from "hooks"; import { useTransactionsQuery } from "apollo"; +import { useHistoryFilter } from "hooks"; import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "lib/chains"; import { useRouter } from "next/router"; import numbro from "numbro"; diff --git a/hooks/useHistoryFilter.ts b/hooks/useHistoryFilter.ts index c98ed7b2..1498c312 100644 --- a/hooks/useHistoryFilter.ts +++ b/hooks/useHistoryFilter.ts @@ -57,15 +57,15 @@ export const useHistoryFilter = (mergedEvents: Event[]) => { const handleScroll = (event: globalThis.Event) => { // Find the popover element by data attribute const popoverElement = document.querySelector( - '[data-history-filter-popover]' + "[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( @@ -73,12 +73,12 @@ export const useHistoryFilter = (mergedEvents: Event[]) => { el === popoverElement || (el instanceof Node && popoverElement.contains(el)) ); - + if (isScrollingInsidePopover) { // Scrolling inside popover, don't close return; } - + // Scrolling outside popover, close it setIsFilterOpen(false); }; From ad8be218bfb03a99045a38276b6a1383076cd403 Mon Sep 17 00:00:00 2001 From: roaring30s Date: Tue, 27 Jan 2026 19:42:26 -0500 Subject: [PATCH 3/6] fix: add margin on popover from window --- components/HistoryView/HistoryFilter.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/HistoryView/HistoryFilter.tsx b/components/HistoryView/HistoryFilter.tsx index 8438f52f..1a2d2494 100644 --- a/components/HistoryView/HistoryFilter.tsx +++ b/components/HistoryView/HistoryFilter.tsx @@ -82,6 +82,9 @@ const HistoryFilter = ({ "0px 5px 14px rgba(0, 0, 0, 0.22), 0px 0px 2px rgba(0, 0, 0, 0.2)", border: "1px solid $neutral6", zIndex: 9, + "@bp2": { + marginRight: "$3", + }, }} onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} From 1eafa8de0925e8f548c1ef6dd364a959adb40847 Mon Sep 17 00:00:00 2001 From: roaring30s Date: Tue, 27 Jan 2026 20:34:30 -0500 Subject: [PATCH 4/6] fix: add UI changes to filter --- components/HistoryView/HistoryFilter.tsx | 98 +++++++++++++----------- hooks/{ => filter}/useHistoryFilter.ts | 42 +++++++++- hooks/index.tsx | 2 +- 3 files changed, 97 insertions(+), 45 deletions(-) rename hooks/{ => filter}/useHistoryFilter.ts (69%) diff --git a/components/HistoryView/HistoryFilter.tsx b/components/HistoryView/HistoryFilter.tsx index 1a2d2494..b36d1b06 100644 --- a/components/HistoryView/HistoryFilter.tsx +++ b/components/HistoryView/HistoryFilter.tsx @@ -82,63 +82,75 @@ const HistoryFilter = ({ "0px 5px 14px rgba(0, 0, 0, 0.22), 0px 0px 2px rgba(0, 0, 0, 0.2)", border: "1px solid $neutral6", zIndex: 9, - "@bp2": { - marginRight: "$3", - }, + display: "flex", + flexDirection: "column", + maxHeight: "400px", + marginRight: "$3", + overflow: "hidden", }} onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} placeholder={undefined} > + {/* Header - Sticky */} - {/* Header */} - - - Filters - + Filters + - + }, + }} + > + Done + + - {/* Event type section */} + {/* Event type section - Scrollable */} + { const [selectedEventTypes, setSelectedEventTypes] = useState([]); const [isFilterOpen, setIsFilterOpen] = useState(false); + const scrollPositionRef = useRef(0); const filteredEvents = useMemo(() => { if (selectedEventTypes.length === 0) { @@ -50,6 +51,45 @@ export const useHistoryFilter = (mergedEvents: Event[]) => { setSelectedEventTypes([]); }; + // Save scroll position when scrolling inside the popover + useEffect(() => { + if (!isFilterOpen) return; + + const scrollableContainer = document.querySelector( + "[data-history-filter-scrollable]" + ) as HTMLElement; + + if (!scrollableContainer) return; + + const handleScrollSave = () => { + scrollPositionRef.current = scrollableContainer.scrollTop; + }; + + scrollableContainer.addEventListener("scroll", handleScrollSave); + + return () => { + scrollableContainer.removeEventListener("scroll", handleScrollSave); + }; + }, [isFilterOpen]); + + // Restore scroll position when popover opens + useEffect(() => { + if (!isFilterOpen) return; + + // Use requestAnimationFrame to ensure the DOM is painted before restoring scroll + const rafId = requestAnimationFrame(() => { + const scrollableContainer = document.querySelector( + "[data-history-filter-scrollable]" + ) as HTMLElement; + + if (scrollableContainer && scrollPositionRef.current > 0) { + scrollableContainer.scrollTop = scrollPositionRef.current; + } + }); + + return () => cancelAnimationFrame(rafId); + }, [isFilterOpen]); + // Close filter when scrolling outside the filter area (page scroll) useEffect(() => { if (!isFilterOpen) return; diff --git a/hooks/index.tsx b/hooks/index.tsx index 8bafb0e9..ec78339f 100644 --- a/hooks/index.tsx +++ b/hooks/index.tsx @@ -1,8 +1,8 @@ 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 "./useHistoryFilter"; export * from "./useSwr"; export * from "./wallet"; From f236eca6a1642e413bf67a465e4aff248559c8eb Mon Sep 17 00:00:00 2001 From: roaring30s Date: Tue, 27 Jan 2026 21:07:40 -0500 Subject: [PATCH 5/6] fix: remove auto scroll --- hooks/filter/useHistoryFilter.ts | 42 +------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/hooks/filter/useHistoryFilter.ts b/hooks/filter/useHistoryFilter.ts index 635fd580..1498c312 100644 --- a/hooks/filter/useHistoryFilter.ts +++ b/hooks/filter/useHistoryFilter.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; type Event = { __typename: string; @@ -28,7 +28,6 @@ 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 scrollPositionRef = useRef(0); const filteredEvents = useMemo(() => { if (selectedEventTypes.length === 0) { @@ -51,45 +50,6 @@ export const useHistoryFilter = (mergedEvents: Event[]) => { setSelectedEventTypes([]); }; - // Save scroll position when scrolling inside the popover - useEffect(() => { - if (!isFilterOpen) return; - - const scrollableContainer = document.querySelector( - "[data-history-filter-scrollable]" - ) as HTMLElement; - - if (!scrollableContainer) return; - - const handleScrollSave = () => { - scrollPositionRef.current = scrollableContainer.scrollTop; - }; - - scrollableContainer.addEventListener("scroll", handleScrollSave); - - return () => { - scrollableContainer.removeEventListener("scroll", handleScrollSave); - }; - }, [isFilterOpen]); - - // Restore scroll position when popover opens - useEffect(() => { - if (!isFilterOpen) return; - - // Use requestAnimationFrame to ensure the DOM is painted before restoring scroll - const rafId = requestAnimationFrame(() => { - const scrollableContainer = document.querySelector( - "[data-history-filter-scrollable]" - ) as HTMLElement; - - if (scrollableContainer && scrollPositionRef.current > 0) { - scrollableContainer.scrollTop = scrollPositionRef.current; - } - }); - - return () => cancelAnimationFrame(rafId); - }, [isFilterOpen]); - // Close filter when scrolling outside the filter area (page scroll) useEffect(() => { if (!isFilterOpen) return; From 2c8ea1727f7f8533f0763611fd97dcbc450445f8 Mon Sep 17 00:00:00 2001 From: roaring30s Date: Thu, 5 Feb 2026 18:45:44 -0500 Subject: [PATCH 6/6] fix: remove lint error --- components/HistoryView/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/HistoryView/index.tsx b/components/HistoryView/index.tsx index fe64fbcc..28e28459 100644 --- a/components/HistoryView/index.tsx +++ b/components/HistoryView/index.tsx @@ -14,8 +14,6 @@ import { Link as A, styled, } from "@livepeer/design-system"; -import { ExternalLinkIcon } from "@modulz/radix-icons"; -import { useHistoryFilter } from "hooks"; import { TransactionsQuery, TreasuryVoteEvent, @@ -23,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";