diff --git a/src/client.ts b/src/client.ts index 8fb3e10..b127d9a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4,3 +4,5 @@ export * from "./primitives/MarkdownEditor"; export * from "./components/Form"; export * from "./components/GenericProgressForm"; export * from "./components/MultipleSelect"; +export * from "./features/pool/components/PoolList"; +export * from "./features/program/components/ProgramList"; diff --git a/src/components/GenericProgressForm/GenericProgressForm.stories.tsx b/src/components/GenericProgressForm/GenericProgressForm.stories.tsx index 6592754..2afd9c4 100644 --- a/src/components/GenericProgressForm/GenericProgressForm.stories.tsx +++ b/src/components/GenericProgressForm/GenericProgressForm.stories.tsx @@ -1,8 +1,11 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { GenericProgressForm } from "./GenericProgressForm"; import { roundSetupSteps } from "./mocks/RoundSetup"; +const onSubmit = action("onSubmit"); + const meta: Meta<typeof GenericProgressForm> = { title: "Components/GenericProgressForm", component: GenericProgressForm, @@ -16,9 +19,7 @@ export const Default: Story = { args: { name: "Round setup", steps: roundSetupSteps, - onSubmit: async (values: any) => { - console.log("Submitted final values:", values); - }, + onSubmit: async (values: any) => onSubmit(values), dbName: "formDB", storeName: "formDrafts", }, diff --git a/src/features/pool/components/PoolCard/PoolCard.stories.tsx b/src/features/pool/components/PoolCard/PoolCard.stories.tsx index 62d65d3..563fbe9 100644 --- a/src/features/pool/components/PoolCard/PoolCard.stories.tsx +++ b/src/features/pool/components/PoolCard/PoolCard.stories.tsx @@ -6,7 +6,7 @@ import { PoolStatus, PoolType } from "@/types"; import { PoolCard, PoolCardProps, PoolCardQueryProps } from "./PoolCard"; -const onProgramClick = action("Pool Clicked!"); +const onPoolClick = action("Pool Clicked!"); const simpleRound = { roundName: "Grants Round Defi", @@ -21,7 +21,8 @@ const simpleRound = { operatorsCount: 10, logoImg: "https://cdn.prod.website-files.com/6433c5d029c6bb75f3f00bd5/66f47dd26d8ec8d0e48a22d0_gitcoin-profile.png", - onClick: () => onProgramClick, + onClick: (pool?: { chainId: number; roundId: string }) => onPoolClick(pool), + createdAtBlock: 123456, }; export default { @@ -39,6 +40,8 @@ export default { votingEndDate: { control: "date" }, operatorsCount: { control: "number" }, queryResult: { table: { disable: true } }, // Hide queryResult from controls + createdAtBlock: { control: "number" }, + onClick: { action: "onClick" }, }, } as Meta<typeof PoolCard>; diff --git a/src/features/pool/components/PoolCard/PoolDataCard.tsx b/src/features/pool/components/PoolCard/PoolDataCard.tsx index fabba0e..1c0c5a5 100644 --- a/src/features/pool/components/PoolCard/PoolDataCard.tsx +++ b/src/features/pool/components/PoolCard/PoolDataCard.tsx @@ -15,8 +15,13 @@ export function PoolDataCard({ data }: PoolDataCardProps) { const { name, icon } = getChainInfo(data.chainId); return ( <div - onClick={data.onClick} - className="inline-flex h-60 w-full items-center justify-between rounded-2xl border border-grey-100 p-6" + onClick={() => + data.onClick?.({ + chainId: data.chainId, + roundId: data.roundId, + }) + } + className="inline-flex h-60 w-full cursor-pointer items-center justify-between rounded-2xl border border-grey-100 p-6" > <div className="flex items-center justify-start gap-6"> <img className="relative size-48 rounded-2xl" src={data.logoImg} /> diff --git a/src/features/pool/components/PoolCardGroup/PoolCardGroup.stories.tsx b/src/features/pool/components/PoolCardGroup/PoolCardGroup.stories.tsx index dda5e12..669e0e6 100644 --- a/src/features/pool/components/PoolCardGroup/PoolCardGroup.stories.tsx +++ b/src/features/pool/components/PoolCardGroup/PoolCardGroup.stories.tsx @@ -5,7 +5,7 @@ import { PoolStatus, PoolType } from "@/types"; import { PoolCardGroup } from "./PoolCardGroup"; -const onProgramClick = action("Pool Clicked!"); +const onPoolClick = action("Pool Clicked!"); const pools = [ { @@ -19,9 +19,10 @@ const pools = [ votingStartDate: new Date("2024-12-09T19:22:56.413Z"), votingEndDate: new Date("2024-12-10T19:23:30.678Z"), operatorsCount: 10, + createdAtBlock: 1234567890, logoImg: "https://cdn.prod.website-files.com/6433c5d029c6bb75f3f00bd5/66f47dd26d8ec8d0e48a22d0_gitcoin-profile.png", - onClick: () => onProgramClick, + onClick: (pool?: { chainId: number; roundId: string }) => onPoolClick(pool), }, { roundName: "Uniswap", @@ -35,7 +36,8 @@ const pools = [ votingEndDate: new Date("2024-12-10T19:23:30.678Z"), operatorsCount: 5, logoImg: "https://thegivingblock.com/wp-content/uploads/2021/07/Uniswap-Logo.png", - onClick: () => onProgramClick, + createdAtBlock: 1234567890, + onClick: (pool?: { chainId: number; roundId: string }) => onPoolClick(pool), }, ]; diff --git a/src/features/pool/components/PoolCardGroup/PoolCardGroup.tsx b/src/features/pool/components/PoolCardGroup/PoolCardGroup.tsx index 57451a4..99e56b1 100644 --- a/src/features/pool/components/PoolCardGroup/PoolCardGroup.tsx +++ b/src/features/pool/components/PoolCardGroup/PoolCardGroup.tsx @@ -39,8 +39,8 @@ const justifyVariants = tv({ export const PoolCardGroup = ({ pools, justify }: PoolCardGroupProps) => { return ( <div className={justifyVariants({ justify })}> - {pools.map((stat, index) => ( - <PoolCard key={index} {...stat} /> + {pools.map((pool, index) => ( + <PoolCard key={index} {...pool} /> ))} </div> ); diff --git a/src/features/pool/components/PoolList/PoolList.stories.tsx b/src/features/pool/components/PoolList/PoolList.stories.tsx new file mode 100644 index 0000000..9d86d61 --- /dev/null +++ b/src/features/pool/components/PoolList/PoolList.stories.tsx @@ -0,0 +1,67 @@ +import { action } from "@storybook/addon-actions"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { PoolType } from "@/types"; +import { PoolStatus } from "@/types/pool"; + +import { PoolList } from "./PoolList"; + +const onPoolClick = action("Pool clicked!"); + +const meta: Meta<typeof PoolList> = { + title: "Features/Pool/PoolList", + component: PoolList, +}; + +export default meta; +type Story = StoryObj<typeof PoolList>; + +const mockPools = [ + { + roundName: "Grants Round Defi", + roundId: "90", + chainId: 10, + poolType: PoolType.QuadraticFunding, + poolStatus: PoolStatus.ApplicationsInProgress, + applicationStartDate: new Date("2024-12-08T19:22:56.413Z"), + applicationEndDate: new Date("2024-12-09T19:23:30.678Z"), + votingStartDate: new Date("2024-12-09T19:22:56.413Z"), + votingEndDate: new Date("2024-12-10T19:23:30.678Z"), + operatorsCount: 10, + createdAtBlock: 100000, + logoImg: + "https://cdn.prod.website-files.com/6433c5d029c6bb75f3f00bd5/66f47dd26d8ec8d0e48a22d0_gitcoin-profile.png", + }, + { + roundName: "Uniswap", + roundId: "91", + chainId: 8453, + poolType: PoolType.DirectGrants, + poolStatus: PoolStatus.FundingPending, + applicationStartDate: new Date("2024-12-08T19:22:56.413Z"), + applicationEndDate: new Date("2024-12-09T19:23:30.678Z"), + votingStartDate: new Date("2024-12-09T19:22:56.413Z"), + votingEndDate: new Date("2024-12-10T19:23:30.678Z"), + operatorsCount: 5, + createdAtBlock: 1000, + logoImg: "https://thegivingblock.com/wp-content/uploads/2021/07/Uniswap-Logo.png", + }, +]; + +export const Default: Story = { + args: { + pools: mockPools.map((pool) => ({ + ...pool, + onClick: (pool?: { chainId: number; roundId: string }) => onPoolClick(pool), + })), + title: "Available Pools", + noPoolsPlaceholder: "No pools found", + }, +}; + +export const Empty: Story = { + args: { + ...Default.args, + pools: [], + }, +}; diff --git a/src/features/pool/components/PoolList/PoolList.tsx b/src/features/pool/components/PoolList/PoolList.tsx new file mode 100644 index 0000000..be4a0e4 --- /dev/null +++ b/src/features/pool/components/PoolList/PoolList.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useState } from "react"; + +import { MultipleSelect } from "@/components/MultipleSelect"; + +import { PoolCardProps } from "../PoolCard"; +import { PoolCardGroup } from "../PoolCardGroup"; +import { useFilteredAndOrderedPools } from "./hooks/useFilteredAndOrderedPools"; +import { getSortFilterOptions } from "./utils"; + +export interface PoolListProps { + pools: PoolCardProps[]; + title?: string; + noPoolsPlaceholder?: string; +} + +export const PoolList = ({ + pools, + title = "Pools", + noPoolsPlaceholder = "No pools found", +}: PoolListProps) => { + const [order, setOrder] = useState<Record<string, string[]>>({}); + const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>({}); + + const { orderOptions, filterOptions } = getSortFilterOptions(pools); + + const handleFilterChange = (values: Record<string, string[]>) => { + setSelectedFilters(values); + }; + + const handleOrderChange = (values: Record<string, string[]>) => { + setOrder(values); + }; + + const filteredAndOrderedPools = useFilteredAndOrderedPools({ pools, order, selectedFilters }); + + return ( + <div className="flex w-full flex-col gap-4"> + <div className="flex w-full items-center justify-between"> + <div className="flex-1 text-start font-ui-sans text-2xl font-medium"> + {`${title} (${pools.length})`} + </div> + <div className="flex items-center gap-2"> + <div className="flex items-center gap-2"> + <div className="text-nowrap font-ui-sans text-body font-medium">Order by</div> + <MultipleSelect + options={orderOptions} + onChange={handleOrderChange} + defaultValue={{ "ORDER BY TIME": ["Recent"] }} + className="w-40" + variants={{ triggerTextColor: "green", itemsPosition: "end", headerPosition: "end" }} + /> + </div> + <div className="flex items-center gap-2"> + <div className="text-nowrap font-ui-sans text-body font-medium">Filter by</div> + <MultipleSelect + options={filterOptions} + onChange={handleFilterChange} + defaultValue={{ ungrouped: ["All"] }} // so it starts with 'All' selected + className="w-64" + variants={{ triggerTextColor: "red" }} + /> + </div> + </div> + </div> + <div className="w-full"> + {filteredAndOrderedPools.length > 0 ? ( + <PoolCardGroup pools={filteredAndOrderedPools} /> + ) : ( + <div className="font-ui-sans text-lg">{noPoolsPlaceholder}</div> + )} + </div> + </div> + ); +}; diff --git a/src/features/pool/components/PoolList/hooks/index.ts b/src/features/pool/components/PoolList/hooks/index.ts new file mode 100644 index 0000000..5a7e0c1 --- /dev/null +++ b/src/features/pool/components/PoolList/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useFilteredAndOrderedPools"; diff --git a/src/features/pool/components/PoolList/hooks/useFilteredAndOrderedPools.ts b/src/features/pool/components/PoolList/hooks/useFilteredAndOrderedPools.ts new file mode 100644 index 0000000..e0380a5 --- /dev/null +++ b/src/features/pool/components/PoolList/hooks/useFilteredAndOrderedPools.ts @@ -0,0 +1,70 @@ +import { useMemo } from "react"; + +import { PoolData, PoolStatus } from "@/types/pool"; + +interface UseFilteredAndOrderedPoolsProps { + pools: PoolData[]; + selectedFilters: Record<string, string[]>; + order: Record<string, string[]>; +} + +export const useFilteredAndOrderedPools = ({ + pools, + selectedFilters, + order, +}: UseFilteredAndOrderedPoolsProps) => { + return useMemo(() => { + let result = [...pools]; + + // 1) Check for "All" filter + const ungrouped = selectedFilters["ungrouped"]; + const hasAllFilter = ungrouped?.includes("All"); + + // 2) Filter by network + const selectedNetworks = selectedFilters["Network"] || []; + if (!hasAllFilter && selectedNetworks.length > 0) { + result = result.filter((pool) => selectedNetworks.includes(pool.chainId.toString())); + } + + // 3) Filter by status + const selectedStatuses = selectedFilters["Status"] || []; + if (!hasAllFilter && selectedStatuses.length > 0) { + result = result.filter((pool) => { + return selectedStatuses.some((status) => { + switch (status) { + case "active": + return ( + pool.poolStatus === PoolStatus.RoundInProgress || + pool.poolStatus === PoolStatus.ApplicationsInProgress + ); + case "applications": + return pool.poolStatus === PoolStatus.ApplicationsInProgress; + case "finished": + return pool.poolStatus === PoolStatus.FundingPending; + default: + return false; + } + }); + }); + } + + // 4) Apply ordering + const orderValue = order["ORDER BY TIME"]?.[0] || order["ORDER BY NAME"]?.[0] || "Recent"; + switch (orderValue) { + case "Recent": + result.sort((a, b) => b.createdAtBlock - a.createdAtBlock); + break; + case "Oldest": + result.sort((a, b) => a.createdAtBlock - b.createdAtBlock); + break; + case "A-Z": + result.sort((a, b) => a.roundName.localeCompare(b.roundName)); + break; + case "Z-A": + result.sort((a, b) => b.roundName.localeCompare(a.roundName)); + break; + } + + return result; + }, [pools, order, selectedFilters]); +}; diff --git a/src/features/pool/components/PoolList/index.ts b/src/features/pool/components/PoolList/index.ts new file mode 100644 index 0000000..c8dbaa9 --- /dev/null +++ b/src/features/pool/components/PoolList/index.ts @@ -0,0 +1,3 @@ +export * from "./PoolList"; +export * from "./utils"; +export * from "./hooks"; diff --git a/src/features/pool/components/PoolList/utils.ts b/src/features/pool/components/PoolList/utils.ts new file mode 100644 index 0000000..e344da3 --- /dev/null +++ b/src/features/pool/components/PoolList/utils.ts @@ -0,0 +1,76 @@ +import { MultipleSelectGroup } from "@/components/MultipleSelect/types"; +import { getChainInfo } from "@/lib"; +import { PoolData } from "@/types/pool"; + +export const getSortFilterOptions = (pools: PoolData[]) => { + const orderOptions = [ + { + groupLabel: "ORDER BY TIME", + multiple: false, + items: ["Recent", "Oldest"].map((value) => ({ + label: value, + value, + exclusive: true, + exclusiveScope: "global", + })), + }, + { + groupLabel: "ORDER BY NAME", + multiple: false, + items: ["A-Z", "Z-A"].map((value) => ({ + label: value, + value, + exclusive: true, + exclusiveScope: "global", + })), + }, + ] satisfies MultipleSelectGroup[]; + + // Example "All" ungrouped + networks + statuses + const filterOptions = [ + { + multiple: false, + items: [ + { + label: "All", + value: "All", + exclusive: true, + exclusiveScope: "global", + }, + ], + }, + { + groupLabel: "Network", + multiple: true, + collapsible: true, + items: [...new Set(pools.map((pool) => pool.chainId))].map((chainId) => { + const chainInfo = getChainInfo(chainId); + return { + label: `Rounds on ${chainInfo.name}`, + value: chainId.toString(), + }; + }), + }, + { + groupLabel: "Status", + multiple: true, + collapsible: true, + items: [ + { + label: "Active", + value: "active", + }, + { + label: "Taking Applications", + value: "applications", + }, + { + label: "Finished", + value: "finished", + }, + ], + }, + ] satisfies MultipleSelectGroup[]; + + return { orderOptions, filterOptions }; +}; diff --git a/src/features/pool/index.ts b/src/features/pool/index.ts index 0eedc81..84fdd0d 100644 --- a/src/features/pool/index.ts +++ b/src/features/pool/index.ts @@ -1,6 +1,7 @@ export * from "./components/PoolBadge"; export * from "./components/PoolCard"; export * from "./components/PoolCardGroup"; +export * from "./components/PoolList"; export * from "./components/PoolStatusBadge"; export * from "./components/PoolSummary"; export * from "./components/PoolTypeBadge"; diff --git a/src/features/program/components/ProgramCard/ProgramCard.stories.tsx b/src/features/program/components/ProgramCard/ProgramCard.stories.tsx index d5345c6..64da8cf 100644 --- a/src/features/program/components/ProgramCard/ProgramCard.stories.tsx +++ b/src/features/program/components/ProgramCard/ProgramCard.stories.tsx @@ -13,7 +13,8 @@ const program: ProgramCardProps = { title: "Gitcoin Grants Stack", operatorsCount: 2, roundsCount: 10, - onClick: () => onProgramClick(), + createdAtBlock: 1000000, + onClick: (program?: { chainId: number; programId: string }) => onProgramClick(program), }; export default { diff --git a/src/features/program/components/ProgramCard/ProgramCard.tsx b/src/features/program/components/ProgramCard/ProgramCard.tsx index b2d5613..af19f1d 100644 --- a/src/features/program/components/ProgramCard/ProgramCard.tsx +++ b/src/features/program/components/ProgramCard/ProgramCard.tsx @@ -16,7 +16,8 @@ export interface ProgramCardProps { title: string; operatorsCount: number; roundsCount: number; - onClick?: () => void; + createdAtBlock: number; + onClick?: (program?: { chainId: number; programId: string }) => void; } export interface ProgramCardQueryProps { queryResult: UseQueryResult<ProgramCardProps, Error>; @@ -42,8 +43,8 @@ export function ProgramDataCard({ data }: ProgramDataCardProps) { const { name, icon } = getChainInfo(data.chainId); return ( <Card - className="block w-[304px] overflow-hidden border-grey-300 bg-grey-50" - onClick={data.onClick} + className="block w-[304px] cursor-pointer overflow-hidden border-grey-300 bg-grey-50" + onClick={() => data.onClick?.({ chainId: data.chainId, programId: data.id })} > <CardContent className="flex flex-col gap-3 p-6"> <h2 className="truncate font-ui-sans text-2xl font-bold">{data.title}</h2> diff --git a/src/features/program/components/ProgramCardGroup/ProgramCardGroup.stories.tsx b/src/features/program/components/ProgramCardGroup/ProgramCardGroup.stories.tsx index dd283d7..4c8273a 100644 --- a/src/features/program/components/ProgramCardGroup/ProgramCardGroup.stories.tsx +++ b/src/features/program/components/ProgramCardGroup/ProgramCardGroup.stories.tsx @@ -12,7 +12,8 @@ const programs = [ title: "Gitcoin Grants Stack", operatorsCount: 2, roundsCount: 10, - onClick: () => onProgramClick(), + createdAtBlock: 1000, + onClick: (program?: { chainId: number; programId: string }) => onProgramClick(program), }, { id: "0x3456", @@ -20,7 +21,8 @@ const programs = [ title: "Allo Protocol", operatorsCount: 4, roundsCount: 2, - onClick: () => onProgramClick(), + createdAtBlock: 1000000, + onClick: (program?: { chainId: number; programId: string }) => onProgramClick(program), }, ]; @@ -96,6 +98,7 @@ export const withFourCard: Story = { title: "Pump Fun", operatorsCount: 4, roundsCount: 2, + createdAtBlock: 1000000, onClick: () => onProgramClick(), }, ], @@ -112,6 +115,7 @@ export const withFiveCard: Story = { title: "Pump Fun", operatorsCount: 4, roundsCount: 2, + createdAtBlock: 1000000, onClick: () => onProgramClick(), }, { @@ -120,6 +124,7 @@ export const withFiveCard: Story = { title: "Eigen Protocol", operatorsCount: 4, roundsCount: 2, + createdAtBlock: 1000000, onClick: () => onProgramClick(), }, ], diff --git a/src/features/program/components/ProgramList/ProgramList.stories.tsx b/src/features/program/components/ProgramList/ProgramList.stories.tsx new file mode 100644 index 0000000..cf7701d --- /dev/null +++ b/src/features/program/components/ProgramList/ProgramList.stories.tsx @@ -0,0 +1,51 @@ +import { action } from "@storybook/addon-actions"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { ProgramList } from "./ProgramList"; + +const onProgramClick = action("Program clicked!"); + +const meta: Meta<typeof ProgramList> = { + title: "Features/Program/ProgramList", + component: ProgramList, +}; + +export default meta; +type Story = StoryObj<typeof ProgramList>; + +const mockPrograms = [ + { + id: "0x123456789", + chainId: 1, + title: "Gitcoin Grants Stack", + operatorsCount: 2, + roundsCount: 10, + createdAtBlock: 100000000, + }, + { + id: "0x3456", + chainId: 10, + title: "Allo Protocol", + operatorsCount: 4, + roundsCount: 2, + createdAtBlock: 1000000, + }, +]; + +export const Default: Story = { + args: { + programs: mockPrograms.map((program) => ({ + ...program, + onClick: (program?: { programId: string; chainId: number }) => onProgramClick(program), + })), + title: "Available Programs", + noProgramsPlaceholder: "No programs found", + }, +}; + +export const Empty: Story = { + args: { + ...Default.args, + programs: [], + }, +}; diff --git a/src/features/program/components/ProgramList/ProgramList.tsx b/src/features/program/components/ProgramList/ProgramList.tsx new file mode 100644 index 0000000..d741bf1 --- /dev/null +++ b/src/features/program/components/ProgramList/ProgramList.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useState } from "react"; + +import { MultipleSelect } from "@/components/MultipleSelect"; + +import { ProgramCardProps } from "../ProgramCard"; +import { ProgramCardGroup } from "../ProgramCardGroup"; +import { useFilteredAndOrderedPrograms } from "./hooks/useFilteredAndOrderedPrograms"; +import { getOrderAndFilterOptions } from "./utils"; + +export interface ProgramListProps { + programs: ProgramCardProps[]; + title?: string; + noProgramsPlaceholder: string; +} + +export const ProgramList = ({ + programs, + noProgramsPlaceholder = "No programs found", + title = "Programs", +}: ProgramListProps) => { + const [order, setOrder] = useState<Record<string, string[]>>({ "ORDER BY TIME": ["Recent"] }); + const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>({ + ungrouped: ["All"], + }); + + const { orderOptions, filterOptions } = getOrderAndFilterOptions(programs); + + const handleOrderChange = (values: Record<string, string[]>) => { + setOrder(values); + }; + + const handleFilterChange = (values: Record<string, string[]>) => { + setSelectedFilters(values); + }; + + const filteredAndOrderedPrograms = useFilteredAndOrderedPrograms({ + programs, + selectedFilters, + order, + }); + + return ( + <div className="flex flex-col gap-4"> + <div className="flex items-center justify-between"> + <div className="flex font-ui-sans text-2xl font-medium">{`${title} (${programs.length})`}</div> + <div className="flex gap-2"> + <div className="flex items-center justify-center gap-2"> + <span className="text-nowrap font-ui-sans text-body font-medium">Order by</span> + <MultipleSelect + options={orderOptions} + onChange={handleOrderChange} + defaultValue={{ "ORDER BY TIME": ["Recent"] }} + placeholder="Select order" + variants={{ triggerTextColor: "green", headerPosition: "end", itemsPosition: "end" }} + className="w-40" + /> + </div> + <div className="flex items-center gap-2"> + <div className="text-nowrap font-ui-sans text-body font-medium">Filter by</div> + <MultipleSelect + options={filterOptions} + onChange={handleFilterChange} + defaultValue={{ ungrouped: ["All"] }} + placeholder="Select filters" + variants={{ triggerTextColor: "red" }} + className="w-64" + /> + </div> + </div> + </div> + <div className="w-full"> + {filteredAndOrderedPrograms.length > 0 ? ( + <ProgramCardGroup programs={filteredAndOrderedPrograms} /> + ) : ( + <div className="font-ui-sans text-lg">{noProgramsPlaceholder}</div> + )} + </div> + </div> + ); +}; diff --git a/src/features/program/components/ProgramList/hooks/useFilteredAndOrderedPrograms.ts b/src/features/program/components/ProgramList/hooks/useFilteredAndOrderedPrograms.ts new file mode 100644 index 0000000..e329575 --- /dev/null +++ b/src/features/program/components/ProgramList/hooks/useFilteredAndOrderedPrograms.ts @@ -0,0 +1,48 @@ +import { useMemo } from "react"; + +import { ProgramCardProps } from "@/features/program"; + +interface UseFilteredAndOrderedProgramsProps { + programs: ProgramCardProps[]; + selectedFilters: Record<string, string[]>; + order: Record<string, string[]>; +} + +export const useFilteredAndOrderedPrograms = ({ + programs, + selectedFilters, + order, +}: UseFilteredAndOrderedProgramsProps) => { + return useMemo(() => { + let result = [...programs]; + + // Apply filters + const ungrouped = selectedFilters["ungrouped"]; + const hasAllFilter = ungrouped?.includes("All"); + + // Apply network filters if not "All" + const selectedNetworks = selectedFilters["Network"] || []; + if (!hasAllFilter && selectedNetworks.length > 0) { + result = result.filter((program) => selectedNetworks.includes(program.chainId.toString())); + } + + // Apply ordering + const orderValue = order["ORDER BY TIME"]?.[0] || order["ORDER BY NAME"]?.[0] || "Recent"; + switch (orderValue) { + case "Recent": + result.sort((a, b) => b.createdAtBlock - a.createdAtBlock); + break; + case "Oldest": + result.sort((a, b) => a.createdAtBlock - b.createdAtBlock); + break; + case "A-Z": + result.sort((a, b) => a.title.localeCompare(b.title)); + break; + case "Z-A": + result.sort((a, b) => b.title.localeCompare(a.title)); + break; + } + + return result; + }, [programs, order, selectedFilters]); +}; diff --git a/src/features/program/components/ProgramList/index.ts b/src/features/program/components/ProgramList/index.ts new file mode 100644 index 0000000..57aa0dd --- /dev/null +++ b/src/features/program/components/ProgramList/index.ts @@ -0,0 +1 @@ +export * from "./ProgramList"; diff --git a/src/features/program/components/ProgramList/utils.ts b/src/features/program/components/ProgramList/utils.ts new file mode 100644 index 0000000..51442b2 --- /dev/null +++ b/src/features/program/components/ProgramList/utils.ts @@ -0,0 +1,53 @@ +import { MultipleSelectGroup } from "@/components/MultipleSelect"; +import { ProgramCardProps } from "@/features/program"; +import { getChainInfo } from "@/lib"; + +export const getOrderAndFilterOptions = (programs: ProgramCardProps[]) => { + const orderOptions = [ + { + groupLabel: "ORDER BY TIME", + multiple: false, + items: ["Recent", "Oldest"].map((value) => ({ + label: value, + value, + exclusive: true, + exclusiveScope: "global", + })), + }, + { + groupLabel: "ORDER BY NAME", + multiple: false, + items: ["A-Z", "Z-A"].map((value) => ({ + label: value, + value, + exclusive: true, + exclusiveScope: "global", + })), + }, + ] satisfies MultipleSelectGroup[]; + + const filterOptions = [ + { + multiple: false, + items: [ + { + label: "All", + value: "All", + exclusive: true, + exclusiveScope: "global", + }, + ], + }, + { + groupLabel: "Network", + multiple: true, + collapsible: true, + items: [...new Set(programs.map((program) => program.chainId))].map((chainId) => ({ + label: `Rounds on ${getChainInfo(chainId).name}`, + value: chainId.toString(), + })), + }, + ] satisfies MultipleSelectGroup[]; + + return { orderOptions, filterOptions }; +}; diff --git a/src/features/program/index.ts b/src/features/program/index.ts index 4720f4f..f5c9a4d 100644 --- a/src/features/program/index.ts +++ b/src/features/program/index.ts @@ -1,2 +1,3 @@ export * from "./components/ProgramCard"; export * from "./components/ProgramCardGroup"; +export * from "./components/ProgramList"; diff --git a/src/types/pool.ts b/src/types/pool.ts index e8d3a7e..593a048 100644 --- a/src/types/pool.ts +++ b/src/types/pool.ts @@ -30,8 +30,9 @@ export interface PoolData { votingEndDate: Date; poolStatus: PoolStatus; operatorsCount: number; + createdAtBlock: number; logoImg?: string; - onClick?: () => void; + onClick?: (pool?: { chainId: number; roundId: string }) => void; } // Type guard for PoolData