diff --git a/__tests__/app/tools/subscriptions-report.test.tsx b/__tests__/app/tools/subscriptions-report.test.tsx index 3ce32da603..e3f8fc11ab 100644 --- a/__tests__/app/tools/subscriptions-report.test.tsx +++ b/__tests__/app/tools/subscriptions-report.test.tsx @@ -6,13 +6,9 @@ import React from "react"; jest.mock("@/components/meme-calendar/meme-calendar.helpers", () => ({ __esModule: true, - displayedSeasonNumberFromIndex: jest.fn(() => 1), - formatFullDate: jest.fn((date: Date) => date.toISOString()), getCardsRemainingUntilEndOf: jest.fn(() => 2), - getSeasonIndexForDate: jest.fn(() => 0), - getUpcomingMintsForCurrentOrNextSeason: jest.fn(() => ({ rows: [] })), + getUpcomingMintsAcrossSeasons: jest.fn(() => []), isMintingToday: jest.fn(() => false), - nextMintDateOnOrAfter: jest.fn(() => new Date("2024-01-01T00:00:00Z")), })); jest.mock("@/services/api/common-api", () => ({ @@ -53,11 +49,10 @@ jest.mock("@/contexts/TitleContext", () => ({ describe("Subscriptions report page", () => { it("sets title and renders component", async () => { - const props = { szn: 1, upcoming: [], redeemed: [] } as any; const { container } = render( - + ); diff --git a/__tests__/components/latest-activity/ActivityFilters.test.tsx b/__tests__/components/latest-activity/ActivityFilters.test.tsx index 830c71c74e..1d05bf91d2 100644 --- a/__tests__/components/latest-activity/ActivityFilters.test.tsx +++ b/__tests__/components/latest-activity/ActivityFilters.test.tsx @@ -1,35 +1,22 @@ -import React from "react"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; import ActivityFilters from "@/components/latest-activity/ActivityFilters"; -import { TypeFilter, ContractFilter } from "@/hooks/useActivityData"; +import { ContractFilter, TypeFilter } from "@/hooks/useActivityData"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; -// Mock react-bootstrap components to simplify testing -jest.mock("react-bootstrap", () => { - const MockDropdown = ({ children, drop, className, ...props }: any) => ( -
+jest.mock("react-bootstrap", () => ({ + Col: ({ children, sm, md, className }: any) => ( +
{children}
- ); - MockDropdown.Toggle = ({ children, ...props }: any) => ( - - ); - MockDropdown.Menu = ({ children, ...props }: any) => ( -
{children}
- ); - MockDropdown.Item = ({ children, onClick, ...props }: any) => ( - - ); - - return { - Col: ({ children, ...props }: any) =>
{children}
, - Dropdown: MockDropdown, - }; -}); + ), +})); -// Mock the SCSS module -jest.mock("@/components/latest-activity/LatestActivity.module.scss", () => ({ - filterDropdown: "mock-filter-dropdown", +jest.mock("framer-motion", () => ({ + useAnimate: () => [{ current: null }, jest.fn()], + AnimatePresence: ({ children }: any) => <>{children}, + motion: { + div: ({ children, ...props }: any) =>
{children}
, + }, })); describe("ActivityFilters", () => { @@ -53,14 +40,24 @@ describe("ActivityFilters", () => { it("renders both dropdown filters", () => { render(); - const dropdowns = screen.getAllByTestId("dropdown"); - expect(dropdowns).toHaveLength(2); + expect( + screen.getByRole("button", { name: /Collection:/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /Filter:/i }) + ).toBeInTheDocument(); }); - it("displays current filter values in dropdown toggles", () => { + it("displays current filter values in dropdown buttons", () => { render(); - expect(screen.getByText("Collection: All")).toBeInTheDocument(); - expect(screen.getByText("Filter: All")).toBeInTheDocument(); + expect( + screen.getByRole("button", { + name: `Collection: ${ContractFilter.ALL}`, + }) + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: `Filter: ${TypeFilter.ALL}` }) + ).toBeInTheDocument(); }); it("displays custom filter values when provided", () => { @@ -70,8 +67,14 @@ describe("ActivityFilters", () => { selectedContract: ContractFilter.MEMES, }; render(); - expect(screen.getByText("Collection: Memes")).toBeInTheDocument(); - expect(screen.getByText("Filter: Sales")).toBeInTheDocument(); + expect( + screen.getByRole("button", { + name: `Collection: ${ContractFilter.MEMES}`, + }) + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: `Filter: ${TypeFilter.SALES}` }) + ).toBeInTheDocument(); }); }); @@ -91,174 +94,216 @@ describe("ActivityFilters", () => { }); }); - describe("Dropdown Options", () => { - it("renders all TypeFilter options", () => { - const { container } = render(); - - // Check that all enum values would be rendered - const typeFilterValues = Object.values(TypeFilter); - expect(typeFilterValues).toContain(TypeFilter.ALL); - expect(typeFilterValues).toContain(TypeFilter.SALES); - expect(typeFilterValues).toContain(TypeFilter.TRANSFERS); - expect(typeFilterValues).toContain(TypeFilter.AIRDROPS); - expect(typeFilterValues).toContain(TypeFilter.MINTS); - expect(typeFilterValues).toContain(TypeFilter.BURNS); - }); - - it("renders all ContractFilter options", () => { - const { container } = render(); - - // Check that all enum values would be rendered - const contractFilterValues = Object.values(ContractFilter); - expect(contractFilterValues).toContain(ContractFilter.ALL); - expect(contractFilterValues).toContain(ContractFilter.MEMES); - expect(contractFilterValues).toContain(ContractFilter.NEXTGEN); - expect(contractFilterValues).toContain(ContractFilter.GRADIENTS); - }); - }); - describe("CSS Classes", () => { - it("applies correct CSS classes to dropdowns", () => { - render(); - const dropdowns = screen.getAllByTestId("dropdown"); - dropdowns.forEach(dropdown => { - expect(dropdown.className).toContain("mock-filter-dropdown"); - expect(dropdown).toHaveAttribute("drop", "down-centered"); - }); - }); - it("applies correct Bootstrap classes to Col", () => { render(); const col = screen.getByTestId("col"); - expect(col).toHaveAttribute("sm", "12"); - expect(col).toHaveAttribute("md", "6"); + expect(col).toHaveAttribute("data-sm", "12"); + expect(col).toHaveAttribute("data-md", "6"); expect(col.className).toContain("d-flex"); expect(col.className).toContain("align-items-center"); expect(col.className).toContain("gap-4"); }); + + it("applies tailwind-scope class to Col", () => { + render(); + const col = screen.getByTestId("col"); + expect(col.className).toContain("tailwind-scope"); + }); }); describe("Props Validation", () => { it("handles all TypeFilter enum values correctly", () => { - Object.values(TypeFilter).forEach((typeFilter) => { + for (const typeFilter of Object.values(TypeFilter)) { const props = { ...mockProps, typeFilter }; - render(); - expect(screen.getByText(`Filter: ${typeFilter}`)).toBeInTheDocument(); - }); + const { unmount } = render(); + expect( + screen.getByRole("button", { name: `Filter: ${typeFilter}` }) + ).toBeInTheDocument(); + unmount(); + } }); it("handles all ContractFilter enum values correctly", () => { - Object.values(ContractFilter).forEach((selectedContract) => { + for (const selectedContract of Object.values(ContractFilter)) { const props = { ...mockProps, selectedContract }; - render(); - expect(screen.getByText(`Collection: ${selectedContract}`)).toBeInTheDocument(); + const { unmount } = render(); + expect( + screen.getByRole("button", { + name: `Collection: ${selectedContract}`, + }) + ).toBeInTheDocument(); + unmount(); + } + }); + }); + + describe("User Interactions", () => { + it("opens contract dropdown when clicked", async () => { + const user = userEvent.setup(); + render(); + + const contractButton = screen.getByRole("button", { + name: `Collection: ${ContractFilter.ALL}`, }); + await user.click(contractButton); + + const menuItems = screen.getAllByRole("menuitem"); + expect(menuItems.length).toBeGreaterThan(0); }); - it("preserves callback function references", () => { - const onTypeFilterChange = jest.fn(); + it("opens type filter dropdown when clicked", async () => { + const user = userEvent.setup(); + render(); + + const typeButton = screen.getByRole("button", { + name: `Filter: ${TypeFilter.ALL}`, + }); + await user.click(typeButton); + + const menuItems = screen.getAllByRole("menuitem"); + expect(menuItems.length).toBeGreaterThan(0); + }); + + it("calls onContractFilterChange when contract option is selected", async () => { + const user = userEvent.setup(); const onContractFilterChange = jest.fn(); - const props = { - ...mockProps, - onTypeFilterChange, - onContractFilterChange, - }; - - render(); - - // Callbacks should be preserved (this tests the component doesn't recreate them) - expect(typeof props.onTypeFilterChange).toBe("function"); - expect(typeof props.onContractFilterChange).toBe("function"); + render( + + ); + + const contractButton = screen.getByRole("button", { + name: `Collection: ${ContractFilter.ALL}`, + }); + await user.click(contractButton); + + const memesOption = screen.getByRole("menuitem", { + name: ContractFilter.MEMES, + }); + await user.click(memesOption); + + expect(onContractFilterChange).toHaveBeenCalledWith(ContractFilter.MEMES); + }); + + it("calls onTypeFilterChange when type option is selected", async () => { + const user = userEvent.setup(); + const onTypeFilterChange = jest.fn(); + render( + + ); + + const typeButton = screen.getByRole("button", { + name: `Filter: ${TypeFilter.ALL}`, + }); + await user.click(typeButton); + + const salesOption = screen.getByRole("menuitem", { + name: TypeFilter.SALES, + }); + await user.click(salesOption); + + expect(onTypeFilterChange).toHaveBeenCalledWith(TypeFilter.SALES); + }); + + it("shows all ContractFilter options in dropdown", async () => { + const user = userEvent.setup(); + render(); + + const contractButton = screen.getByRole("button", { + name: `Collection: ${ContractFilter.ALL}`, + }); + await user.click(contractButton); + + for (const contract of Object.values(ContractFilter)) { + expect( + screen.getByRole("menuitem", { name: contract }) + ).toBeInTheDocument(); + } + }); + + it("shows all TypeFilter options in dropdown", async () => { + const user = userEvent.setup(); + render(); + + const typeButton = screen.getByRole("button", { + name: `Filter: ${TypeFilter.ALL}`, + }); + await user.click(typeButton); + + for (const type of Object.values(TypeFilter)) { + expect( + screen.getByRole("menuitem", { name: type }) + ).toBeInTheDocument(); + } }); }); describe("Component Integration", () => { it("maintains proper component structure", () => { const { container } = render(); - - // Should have proper nesting: Col > (Dropdown * 2) + const col = container.querySelector('[data-testid="col"]'); - const dropdowns = col?.querySelectorAll('[data-testid="dropdown"]'); - expect(col).toBeInTheDocument(); - expect(dropdowns).toHaveLength(2); + + const buttons = within(col as HTMLElement).getAllByRole("button", { + name: /Collection:|Filter:/i, + }); + expect(buttons).toHaveLength(2); }); it("renders consistently with different boolean values", () => { - // Test with isMobile true - const { rerender } = render(); + const { rerender } = render( + + ); expect(screen.getByTestId("col")).toBeInTheDocument(); - - // Test with isMobile false + rerender(); expect(screen.getByTestId("col")).toBeInTheDocument(); }); }); - describe("User Interactions", () => { - it("calls onContractFilterChange when contract dropdown item is clicked", async () => { - const onContractFilterChange = jest.fn(); - const props = { - ...mockProps, - onContractFilterChange, - }; - - render(); - - // Find all dropdown items and click one that should be a ContractFilter - const dropdownItems = screen.getAllByTestId("dropdown-item"); - - // The first dropdown is for contracts, so we'll find items there - // Since we're using mocks, we can simulate clicking on a known filter - await userEvent.click(dropdownItems[0]); // This should trigger onClick - - expect(onContractFilterChange).toHaveBeenCalled(); - }); + describe("Accessibility", () => { + it("dropdown buttons have correct aria-haspopup attribute", () => { + render(); - it("calls onTypeFilterChange when type dropdown item is clicked", async () => { - const onTypeFilterChange = jest.fn(); - const props = { - ...mockProps, - onTypeFilterChange, - }; - - render(); - - // Find all dropdown items - const dropdownItems = screen.getAllByTestId("dropdown-item"); - - // The second set of dropdown items are for type filters - // Since ContractFilter has 4 values (All, Memes, NextGen, Gradients) - // TypeFilter items start after that - const typeFilterItemIndex = Object.values(ContractFilter).length; // Should be 4 - if (dropdownItems[typeFilterItemIndex]) { - await userEvent.click(dropdownItems[typeFilterItemIndex]); + const buttons = screen.getAllByRole("button", { + name: /Collection:|Filter:/i, + }); + for (const button of buttons) { + expect(button).toHaveAttribute("aria-haspopup", "true"); } - - expect(onTypeFilterChange).toHaveBeenCalled(); }); - }); - describe("Error Boundaries and Edge Cases", () => { - it("handles undefined enum values gracefully", () => { - // This tests that the component doesn't break with unexpected enum values - const propsWithUndefined = { - ...mockProps, - typeFilter: undefined as any, - selectedContract: undefined as any, - }; - - expect(() => render()).not.toThrow(); + it("dropdown buttons have correct aria-expanded attribute", async () => { + const user = userEvent.setup(); + render(); + + const contractButton = screen.getByRole("button", { + name: `Collection: ${ContractFilter.ALL}`, + }); + expect(contractButton).toHaveAttribute("aria-expanded", "false"); + + await user.click(contractButton); + expect(contractButton).toHaveAttribute("aria-expanded", "true"); }); - it("handles missing callback functions", () => { - const propsWithoutCallbacks = { - ...mockProps, - onTypeFilterChange: undefined as any, - onContractFilterChange: undefined as any, - }; - - expect(() => render()).not.toThrow(); + it("dropdown items have correct role", async () => { + const user = userEvent.setup(); + render(); + + const contractButton = screen.getByRole("button", { + name: `Collection: ${ContractFilter.ALL}`, + }); + await user.click(contractButton); + + const menuItems = screen.getAllByRole("menuitem"); + expect(menuItems.length).toBe(Object.values(ContractFilter).length); }); }); }); diff --git a/__tests__/components/latest-activity/ActivityHeader.test.tsx b/__tests__/components/latest-activity/ActivityHeader.test.tsx index f0e665615a..da11c8f42d 100644 --- a/__tests__/components/latest-activity/ActivityHeader.test.tsx +++ b/__tests__/components/latest-activity/ActivityHeader.test.tsx @@ -1,143 +1,166 @@ -import { render, screen } from '@testing-library/react'; -import React from 'react'; -import ActivityHeader from '@/components/latest-activity/ActivityHeader'; +import ActivityHeader from "@/components/latest-activity/ActivityHeader"; +import { render, screen } from "@testing-library/react"; // Mock the DotLoader component -jest.mock('@/components/dotLoader/DotLoader', () => { +jest.mock("@/components/dotLoader/DotLoader", () => { return function MockDotLoader() { return
Loading...
; }; }); -// Mock the SCSS module -jest.mock('@/styles/Home.module.scss', () => ({ - viewAllLink: 'mocked-view-all-link-class', -})); - -describe('ActivityHeader', () => { - it('renders the NFT Activity header text', () => { +describe("ActivityHeader", () => { + it("renders the NFT Activity header text", () => { render(); - - expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument(); + + expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument(); expect(screen.getByText(/Activity/)).toBeInTheDocument(); }); - it('renders with correct Bootstrap column classes', () => { - const { container } = render(); - + it("renders with correct Bootstrap column classes", () => { + const { container } = render( + + ); + const colElement = container.firstChild as HTMLElement; - expect(colElement).toHaveClass('col-sm-12'); - expect(colElement).toHaveClass('col-md-6'); - expect(colElement).toHaveClass('d-flex'); - expect(colElement).toHaveClass('align-items-center'); - expect(colElement).toHaveClass('justify-content-between'); + expect(colElement).toHaveClass("col-sm-12"); + expect(colElement).toHaveClass("col-md-6"); + expect(colElement).toHaveClass("d-flex"); + expect(colElement).toHaveClass("align-items-center"); + expect(colElement).toHaveClass("justify-content-between"); }); - describe('when showViewAll is true', () => { - it('renders the View All link', () => { + describe("when showViewAll is true", () => { + it("renders the View All link", () => { render(); - - const link = screen.getByRole('link', { name: 'View All' }); + + const link = screen.getByRole("link", { name: "View All" }); expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute('href', '/nft-activity'); - expect(link).toHaveClass('mocked-view-all-link-class'); + expect(link).toHaveAttribute("href", "/nft-activity"); + const span = link.querySelector("span"); + expect(span).toHaveClass( + "tw-whitespace-nowrap", + "tw-text-sm", + "tw-font-semibold" + ); }); - it('does not render DotLoader even when fetching is true', () => { + it("does not render DotLoader even when fetching is true", () => { render(); - - expect(screen.getByRole('link', { name: 'View All' })).toBeInTheDocument(); - expect(screen.queryByTestId('dot-loader')).not.toBeInTheDocument(); + + expect( + screen.getByRole("link", { name: "View All" }) + ).toBeInTheDocument(); + expect(screen.queryByTestId("dot-loader")).not.toBeInTheDocument(); }); }); - describe('when showViewAll is false', () => { - it('does not render the View All link', () => { + describe("when showViewAll is false", () => { + it("does not render the View All link", () => { render(); - - expect(screen.queryByRole('link', { name: 'View All' })).not.toBeInTheDocument(); + + expect( + screen.queryByRole("link", { name: "View All" }) + ).not.toBeInTheDocument(); }); - it('renders DotLoader when fetching is true', () => { + it("renders DotLoader when fetching is true", () => { render(); - - expect(screen.getByTestId('dot-loader')).toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'View All' })).not.toBeInTheDocument(); + + expect(screen.getByTestId("dot-loader")).toBeInTheDocument(); + expect( + screen.queryByRole("link", { name: "View All" }) + ).not.toBeInTheDocument(); }); - it('does not render DotLoader when fetching is false', () => { + it("does not render DotLoader when fetching is false", () => { render(); - - expect(screen.queryByTestId('dot-loader')).not.toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'View All' })).not.toBeInTheDocument(); + + expect(screen.queryByTestId("dot-loader")).not.toBeInTheDocument(); + expect( + screen.queryByRole("link", { name: "View All" }) + ).not.toBeInTheDocument(); }); }); - describe('prop combinations', () => { - it('handles showViewAll=false and fetching=false (minimal state)', () => { + describe("prop combinations", () => { + it("handles showViewAll=false and fetching=false (minimal state)", () => { render(); - - expect(screen.getByRole('heading', { name: 'NFT Activity' })).toBeInTheDocument(); - expect(screen.queryByRole('link')).not.toBeInTheDocument(); - expect(screen.queryByTestId('dot-loader')).not.toBeInTheDocument(); + + expect( + screen.getByRole("heading", { name: "NFT Activity" }) + ).toBeInTheDocument(); + expect(screen.queryByRole("link")).not.toBeInTheDocument(); + expect(screen.queryByTestId("dot-loader")).not.toBeInTheDocument(); }); - it('handles showViewAll=false and fetching=true (loading state)', () => { + it("handles showViewAll=false and fetching=true (loading state)", () => { render(); - - expect(screen.getByRole('heading', { name: 'NFT Activity' })).toBeInTheDocument(); - expect(screen.queryByRole('link')).not.toBeInTheDocument(); - expect(screen.getByTestId('dot-loader')).toBeInTheDocument(); + + expect( + screen.getByRole("heading", { name: "NFT Activity" }) + ).toBeInTheDocument(); + expect(screen.queryByRole("link")).not.toBeInTheDocument(); + expect(screen.getByTestId("dot-loader")).toBeInTheDocument(); }); - it('handles showViewAll=true and fetching=false (view all state)', () => { + it("handles showViewAll=true and fetching=false (view all state)", () => { render(); - - expect(screen.getByRole('heading', { name: 'NFT Activity' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'View All' })).toBeInTheDocument(); - expect(screen.queryByTestId('dot-loader')).not.toBeInTheDocument(); + + expect( + screen.getByRole("heading", { name: "NFT Activity" }) + ).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: "View All" }) + ).toBeInTheDocument(); + expect(screen.queryByTestId("dot-loader")).not.toBeInTheDocument(); }); - it('handles showViewAll=true and fetching=true (view all takes precedence)', () => { + it("handles showViewAll=true and fetching=true (view all takes precedence)", () => { render(); - - expect(screen.getByRole('heading', { name: 'NFT Activity' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'View All' })).toBeInTheDocument(); - expect(screen.queryByTestId('dot-loader')).not.toBeInTheDocument(); + + expect( + screen.getByRole("heading", { name: "NFT Activity" }) + ).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: "View All" }) + ).toBeInTheDocument(); + expect(screen.queryByTestId("dot-loader")).not.toBeInTheDocument(); }); }); - describe('accessibility', () => { - it('has proper heading structure', () => { + describe("accessibility", () => { + it("has proper heading structure", () => { render(); - - const heading = screen.getByRole('heading', { level: 1 }); - expect(heading).toHaveTextContent('NFT Activity'); + + const heading = screen.getByRole("heading", { level: 1 }); + expect(heading).toHaveTextContent("NFT Activity"); }); - it('link has proper accessible text', () => { + it("link has proper accessible text", () => { render(); - - const link = screen.getByRole('link', { name: 'View All' }); - expect(link).toHaveAccessibleName('View All'); + + const link = screen.getByRole("link", { name: "View All" }); + expect(link).toHaveAccessibleName("View All"); }); }); - describe('styling and layout', () => { - it('has correct flex container structure', () => { - const { container } = render(); - - const spanElement = container.querySelector('span.d-flex'); - expect(spanElement).toHaveClass('d-flex'); - expect(spanElement).toHaveClass('flex-wrap'); - expect(spanElement).toHaveClass('align-items-center'); - expect(spanElement).toHaveClass('gap-3'); + describe("styling and layout", () => { + it("has correct flex container structure", () => { + const { container } = render( + + ); + + const spanElement = container.querySelector("span.d-flex"); + expect(spanElement).toHaveClass("d-flex"); + expect(spanElement).toHaveClass("flex-wrap"); + expect(spanElement).toHaveClass("align-items-center"); + expect(spanElement).toHaveClass("gap-3"); }); - it('displays NFT Activity heading text', () => { + it("displays NFT Activity heading text", () => { render(); expect( - screen.getByRole('heading', { name: 'NFT Activity' }) + screen.getByRole("heading", { name: "NFT Activity" }) ).toBeInTheDocument(); }); }); diff --git a/__tests__/components/latest-activity/LatestActivity.test.tsx b/__tests__/components/latest-activity/LatestActivity.test.tsx index ff7d8d7c7e..7afc1cca39 100644 --- a/__tests__/components/latest-activity/LatestActivity.test.tsx +++ b/__tests__/components/latest-activity/LatestActivity.test.tsx @@ -113,8 +113,8 @@ describe("LatestActivity", () => { expect(fetchUrl).toHaveBeenCalledWith( "https://api.test.6529.io/api/transactions?page_size=10&page=1" ); - await userEvent.click(screen.getByText("Collection: All")); - await userEvent.click(screen.getByText("Memes")); + await userEvent.click(screen.getByText("All Collections")); + await userEvent.click(screen.getByText("The Memes")); await waitFor(() => expect(fetchUrl).toHaveBeenLastCalledWith( "https://api.test.6529.io/api/transactions?page_size=10&page=1&contract=0x33FD426905F149f8376e227d0C9D3340AaD17aF1" @@ -170,7 +170,7 @@ describe("LatestActivity", () => { ); // Change filter - this should trigger a fetch - await userEvent.click(screen.getByText("Filter: All")); + await userEvent.click(screen.getByText("All Transactions")); await userEvent.click(screen.getByText("Sales")); await waitFor(() => { @@ -406,7 +406,7 @@ describe("LatestActivity", () => { }); // Change type filter - await userEvent.click(screen.getByText("Filter: All")); + await userEvent.click(screen.getByText("All Transactions")); await userEvent.click(screen.getByText("Sales")); await waitFor(() => { @@ -426,8 +426,8 @@ describe("LatestActivity", () => { }); // Change contract filter - await userEvent.click(screen.getByText("Collection: All")); - await userEvent.click(screen.getByText("Memes")); + await userEvent.click(screen.getByText("All Collections")); + await userEvent.click(screen.getByText("The Memes")); await waitFor(() => { expect(fetchUrl).toHaveBeenCalledWith( @@ -452,8 +452,8 @@ describe("LatestActivity", () => { expect( screen.getByRole("heading", { name: "NFT Activity" }) ).toBeInTheDocument(); - expect(screen.getByText("Collection: All")).toBeInTheDocument(); - expect(screen.getByText("Filter: All")).toBeInTheDocument(); + expect(screen.getByText("All Collections")).toBeInTheDocument(); + expect(screen.getByText("All Transactions")).toBeInTheDocument(); }); it("starts with empty activity array when no data provided", async () => { diff --git a/__tests__/components/seasons-dropdown/SeasonsDropdown.test.tsx b/__tests__/components/seasons-dropdown/SeasonsDropdown.test.tsx deleted file mode 100644 index 103d6cf6ca..0000000000 --- a/__tests__/components/seasons-dropdown/SeasonsDropdown.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import SeasonsDropdown from '@/components/seasons-dropdown/SeasonsDropdown'; - -const seasons = [1, 2, 3]; - -it('displays selected season and calls setter', async () => { - const setSelected = jest.fn(); - const user = userEvent.setup(); - render(); - expect(screen.getByText('SZN: 2')).toBeInTheDocument(); - - // Click on the dropdown toggle to open the menu - await user.click(screen.getByRole('button')); - - // Then click on the SZN3 option - await user.click(screen.getByText('SZN3')); - expect(setSelected).toHaveBeenCalledWith(3); -}); diff --git a/__tests__/components/user/collected/cards/UserPageCollectedCardsNoCards.test.tsx b/__tests__/components/user/collected/cards/UserPageCollectedCardsNoCards.test.tsx index ad1c4fb44d..629265ecd4 100644 --- a/__tests__/components/user/collected/cards/UserPageCollectedCardsNoCards.test.tsx +++ b/__tests__/components/user/collected/cards/UserPageCollectedCardsNoCards.test.tsx @@ -1,31 +1,61 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import UserPageCollectedCardsNoCards from '@/components/user/collected/cards/UserPageCollectedCardsNoCards'; -import { CollectedCollectionType, CollectionSeized } from '@/entities/IProfile'; -import { MEMES_SEASON } from '@/enums'; +import UserPageCollectedCardsNoCards from "@/components/user/collected/cards/UserPageCollectedCardsNoCards"; +import { CollectedCollectionType, CollectionSeized } from "@/entities/IProfile"; +import { MemeSeason } from "@/entities/ISeason"; +import { render, screen } from "@testing-library/react"; -describe('UserPageCollectedCardsNoCards messages', () => { +describe("UserPageCollectedCardsNoCards messages", () => { function renderComponent(filters: any) { return render(); } - it('shows generic message when seized filter active', () => { - renderComponent({ seized: CollectionSeized.SEIZED, collection: null, szn: null }); - expect(screen.getByText('No cards to display')).toBeInTheDocument(); + const mockSeason: MemeSeason = { + id: 4, + start_index: 1, + end_index: 100, + count: 100, + name: "SZN4", + display: "SZN 4", + }; + + it("shows generic message when seized filter active", () => { + renderComponent({ + seized: CollectionSeized.SEIZED, + collection: null, + szn: null, + }); + expect(screen.getByText("No cards to display")).toBeInTheDocument(); }); - it('shows full setter when no collection selected', () => { - renderComponent({ seized: CollectionSeized.NOT_SEIZED, collection: null, szn: null }); - expect(screen.getByText('Congratulations, full setter!')).toBeInTheDocument(); + it("shows full setter when no collection selected", () => { + renderComponent({ + seized: CollectionSeized.NOT_SEIZED, + collection: null, + szn: null, + }); + expect( + screen.getByText("Congratulations, full setter!") + ).toBeInTheDocument(); }); - it('shows season specific message for memes season', () => { - renderComponent({ seized: CollectionSeized.NOT_SEIZED, collection: CollectedCollectionType.MEMES, szn: MEMES_SEASON.SZN4 }); - expect(screen.getByText('Congratulations, SZN4 full setter!')).toBeInTheDocument(); + it("shows season specific message for memes season", () => { + renderComponent({ + seized: CollectionSeized.NOT_SEIZED, + collection: CollectedCollectionType.MEMES, + szn: mockSeason, + }); + expect( + screen.getByText("Congratulations, SZN 4 full setter!") + ).toBeInTheDocument(); }); - it('shows gradient message', () => { - renderComponent({ seized: CollectionSeized.NOT_SEIZED, collection: CollectedCollectionType.GRADIENTS, szn: null }); - expect(screen.getByText('Congratulations, Gradient full setter!')).toBeInTheDocument(); + it("shows gradient message", () => { + renderComponent({ + seized: CollectionSeized.NOT_SEIZED, + collection: CollectedCollectionType.GRADIENTS, + szn: null, + }); + expect( + screen.getByText("Congratulations, Gradient full setter!") + ).toBeInTheDocument(); }); }); diff --git a/__tests__/components/user/collected/filters/UserPageCollectedFilters.test.tsx b/__tests__/components/user/collected/filters/UserPageCollectedFilters.test.tsx index 77886affab..04ca2ead4d 100644 --- a/__tests__/components/user/collected/filters/UserPageCollectedFilters.test.tsx +++ b/__tests__/components/user/collected/filters/UserPageCollectedFilters.test.tsx @@ -1,97 +1,144 @@ -import React from 'react'; -import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; -import { RefObject } from 'react'; -import UserPageCollectedFilters from '@/components/user/collected/filters/UserPageCollectedFilters'; -import { CollectedCollectionType, CollectionSeized, CollectionSort } from '@/entities/IProfile'; -import { SortDirection } from '@/entities/ISort'; -import { MEMES_SEASON } from '@/enums'; -import { ApiIdentity } from '@/generated/models/ApiIdentity'; -import { ApiProfileClassification } from '@/generated/models/ApiProfileClassification'; - -// Mock the child components -jest.mock('@/components/user/collected/filters/UserPageCollectedFiltersCollection', () => { - return function MockUserPageCollectedFiltersCollection({ selected, setSelected }: any) { - return ( -
- - Current: {selected || 'none'} -
- ); - }; -}); - -jest.mock('@/components/user/collected/filters/UserPageCollectedFiltersSortBy', () => { - return function MockUserPageCollectedFiltersSortBy({ selected, setSelected }: any) { - return ( -
- - Current: {selected || 'none'} -
- ); - }; -}); - -jest.mock('@/components/user/collected/filters/UserPageCollectedFiltersSeized', () => { - return function MockUserPageCollectedFiltersSeized({ selected, setSelected }: any) { - return ( -
- - Current: {selected || 'none'} -
- ); - }; -}); - -jest.mock('@/components/user/collected/filters/UserPageCollectedFiltersSzn', () => { - return function MockUserPageCollectedFiltersSzn({ selected, setSelected }: any) { - return ( -
- - Current: {selected || 'none'} -
- ); - }; -}); - -jest.mock('@/components/user/utils/addresses-select/UserAddressesSelectDropdown', () => { - return function MockUserAddressesSelectDropdown({ wallets, onActiveAddress }: any) { - return ( -
- - Wallets: {wallets.length} -
- ); - }; -}); - -// Mock the helpers -jest.mock('@/components/user/collected/filters/user-page-collected-filters.helpers', () => ({ - COLLECTED_COLLECTIONS_META: { - [CollectedCollectionType.MEMES]: { - filters: { - seized: true, - szn: true, +import UserPageCollectedFilters from "@/components/user/collected/filters/UserPageCollectedFilters"; +import { + CollectedCollectionType, + CollectionSeized, + CollectionSort, +} from "@/entities/IProfile"; +import { MemeSeason } from "@/entities/ISeason"; +import { SortDirection } from "@/entities/ISort"; +import { ApiIdentity } from "@/generated/models/ApiIdentity"; +import { ApiProfileClassification } from "@/generated/models/ApiProfileClassification"; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; +import { RefObject } from "react"; + +jest.mock( + "@/components/user/collected/filters/UserPageCollectedFiltersCollection", + () => { + return function MockUserPageCollectedFiltersCollection({ + selected, + setSelected, + }: any) { + return ( +
+ + Current: {selected || "none"} +
+ ); + }; + } +); + +jest.mock( + "@/components/user/collected/filters/UserPageCollectedFiltersSortBy", + () => { + return function MockUserPageCollectedFiltersSortBy({ + selected, + setSelected, + }: any) { + return ( +
+ + Current: {selected || "none"} +
+ ); + }; + } +); + +jest.mock( + "@/components/user/collected/filters/UserPageCollectedFiltersSeized", + () => { + return function MockUserPageCollectedFiltersSeized({ + selected, + setSelected, + }: any) { + return ( +
+ + Current: {selected || "none"} +
+ ); + }; + } +); + +jest.mock( + "@/components/user/collected/filters/UserPageCollectedFiltersSzn", + () => { + return function MockUserPageCollectedFiltersSzn({ + selected, + setSelected, + }: any) { + return ( +
+ + Current: {selected?.display || "none"} +
+ ); + }; + } +); + +jest.mock( + "@/components/user/utils/addresses-select/UserAddressesSelectDropdown", + () => { + return function MockUserAddressesSelectDropdown({ + wallets, + onActiveAddress, + }: any) { + return ( +
+ + Wallets: {wallets.length} +
+ ); + }; + } +); + +jest.mock( + "@/components/user/collected/filters/user-page-collected-filters.helpers", + () => ({ + COLLECTED_COLLECTIONS_META: { + [CollectedCollectionType.MEMES]: { + filters: { + seized: true, + szn: true, + }, }, - }, - [CollectedCollectionType.GRADIENTS]: { - filters: { - seized: false, - szn: false, + [CollectedCollectionType.GRADIENTS]: { + filters: { + seized: false, + szn: false, + }, }, - }, - [CollectedCollectionType.NETWORK]: { - filters: { - seized: false, - szn: false, + [CollectedCollectionType.NETWORK]: { + filters: { + seized: false, + szn: false, + }, }, }, - }, -})); + }) +); -// Mock window.matchMedia -Object.defineProperty(window, 'matchMedia', { +Object.defineProperty(globalThis, "matchMedia", { writable: true, - value: jest.fn().mockImplementation(query => ({ + value: jest.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, @@ -103,13 +150,13 @@ Object.defineProperty(window, 'matchMedia', { })), }); -describe('UserPageCollectedFilters', () => { +describe("UserPageCollectedFilters", () => { const mockProfile: ApiIdentity = { - id: '1', - handle: 'testuser', - normalised_handle: 'testuser', - wallet: '0x123', - display: 'Test User', + id: "1", + handle: "testuser", + normalised_handle: "testuser", + wallet: "0x123", + display: "Test User", pfp: null, pfp_url: null, cic: 0, @@ -122,15 +169,15 @@ describe('UserPageCollectedFilters', () => { consolidation_key: null, classification: ApiProfileClassification.Pseudonym, sub_classification: null, - primary_wallet: '0x123', + primary_wallet: "0x123", banner1: null, banner2: null, active_main_stage_submission_ids: [], winner_main_stage_drop_ids: [], wallets: [ - { wallet: '0x123', display: '0x123', tdh: 0 }, - { wallet: '0x456', display: '0x456', tdh: 0 } - ] + { wallet: "0x123", display: "0x123", tdh: 0 }, + { wallet: "0x456", display: "0x456", tdh: 0 }, + ], } as unknown as ApiIdentity; const mockFilters = { @@ -138,11 +185,13 @@ describe('UserPageCollectedFilters', () => { sortBy: CollectionSort.TOKEN_ID, sortDirection: SortDirection.ASC, seized: null as CollectionSeized | null, - szn: null as MEMES_SEASON | null, - handleOrWallet: 'testuser', + szn: null as MemeSeason | null, + initialSznId: null as number | null, + handleOrWallet: "testuser", accountForConsolidations: false, page: 1, pageSize: 20, + subcollection: null, }; const mockSetters = { @@ -150,6 +199,7 @@ describe('UserPageCollectedFilters', () => { setSortBy: jest.fn(), setSeized: jest.fn(), setSzn: jest.fn(), + setSubcollection: jest.fn(), showTransfer: false, }; @@ -158,17 +208,17 @@ describe('UserPageCollectedFilters', () => { beforeEach(() => { jest.clearAllMocks(); mockContainerRef = { - current: document.createElement('div') + current: document.createElement("div"), }; - globalThis.ResizeObserver = jest.fn().mockImplementation((callback) => ({ + globalThis.ResizeObserver = jest.fn().mockImplementation(() => ({ observe: jest.fn(), unobserve: jest.fn(), disconnect: jest.fn(), })); }); - it('renders filters correctly', () => { + it("renders filters correctly", () => { render( { /> ); - expect(screen.getByTestId('collection-filter')).toBeInTheDocument(); - expect(screen.getByTestId('sort-by-filter')).toBeInTheDocument(); - expect(screen.getByTestId('address-select')).toBeInTheDocument(); + expect(screen.getByTestId("sort-by-filter")).toBeInTheDocument(); + expect(screen.getByTestId("address-select")).toBeInTheDocument(); }); - it('shows seized filter when collection supports it', () => { - const filtersWithMemes = { ...mockFilters, collection: CollectedCollectionType.MEMES }; + it("shows seized filter when collection supports it", () => { + const filtersWithMemes = { + ...mockFilters, + collection: CollectedCollectionType.MEMES, + }; render( { /> ); - expect(screen.getByTestId('seized-filter')).toBeInTheDocument(); + expect(screen.getByTestId("seized-filter")).toBeInTheDocument(); }); - it('shows season filter when collection supports it', () => { - const filtersWithMemes = { ...mockFilters, collection: CollectedCollectionType.MEMES }; + it("shows season filter when collection supports it", () => { + const filtersWithMemes = { + ...mockFilters, + collection: CollectedCollectionType.MEMES, + }; render( { /> ); - expect(screen.getByTestId('szn-filter')).toBeInTheDocument(); + expect(screen.getByTestId("szn-filter")).toBeInTheDocument(); }); - it('hides seized and season filters when collection does not support them', () => { - const filtersWithGradients = { ...mockFilters, collection: CollectedCollectionType.GRADIENTS }; + it("hides seized and season filters when collection does not support them", () => { + const filtersWithGradients = { + ...mockFilters, + collection: CollectedCollectionType.GRADIENTS, + }; render( { /> ); - expect(screen.queryByTestId('seized-filter')).not.toBeInTheDocument(); - expect(screen.queryByTestId('szn-filter')).not.toBeInTheDocument(); + expect(screen.queryByTestId("seized-filter")).not.toBeInTheDocument(); + expect(screen.queryByTestId("szn-filter")).not.toBeInTheDocument(); }); - it('calls setCollection when collection filter is used', () => { + it("calls setSortBy when sort filter is used", () => { render( { /> ); - fireEvent.click(screen.getByText('Collection Filter')); - expect(mockSetters.setCollection).toHaveBeenCalledWith(CollectedCollectionType.MEMES); - }); - - it('calls setSortBy when sort filter is used', () => { - render( - - ); - - fireEvent.click(screen.getByText('Sort By Filter')); + fireEvent.click(screen.getByText("Sort By Filter")); expect(mockSetters.setSortBy).toHaveBeenCalledWith(CollectionSort.TOKEN_ID); }); - it('shows scroll arrows when filters are not fully visible', async () => { + it("shows scroll arrows when filters are not fully visible", async () => { const { container } = render( { ); await waitFor(() => { - const scrollContainer = container.querySelector('[class*="tw-overflow-x-auto"]') as HTMLDivElement; + const scrollContainer = container.querySelector( + '[class*="tw-overflow-x-auto"]' + ) as HTMLDivElement; expect(scrollContainer).toBeTruthy(); }); - const scrollContainer = container.querySelector('[class*="tw-overflow-x-auto"]') as HTMLDivElement; + const scrollContainer = container.querySelector( + '[class*="tw-overflow-x-auto"]' + ) as HTMLDivElement; if (!scrollContainer) { - throw new Error('Scroll container not found'); + throw new Error("Scroll container not found"); } await act(async () => { - Object.defineProperty(scrollContainer, 'scrollLeft', { + Object.defineProperty(scrollContainer, "scrollLeft", { writable: true, configurable: true, value: 50, }); - Object.defineProperty(scrollContainer, 'scrollWidth', { + Object.defineProperty(scrollContainer, "scrollWidth", { writable: true, configurable: true, value: 300, }); - Object.defineProperty(scrollContainer, 'clientWidth', { + Object.defineProperty(scrollContainer, "clientWidth", { writable: true, configurable: true, value: 100, }); - const scrollEvent = new Event('scroll', { bubbles: true }); + const scrollEvent = new Event("scroll", { bubbles: true }); scrollContainer.dispatchEvent(scrollEvent); - await new Promise(resolve => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); }); await waitFor(() => { - expect(screen.getByLabelText('Scroll filters left')).toBeInTheDocument(); - expect(screen.getByLabelText('Scroll filters right')).toBeInTheDocument(); + expect(screen.getByLabelText("Scroll filters left")).toBeInTheDocument(); + expect(screen.getByLabelText("Scroll filters right")).toBeInTheDocument(); }); }); - it('calls scrollHorizontally when scroll arrows are clicked', async () => { + it("calls scrollHorizontally when scroll arrows are clicked", async () => { const scrollBySpy = jest.fn(); const { container } = render( { ); await waitFor(() => { - const scrollContainer = container.querySelector('[class*="tw-overflow-x-auto"]') as HTMLDivElement; + const scrollContainer = container.querySelector( + '[class*="tw-overflow-x-auto"]' + ) as HTMLDivElement; expect(scrollContainer).toBeTruthy(); }); - const scrollContainer = container.querySelector('[class*="tw-overflow-x-auto"]') as HTMLDivElement; + const scrollContainer = container.querySelector( + '[class*="tw-overflow-x-auto"]' + ) as HTMLDivElement; if (!scrollContainer) { - throw new Error('Scroll container not found'); + throw new Error("Scroll container not found"); } scrollContainer.scrollBy = scrollBySpy; await act(async () => { - Object.defineProperty(scrollContainer, 'scrollLeft', { + Object.defineProperty(scrollContainer, "scrollLeft", { writable: true, configurable: true, value: 50, }); - Object.defineProperty(scrollContainer, 'scrollWidth', { + Object.defineProperty(scrollContainer, "scrollWidth", { writable: true, configurable: true, value: 300, }); - Object.defineProperty(scrollContainer, 'clientWidth', { + Object.defineProperty(scrollContainer, "clientWidth", { writable: true, configurable: true, value: 100, }); - const scrollEvent = new Event('scroll', { bubbles: true }); + const scrollEvent = new Event("scroll", { bubbles: true }); scrollContainer.dispatchEvent(scrollEvent); - await new Promise(resolve => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); }); await waitFor(() => { - expect(screen.getByLabelText('Scroll filters left')).toBeInTheDocument(); - expect(screen.getByLabelText('Scroll filters right')).toBeInTheDocument(); + expect(screen.getByLabelText("Scroll filters left")).toBeInTheDocument(); + expect(screen.getByLabelText("Scroll filters right")).toBeInTheDocument(); }); - const leftArrow = screen.getByLabelText('Scroll filters left'); - const rightArrow = screen.getByLabelText('Scroll filters right'); + const leftArrow = screen.getByLabelText("Scroll filters left"); + const rightArrow = screen.getByLabelText("Scroll filters right"); fireEvent.click(leftArrow); - expect(scrollBySpy).toHaveBeenCalledWith({ left: -150, behavior: 'smooth' }); + expect(scrollBySpy).toHaveBeenCalledWith({ + left: -150, + behavior: "smooth", + }); fireEvent.click(rightArrow); - expect(scrollBySpy).toHaveBeenCalledWith({ left: 150, behavior: 'smooth' }); + expect(scrollBySpy).toHaveBeenCalledWith({ left: 150, behavior: "smooth" }); }); - it('sets up event listeners on mount and cleans up on unmount', async () => { - const addEventListenerSpy = jest.spyOn(HTMLDivElement.prototype, 'addEventListener'); - const removeEventListenerSpy = jest.spyOn(HTMLDivElement.prototype, 'removeEventListener'); - const windowAddEventListenerSpy = jest.spyOn(window, 'addEventListener'); - const windowRemoveEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + it("sets up event listeners on mount and cleans up on unmount", async () => { + const addEventListenerSpy = jest.spyOn( + HTMLDivElement.prototype, + "addEventListener" + ); + const removeEventListenerSpy = jest.spyOn( + HTMLDivElement.prototype, + "removeEventListener" + ); + const windowAddEventListenerSpy = jest.spyOn( + globalThis, + "addEventListener" + ); + const windowRemoveEventListenerSpy = jest.spyOn( + globalThis, + "removeEventListener" + ); const { container, unmount } = render( { ); await waitFor(() => { - const scrollContainer = container.querySelector('[class*="tw-overflow-x-auto"]') as HTMLDivElement; + const scrollContainer = container.querySelector( + '[class*="tw-overflow-x-auto"]' + ) as HTMLDivElement; expect(scrollContainer).toBeTruthy(); }); await waitFor(() => { - expect(addEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function)); - expect(windowAddEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function)); + expect(addEventListenerSpy).toHaveBeenCalledWith( + "scroll", + expect.any(Function) + ); + expect(windowAddEventListenerSpy).toHaveBeenCalledWith( + "resize", + expect.any(Function) + ); }); unmount(); - expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function)); - expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function)); + expect(removeEventListenerSpy).toHaveBeenCalledWith( + "scroll", + expect.any(Function) + ); + expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith( + "resize", + expect.any(Function) + ); addEventListenerSpy.mockRestore(); removeEventListenerSpy.mockRestore(); windowAddEventListenerSpy.mockRestore(); windowRemoveEventListenerSpy.mockRestore(); }); - -}); \ No newline at end of file +}); diff --git a/__tests__/components/user/collected/filters/UserPageCollectedFiltersSzn.test.tsx b/__tests__/components/user/collected/filters/UserPageCollectedFiltersSzn.test.tsx index 1921d9e808..17180feaf9 100644 --- a/__tests__/components/user/collected/filters/UserPageCollectedFiltersSzn.test.tsx +++ b/__tests__/components/user/collected/filters/UserPageCollectedFiltersSzn.test.tsx @@ -1,29 +1,58 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import UserPageCollectedFiltersSzn from '@/components/user/collected/filters/UserPageCollectedFiltersSzn'; -import { MEMES_SEASON } from '@/enums'; +import UserPageCollectedFiltersSzn from "@/components/user/collected/filters/UserPageCollectedFiltersSzn"; +import { MemeSeason } from "@/entities/ISeason"; +import { render } from "@testing-library/react"; let capturedProps: any = null; -jest.mock('@/components/utils/select/dropdown/CommonDropdown', () => (props: any) => { - capturedProps = props; - return
; -}); +jest.mock( + "@/components/utils/select/dropdown/SeasonsGridDropdown", + () => (props: any) => { + capturedProps = props; + return
; + } +); -describe('UserPageCollectedFiltersSzn', () => { +describe("UserPageCollectedFiltersSzn", () => { beforeEach(() => { capturedProps = null; }); - it('passes items and active selection to dropdown', () => { - const ref = { current: null } as React.RefObject; + it("passes props to SeasonsGridDropdown", () => { + const mockSeason: MemeSeason = { + id: 1, + start_index: 1, + end_index: 100, + count: 100, + name: "SZN1", + display: "SZN 1", + }; + const setSelected = jest.fn(); + render( - + ); - const values = capturedProps.items.map((i: any) => i.value); - expect(values).toEqual([null, ...Object.values(MEMES_SEASON)]); - expect(capturedProps.activeItem).toBe(MEMES_SEASON.SZN1); - expect(capturedProps.filterLabel).toBe('Season'); - expect(capturedProps.containerRef).toBe(ref); + + expect(capturedProps.selected).toBe(mockSeason); + expect(capturedProps.initialSeasonId).toBe(1); + expect(capturedProps.setSelected).toBe(setSelected); + }); + + it("passes null when no season selected", () => { + const setSelected = jest.fn(); + + render( + + ); + + expect(capturedProps.selected).toBeNull(); + expect(capturedProps.initialSeasonId).toBeNull(); }); }); diff --git a/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.test.tsx b/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.test.tsx index 00fb91c976..b0d9b0fccf 100644 --- a/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.test.tsx +++ b/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.test.tsx @@ -1,52 +1,57 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { useQuery } from '@tanstack/react-query'; -import { AuthContext } from '@/components/auth/Auth'; -import UserPageSubscriptionsUpcoming from '@/components/user/subscriptions/UserPageSubscriptionsUpcoming'; -import { NFTSubscription, SubscriptionDetails } from '@/entities/ISubscription'; -import { createMockAuthContext } from '@/__tests__/utils/testContexts'; - -jest.mock('@tanstack/react-query', () => ({ +import { createMockAuthContext } from "@/__tests__/utils/testContexts"; +import { AuthContext } from "@/components/auth/Auth"; +import UserPageSubscriptionsUpcoming from "@/components/user/subscriptions/UserPageSubscriptionsUpcoming"; +import { NFTSubscription, SubscriptionDetails } from "@/entities/ISubscription"; +import { useQuery } from "@tanstack/react-query"; +import { fireEvent, render, screen } from "@testing-library/react"; + +jest.mock("@tanstack/react-query", () => ({ useQuery: jest.fn(), })); const mockUpcomingRows = [ { - utcDay: new Date('2024-01-01T00:00:00Z'), - instantUtc: new Date('2024-01-01T15:40:00Z'), + utcDay: new Date("2024-01-01T00:00:00Z"), + instantUtc: new Date("2024-01-01T15:40:00Z"), meme: 201, + seasonIndex: 1, }, { - utcDay: new Date('2024-01-02T00:00:00Z'), - instantUtc: new Date('2024-01-02T15:40:00Z'), + utcDay: new Date("2024-01-02T00:00:00Z"), + instantUtc: new Date("2024-01-02T15:40:00Z"), meme: 202, + seasonIndex: 1, }, { - utcDay: new Date('2024-01-03T00:00:00Z'), - instantUtc: new Date('2024-01-03T15:40:00Z'), + utcDay: new Date("2024-01-03T00:00:00Z"), + instantUtc: new Date("2024-01-03T15:40:00Z"), meme: 203, + seasonIndex: 1, }, { - utcDay: new Date('2024-01-04T00:00:00Z'), - instantUtc: new Date('2024-01-04T15:40:00Z'), + utcDay: new Date("2024-01-04T00:00:00Z"), + instantUtc: new Date("2024-01-04T15:40:00Z"), meme: 204, + seasonIndex: 1, }, ]; -jest.mock('@/components/meme-calendar/meme-calendar.helpers', () => ({ +jest.mock("@/components/meme-calendar/meme-calendar.helpers", () => ({ __esModule: true, formatFullDate: jest.fn((date: Date) => { - const iso = date.toISOString().split('T')[0]; - const day = date.toLocaleDateString('en-US', { - weekday: 'long', - timeZone: 'UTC', + const iso = date.toISOString().split("T")[0]; + const day = date.toLocaleDateString("en-US", { + weekday: "long", + timeZone: "UTC", }); return `${iso} / ${day}`; }), - getUpcomingMintsForCurrentOrNextSeason: jest.fn(() => ({ rows: mockUpcomingRows })), + getUpcomingMintsAcrossSeasons: jest.fn(() => mockUpcomingRows), isMintingToday: jest.fn(() => false), + displayedSeasonNumberFromIndex: jest.fn((idx: number) => idx + 1), })); -jest.mock('@/services/api/common-api', () => ({ +jest.mock("@/services/api/common-api", () => ({ commonApiFetch: jest.fn(), commonApiPost: jest.fn(), })); @@ -54,31 +59,31 @@ jest.mock('@/services/api/common-api', () => ({ const mockSubscriptions: NFTSubscription[] = [ { token_id: 1, - contract: '0x123', + contract: "0x123", subscribed: true, } as NFTSubscription, { token_id: 2, - contract: '0x123', + contract: "0x123", subscribed: false, } as NFTSubscription, { token_id: 3, - contract: '0x123', + contract: "0x123", subscribed: true, } as NFTSubscription, { token_id: 4, - contract: '0x123', + contract: "0x123", subscribed: false, } as NFTSubscription, ]; const mockDetails: SubscriptionDetails = { - profile: 'testuser', + profile: "testuser", } as SubscriptionDetails; -describe('UserPageSubscriptionsUpcoming', () => { +describe("UserPageSubscriptionsUpcoming", () => { const useQueryMock = useQuery as jest.Mock; const mockRefresh = jest.fn(); const mockRequestAuth = jest.fn(); @@ -86,12 +91,14 @@ describe('UserPageSubscriptionsUpcoming', () => { beforeEach(() => { useQueryMock.mockImplementation(({ enabled }) => ({ - data: enabled ? { - phase: 'Phase 1', - phase_position: 100, - phase_subscriptions: 500, - airdrop_address: '0xabc123', - } : null, + data: enabled + ? { + phase: "Phase 1", + phase_position: 100, + phase_subscriptions: 500, + airdrop_address: "0xabc123", + } + : null, })); mockRequestAuth.mockResolvedValue({ success: true }); }); @@ -107,7 +114,7 @@ describe('UserPageSubscriptionsUpcoming', () => { }); const defaultProps = { - profileKey: 'testuser', + profileKey: "testuser", details: mockDetails, memes_subscriptions: mockSubscriptions, readonly: false, @@ -122,125 +129,118 @@ describe('UserPageSubscriptionsUpcoming', () => { ); }; - it('renders upcoming drops title', () => { + it("renders upcoming drops title", () => { renderComponent(); - expect(screen.getByText('Upcoming Drops')).toBeInTheDocument(); + expect(screen.getByText("Upcoming Drops")).toBeInTheDocument(); }); - it('displays first 3 subscriptions by default', () => { + it("displays first 3 subscriptions by default", () => { renderComponent(); - expect(screen.getByText('The Memes #1')).toBeInTheDocument(); - expect(screen.getByText('The Memes #2')).toBeInTheDocument(); - expect(screen.getByText('The Memes #3')).toBeInTheDocument(); - expect(screen.queryByText('The Memes #4')).not.toBeInTheDocument(); + expect(screen.getByText("The Memes #1")).toBeInTheDocument(); + expect(screen.getByText("The Memes #2")).toBeInTheDocument(); + expect(screen.getByText("The Memes #3")).toBeInTheDocument(); + expect(screen.queryByText("The Memes #4")).not.toBeInTheDocument(); }); - it('shows expand button when there are more than 3 subscriptions', () => { + it("shows expand button when there are more than 3 subscriptions", () => { renderComponent(); - expect(screen.getByText('Show More')).toBeInTheDocument(); + expect(screen.getByText("Show More")).toBeInTheDocument(); }); - it('expands to show all subscriptions when Show More clicked', () => { + it("expands to show all subscriptions when Show More clicked", () => { renderComponent(); - - fireEvent.click(screen.getByText('Show More')); - - expect(screen.getByText('The Memes #4')).toBeInTheDocument(); - expect(screen.getByText('Show Less')).toBeInTheDocument(); + + fireEvent.click(screen.getByText("Show More")); + + expect(screen.getByText("The Memes #4")).toBeInTheDocument(); + expect(screen.getByText("Show Less")).toBeInTheDocument(); }); - it('collapses back to 3 subscriptions when Show Less clicked', () => { + it("collapses back to 3 subscriptions when Show Less clicked", () => { renderComponent(); - - fireEvent.click(screen.getByText('Show More')); - fireEvent.click(screen.getByText('Show Less')); - - expect(screen.queryByText('The Memes #4')).not.toBeInTheDocument(); - expect(screen.getByText('Show More')).toBeInTheDocument(); + + fireEvent.click(screen.getByText("Show More")); + fireEvent.click(screen.getByText("Show Less")); + + expect(screen.queryByText("The Memes #4")).not.toBeInTheDocument(); + expect(screen.getByText("Show More")).toBeInTheDocument(); }); - it('displays subscription toggles in correct state', () => { + it("displays subscription toggles in correct state", () => { renderComponent(); - - const toggles = screen.getAllByRole('button'); - const subscriptionToggles = toggles.filter(toggle => - toggle.getAttribute('aria-label')?.includes('Toggle subscription') - ); - - // Since the component may not render subscription toggles in this test setup, - // we'll just verify that buttons exist on the page (Show More, etc.) + + const toggles = screen.getAllByRole("button"); expect(toggles.length).toBeGreaterThan(0); }); - it('shows phase information for first subscription', () => { + it("shows phase information for first subscription", () => { renderComponent(); - + expect(screen.getByText(/Phase: Phase 1/)).toBeInTheDocument(); expect(screen.getByText(/Subscription Position: 100/)).toBeInTheDocument(); expect(screen.getByText(/Airdrop Address: 0xabc123/)).toBeInTheDocument(); }); - it('does not show phase information when not available', () => { + it("does not show phase information when not available", () => { useQueryMock.mockReturnValue({ data: null, }); - + renderComponent(); - + expect(screen.queryByText(/Phase:/)).not.toBeInTheDocument(); }); - it('displays dates for subscriptions', () => { + it("displays dates for subscriptions", () => { renderComponent(); - - expect(screen.getByText('2024-01-01 / Monday')).toBeInTheDocument(); - expect(screen.getByText('2024-01-02 / Tuesday')).toBeInTheDocument(); + + expect(screen.getByText("2024-01-01 / Monday")).toBeInTheDocument(); + expect(screen.getByText("2024-01-02 / Tuesday")).toBeInTheDocument(); }); - it('shows minting today message when applicable', () => { - const { isMintingToday } = require('@/components/meme-calendar/meme-calendar.helpers'); + it("shows minting today message when applicable", () => { + const { + isMintingToday, + } = require("@/components/meme-calendar/meme-calendar.helpers"); isMintingToday.mockReturnValue(true); - + renderComponent(); - - expect(screen.getByText('- Minting Today')).toBeInTheDocument(); + + expect(screen.getByText("Minting Today")).toBeInTheDocument(); }); - it('disables toggles in readonly mode', () => { + it("disables toggles in readonly mode", () => { renderComponent({ readonly: true }); - - const toggles = screen.getAllByRole('button'); - const subscriptionToggles = toggles.filter(toggle => - toggle.getAttribute('aria-label')?.includes('Toggle subscription') + + const toggles = screen.getAllByRole("button"); + const subscriptionToggles = toggles.filter((toggle) => + toggle.getAttribute("aria-label")?.includes("Toggle subscription") ); - - subscriptionToggles.forEach(toggle => { + + subscriptionToggles.forEach((toggle) => { expect(toggle).toBeDisabled(); }); }); - it('handles empty subscriptions list', () => { + it("handles empty subscriptions list", () => { renderComponent({ memes_subscriptions: [] }); - - expect(screen.getByText('Upcoming Drops')).toBeInTheDocument(); - expect(screen.queryByText('Show More')).not.toBeInTheDocument(); + + expect(screen.getByText("Upcoming Drops")).toBeInTheDocument(); + expect(screen.queryByText("Show More")).not.toBeInTheDocument(); }); - it('does not show expand button when no subscriptions', () => { + it("does not show expand button when no subscriptions", () => { renderComponent({ memes_subscriptions: [] }); - - expect(screen.queryByText('Show More')).not.toBeInTheDocument(); + + expect(screen.queryByText("Show More")).not.toBeInTheDocument(); }); - it('calls query for final subscription data only for first subscription', () => { + it("calls query for final subscription data only for first subscription", () => { renderComponent(); - + expect(useQueryMock).toHaveBeenCalledWith( expect.objectContaining({ - queryKey: [ - 'consolidation-final-subscription', - 'testuser-0x123-1', - ], + queryKey: ["consolidation-final-subscription", "testuser-0x123-1"], enabled: true, }) ); diff --git a/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.toggle.test.tsx b/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.toggle.test.tsx index 558d6e75b3..4b3acc3bf7 100644 --- a/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.toggle.test.tsx +++ b/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.toggle.test.tsx @@ -1,46 +1,51 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { useQuery } from '@tanstack/react-query'; -import UserPageSubscriptionsUpcoming from '@/components/user/subscriptions/UserPageSubscriptionsUpcoming'; -import { AuthContext } from '@/components/auth/Auth'; -import { createMockAuthContext } from '@/__tests__/utils/testContexts'; - -jest.mock('react-toggle', () => (props: any) => ( +import { createMockAuthContext } from "@/__tests__/utils/testContexts"; +import { AuthContext } from "@/components/auth/Auth"; +import UserPageSubscriptionsUpcoming from "@/components/user/subscriptions/UserPageSubscriptionsUpcoming"; +import { useQuery } from "@tanstack/react-query"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +jest.mock("react-toggle", () => (props: any) => ( )); -jest.mock('@tanstack/react-query', () => ({ useQuery: jest.fn() })); +jest.mock("@tanstack/react-query", () => ({ useQuery: jest.fn() })); const mockUpcomingRows = [ { - utcDay: new Date('2024-01-01T00:00:00Z'), - instantUtc: new Date('2024-01-01T15:40:00Z'), + utcDay: new Date("2024-01-01T00:00:00Z"), + instantUtc: new Date("2024-01-01T15:40:00Z"), meme: 201, + seasonIndex: 1, }, ]; -jest.mock('@/components/meme-calendar/meme-calendar.helpers', () => ({ +jest.mock("@/components/meme-calendar/meme-calendar.helpers", () => ({ __esModule: true, - formatFullDate: jest.fn(() => '2024-01-01 / Monday'), - getUpcomingMintsForCurrentOrNextSeason: jest.fn(() => ({ rows: mockUpcomingRows })), + getUpcomingMintsAcrossSeasons: jest.fn(() => mockUpcomingRows), isMintingToday: jest.fn(() => false), + displayedSeasonNumberFromIndex: jest.fn((idx: number) => idx + 1), + formatFullDate: jest.fn(() => "Mon, 1 Jan 2024"), })); -jest.mock('@/services/api/common-api', () => ({ +jest.mock("@/services/api/common-api", () => ({ commonApiFetch: jest.fn(), commonApiPost: jest.fn(), })); -const { commonApiPost } = require('@/services/api/common-api'); +const { commonApiPost } = require("@/services/api/common-api"); -const sub = { token_id:1, contract:'0x123', subscribed:true } as any; -const details = { profile:'test' } as any; +const sub = { token_id: 1, contract: "0x123", subscribed: true } as any; +const details = { profile: "test" } as any; const useQueryMock = useQuery as jest.Mock; function renderComp() { useQueryMock.mockReturnValue({ data: null }); - const auth = createMockAuthContext({ requestAuth: jest.fn(async () => ({ success: true })), setToast: jest.fn() }); + const auth = createMockAuthContext({ + requestAuth: jest.fn(async () => ({ success: true })), + setToast: jest.fn(), + }); return render( { +test("toggles subscription and posts update", async () => { const user = userEvent.setup(); - (commonApiPost as jest.Mock).mockResolvedValue({ token_id:1, subscribed:false }); + (commonApiPost as jest.Mock).mockResolvedValue({ + token_id: 1, + subscribed: false, + }); const { container } = renderComp(); - await user.click(screen.getByTestId('toggle')); + await user.click(screen.getByTestId("toggle")); expect(commonApiPost).toHaveBeenCalled(); }); diff --git a/__tests__/components/utils/select/dropdown/SeasonsGridDropdown.test.tsx b/__tests__/components/utils/select/dropdown/SeasonsGridDropdown.test.tsx new file mode 100644 index 0000000000..69ac316026 --- /dev/null +++ b/__tests__/components/utils/select/dropdown/SeasonsGridDropdown.test.tsx @@ -0,0 +1,299 @@ +import SeasonsGridDropdown from "@/components/utils/select/dropdown/SeasonsGridDropdown"; +import { MemeSeason } from "@/entities/ISeason"; +import { act, fireEvent, render, screen } from "@testing-library/react"; + +const mockSeasons: MemeSeason[] = [ + { + id: 1, + start_index: 1, + end_index: 100, + count: 100, + name: "SZN1", + display: "SZN 1", + }, + { + id: 2, + start_index: 101, + end_index: 200, + count: 100, + name: "SZN2", + display: "SZN 2", + }, + { + id: 3, + start_index: 201, + end_index: 300, + count: 100, + name: "SZN3", + display: "SZN 3", + }, +]; + +const flushPromises = () => act(() => Promise.resolve()); + +jest.mock("@/services/api/common-api", () => ({ + commonApiFetch: jest.fn(() => Promise.resolve(mockSeasons)), +})); + +jest.mock("framer-motion", () => ({ + useAnimate: () => [{ current: null }, jest.fn()], + AnimatePresence: ({ children }: any) => <>{children}, + motion: { + div: ({ children, ...props }: any) =>
{children}
, + }, +})); + +jest.mock("react-use", () => ({ + createBreakpoint: () => () => "LG", + useClickAway: jest.fn(), + useKeyPressEvent: jest.fn(), +})); + +Object.defineProperty(globalThis, "matchMedia", { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +describe("SeasonsGridDropdown", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("renders with 'All Seasons' label when nothing is selected", async () => { + const setSelected = jest.fn(); + + render(); + await flushPromises(); + + expect( + screen.getByRole("button", { name: /Season: All Seasons/i }) + ).toBeInTheDocument(); + }); + + it("displays selected season label when a season is selected", async () => { + const setSelected = jest.fn(); + const selectedSeason = mockSeasons[1]; + + render( + + ); + await flushPromises(); + + expect( + screen.getByRole("button", { name: /Season: SZN 2/i }) + ).toBeInTheDocument(); + }); + + it("opens dropdown on button click", async () => { + const setSelected = jest.fn(); + + render(); + await flushPromises(); + + const button = screen.getByRole("button", { name: /Season: All Seasons/i }); + fireEvent.click(button); + + expect(screen.getByRole("menu")).toBeInTheDocument(); + }); + + it("fetches and displays seasons from API", async () => { + const setSelected = jest.fn(); + + render(); + await flushPromises(); + + const button = screen.getByRole("button", { name: /Season: All Seasons/i }); + fireEvent.click(button); + + expect(screen.getByRole("menu")).toBeInTheDocument(); + + const menuItems = screen.getAllByRole("menuitem"); + expect(menuItems).toHaveLength(4); + expect(menuItems[0]).toHaveTextContent("All Seasons"); + expect(menuItems[1]).toHaveTextContent("SZN 1"); + expect(menuItems[2]).toHaveTextContent("SZN 2"); + expect(menuItems[3]).toHaveTextContent("SZN 3"); + }); + + it("calls setSelected with null when 'All Seasons' is clicked", async () => { + const setSelected = jest.fn(); + + render( + + ); + await flushPromises(); + + const button = screen.getByRole("button", { name: /Season: SZN 1/i }); + fireEvent.click(button); + + const allSeasonsButton = screen.getByRole("menuitem", { + name: /All Seasons/i, + }); + fireEvent.click(allSeasonsButton); + + expect(setSelected).toHaveBeenCalledWith(null); + }); + + it("calls setSelected with season when a season is clicked", async () => { + const setSelected = jest.fn(); + + render(); + await flushPromises(); + + const button = screen.getByRole("button", { name: /Season: All Seasons/i }); + fireEvent.click(button); + + const szn2Button = screen.getByRole("menuitem", { name: /SZN 2/i }); + fireEvent.click(szn2Button); + + expect(setSelected).toHaveBeenCalledWith(mockSeasons[1]); + }); + + it("applies selected styles to 'All Seasons' when selected is null", async () => { + const setSelected = jest.fn(); + + render(); + await flushPromises(); + + const button = screen.getByRole("button", { name: /Season: All Seasons/i }); + fireEvent.click(button); + + const allSeasonsButton = screen.getByRole("menuitem", { + name: /All Seasons/i, + }); + expect(allSeasonsButton).toHaveClass("tw-bg-primary-500/20"); + expect(allSeasonsButton).toHaveClass("tw-border-primary-500"); + expect(allSeasonsButton).toHaveClass("tw-text-primary-300"); + }); + + it("applies selected styles to the selected season", async () => { + const setSelected = jest.fn(); + + render( + + ); + await flushPromises(); + + const button = screen.getByRole("button", { name: /Season: SZN 1/i }); + fireEvent.click(button); + + const szn1Button = screen.getByRole("menuitem", { name: /SZN 1/i }); + expect(szn1Button).toHaveClass("tw-bg-primary-500/20"); + expect(szn1Button).toHaveClass("tw-border-primary-500"); + expect(szn1Button).toHaveClass("tw-text-primary-300"); + }); + + it("applies non-selected styles to unselected items", async () => { + const setSelected = jest.fn(); + + render( + + ); + await flushPromises(); + + const button = screen.getByRole("button", { name: /Season: SZN 1/i }); + fireEvent.click(button); + + const szn2Button = screen.getByRole("menuitem", { name: /SZN 2/i }); + expect(szn2Button).toHaveClass("tw-bg-transparent"); + expect(szn2Button).toHaveClass("tw-border-iron-700"); + expect(szn2Button).toHaveClass("tw-text-iron-200"); + }); + + it("applies initial season from initialSeasonId prop", async () => { + const setSelected = jest.fn(); + + render( + + ); + await flushPromises(); + + expect(setSelected).toHaveBeenCalledWith(mockSeasons[1]); + }); + + it("does not apply initial season when initialSeasonId is null", async () => { + const setSelected = jest.fn(); + + render( + + ); + await flushPromises(); + + expect( + screen.getByRole("button", { name: /Season: All Seasons/i }) + ).toBeInTheDocument(); + expect(setSelected).not.toHaveBeenCalled(); + }); + + it("disables button when disabled prop is true", async () => { + const setSelected = jest.fn(); + + render( + + ); + await flushPromises(); + + const button = screen.getByRole("button", { name: /Season: All Seasons/i }); + expect(button).toBeDisabled(); + expect(button).toHaveClass("tw-opacity-50"); + }); + + it("closes dropdown when selecting an item", async () => { + const setSelected = jest.fn(); + + render(); + await flushPromises(); + + const button = screen.getByRole("button", { name: /Season: All Seasons/i }); + fireEvent.click(button); + + const szn1Button = screen.getByRole("menuitem", { name: /SZN 1/i }); + fireEvent.click(szn1Button); + + expect(button).toHaveAttribute("aria-expanded", "false"); + }); + + it("has correct aria attributes", async () => { + const setSelected = jest.fn(); + + render(); + await flushPromises(); + + const button = screen.getByRole("button", { name: /Season: All Seasons/i }); + expect(button).toHaveAttribute("aria-haspopup", "true"); + expect(button).toHaveAttribute("aria-expanded", "false"); + + fireEvent.click(button); + + expect(button).toHaveAttribute("aria-expanded", "true"); + }); +}); diff --git a/__tests__/hooks/useActivityFilters.test.ts b/__tests__/hooks/useActivityFilters.test.ts index 595894b432..144c779c88 100644 --- a/__tests__/hooks/useActivityFilters.test.ts +++ b/__tests__/hooks/useActivityFilters.test.ts @@ -1,208 +1,208 @@ -import { renderHook, act } from '@testing-library/react'; -import { useActivityFilters } from '@/hooks/useActivityFilters'; -import { TypeFilter, ContractFilter } from '@/hooks/useActivityData'; +import { ContractFilter, TypeFilter } from "@/hooks/useActivityData"; +import { useActivityFilters } from "@/hooks/useActivityFilters"; +import { act, renderHook } from "@testing-library/react"; -describe('useActivityFilters', () => { - describe('Initial State', () => { - it('initializes with correct default state', () => { +describe("useActivityFilters", () => { + describe("Initial State", () => { + it("initializes with correct default state", () => { const { result } = renderHook(() => useActivityFilters()); - + expect(result.current.typeFilter).toBe(TypeFilter.ALL); expect(result.current.selectedContract).toBe(ContractFilter.ALL); - expect(typeof result.current.setTypeFilter).toBe('function'); - expect(typeof result.current.setSelectedContract).toBe('function'); - expect(typeof result.current.resetFilters).toBe('function'); + expect(typeof result.current.setTypeFilter).toBe("function"); + expect(typeof result.current.setSelectedContract).toBe("function"); + expect(typeof result.current.resetFilters).toBe("function"); }); - it('returns consistent object reference structure', () => { + it("returns consistent object reference structure", () => { const { result } = renderHook(() => useActivityFilters()); - + const returnedKeys = Object.keys(result.current); const expectedKeys = [ - 'typeFilter', - 'selectedContract', - 'setTypeFilter', - 'setSelectedContract', - 'resetFilters' + "typeFilter", + "selectedContract", + "setTypeFilter", + "setSelectedContract", + "resetFilters", ]; - + expect(returnedKeys.sort()).toEqual(expectedKeys.sort()); }); }); - describe('TypeFilter State Management', () => { - it('updates typeFilter when setTypeFilter is called', () => { + describe("TypeFilter State Management", () => { + it("updates typeFilter when setTypeFilter is called", () => { const { result } = renderHook(() => useActivityFilters()); - + expect(result.current.typeFilter).toBe(TypeFilter.ALL); - + act(() => { result.current.setTypeFilter(TypeFilter.SALES); }); - + expect(result.current.typeFilter).toBe(TypeFilter.SALES); }); - it('handles all TypeFilter enum values correctly', () => { + it("handles all TypeFilter enum values correctly", () => { const { result } = renderHook(() => useActivityFilters()); - + const allTypeFilters = [ TypeFilter.ALL, TypeFilter.AIRDROPS, TypeFilter.MINTS, TypeFilter.SALES, TypeFilter.TRANSFERS, - TypeFilter.BURNS + TypeFilter.BURNS, ]; - - allTypeFilters.forEach(filterValue => { + + allTypeFilters.forEach((filterValue) => { act(() => { result.current.setTypeFilter(filterValue); }); - + expect(result.current.typeFilter).toBe(filterValue); }); }); - it('maintains selectedContract state when typeFilter changes', () => { + it("maintains selectedContract state when typeFilter changes", () => { const { result } = renderHook(() => useActivityFilters()); - + // First set a contract filter act(() => { result.current.setSelectedContract(ContractFilter.MEMES); }); - + expect(result.current.selectedContract).toBe(ContractFilter.MEMES); - + // Then change type filter act(() => { result.current.setTypeFilter(TypeFilter.SALES); }); - + // Contract filter should remain unchanged expect(result.current.selectedContract).toBe(ContractFilter.MEMES); expect(result.current.typeFilter).toBe(TypeFilter.SALES); }); - it('allows rapid consecutive typeFilter updates', () => { + it("allows rapid consecutive typeFilter updates", () => { const { result } = renderHook(() => useActivityFilters()); - + act(() => { result.current.setTypeFilter(TypeFilter.SALES); result.current.setTypeFilter(TypeFilter.TRANSFERS); result.current.setTypeFilter(TypeFilter.MINTS); }); - + expect(result.current.typeFilter).toBe(TypeFilter.MINTS); }); }); - describe('ContractFilter State Management', () => { - it('updates selectedContract when setSelectedContract is called', () => { + describe("ContractFilter State Management", () => { + it("updates selectedContract when setSelectedContract is called", () => { const { result } = renderHook(() => useActivityFilters()); - + expect(result.current.selectedContract).toBe(ContractFilter.ALL); - + act(() => { result.current.setSelectedContract(ContractFilter.MEMES); }); - + expect(result.current.selectedContract).toBe(ContractFilter.MEMES); }); - it('handles all ContractFilter enum values correctly', () => { + it("handles all ContractFilter enum values correctly", () => { const { result } = renderHook(() => useActivityFilters()); - + const allContractFilters = [ ContractFilter.ALL, ContractFilter.MEMES, ContractFilter.NEXTGEN, - ContractFilter.GRADIENTS + ContractFilter.GRADIENTS, ]; - - allContractFilters.forEach(filterValue => { + + allContractFilters.forEach((filterValue) => { act(() => { result.current.setSelectedContract(filterValue); }); - + expect(result.current.selectedContract).toBe(filterValue); }); }); - it('maintains typeFilter state when selectedContract changes', () => { + it("maintains typeFilter state when selectedContract changes", () => { const { result } = renderHook(() => useActivityFilters()); - + // First set a type filter act(() => { result.current.setTypeFilter(TypeFilter.SALES); }); - + expect(result.current.typeFilter).toBe(TypeFilter.SALES); - + // Then change contract filter act(() => { result.current.setSelectedContract(ContractFilter.MEMES); }); - + // Type filter should remain unchanged expect(result.current.typeFilter).toBe(TypeFilter.SALES); expect(result.current.selectedContract).toBe(ContractFilter.MEMES); }); - it('allows rapid consecutive selectedContract updates', () => { + it("allows rapid consecutive selectedContract updates", () => { const { result } = renderHook(() => useActivityFilters()); - + act(() => { result.current.setSelectedContract(ContractFilter.MEMES); result.current.setSelectedContract(ContractFilter.NEXTGEN); result.current.setSelectedContract(ContractFilter.GRADIENTS); }); - + expect(result.current.selectedContract).toBe(ContractFilter.GRADIENTS); }); }); - describe('Reset Functionality', () => { - it('resets both filters to default state', () => { + describe("Reset Functionality", () => { + it("resets both filters to default state", () => { const { result } = renderHook(() => useActivityFilters()); - + // Set both filters to non-default values act(() => { result.current.setTypeFilter(TypeFilter.SALES); result.current.setSelectedContract(ContractFilter.MEMES); }); - + expect(result.current.typeFilter).toBe(TypeFilter.SALES); expect(result.current.selectedContract).toBe(ContractFilter.MEMES); - + // Reset filters act(() => { result.current.resetFilters(); }); - + expect(result.current.typeFilter).toBe(TypeFilter.ALL); expect(result.current.selectedContract).toBe(ContractFilter.ALL); }); - it('resets filters even when already at default state', () => { + it("resets filters even when already at default state", () => { const { result } = renderHook(() => useActivityFilters()); - + // Filters are already at default state expect(result.current.typeFilter).toBe(TypeFilter.ALL); expect(result.current.selectedContract).toBe(ContractFilter.ALL); - + // Reset should still work without issues act(() => { result.current.resetFilters(); }); - + expect(result.current.typeFilter).toBe(TypeFilter.ALL); expect(result.current.selectedContract).toBe(ContractFilter.ALL); }); - it('can reset after multiple filter changes', () => { + it("can reset after multiple filter changes", () => { const { result } = renderHook(() => useActivityFilters()); - + // Make multiple changes act(() => { result.current.setTypeFilter(TypeFilter.SALES); @@ -211,190 +211,216 @@ describe('useActivityFilters', () => { result.current.setSelectedContract(ContractFilter.NEXTGEN); result.current.setTypeFilter(TypeFilter.BURNS); }); - + expect(result.current.typeFilter).toBe(TypeFilter.BURNS); expect(result.current.selectedContract).toBe(ContractFilter.NEXTGEN); - + // Reset should bring everything back to defaults act(() => { result.current.resetFilters(); }); - + expect(result.current.typeFilter).toBe(TypeFilter.ALL); expect(result.current.selectedContract).toBe(ContractFilter.ALL); }); }); - describe('Callback Integration', () => { - describe('setTypeFilter with resetPage callback', () => { - it('calls resetPage callback when provided to setTypeFilter', () => { + describe("Callback Integration", () => { + describe("setTypeFilter with resetPage callback", () => { + it("calls resetPage callback when provided to setTypeFilter", () => { const { result } = renderHook(() => useActivityFilters()); const mockResetPage = jest.fn(); - + act(() => { result.current.setTypeFilter(TypeFilter.SALES, mockResetPage); }); - + expect(mockResetPage).toHaveBeenCalledTimes(1); expect(result.current.typeFilter).toBe(TypeFilter.SALES); }); - it('does not crash when no resetPage callback provided to setTypeFilter', () => { + it("does not crash when no resetPage callback provided to setTypeFilter", () => { const { result } = renderHook(() => useActivityFilters()); - + expect(() => { act(() => { result.current.setTypeFilter(TypeFilter.SALES); }); }).not.toThrow(); - + expect(result.current.typeFilter).toBe(TypeFilter.SALES); }); - it('calls resetPage before state setter is called', () => { + it("calls resetPage before state setter is called", () => { const { result } = renderHook(() => useActivityFilters()); const executionOrder: string[] = []; - + const mockResetPage = jest.fn(() => { // Capture the typeFilter state at the moment resetPage is called - executionOrder.push(`resetPage called, typeFilter: ${result.current.typeFilter}`); + executionOrder.push( + `resetPage called, typeFilter: ${result.current.typeFilter}` + ); }); - + // Initial state expect(result.current.typeFilter).toBe(TypeFilter.ALL); - + act(() => { result.current.setTypeFilter(TypeFilter.SALES, mockResetPage); }); - + expect(mockResetPage).toHaveBeenCalledTimes(1); // resetPage is called while state is still at old value expect(executionOrder).toEqual([ - 'resetPage called, typeFilter: All' + "resetPage called, typeFilter: All Transactions", ]); // But after the act() block, state should be updated expect(result.current.typeFilter).toBe(TypeFilter.SALES); }); - it('handles resetPage callback that throws an error', () => { + it("handles resetPage callback that throws an error", () => { const { result } = renderHook(() => useActivityFilters()); - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - + const consoleErrorSpy = jest + .spyOn(console, "error") + .mockImplementation(() => {}); + const mockResetPageWithError = jest.fn(() => { - throw new Error('Reset page failed'); + throw new Error("Reset page failed"); }); - + // The implementation calls resetPage first, then setTypeFilterState // If resetPage throws, the setTypeFilterState never gets called expect(() => { act(() => { - result.current.setTypeFilter(TypeFilter.SALES, mockResetPageWithError); + result.current.setTypeFilter( + TypeFilter.SALES, + mockResetPageWithError + ); }); - }).toThrow('Reset page failed'); - + }).toThrow("Reset page failed"); + expect(mockResetPageWithError).toHaveBeenCalledTimes(1); // State should NOT be updated if callback throws before state setter is called expect(result.current.typeFilter).toBe(TypeFilter.ALL); - + consoleErrorSpy.mockRestore(); }); }); - describe('setSelectedContract with resetPage callback', () => { - it('calls resetPage callback when provided to setSelectedContract', () => { + describe("setSelectedContract with resetPage callback", () => { + it("calls resetPage callback when provided to setSelectedContract", () => { const { result } = renderHook(() => useActivityFilters()); const mockResetPage = jest.fn(); - + act(() => { - result.current.setSelectedContract(ContractFilter.MEMES, mockResetPage); + result.current.setSelectedContract( + ContractFilter.MEMES, + mockResetPage + ); }); - + expect(mockResetPage).toHaveBeenCalledTimes(1); expect(result.current.selectedContract).toBe(ContractFilter.MEMES); }); - it('does not crash when no resetPage callback provided to setSelectedContract', () => { + it("does not crash when no resetPage callback provided to setSelectedContract", () => { const { result } = renderHook(() => useActivityFilters()); - + expect(() => { act(() => { result.current.setSelectedContract(ContractFilter.MEMES); }); }).not.toThrow(); - + expect(result.current.selectedContract).toBe(ContractFilter.MEMES); }); - it('calls resetPage before state setter is called', () => { + it("calls resetPage before state setter is called", () => { const { result } = renderHook(() => useActivityFilters()); const executionOrder: string[] = []; - + const mockResetPage = jest.fn(() => { - executionOrder.push(`resetPage called, selectedContract: ${result.current.selectedContract}`); + executionOrder.push( + `resetPage called, selectedContract: ${result.current.selectedContract}` + ); }); - + expect(result.current.selectedContract).toBe(ContractFilter.ALL); - + act(() => { - result.current.setSelectedContract(ContractFilter.MEMES, mockResetPage); + result.current.setSelectedContract( + ContractFilter.MEMES, + mockResetPage + ); }); - + expect(mockResetPage).toHaveBeenCalledTimes(1); expect(executionOrder).toEqual([ - 'resetPage called, selectedContract: All' + "resetPage called, selectedContract: All Collections", ]); // But after the act() block, state should be updated expect(result.current.selectedContract).toBe(ContractFilter.MEMES); }); - it('handles resetPage callback that throws an error', () => { + it("handles resetPage callback that throws an error", () => { const { result } = renderHook(() => useActivityFilters()); - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - + const consoleErrorSpy = jest + .spyOn(console, "error") + .mockImplementation(() => {}); + const mockResetPageWithError = jest.fn(() => { - throw new Error('Reset page failed'); + throw new Error("Reset page failed"); }); - + expect(() => { act(() => { - result.current.setSelectedContract(ContractFilter.MEMES, mockResetPageWithError); + result.current.setSelectedContract( + ContractFilter.MEMES, + mockResetPageWithError + ); }); - }).toThrow('Reset page failed'); - + }).toThrow("Reset page failed"); + expect(mockResetPageWithError).toHaveBeenCalledTimes(1); // State should NOT be updated if callback throws before state setter is called expect(result.current.selectedContract).toBe(ContractFilter.ALL); - + consoleErrorSpy.mockRestore(); }); }); - describe('Callback with both filters', () => { - it('calls resetPage callback for both filter setters independently', () => { + describe("Callback with both filters", () => { + it("calls resetPage callback for both filter setters independently", () => { const { result } = renderHook(() => useActivityFilters()); const mockResetPage1 = jest.fn(); const mockResetPage2 = jest.fn(); - + act(() => { result.current.setTypeFilter(TypeFilter.SALES, mockResetPage1); - result.current.setSelectedContract(ContractFilter.MEMES, mockResetPage2); + result.current.setSelectedContract( + ContractFilter.MEMES, + mockResetPage2 + ); }); - + expect(mockResetPage1).toHaveBeenCalledTimes(1); expect(mockResetPage2).toHaveBeenCalledTimes(1); expect(result.current.typeFilter).toBe(TypeFilter.SALES); expect(result.current.selectedContract).toBe(ContractFilter.MEMES); }); - it('can use the same callback function for both filters', () => { + it("can use the same callback function for both filters", () => { const { result } = renderHook(() => useActivityFilters()); const mockResetPage = jest.fn(); - + act(() => { result.current.setTypeFilter(TypeFilter.SALES, mockResetPage); - result.current.setSelectedContract(ContractFilter.MEMES, mockResetPage); + result.current.setSelectedContract( + ContractFilter.MEMES, + mockResetPage + ); }); - + expect(mockResetPage).toHaveBeenCalledTimes(2); expect(result.current.typeFilter).toBe(TypeFilter.SALES); expect(result.current.selectedContract).toBe(ContractFilter.MEMES); @@ -402,193 +428,198 @@ describe('useActivityFilters', () => { }); }); - describe('Combined Filter Operations', () => { - it('can set both filters and then reset them', () => { + describe("Combined Filter Operations", () => { + it("can set both filters and then reset them", () => { const { result } = renderHook(() => useActivityFilters()); - + // Set both filters act(() => { result.current.setTypeFilter(TypeFilter.SALES); result.current.setSelectedContract(ContractFilter.MEMES); }); - + expect(result.current.typeFilter).toBe(TypeFilter.SALES); expect(result.current.selectedContract).toBe(ContractFilter.MEMES); - + // Reset act(() => { result.current.resetFilters(); }); - + expect(result.current.typeFilter).toBe(TypeFilter.ALL); expect(result.current.selectedContract).toBe(ContractFilter.ALL); }); - it('handles complex filter sequences correctly', () => { + it("handles complex filter sequences correctly", () => { const { result } = renderHook(() => useActivityFilters()); const mockResetPage = jest.fn(); - + // Complex sequence of operations act(() => { result.current.setTypeFilter(TypeFilter.SALES, mockResetPage); result.current.setSelectedContract(ContractFilter.MEMES, mockResetPage); result.current.setTypeFilter(TypeFilter.TRANSFERS, mockResetPage); result.current.resetFilters(); - result.current.setSelectedContract(ContractFilter.NEXTGEN, mockResetPage); + result.current.setSelectedContract( + ContractFilter.NEXTGEN, + mockResetPage + ); }); - + expect(mockResetPage).toHaveBeenCalledTimes(4); // 3 setter calls + 0 for resetFilters expect(result.current.typeFilter).toBe(TypeFilter.ALL); expect(result.current.selectedContract).toBe(ContractFilter.NEXTGEN); }); }); - describe('Edge Cases and Error Scenarios', () => { - it('maintains referential stability for function references across renders', () => { + describe("Edge Cases and Error Scenarios", () => { + it("maintains referential stability for function references across renders", () => { const { result, rerender } = renderHook(() => useActivityFilters()); - + const firstRenderFunctions = { setTypeFilter: result.current.setTypeFilter, setSelectedContract: result.current.setSelectedContract, - resetFilters: result.current.resetFilters + resetFilters: result.current.resetFilters, }; - + // Trigger re-render by changing some internal state act(() => { result.current.setTypeFilter(TypeFilter.SALES); }); - + rerender(); - + // Function references should remain the same (though with React hooks this might not be guaranteed without useCallback) // This test documents current behavior - if functions are recreated on each render, this test will fail // and that's actually expected behavior for useState-based hooks without memoization - expect(typeof result.current.setTypeFilter).toBe('function'); - expect(typeof result.current.setSelectedContract).toBe('function'); - expect(typeof result.current.resetFilters).toBe('function'); + expect(typeof result.current.setTypeFilter).toBe("function"); + expect(typeof result.current.setSelectedContract).toBe("function"); + expect(typeof result.current.resetFilters).toBe("function"); }); - it('handles undefined resetPage callback gracefully using optional chaining', () => { + it("handles undefined resetPage callback gracefully using optional chaining", () => { const { result } = renderHook(() => useActivityFilters()); - + // Pass undefined explicitly (though TypeScript should prevent this in real usage) act(() => { result.current.setTypeFilter(TypeFilter.SALES, undefined); result.current.setSelectedContract(ContractFilter.MEMES, undefined); }); - + expect(result.current.typeFilter).toBe(TypeFilter.SALES); expect(result.current.selectedContract).toBe(ContractFilter.MEMES); }); - it('works correctly when hook is used multiple times in same component', () => { + it("works correctly when hook is used multiple times in same component", () => { // Simulate multiple instances of the hook const { result: instance1 } = renderHook(() => useActivityFilters()); const { result: instance2 } = renderHook(() => useActivityFilters()); - + // Each instance should be independent act(() => { instance1.current.setTypeFilter(TypeFilter.SALES); instance2.current.setTypeFilter(TypeFilter.TRANSFERS); }); - + expect(instance1.current.typeFilter).toBe(TypeFilter.SALES); expect(instance2.current.typeFilter).toBe(TypeFilter.TRANSFERS); - + // Reset one instance should not affect the other act(() => { instance1.current.resetFilters(); }); - + expect(instance1.current.typeFilter).toBe(TypeFilter.ALL); expect(instance2.current.typeFilter).toBe(TypeFilter.TRANSFERS); }); }); - describe('TypeScript Type Safety', () => { - it('returns correct TypeScript types for all properties', () => { + describe("TypeScript Type Safety", () => { + it("returns correct TypeScript types for all properties", () => { const { result } = renderHook(() => useActivityFilters()); - + // Test that the return values are of correct types expect(Object.values(TypeFilter)).toContain(result.current.typeFilter); - expect(Object.values(ContractFilter)).toContain(result.current.selectedContract); - expect(typeof result.current.setTypeFilter).toBe('function'); - expect(typeof result.current.setSelectedContract).toBe('function'); - expect(typeof result.current.resetFilters).toBe('function'); + expect(Object.values(ContractFilter)).toContain( + result.current.selectedContract + ); + expect(typeof result.current.setTypeFilter).toBe("function"); + expect(typeof result.current.setSelectedContract).toBe("function"); + expect(typeof result.current.resetFilters).toBe("function"); }); - it('ensures enum values are properly typed (not strings)', () => { + it("ensures enum values are properly typed (not strings)", () => { const { result } = renderHook(() => useActivityFilters()); - + // Verify that the returned enum values are actual enum members expect(result.current.typeFilter).toBe(TypeFilter.ALL); expect(result.current.selectedContract).toBe(ContractFilter.ALL); - + // The enum values should be equal to their string representations - expect(TypeFilter.ALL).toBe('All'); - expect(ContractFilter.ALL).toBe('All'); - + expect(TypeFilter.ALL).toBe("All Transactions"); + expect(ContractFilter.ALL).toBe("All Collections"); + // But when we set them, we should use the enum, not strings act(() => { result.current.setTypeFilter(TypeFilter.SALES); result.current.setSelectedContract(ContractFilter.MEMES); }); - + expect(result.current.typeFilter).toBe(TypeFilter.SALES); expect(result.current.selectedContract).toBe(ContractFilter.MEMES); - expect(result.current.typeFilter).toBe('Sales'); - expect(result.current.selectedContract).toBe('Memes'); + expect(result.current.typeFilter).toBe("Sales"); + expect(result.current.selectedContract).toBe("The Memes"); }); }); - describe('Hook Interface Contract', () => { - it('implements the UseActivityFiltersReturn interface correctly', () => { + describe("Hook Interface Contract", () => { + it("implements the UseActivityFiltersReturn interface correctly", () => { const { result } = renderHook(() => useActivityFilters()); - + // Verify all required properties exist const requiredProperties = [ - 'typeFilter', - 'selectedContract', - 'setTypeFilter', - 'setSelectedContract', - 'resetFilters' + "typeFilter", + "selectedContract", + "setTypeFilter", + "setSelectedContract", + "resetFilters", ]; - - requiredProperties.forEach(prop => { + + requiredProperties.forEach((prop) => { expect(result.current).toHaveProperty(prop); }); - + // Verify no unexpected properties exist const actualProperties = Object.keys(result.current); expect(actualProperties.sort()).toEqual(requiredProperties.sort()); }); - it('maintains consistent return object shape across state changes', () => { + it("maintains consistent return object shape across state changes", () => { const { result } = renderHook(() => useActivityFilters()); - + const initialShape = Object.keys(result.current).sort(); - + // Make various state changes act(() => { result.current.setTypeFilter(TypeFilter.SALES); }); - + const afterTypeFilterChange = Object.keys(result.current).sort(); expect(afterTypeFilterChange).toEqual(initialShape); - + act(() => { result.current.setSelectedContract(ContractFilter.MEMES); }); - + const afterContractFilterChange = Object.keys(result.current).sort(); expect(afterContractFilterChange).toEqual(initialShape); - + act(() => { result.current.resetFilters(); }); - + const afterReset = Object.keys(result.current).sort(); expect(afterReset).toEqual(initialShape); }); }); -}); \ No newline at end of file +}); diff --git a/components/home/Home.tsx b/components/home/Home.tsx index 81ad8b7de5..13ab12a930 100644 --- a/components/home/Home.tsx +++ b/components/home/Home.tsx @@ -39,16 +39,17 @@ export default function Home({ {featuredNextgen && !isEmptyObject(featuredNextgen) && ( - -

- Discover NextGen -{" "} - {featuredNextgen.name}{" "} + +

+ Discover NextGen - {featuredNextgen.name}{" "}

- View Collection + + View Collection +
diff --git a/components/latest-activity/ActivityFilters.tsx b/components/latest-activity/ActivityFilters.tsx index f3849cfcf4..db24e2e2e8 100644 --- a/components/latest-activity/ActivityFilters.tsx +++ b/components/latest-activity/ActivityFilters.tsx @@ -1,8 +1,9 @@ "use client"; -import { Col, Dropdown } from "react-bootstrap"; -import styles from "./LatestActivity.module.scss"; -import { TypeFilter, ContractFilter } from "@/hooks/useActivityData"; +import CommonDropdown from "@/components/utils/select/dropdown/CommonDropdown"; +import { ContractFilter, TypeFilter } from "@/hooks/useActivityData"; +import { useMemo } from "react"; +import { Col } from "react-bootstrap"; interface ActivityFiltersProps { readonly typeFilter: TypeFilter; @@ -19,40 +20,45 @@ export default function ActivityFilters({ onContractFilterChange, isMobile, }: ActivityFiltersProps) { + const contractItems = useMemo( + () => + Object.values(ContractFilter).map((contract) => ({ + key: contract, + label: contract, + value: contract, + })), + [] + ); + + const typeItems = useMemo( + () => + Object.values(TypeFilter).map((type) => ({ + key: type, + label: type, + value: type, + })), + [] + ); + return ( - - Collection: {selectedContract} - - {Object.values(ContractFilter).map((contract) => ( - onContractFilterChange(contract)} - > - {contract} - - ))} - - - - Filter: {typeFilter} - - {Object.values(TypeFilter).map((filter) => ( - onTypeFilterChange(filter)} - > - {filter} - - ))} - - + }`}> + + ); } diff --git a/components/latest-activity/ActivityHeader.tsx b/components/latest-activity/ActivityHeader.tsx index 503d088a85..71f50ddbb9 100644 --- a/components/latest-activity/ActivityHeader.tsx +++ b/components/latest-activity/ActivityHeader.tsx @@ -2,7 +2,6 @@ import Link from "next/link"; import { Col } from "react-bootstrap"; -import homeStyles from "@/styles/Home.module.scss"; import DotLoader from "../dotLoader/DotLoader"; interface ActivityHeaderProps { @@ -18,13 +17,14 @@ export default function ActivityHeader({ + className="tw-py-2 d-flex align-items-center justify-content-between"> -

NFT Activity

+

NFT Activity

{showViewAll ? ( - - View All + + + View All + ) : ( fetching && diff --git a/components/latest-activity/LatestActivity.module.scss b/components/latest-activity/LatestActivity.module.scss index 081085766f..19fb2b6e46 100644 --- a/components/latest-activity/LatestActivity.module.scss +++ b/components/latest-activity/LatestActivity.module.scss @@ -65,7 +65,6 @@ } .filterDropdown { - line-height: 48px; text-align: center; background-color: transparent; border: none; diff --git a/components/meme-calendar/MemeCalendarOverview.tsx b/components/meme-calendar/MemeCalendarOverview.tsx index 4258c2d936..4c6d112336 100644 --- a/components/meme-calendar/MemeCalendarOverview.tsx +++ b/components/meme-calendar/MemeCalendarOverview.tsx @@ -25,6 +25,7 @@ import { getMintTimelineDetails, getNextMintStart, getUpcomingMintsForCurrentOrNextSeason, + getUpcomingMintsForSeasonIndex, printCalendarInvites, ymd, } from "./meme-calendar.helpers"; @@ -44,12 +45,10 @@ export default function MemeCalendarOverview({ return (
-

- The Memes Minting Calendar -

+

The Memes Minting Calendar

{showViewAll && ( - - + + View Full Calendar @@ -414,38 +413,61 @@ function MemeCalendarOverviewUpcomingMints({ }: MemeCalendarOverviewUpcomingMintsProps) { const [now] = useState(new Date()); - const { seasonStart, seasonEndInclusive, seasonIndex, rows } = - useMemo( - () => getUpcomingMintsForCurrentOrNextSeason(now), - [now] - ); + const currentSeason = useMemo( + () => getUpcomingMintsForCurrentOrNextSeason(now), + [now] + ); const canonicalNextMintNumber = useMemo( () => getCanonicalNextMintNumber(now), [now] ); - const { filteredRows, hasCanonicalNext } = useMemo(() => { - const containsCanonical = rows.some( + const { + seasonStart, + seasonEndInclusive, + seasonIndex, + filteredRows, + isNextSeason, + } = useMemo(() => { + const containsCanonical = currentSeason.rows.some( (row) => row.meme === canonicalNextMintNumber ); + const filtered = containsCanonical + ? currentSeason.rows.filter((row) => row.meme !== canonicalNextMintNumber) + : currentSeason.rows; + + if (filtered.length === 0 && containsCanonical) { + const nextSeason = getUpcomingMintsForSeasonIndex( + currentSeason.seasonIndex + 1, + now + ); + return { + seasonStart: nextSeason.seasonStart, + seasonEndInclusive: nextSeason.seasonEndInclusive, + seasonIndex: nextSeason.seasonIndex, + filteredRows: nextSeason.rows, + isNextSeason: true, + }; + } + return { - filteredRows: containsCanonical - ? rows.filter((row) => row.meme !== canonicalNextMintNumber) - : rows, - hasCanonicalNext: containsCanonical, - } as const; - }, [rows, canonicalNextMintNumber]); + seasonStart: currentSeason.seasonStart, + seasonEndInclusive: currentSeason.seasonEndInclusive, + seasonIndex: currentSeason.seasonIndex, + filteredRows: filtered, + isNextSeason: false, + }; + }, [currentSeason, canonicalNextMintNumber, now]); - const emptyStateCopy = hasCanonicalNext - ? "No additional mints scheduled in this season." - : "No upcoming mints in this season."; + const emptyStateCopy = "No upcoming mints in this season."; return (
- Upcoming Mints for SZN {displayedSeasonNumberFromIndex(seasonIndex)} + {isNextSeason ? "Upcoming SZN" : "Upcoming Mints for SZN"}{" "} + {displayedSeasonNumberFromIndex(seasonIndex)}
{formatFullDate(seasonStart, displayTz)} -{" "} diff --git a/components/meme-calendar/meme-calendar.helpers.tsx b/components/meme-calendar/meme-calendar.helpers.tsx index f78e74a7e3..1837f5f938 100644 --- a/components/meme-calendar/meme-calendar.helpers.tsx +++ b/components/meme-calendar/meme-calendar.helpers.tsx @@ -847,6 +847,7 @@ export interface SeasonMintRow { utcDay: Date; instantUtc: Date; meme: number; + seasonIndex: number; } export interface SeasonMintScanResult { @@ -886,6 +887,7 @@ function getUpcomingMintsBetween( Math.max(todayUtcDay.getTime(), seasonStart.getTime()) ); + const seasonIndex = getSeasonIndexForDate(seasonStart); const out: SeasonMintRow[] = []; const cursor = new Date(scanStart); while (+cursor <= +seasonEndInclusive) { @@ -895,13 +897,13 @@ function getUpcomingMintsBetween( utcDay: new Date(cursor), instantUtc: mintInstant, meme: getMintNumberForMintDate(cursor), + seasonIndex, }); } cursor.setUTCDate(cursor.getUTCDate() + 1); } const rows = out.filter((x) => x.instantUtc.getTime() > now.getTime()); - const seasonIndex = getSeasonIndexForDate(seasonStart); return { seasonStart, seasonEndInclusive, seasonIndex, rows }; } @@ -934,6 +936,32 @@ export function getUpcomingMintsForCurrentOrNextSeason( }; } +export function getUpcomingMintsForSeasonIndex( + seasonIndex: number, + now: Date = new Date() +): SeasonMintScanResult { + const start = getSeasonStartDate(seasonIndex); + const end = addMonths(start, 2); + return getUpcomingMintsBetween(start, end, now); +} + +export function getUpcomingMintsAcrossSeasons( + minCount: number, + now: Date = new Date() +): SeasonMintRow[] { + const result: SeasonMintRow[] = []; + let idx = getSeasonIndexForDate(now); + + while (result.length < minCount) { + const season = getUpcomingMintsForSeasonIndex(idx, now); + result.push(...season.rows); + idx++; + if (idx > getSeasonIndexForDate(now) + 4) break; + } + + return result.slice(0, minCount); +} + export function getCanonicalNextMintNumber(now: Date = new Date()): number { const upcomingInstant = getNextMintStart(now); const upcomingUtcDay = new Date( diff --git a/components/seasons-dropdown/SeasonsDropdown.module.scss b/components/seasons-dropdown/SeasonsDropdown.module.scss deleted file mode 100644 index 316b1a8ae2..0000000000 --- a/components/seasons-dropdown/SeasonsDropdown.module.scss +++ /dev/null @@ -1,59 +0,0 @@ -@use "../../styles/variables.scss"; - -.seasonDropdown { - text-align: left; - background-color: transparent; - border: none; - padding-right: 25px; - a { - line-height: 30px; - } - button { - font-size: larger !important; - font-weight: bolder !important; - cursor: copy; - padding: 0; - text-align: left; - background-color: transparent !important; - border-color: transparent !important; - position: relative; - &::after { - position: absolute; - margin-top: 0; - top: 50%; - margin-left: 0.5rem; - transform: translateY(-50%); - font-size: 1.2em; /* Make the arrow bigger */ - } - } - div { - width: 60%; - } -} - -@media only screen and (max-width: 800px) { - .seasonDropdown { - width: auto; /* Changed from 100% to auto to prevent full width */ - display: inline-block; /* Keeps it to the size of its content */ - - button { - text-align: left; - width: auto; /* Changed from 100% to auto */ - position: relative; - padding-right: 1.75rem; /* Make space for the arrow */ - - &::after { - position: absolute; - right: 0; /* Position at the right edge of button */ - top: 50%; - transform: translateY(-50%); - font-size: 1.2em; /* Make the arrow bigger */ - } - } - - div { - width: auto; - min-width: 120px; /* Ensure dropdown menu has reasonable width */ - } - } -} diff --git a/components/seasons-dropdown/SeasonsDropdown.tsx b/components/seasons-dropdown/SeasonsDropdown.tsx deleted file mode 100644 index 231c88a39e..0000000000 --- a/components/seasons-dropdown/SeasonsDropdown.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import styles from "./SeasonsDropdown.module.scss"; -import { Dropdown } from "react-bootstrap"; - -interface Props { - seasons: number[]; - selectedSeason: number; - setSelectedSeason(season: number): void; -} - -export default function SeasonsDropdown(props: Readonly) { - return ( - - - SZN: {props.selectedSeason === 0 ? `All` : props.selectedSeason} - - - { - props.setSelectedSeason(0); - }}> - All - - {props.seasons.map((s) => ( - { - props.setSelectedSeason(s); - }}> - SZN{s} - - ))} - - - ); -} diff --git a/components/subscriptions-report/SubscriptionsReport.tsx b/components/subscriptions-report/SubscriptionsReport.tsx index ccd00d812c..1da57aaf4e 100644 --- a/components/subscriptions-report/SubscriptionsReport.tsx +++ b/components/subscriptions-report/SubscriptionsReport.tsx @@ -9,12 +9,9 @@ import { displayedSeasonNumberFromIndex, formatFullDate, getCardsRemainingUntilEndOf, - getSeasonIndexForDate, - getUpcomingMintsForCurrentOrNextSeason, + getUpcomingMintsAcrossSeasons, isMintingToday, - nextMintDateOnOrAfter, SeasonMintRow, - SeasonMintScanResult, } from "@/components/meme-calendar/meme-calendar.helpers"; import Pagination, { Paginated } from "@/components/pagination/Pagination"; import ShowMoreButton from "@/components/show-more-button/ShowMoreButton"; @@ -69,14 +66,10 @@ export default function SubscriptionsReportComponent() { prevUpcomingExpandedRef.current = upcomingExpanded; }, [upcomingExpanded, upcomingCounts.length]); - const nextMintDate = nextMintDateOnOrAfter(); - const idx = getSeasonIndexForDate(nextMintDate); - const szn = displayedSeasonNumberFromIndex(idx); - const [now] = useState(new Date()); - const { rows } = useMemo( - () => getUpcomingMintsForCurrentOrNextSeason(now), - [now] + const rows = useMemo( + () => getUpcomingMintsAcrossSeasons(upcomingCounts.length || 50, now), + [now, upcomingCounts.length] ); async function fetchUpcomingCounts(count: number) { @@ -149,12 +142,10 @@ export default function SubscriptionsReportComponent() { } return ( - + -

- Subscriptions Report -

+

Subscriptions Report

{connectedProfile && (!capacitor.isIos || country === "US") && ( - Upcoming Drops for SZN{szn} + Upcoming Drops {upcomingLoading && } @@ -284,7 +275,7 @@ export default function SubscriptionsReportComponent() { {totalRedeemed > PAGE_SIZE && redeemedPage !== null && (
The Memes #{props.count.token_id} + SZN {displayedSeasonNumberFromIndex(props.date.seasonIndex)} + {" / "} {formatFullDate(props.date.utcDay)}
@@ -355,8 +348,9 @@ function RedeemedSubscriptionDetails( #{props.count.token_id} - {props.count.name} - {dateTime.toIsoDateString()} / {dateTime.toDayName()} / SZN - {props.count.szn} + SZN {props.count.szn} + {" / "} + {formatFullDate(dateTime.toDate())}
diff --git a/components/the-memes/TheMemes.tsx b/components/the-memes/TheMemes.tsx index 7d6bd0af64..0de1cf3b6e 100644 --- a/components/the-memes/TheMemes.tsx +++ b/components/the-memes/TheMemes.tsx @@ -1,5 +1,13 @@ "use client"; +import { AuthContext } from "@/components/auth/Auth"; +import CollectionsDropdown from "@/components/collections-dropdown/CollectionsDropdown"; +import DotLoader from "@/components/dotLoader/DotLoader"; +import { LFGButton } from "@/components/lfg-slideshow/LFGSlideshow"; +import NFTImage from "@/components/nft-image/NFTImage"; +import { VolumeTypeDropdown } from "@/components/the-memes/MemeShared"; +import styles from "@/components/the-memes/TheMemes.module.scss"; +import SeasonsGridDropdown from "@/components/utils/select/dropdown/SeasonsGridDropdown"; import { publicEnv } from "@/config/env"; import { MEMES_CONTRACT } from "@/constants"; import { useSetTitle } from "@/contexts/TitleContext"; @@ -10,7 +18,6 @@ import { SortDirection } from "@/entities/ISort"; import { MemeLabSort, MEMES_EXTENDED_SORT, MemesSort } from "@/enums"; import { numberWithCommas, printMintDate } from "@/helpers/Helpers"; import { fetchUrl } from "@/services/6529api"; -import { commonApiFetch } from "@/services/api/common-api"; import { faChevronCircleDown, faChevronCircleUp, @@ -20,14 +27,6 @@ import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { useContext, useEffect, useState } from "react"; import { Col, Container, Row } from "react-bootstrap"; -import { AuthContext } from "../auth/Auth"; -import CollectionsDropdown from "../collections-dropdown/CollectionsDropdown"; -import DotLoader from "../dotLoader/DotLoader"; -import { LFGButton } from "../lfg-slideshow/LFGSlideshow"; -import NFTImage from "../nft-image/NFTImage"; -import SeasonsDropdown from "../seasons-dropdown/SeasonsDropdown"; -import { VolumeTypeDropdown } from "./MemeShared"; -import styles from "./TheMemes.module.scss"; interface Meme { meme: number; @@ -102,8 +101,8 @@ export default function TheMemesComponent() { const { connectedProfile } = useContext(AuthContext); - const [selectedSeason, setSelectedSeason] = useState(0); - const [seasons, setSeasons] = useState([]); + const [selectedSeason, setSelectedSeason] = useState(null); + const [initialSeasonId, setInitialSeasonId] = useState(null); const [routerLoaded, setRouterLoaded] = useState(false); @@ -113,7 +112,7 @@ export default function TheMemesComponent() { let initialSortDir = SortDirection.ASC; let initialSort = MemesSort.AGE; let initialVolume = VolumeType.ALL_TIME; - let initialSzn = 0; + let initialSznId: number | null = null; const routerSortDir = searchParams?.get("sort_dir"); if (routerSortDir) { @@ -150,16 +149,15 @@ export default function TheMemesComponent() { const routerSzn = searchParams?.get("szn"); if (routerSzn) { - if (Array.isArray(routerSzn)) { - initialSzn = parseInt(routerSzn[0]); - } else { - initialSzn = parseInt(routerSzn); + const parsed = Number.parseInt(routerSzn); + if (!Number.isNaN(parsed) && parsed > 0) { + initialSznId = parsed; } } setSort(initialSort); setSortDir(initialSortDir); - setSelectedSeason(initialSzn); + setInitialSeasonId(initialSznId); setVolumeType(initialVolume); setRouterLoaded(true); }, [searchParams]); @@ -167,8 +165,8 @@ export default function TheMemesComponent() { const getNftsNextPage = () => { const mySort = getApiSort(sort, volumeType); let seasonFilter = ""; - if (selectedSeason > 0) { - seasonFilter = `&season=${selectedSeason}`; + if (selectedSeason) { + seasonFilter = `&season=${selectedSeason.id}`; } return `${publicEnv.API_ENDPOINT}/api/memes_extended_data?page_size=48&sort_direction=${sortDir}&sort=${mySort}${seasonFilter}`; }; @@ -187,14 +185,6 @@ export default function TheMemesComponent() { Map >(new Map()); - useEffect(() => { - commonApiFetch({ - endpoint: "new_memes_seasons", - }).then((response) => { - setSeasons(response); - }); - }, []); - useEffect(() => { let sortParam: string; @@ -216,11 +206,11 @@ export default function TheMemesComponent() { } let queryString = `sort=${sortParam}&sort_dir=${sortDir.toLowerCase()}`; - if (selectedSeason > 0) { - queryString += `&szn=${selectedSeason}`; + if (selectedSeason) { + queryString += `&szn=${selectedSeason.id}`; } router.push(`the-memes?${queryString}`); - }, [sort, sortDir, selectedSeason, volumeType]); + }, [sort, sortDir, selectedSeason, volumeType, router]); useEffect(() => { const memesMap = new Map< @@ -461,16 +451,14 @@ export default function TheMemesComponent() { -

- The Memes -

+

The Memes

-
- s.id)} - selectedSeason={selectedSeason} - setSelectedSeason={setSelectedSeason} +
+
@@ -486,11 +474,11 @@ export default function TheMemesComponent() { -
- s.id)} - selectedSeason={selectedSeason} - setSelectedSeason={setSelectedSeason} +
+
@@ -591,4 +579,4 @@ export function printVolumeTypeDropdown( setVolumeSort={setVolumeSort} /> ); -} \ No newline at end of file +} diff --git a/components/user/collected/UserPageCollected.tsx b/components/user/collected/UserPageCollected.tsx index be52b1e6da..fb29303dba 100644 --- a/components/user/collected/UserPageCollected.tsx +++ b/components/user/collected/UserPageCollected.tsx @@ -11,8 +11,8 @@ import { CollectionSeized, CollectionSort, } from "@/entities/IProfile"; +import { MemeSeason } from "@/entities/ISeason"; import { SortDirection } from "@/entities/ISort"; -import { MEMES_SEASON } from "@/enums"; import { ApiIdentity } from "@/generated/models/ObjectSerializer"; import { areEqualAddresses } from "@/helpers/Helpers"; import { Page } from "@/helpers/Types"; @@ -42,7 +42,8 @@ export interface ProfileCollectedFilters { readonly collection: CollectedCollectionType | null; readonly subcollection: string | null; readonly seized: CollectionSeized | null; - readonly szn: MEMES_SEASON | null; + readonly szn: MemeSeason | null; + readonly initialSznId: number | null; readonly page: number; readonly pageSize: number; readonly sortBy: CollectionSort; @@ -65,22 +66,6 @@ const SEARCH_PARAMS_FIELDS = { sortDirection: "sort-direction", } as const; -const SZN_TO_SEARCH_PARAMS: Record = { - [MEMES_SEASON.SZN1]: "1", - [MEMES_SEASON.SZN2]: "2", - [MEMES_SEASON.SZN3]: "3", - [MEMES_SEASON.SZN4]: "4", - [MEMES_SEASON.SZN5]: "5", - [MEMES_SEASON.SZN6]: "6", - [MEMES_SEASON.SZN7]: "7", - [MEMES_SEASON.SZN8]: "8", - [MEMES_SEASON.SZN9]: "9", - [MEMES_SEASON.SZN10]: "10", - [MEMES_SEASON.SZN11]: "11", - [MEMES_SEASON.SZN12]: "12", - [MEMES_SEASON.SZN13]: "13", -}; - export default function UserPageCollected({ profile, }: { @@ -116,20 +101,18 @@ export default function UserPageCollected({ ); }; - const convertSzn = ({ + const convertSznId = ({ szn, collection, }: { readonly szn: string | null; readonly collection: CollectedCollectionType | null; - }): MEMES_SEASON | null => { + }): number | null => { if (!collection) return null; if (!COLLECTED_COLLECTIONS_META[collection].filters.szn) return null; if (!szn) return null; - const entry = Object.entries(SZN_TO_SEARCH_PARAMS).find( - ([k, v]) => v === szn - ); - return entry ? (entry[0] as MEMES_SEASON) : null; + const parsed = Number.parseInt(szn, 10); + return Number.isNaN(parsed) ? null : parsed; }; const convertCollection = ( @@ -196,8 +179,12 @@ export default function UserPageCollected({ seized: seized ?? null, collection: convertedCollection, }), - szn: convertSzn({ szn: szn ?? null, collection: convertedCollection }), - page: page ? parseInt(page) : 1, + szn: null, + initialSznId: convertSznId({ + szn: szn ?? null, + collection: convertedCollection, + }), + page: page ? Number.parseInt(page, 10) : 1, pageSize: PAGE_SIZE, sortBy: convertSortedBy({ sortBy: sortBy ?? null, @@ -362,11 +349,12 @@ export default function UserPageCollected({ await updateFields(items); }; - const setSzn = async (szn: MEMES_SEASON | null): Promise => { + const setSzn = async (szn: MemeSeason | null): Promise => { + setFilters((prev) => ({ ...prev, szn })); const items: QueryUpdateInput[] = [ { name: "szn", - value: szn ? SZN_TO_SEARCH_PARAMS[szn] : null, + value: szn ? szn.id.toString() : null, }, { name: "page", @@ -434,7 +422,7 @@ export default function UserPageCollected({ } if (filters.szn) { - params.szn = SZN_TO_SEARCH_PARAMS[filters.szn]; + params.szn = filters.szn.id.toString(); } return await commonApiFetch>({ diff --git a/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx b/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx index 39a55c969e..cbeda1fee2 100644 --- a/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx +++ b/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx @@ -1,12 +1,8 @@ "use client"; -import { useEffect, useState } from "react"; -import { - CollectedCollectionType, - CollectionSeized, -} from "@/entities/IProfile"; -import { MEMES_SEASON } from "@/enums"; +import { CollectedCollectionType, CollectionSeized } from "@/entities/IProfile"; import { assertUnreachable } from "@/helpers/AllowlistToolHelpers"; +import { useEffect, useState } from "react"; import { ProfileCollectedFilters } from "../UserPageCollected"; export default function UserPageCollectedCardsNoCards({ @@ -22,27 +18,10 @@ export default function UserPageCollectedCardsNoCards({ case null: return "Congratulations, full setter!"; case CollectedCollectionType.MEMES: - switch (filters.szn) { - case null: - return "Congratulations, The Memes full setter!"; - case MEMES_SEASON.SZN1: - case MEMES_SEASON.SZN2: - case MEMES_SEASON.SZN3: - case MEMES_SEASON.SZN4: - case MEMES_SEASON.SZN5: - case MEMES_SEASON.SZN6: - case MEMES_SEASON.SZN7: - case MEMES_SEASON.SZN8: - case MEMES_SEASON.SZN9: - case MEMES_SEASON.SZN10: - case MEMES_SEASON.SZN11: - case MEMES_SEASON.SZN12: - case MEMES_SEASON.SZN13: - return `Congratulations, ${filters.szn} full setter!`; - default: - assertUnreachable(filters.szn); - return ""; + if (filters.szn === null) { + return "Congratulations, The Memes full setter!"; } + return `Congratulations, ${filters.szn.display} full setter!`; case CollectedCollectionType.GRADIENTS: return "Congratulations, Gradient full setter!"; case CollectedCollectionType.MEMELAB: diff --git a/components/user/collected/filters/UserPageCollectedFilters.tsx b/components/user/collected/filters/UserPageCollectedFilters.tsx index d1c74ef494..48bc30d16d 100644 --- a/components/user/collected/filters/UserPageCollectedFilters.tsx +++ b/components/user/collected/filters/UserPageCollectedFilters.tsx @@ -10,7 +10,7 @@ import { CollectionSeized, CollectionSort, } from "@/entities/IProfile"; -import { MEMES_SEASON } from "@/enums"; +import { MemeSeason } from "@/entities/ISeason"; import { ApiIdentity } from "@/generated/models/ApiIdentity"; import { faChevronLeft, @@ -48,7 +48,7 @@ export default function UserPageCollectedFilters({ readonly setCollection: (collection: CollectedCollectionType | null) => void; readonly setSortBy: (sortBy: CollectionSort) => void; readonly setSeized: (seized: CollectionSeized | null) => void; - readonly setSzn: (szn: MEMES_SEASON | null) => void; + readonly setSzn: (szn: MemeSeason | null) => void; readonly setSubcollection: (subcollection: string | null) => void; readonly showTransfer: boolean; }) { @@ -151,12 +151,10 @@ export default function UserPageCollectedFilters({
+ className="tw-w-full tw-overflow-x-auto [&::-webkit-scrollbar]:tw-hidden [-ms-overflow-style:none] [scrollbar-width:none]">
+ className="tw-flex tw-nowrap tw-justify-between tw-gap-x-3 lg:tw-gap-x-4 tw-items-center tw-w-full tw-min-w-max">
{showTransfer && } @@ -198,7 +196,7 @@ export default function UserPageCollectedFilters({ {getShowSzn(filters.collection) && ( )} @@ -220,8 +218,7 @@ export default function UserPageCollectedFilters({ +
+ + + {seasons.map((season) => ( + + ))} + +
+ ); +} diff --git a/components/utils/select/dropdown/SeasonsGridDropdownDesktopWrapper.tsx b/components/utils/select/dropdown/SeasonsGridDropdownDesktopWrapper.tsx new file mode 100644 index 0000000000..e82a6a01ae --- /dev/null +++ b/components/utils/select/dropdown/SeasonsGridDropdownDesktopWrapper.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { AnimatePresence, motion } from "framer-motion"; +import { + ReactNode, + RefObject, + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; +import { createPortal } from "react-dom"; +import { useClickAway, useKeyPressEvent } from "react-use"; + +export default function SeasonsGridDropdownDesktopWrapper({ + isOpen, + setOpen, + buttonRef, + children, +}: { + readonly isOpen: boolean; + readonly setOpen: (isOpen: boolean) => void; + readonly buttonRef: RefObject; + readonly children: ReactNode; +}) { + const listRef = useRef(null); + useClickAway(listRef, (e) => { + if ( + buttonRef.current && + e.target instanceof Node && + buttonRef.current.contains(e.target) + ) { + return; + } + setOpen(false); + }); + useKeyPressEvent("Escape", () => setOpen(false)); + + const dropdownRef = useRef(null); + + const position = useCallback(() => { + if (!isOpen) return; + if (!buttonRef.current || !dropdownRef.current) return; + + const buttonRect = buttonRef.current.getBoundingClientRect(); + const dropdownEl = dropdownRef.current; + + const width = listRef.current?.offsetWidth ?? dropdownEl.offsetWidth ?? 288; + + const scrollX = window.scrollX; + const scrollY = window.scrollY; + + const top = buttonRect.bottom + scrollY; + + let left = buttonRect.left + scrollX; + + if (buttonRect.left + width > window.innerWidth - 16) { + left = buttonRect.right + scrollX - width; + } + + left = Math.max(16, left); + + dropdownEl.style.top = `${top}px`; + dropdownEl.style.left = `${left}px`; + }, [isOpen, buttonRef]); + + useLayoutEffect(() => { + position(); + }, [position]); + + useEffect(() => { + if (!isOpen) return; + const onResize = () => position(); + window.addEventListener("resize", onResize); + window.addEventListener("scroll", onResize); + return () => { + window.removeEventListener("resize", onResize); + window.removeEventListener("scroll", onResize); + }; + }, [isOpen, position]); + + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + return createPortal( +
+ + {isOpen && ( + +
{children}
+
+ )} +
+
, + document.body + ); +} diff --git a/components/utils/select/dropdown/SeasonsGridDropdownItemsWrapper.tsx b/components/utils/select/dropdown/SeasonsGridDropdownItemsWrapper.tsx new file mode 100644 index 0000000000..158668e730 --- /dev/null +++ b/components/utils/select/dropdown/SeasonsGridDropdownItemsWrapper.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { ReactNode, RefObject, useEffect, useState } from "react"; +import { createBreakpoint } from "react-use"; +import SeasonsGridDropdownDesktopWrapper from "./SeasonsGridDropdownDesktopWrapper"; +import SeasonsGridDropdownMobileWrapper from "./SeasonsGridDropdownMobileWrapper"; + +const useBreakpoint = createBreakpoint({ LG: 1024, S: 0 }); + +export default function SeasonsGridDropdownItemsWrapper({ + isOpen, + filterLabel, + buttonRef, + setOpen, + children, +}: { + readonly isOpen: boolean; + readonly filterLabel: string; + readonly buttonRef: RefObject; + readonly setOpen: (isOpen: boolean) => void; + readonly children: ReactNode; +}) { + const breakpoint = useBreakpoint(); + + const getIsMobile = () => breakpoint !== "LG"; + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + setIsMobile(getIsMobile()); + }, [breakpoint]); + + return ( + <> + {isMobile ? ( + + {children} + + ) : ( + + {children} + + )} + + ); +} diff --git a/components/utils/select/dropdown/SeasonsGridDropdownMobileWrapper.tsx b/components/utils/select/dropdown/SeasonsGridDropdownMobileWrapper.tsx new file mode 100644 index 0000000000..3eb086a141 --- /dev/null +++ b/components/utils/select/dropdown/SeasonsGridDropdownMobileWrapper.tsx @@ -0,0 +1,101 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment, ReactNode } from "react"; + +export default function SeasonsGridDropdownMobileWrapper({ + isOpen, + setOpen, + label, + children, +}: { + readonly isOpen: boolean; + readonly setOpen: (isOpen: boolean) => void; + readonly label?: string; + readonly children: ReactNode; +}) { + return ( + + + +
+ + +
+
+
+ + + +
+ +
+
+
+ {label && ( +
+ + {label} + +
+ )} +
+
+ {children} +
+
+
+
+
+
+
+
+
+
+ ); +} + diff --git a/enums.ts b/enums.ts index fb932f1730..202b08cdf5 100644 --- a/enums.ts +++ b/enums.ts @@ -10,22 +10,6 @@ export enum DateIntervalsSelection { CUSTOM_DATES = "Custom Dates", } -export enum MEMES_SEASON { - SZN1 = "SZN1", - SZN2 = "SZN2", - SZN3 = "SZN3", - SZN4 = "SZN4", - SZN5 = "SZN5", - SZN6 = "SZN6", - SZN7 = "SZN7", - SZN8 = "SZN8", - SZN9 = "SZN9", - SZN10 = "SZN10", - SZN11 = "SZN11", - SZN12 = "SZN12", - SZN13 = "SZN13", -} - export enum MemesSort { AGE = "Age", EDITION_SIZE = "Edition Size", diff --git a/hooks/useActivityData.ts b/hooks/useActivityData.ts index 2e1a83e7e7..6046458572 100644 --- a/hooks/useActivityData.ts +++ b/hooks/useActivityData.ts @@ -1,16 +1,16 @@ -import { publicEnv } from "@/config/env"; -import { useEffect, useState } from "react"; import { NEXTGEN_CHAIN_ID, NEXTGEN_CORE, } from "@/components/nextGen/nextgen_contracts"; +import { publicEnv } from "@/config/env"; import { GRADIENT_CONTRACT, MEMES_CONTRACT } from "@/constants"; import { DBResponse } from "@/entities/IDBResponse"; import { Transaction } from "@/entities/ITransaction"; import { fetchUrl } from "@/services/6529api"; +import { useEffect, useState } from "react"; export enum TypeFilter { - ALL = "All", + ALL = "All Transactions", AIRDROPS = "Airdrops", MINTS = "Mints", SALES = "Sales", @@ -19,8 +19,8 @@ export enum TypeFilter { } enum ContractFilter { - ALL = "All", - MEMES = "Memes", + ALL = "All Collections", + MEMES = "The Memes", NEXTGEN = "NextGen", GRADIENTS = "Gradients", } diff --git a/package-lock.json b/package-lock.json index b57ddd28a6..1a5d54bafd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -334,6 +334,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -969,6 +970,7 @@ "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.1.tgz", "integrity": "sha512-0ap4FzPJItaeg3QiiS1WguPNHY2aD67fQ9wr7DojCRzTFuNXQPvFB6lBkqlrVeQyJ9jCw0KV/LXv25oXjDcsyA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -1052,6 +1054,7 @@ "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-4.3.6.tgz", "integrity": "sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@noble/hashes": "1.4.0", "clsx": "1.2.1", @@ -1903,6 +1906,7 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "license": "MIT", + "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" }, @@ -1952,6 +1956,7 @@ "resolved": "https://registry.npmjs.org/@gemini-wallet/core/-/core-0.3.2.tgz", "integrity": "sha512-Z4aHi3ECFf5oWYWM3F1rW83GJfB9OvhBYPTmb5q+VyK3uvzvS48lwo+jwh2eOoCRWEuT/crpb9Vwp2QaS5JqgQ==", "license": "MIT", + "peer": true, "dependencies": { "@metamask/rpc-errors": "7.0.2", "eventemitter3": "5.0.1" @@ -3846,7 +3851,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -4609,6 +4613,7 @@ "version": "0.33.1", "resolved": "https://registry.npmjs.org/@metamask/sdk/-/sdk-0.33.1.tgz", "integrity": "sha512-1mcOQVGr9rSrVcbKPNVzbZ8eCl1K0FATsYH3WJ/MH4WcZDWGECWrXJPNMZoEAkLxWiMe8jOQBumg2pmcDa9zpQ==", + "peer": true, "dependencies": { "@babel/runtime": "^7.26.0", "@metamask/onboarding": "^1.0.1", @@ -4674,6 +4679,7 @@ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", "license": "MIT", + "peer": true, "dependencies": { "node-fetch": "^2.7.0" } @@ -4845,6 +4851,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.9.tgz", "integrity": "sha512-zDntUTReRbAThIfSp3dQZ9kKqI+LjgLp5YZN5c1bgNRDuoeLySAoZg46Bg1a+uV8TMgIRziHocglKGNzr6l+bQ==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.1.0", "iterare": "1.2.1", @@ -5061,6 +5068,7 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -5233,6 +5241,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -5254,6 +5263,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz", "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -5266,6 +5276,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -5281,6 +5292,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz", "integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.208.0", "import-in-the-middle": "^2.0.0", @@ -5668,6 +5680,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -5684,6 +5697,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", @@ -5701,6 +5715,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -6373,6 +6388,7 @@ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.57.0" }, @@ -6388,6 +6404,7 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -8488,6 +8505,7 @@ "resolved": "https://registry.npmjs.org/@solana/kit/-/kit-3.0.3.tgz", "integrity": "sha512-CEEhCDmkvztd1zbgADsEQhmj9GyWOOGeW1hZD+gtwbBSF5YN1uofS/pex5MIh/VIqKRj+A2UnYWI1V+9+q/lyQ==", "license": "MIT", + "peer": true, "dependencies": { "@solana/accounts": "3.0.3", "@solana/addresses": "3.0.3", @@ -9090,6 +9108,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.11.tgz", "integrity": "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -9100,6 +9119,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.11.tgz", "integrity": "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.11" }, @@ -9312,8 +9332,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -9383,7 +9402,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -9394,7 +9412,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -9740,6 +9757,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -9798,6 +9816,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -9807,6 +9826,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -9932,6 +9952,7 @@ "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.48.0", @@ -9972,6 +9993,7 @@ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -10604,7 +10626,6 @@ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.20.0" }, @@ -10699,6 +10720,7 @@ "integrity": "sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw==", "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", "license": "Apache-2.0", + "peer": true, "dependencies": { "@reown/appkit": "1.7.8", "@walletconnect/jsonrpc-http-connection": "1.0.8", @@ -11806,6 +11828,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -12310,7 +12333,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -12320,29 +12342,25 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -12353,15 +12371,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -12374,7 +12390,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -12384,7 +12399,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -12393,15 +12407,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -12418,7 +12430,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -12432,7 +12443,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -12445,7 +12455,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -12460,7 +12469,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -12476,15 +12484,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/abab": { "version": "2.0.6", @@ -12520,6 +12526,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -12552,7 +12559,6 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -12635,7 +12641,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -12653,7 +12658,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12669,8 +12673,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ansi-escapes": { "version": "4.3.2", @@ -13107,6 +13110,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -13196,6 +13200,7 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -13476,6 +13481,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -13558,6 +13564,7 @@ "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -13816,6 +13823,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -13906,7 +13914,6 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.0" } @@ -15141,8 +15148,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -15281,6 +15287,7 @@ "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.16.tgz", "integrity": "sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw==", "license": "MIT", + "peer": true, "dependencies": { "@ecies/ciphers": "^0.2.4", "@noble/ciphers": "^1.3.0", @@ -15328,7 +15335,8 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/emoji-regex": { "version": "10.6.0", @@ -15429,7 +15437,6 @@ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -15588,8 +15595,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -15765,6 +15771,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -15950,6 +15957,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -16596,7 +16604,8 @@ "version": "6.4.9", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/eventemitter3": { "version": "5.0.1", @@ -16799,8 +16808,7 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/fastest-stable-stringify": { "version": "2.0.2", @@ -17413,8 +17421,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/minimatch": { "version": "10.1.1", @@ -18918,7 +18925,6 @@ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "license": "MIT", - "peer": true, "funding": { "type": "GitHub Sponsors ❤", "url": "https://github.com/sponsors/dmonad" @@ -22833,7 +22839,6 @@ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", "license": "MIT", - "peer": true, "dependencies": { "isomorphic.js": "^0.2.4" }, @@ -22932,7 +22937,6 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.11.5" }, @@ -23035,7 +23039,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -24316,6 +24319,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", "integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.0.7", "@swc/helpers": "0.5.15", @@ -25704,6 +25708,7 @@ "resolved": "https://registry.npmjs.org/porto/-/porto-0.2.35.tgz", "integrity": "sha512-gu9FfjjvvYBgQXUHWTp6n3wkTxVtEcqFotM7i3GEZeoQbvLGbssAicCz6hFZ8+xggrJWwi/RLmbwNra50SMmUQ==", "license": "MIT", + "peer": true, "dependencies": { "hono": "^4.10.3", "idb-keyval": "^6.2.1", @@ -25864,6 +25869,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -25896,6 +25902,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -26056,7 +26063,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -26072,7 +26078,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -26085,8 +26090,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/prismjs": { "version": "1.30.0", @@ -26157,6 +26161,7 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -26501,7 +26506,6 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -26511,6 +26515,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -26561,6 +26566,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -26624,6 +26630,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -26816,6 +26823,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", + "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -26876,7 +26884,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -26891,7 +26900,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -27049,7 +27059,6 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -27210,6 +27219,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -27351,6 +27361,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -27449,6 +27460,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz", "integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==", "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -27516,7 +27528,6 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -27553,7 +27564,6 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -27565,8 +27575,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/screenfull": { "version": "5.2.0", @@ -27618,7 +27627,6 @@ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -27972,6 +27980,7 @@ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", "license": "MIT", + "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", @@ -28747,6 +28756,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -28812,6 +28822,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -28863,7 +28874,6 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" }, @@ -28877,7 +28887,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -28896,7 +28905,6 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -28931,7 +28939,6 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -28946,7 +28953,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -28961,15 +28967,13 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/terser/node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -29075,7 +29079,8 @@ "version": "0.163.0", "resolved": "https://registry.npmjs.org/three/-/three-0.163.0.tgz", "integrity": "sha512-HlMgCb2TF/dTLRtknBnjUTsR8FsDqBY43itYop2+Zg822I+Kd0Ua2vs8CvfBVefXkBdNDrLMoRTGCIIpfCuDew==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/throttle-debounce": { "version": "3.0.1", @@ -29491,7 +29496,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.21.0", @@ -29499,6 +29505,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -30114,6 +30121,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -30644,6 +30652,7 @@ "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -30703,6 +30712,7 @@ "resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.7.tgz", "integrity": "sha512-DwJhCDpujuQuKdJ2H84VbTjEJJteaSmqsuUltsfbfdbotVfNeTE4K/qc/Wi57I9x8/2ed4JNdjEna7O6PfavRg==", "license": "MIT", + "peer": true, "dependencies": { "proxy-compare": "^3.0.1" }, @@ -30761,6 +30771,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", @@ -30903,6 +30914,7 @@ "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.19.5.tgz", "integrity": "sha512-RQUfKMv6U+EcSNNGiPbdkDtJwtuFxZWLmvDiQmjjBgkuPulUwDJsKhi7gjynzJdsx2yDqhHCXkKsbbfbIsHfcQ==", "license": "MIT", + "peer": true, "dependencies": { "@wagmi/connectors": "6.2.0", "@wagmi/core": "2.22.1", @@ -30958,6 +30970,7 @@ "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-2.22.1.tgz", "integrity": "sha512-cG/xwQWsBEcKgRTkQVhH29cbpbs/TdcUJVFXCyri3ZknxhMyGv0YEjTcrNpRgt2SaswL1KrvslSNYKKo+5YEAg==", "license": "MIT", + "peer": true, "dependencies": { "eventemitter3": "5.0.1", "mipd": "0.0.7", @@ -30985,6 +30998,7 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -31052,7 +31066,6 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "license": "MIT", - "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -31097,7 +31110,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -31161,7 +31173,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -31175,7 +31186,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -31398,6 +31408,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -31550,6 +31561,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }