Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit 9b95216

Browse files
authoredJan 21, 2025··
feat: add poollist and programlist (#118)
* feat: addition of pool-program lists * chore: added storybook action instead of console.log in genericprogressform story
1 parent 312a3d9 commit 9b95216

23 files changed

+570
-19
lines changed
 

‎src/client.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ export * from "./primitives/MarkdownEditor";
44
export * from "./components/Form";
55
export * from "./components/GenericProgressForm";
66
export * from "./components/MultipleSelect";
7+
export * from "./features/pool/components/PoolList";
8+
export * from "./features/program/components/ProgramList";

‎src/components/GenericProgressForm/GenericProgressForm.stories.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { action } from "@storybook/addon-actions";
12
import type { Meta, StoryObj } from "@storybook/react";
23

34
import { GenericProgressForm } from "./GenericProgressForm";
45
import { roundSetupSteps } from "./mocks/RoundSetup";
56

7+
const onSubmit = action("onSubmit");
8+
69
const meta: Meta<typeof GenericProgressForm> = {
710
title: "Components/GenericProgressForm",
811
component: GenericProgressForm,
@@ -16,9 +19,7 @@ export const Default: Story = {
1619
args: {
1720
name: "Round setup",
1821
steps: roundSetupSteps,
19-
onSubmit: async (values: any) => {
20-
console.log("Submitted final values:", values);
21-
},
22+
onSubmit: async (values: any) => onSubmit(values),
2223
dbName: "formDB",
2324
storeName: "formDrafts",
2425
},

‎src/features/pool/components/PoolCard/PoolCard.stories.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { PoolStatus, PoolType } from "@/types";
66

77
import { PoolCard, PoolCardProps, PoolCardQueryProps } from "./PoolCard";
88

9-
const onProgramClick = action("Pool Clicked!");
9+
const onPoolClick = action("Pool Clicked!");
1010

1111
const simpleRound = {
1212
roundName: "Grants Round Defi",
@@ -21,7 +21,8 @@ const simpleRound = {
2121
operatorsCount: 10,
2222
logoImg:
2323
"https://cdn.prod.website-files.com/6433c5d029c6bb75f3f00bd5/66f47dd26d8ec8d0e48a22d0_gitcoin-profile.png",
24-
onClick: () => onProgramClick,
24+
onClick: (pool?: { chainId: number; roundId: string }) => onPoolClick(pool),
25+
createdAtBlock: 123456,
2526
};
2627

2728
export default {
@@ -39,6 +40,8 @@ export default {
3940
votingEndDate: { control: "date" },
4041
operatorsCount: { control: "number" },
4142
queryResult: { table: { disable: true } }, // Hide queryResult from controls
43+
createdAtBlock: { control: "number" },
44+
onClick: { action: "onClick" },
4245
},
4346
} as Meta<typeof PoolCard>;
4447

‎src/features/pool/components/PoolCard/PoolDataCard.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@ export function PoolDataCard({ data }: PoolDataCardProps) {
1515
const { name, icon } = getChainInfo(data.chainId);
1616
return (
1717
<div
18-
onClick={data.onClick}
19-
className="inline-flex h-60 w-full items-center justify-between rounded-2xl border border-grey-100 p-6"
18+
onClick={() =>
19+
data.onClick?.({
20+
chainId: data.chainId,
21+
roundId: data.roundId,
22+
})
23+
}
24+
className="inline-flex h-60 w-full cursor-pointer items-center justify-between rounded-2xl border border-grey-100 p-6"
2025
>
2126
<div className="flex items-center justify-start gap-6">
2227
<img className="relative size-48 rounded-2xl" src={data.logoImg} />

‎src/features/pool/components/PoolCardGroup/PoolCardGroup.stories.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { PoolStatus, PoolType } from "@/types";
55

66
import { PoolCardGroup } from "./PoolCardGroup";
77

8-
const onProgramClick = action("Pool Clicked!");
8+
const onPoolClick = action("Pool Clicked!");
99

1010
const pools = [
1111
{
@@ -19,9 +19,10 @@ const pools = [
1919
votingStartDate: new Date("2024-12-09T19:22:56.413Z"),
2020
votingEndDate: new Date("2024-12-10T19:23:30.678Z"),
2121
operatorsCount: 10,
22+
createdAtBlock: 1234567890,
2223
logoImg:
2324
"https://cdn.prod.website-files.com/6433c5d029c6bb75f3f00bd5/66f47dd26d8ec8d0e48a22d0_gitcoin-profile.png",
24-
onClick: () => onProgramClick,
25+
onClick: (pool?: { chainId: number; roundId: string }) => onPoolClick(pool),
2526
},
2627
{
2728
roundName: "Uniswap",
@@ -35,7 +36,8 @@ const pools = [
3536
votingEndDate: new Date("2024-12-10T19:23:30.678Z"),
3637
operatorsCount: 5,
3738
logoImg: "https://thegivingblock.com/wp-content/uploads/2021/07/Uniswap-Logo.png",
38-
onClick: () => onProgramClick,
39+
createdAtBlock: 1234567890,
40+
onClick: (pool?: { chainId: number; roundId: string }) => onPoolClick(pool),
3941
},
4042
];
4143

‎src/features/pool/components/PoolCardGroup/PoolCardGroup.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ const justifyVariants = tv({
3939
export const PoolCardGroup = ({ pools, justify }: PoolCardGroupProps) => {
4040
return (
4141
<div className={justifyVariants({ justify })}>
42-
{pools.map((stat, index) => (
43-
<PoolCard key={index} {...stat} />
42+
{pools.map((pool, index) => (
43+
<PoolCard key={index} {...pool} />
4444
))}
4545
</div>
4646
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { action } from "@storybook/addon-actions";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
4+
import { PoolType } from "@/types";
5+
import { PoolStatus } from "@/types/pool";
6+
7+
import { PoolList } from "./PoolList";
8+
9+
const onPoolClick = action("Pool clicked!");
10+
11+
const meta: Meta<typeof PoolList> = {
12+
title: "Features/Pool/PoolList",
13+
component: PoolList,
14+
};
15+
16+
export default meta;
17+
type Story = StoryObj<typeof PoolList>;
18+
19+
const mockPools = [
20+
{
21+
roundName: "Grants Round Defi",
22+
roundId: "90",
23+
chainId: 10,
24+
poolType: PoolType.QuadraticFunding,
25+
poolStatus: PoolStatus.ApplicationsInProgress,
26+
applicationStartDate: new Date("2024-12-08T19:22:56.413Z"),
27+
applicationEndDate: new Date("2024-12-09T19:23:30.678Z"),
28+
votingStartDate: new Date("2024-12-09T19:22:56.413Z"),
29+
votingEndDate: new Date("2024-12-10T19:23:30.678Z"),
30+
operatorsCount: 10,
31+
createdAtBlock: 100000,
32+
logoImg:
33+
"https://cdn.prod.website-files.com/6433c5d029c6bb75f3f00bd5/66f47dd26d8ec8d0e48a22d0_gitcoin-profile.png",
34+
},
35+
{
36+
roundName: "Uniswap",
37+
roundId: "91",
38+
chainId: 8453,
39+
poolType: PoolType.DirectGrants,
40+
poolStatus: PoolStatus.FundingPending,
41+
applicationStartDate: new Date("2024-12-08T19:22:56.413Z"),
42+
applicationEndDate: new Date("2024-12-09T19:23:30.678Z"),
43+
votingStartDate: new Date("2024-12-09T19:22:56.413Z"),
44+
votingEndDate: new Date("2024-12-10T19:23:30.678Z"),
45+
operatorsCount: 5,
46+
createdAtBlock: 1000,
47+
logoImg: "https://thegivingblock.com/wp-content/uploads/2021/07/Uniswap-Logo.png",
48+
},
49+
];
50+
51+
export const Default: Story = {
52+
args: {
53+
pools: mockPools.map((pool) => ({
54+
...pool,
55+
onClick: (pool?: { chainId: number; roundId: string }) => onPoolClick(pool),
56+
})),
57+
title: "Available Pools",
58+
noPoolsPlaceholder: "No pools found",
59+
},
60+
};
61+
62+
export const Empty: Story = {
63+
args: {
64+
...Default.args,
65+
pools: [],
66+
},
67+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
5+
import { MultipleSelect } from "@/components/MultipleSelect";
6+
7+
import { PoolCardProps } from "../PoolCard";
8+
import { PoolCardGroup } from "../PoolCardGroup";
9+
import { useFilteredAndOrderedPools } from "./hooks/useFilteredAndOrderedPools";
10+
import { getSortFilterOptions } from "./utils";
11+
12+
export interface PoolListProps {
13+
pools: PoolCardProps[];
14+
title?: string;
15+
noPoolsPlaceholder?: string;
16+
}
17+
18+
export const PoolList = ({
19+
pools,
20+
title = "Pools",
21+
noPoolsPlaceholder = "No pools found",
22+
}: PoolListProps) => {
23+
const [order, setOrder] = useState<Record<string, string[]>>({});
24+
const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>({});
25+
26+
const { orderOptions, filterOptions } = getSortFilterOptions(pools);
27+
28+
const handleFilterChange = (values: Record<string, string[]>) => {
29+
setSelectedFilters(values);
30+
};
31+
32+
const handleOrderChange = (values: Record<string, string[]>) => {
33+
setOrder(values);
34+
};
35+
36+
const filteredAndOrderedPools = useFilteredAndOrderedPools({ pools, order, selectedFilters });
37+
38+
return (
39+
<div className="flex w-full flex-col gap-4">
40+
<div className="flex w-full items-center justify-between">
41+
<div className="flex-1 text-start font-ui-sans text-2xl font-medium">
42+
{`${title} (${pools.length})`}
43+
</div>
44+
<div className="flex items-center gap-2">
45+
<div className="flex items-center gap-2">
46+
<div className="text-nowrap font-ui-sans text-body font-medium">Order by</div>
47+
<MultipleSelect
48+
options={orderOptions}
49+
onChange={handleOrderChange}
50+
defaultValue={{ "ORDER BY TIME": ["Recent"] }}
51+
className="w-40"
52+
variants={{ triggerTextColor: "green", itemsPosition: "end", headerPosition: "end" }}
53+
/>
54+
</div>
55+
<div className="flex items-center gap-2">
56+
<div className="text-nowrap font-ui-sans text-body font-medium">Filter by</div>
57+
<MultipleSelect
58+
options={filterOptions}
59+
onChange={handleFilterChange}
60+
defaultValue={{ ungrouped: ["All"] }} // so it starts with 'All' selected
61+
className="w-64"
62+
variants={{ triggerTextColor: "red" }}
63+
/>
64+
</div>
65+
</div>
66+
</div>
67+
<div className="w-full">
68+
{filteredAndOrderedPools.length > 0 ? (
69+
<PoolCardGroup pools={filteredAndOrderedPools} />
70+
) : (
71+
<div className="font-ui-sans text-lg">{noPoolsPlaceholder}</div>
72+
)}
73+
</div>
74+
</div>
75+
);
76+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./useFilteredAndOrderedPools";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useMemo } from "react";
2+
3+
import { PoolData, PoolStatus } from "@/types/pool";
4+
5+
interface UseFilteredAndOrderedPoolsProps {
6+
pools: PoolData[];
7+
selectedFilters: Record<string, string[]>;
8+
order: Record<string, string[]>;
9+
}
10+
11+
export const useFilteredAndOrderedPools = ({
12+
pools,
13+
selectedFilters,
14+
order,
15+
}: UseFilteredAndOrderedPoolsProps) => {
16+
return useMemo(() => {
17+
let result = [...pools];
18+
19+
// 1) Check for "All" filter
20+
const ungrouped = selectedFilters["ungrouped"];
21+
const hasAllFilter = ungrouped?.includes("All");
22+
23+
// 2) Filter by network
24+
const selectedNetworks = selectedFilters["Network"] || [];
25+
if (!hasAllFilter && selectedNetworks.length > 0) {
26+
result = result.filter((pool) => selectedNetworks.includes(pool.chainId.toString()));
27+
}
28+
29+
// 3) Filter by status
30+
const selectedStatuses = selectedFilters["Status"] || [];
31+
if (!hasAllFilter && selectedStatuses.length > 0) {
32+
result = result.filter((pool) => {
33+
return selectedStatuses.some((status) => {
34+
switch (status) {
35+
case "active":
36+
return (
37+
pool.poolStatus === PoolStatus.RoundInProgress ||
38+
pool.poolStatus === PoolStatus.ApplicationsInProgress
39+
);
40+
case "applications":
41+
return pool.poolStatus === PoolStatus.ApplicationsInProgress;
42+
case "finished":
43+
return pool.poolStatus === PoolStatus.FundingPending;
44+
default:
45+
return false;
46+
}
47+
});
48+
});
49+
}
50+
51+
// 4) Apply ordering
52+
const orderValue = order["ORDER BY TIME"]?.[0] || order["ORDER BY NAME"]?.[0] || "Recent";
53+
switch (orderValue) {
54+
case "Recent":
55+
result.sort((a, b) => b.createdAtBlock - a.createdAtBlock);
56+
break;
57+
case "Oldest":
58+
result.sort((a, b) => a.createdAtBlock - b.createdAtBlock);
59+
break;
60+
case "A-Z":
61+
result.sort((a, b) => a.roundName.localeCompare(b.roundName));
62+
break;
63+
case "Z-A":
64+
result.sort((a, b) => b.roundName.localeCompare(a.roundName));
65+
break;
66+
}
67+
68+
return result;
69+
}, [pools, order, selectedFilters]);
70+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./PoolList";
2+
export * from "./utils";
3+
export * from "./hooks";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { MultipleSelectGroup } from "@/components/MultipleSelect/types";
2+
import { getChainInfo } from "@/lib";
3+
import { PoolData } from "@/types/pool";
4+
5+
export const getSortFilterOptions = (pools: PoolData[]) => {
6+
const orderOptions = [
7+
{
8+
groupLabel: "ORDER BY TIME",
9+
multiple: false,
10+
items: ["Recent", "Oldest"].map((value) => ({
11+
label: value,
12+
value,
13+
exclusive: true,
14+
exclusiveScope: "global",
15+
})),
16+
},
17+
{
18+
groupLabel: "ORDER BY NAME",
19+
multiple: false,
20+
items: ["A-Z", "Z-A"].map((value) => ({
21+
label: value,
22+
value,
23+
exclusive: true,
24+
exclusiveScope: "global",
25+
})),
26+
},
27+
] satisfies MultipleSelectGroup[];
28+
29+
// Example "All" ungrouped + networks + statuses
30+
const filterOptions = [
31+
{
32+
multiple: false,
33+
items: [
34+
{
35+
label: "All",
36+
value: "All",
37+
exclusive: true,
38+
exclusiveScope: "global",
39+
},
40+
],
41+
},
42+
{
43+
groupLabel: "Network",
44+
multiple: true,
45+
collapsible: true,
46+
items: [...new Set(pools.map((pool) => pool.chainId))].map((chainId) => {
47+
const chainInfo = getChainInfo(chainId);
48+
return {
49+
label: `Rounds on ${chainInfo.name}`,
50+
value: chainId.toString(),
51+
};
52+
}),
53+
},
54+
{
55+
groupLabel: "Status",
56+
multiple: true,
57+
collapsible: true,
58+
items: [
59+
{
60+
label: "Active",
61+
value: "active",
62+
},
63+
{
64+
label: "Taking Applications",
65+
value: "applications",
66+
},
67+
{
68+
label: "Finished",
69+
value: "finished",
70+
},
71+
],
72+
},
73+
] satisfies MultipleSelectGroup[];
74+
75+
return { orderOptions, filterOptions };
76+
};

‎src/features/pool/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./components/PoolBadge";
22
export * from "./components/PoolCard";
33
export * from "./components/PoolCardGroup";
4+
export * from "./components/PoolList";
45
export * from "./components/PoolStatusBadge";
56
export * from "./components/PoolSummary";
67
export * from "./components/PoolTypeBadge";

‎src/features/program/components/ProgramCard/ProgramCard.stories.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ const program: ProgramCardProps = {
1313
title: "Gitcoin Grants Stack",
1414
operatorsCount: 2,
1515
roundsCount: 10,
16-
onClick: () => onProgramClick(),
16+
createdAtBlock: 1000000,
17+
onClick: (program?: { chainId: number; programId: string }) => onProgramClick(program),
1718
};
1819

1920
export default {

‎src/features/program/components/ProgramCard/ProgramCard.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export interface ProgramCardProps {
1616
title: string;
1717
operatorsCount: number;
1818
roundsCount: number;
19-
onClick?: () => void;
19+
createdAtBlock: number;
20+
onClick?: (program?: { chainId: number; programId: string }) => void;
2021
}
2122
export interface ProgramCardQueryProps {
2223
queryResult: UseQueryResult<ProgramCardProps, Error>;
@@ -42,8 +43,8 @@ export function ProgramDataCard({ data }: ProgramDataCardProps) {
4243
const { name, icon } = getChainInfo(data.chainId);
4344
return (
4445
<Card
45-
className="block w-[304px] overflow-hidden border-grey-300 bg-grey-50"
46-
onClick={data.onClick}
46+
className="block w-[304px] cursor-pointer overflow-hidden border-grey-300 bg-grey-50"
47+
onClick={() => data.onClick?.({ chainId: data.chainId, programId: data.id })}
4748
>
4849
<CardContent className="flex flex-col gap-3 p-6">
4950
<h2 className="truncate font-ui-sans text-2xl font-bold">{data.title}</h2>

‎src/features/program/components/ProgramCardGroup/ProgramCardGroup.stories.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ const programs = [
1212
title: "Gitcoin Grants Stack",
1313
operatorsCount: 2,
1414
roundsCount: 10,
15-
onClick: () => onProgramClick(),
15+
createdAtBlock: 1000,
16+
onClick: (program?: { chainId: number; programId: string }) => onProgramClick(program),
1617
},
1718
{
1819
id: "0x3456",
1920
chainId: 1,
2021
title: "Allo Protocol",
2122
operatorsCount: 4,
2223
roundsCount: 2,
23-
onClick: () => onProgramClick(),
24+
createdAtBlock: 1000000,
25+
onClick: (program?: { chainId: number; programId: string }) => onProgramClick(program),
2426
},
2527
];
2628

@@ -96,6 +98,7 @@ export const withFourCard: Story = {
9698
title: "Pump Fun",
9799
operatorsCount: 4,
98100
roundsCount: 2,
101+
createdAtBlock: 1000000,
99102
onClick: () => onProgramClick(),
100103
},
101104
],
@@ -112,6 +115,7 @@ export const withFiveCard: Story = {
112115
title: "Pump Fun",
113116
operatorsCount: 4,
114117
roundsCount: 2,
118+
createdAtBlock: 1000000,
115119
onClick: () => onProgramClick(),
116120
},
117121
{
@@ -120,6 +124,7 @@ export const withFiveCard: Story = {
120124
title: "Eigen Protocol",
121125
operatorsCount: 4,
122126
roundsCount: 2,
127+
createdAtBlock: 1000000,
123128
onClick: () => onProgramClick(),
124129
},
125130
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { action } from "@storybook/addon-actions";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
4+
import { ProgramList } from "./ProgramList";
5+
6+
const onProgramClick = action("Program clicked!");
7+
8+
const meta: Meta<typeof ProgramList> = {
9+
title: "Features/Program/ProgramList",
10+
component: ProgramList,
11+
};
12+
13+
export default meta;
14+
type Story = StoryObj<typeof ProgramList>;
15+
16+
const mockPrograms = [
17+
{
18+
id: "0x123456789",
19+
chainId: 1,
20+
title: "Gitcoin Grants Stack",
21+
operatorsCount: 2,
22+
roundsCount: 10,
23+
createdAtBlock: 100000000,
24+
},
25+
{
26+
id: "0x3456",
27+
chainId: 10,
28+
title: "Allo Protocol",
29+
operatorsCount: 4,
30+
roundsCount: 2,
31+
createdAtBlock: 1000000,
32+
},
33+
];
34+
35+
export const Default: Story = {
36+
args: {
37+
programs: mockPrograms.map((program) => ({
38+
...program,
39+
onClick: (program?: { programId: string; chainId: number }) => onProgramClick(program),
40+
})),
41+
title: "Available Programs",
42+
noProgramsPlaceholder: "No programs found",
43+
},
44+
};
45+
46+
export const Empty: Story = {
47+
args: {
48+
...Default.args,
49+
programs: [],
50+
},
51+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
5+
import { MultipleSelect } from "@/components/MultipleSelect";
6+
7+
import { ProgramCardProps } from "../ProgramCard";
8+
import { ProgramCardGroup } from "../ProgramCardGroup";
9+
import { useFilteredAndOrderedPrograms } from "./hooks/useFilteredAndOrderedPrograms";
10+
import { getOrderAndFilterOptions } from "./utils";
11+
12+
export interface ProgramListProps {
13+
programs: ProgramCardProps[];
14+
title?: string;
15+
noProgramsPlaceholder: string;
16+
}
17+
18+
export const ProgramList = ({
19+
programs,
20+
noProgramsPlaceholder = "No programs found",
21+
title = "Programs",
22+
}: ProgramListProps) => {
23+
const [order, setOrder] = useState<Record<string, string[]>>({ "ORDER BY TIME": ["Recent"] });
24+
const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>({
25+
ungrouped: ["All"],
26+
});
27+
28+
const { orderOptions, filterOptions } = getOrderAndFilterOptions(programs);
29+
30+
const handleOrderChange = (values: Record<string, string[]>) => {
31+
setOrder(values);
32+
};
33+
34+
const handleFilterChange = (values: Record<string, string[]>) => {
35+
setSelectedFilters(values);
36+
};
37+
38+
const filteredAndOrderedPrograms = useFilteredAndOrderedPrograms({
39+
programs,
40+
selectedFilters,
41+
order,
42+
});
43+
44+
return (
45+
<div className="flex flex-col gap-4">
46+
<div className="flex items-center justify-between">
47+
<div className="flex font-ui-sans text-2xl font-medium">{`${title} (${programs.length})`}</div>
48+
<div className="flex gap-2">
49+
<div className="flex items-center justify-center gap-2">
50+
<span className="text-nowrap font-ui-sans text-body font-medium">Order by</span>
51+
<MultipleSelect
52+
options={orderOptions}
53+
onChange={handleOrderChange}
54+
defaultValue={{ "ORDER BY TIME": ["Recent"] }}
55+
placeholder="Select order"
56+
variants={{ triggerTextColor: "green", headerPosition: "end", itemsPosition: "end" }}
57+
className="w-40"
58+
/>
59+
</div>
60+
<div className="flex items-center gap-2">
61+
<div className="text-nowrap font-ui-sans text-body font-medium">Filter by</div>
62+
<MultipleSelect
63+
options={filterOptions}
64+
onChange={handleFilterChange}
65+
defaultValue={{ ungrouped: ["All"] }}
66+
placeholder="Select filters"
67+
variants={{ triggerTextColor: "red" }}
68+
className="w-64"
69+
/>
70+
</div>
71+
</div>
72+
</div>
73+
<div className="w-full">
74+
{filteredAndOrderedPrograms.length > 0 ? (
75+
<ProgramCardGroup programs={filteredAndOrderedPrograms} />
76+
) : (
77+
<div className="font-ui-sans text-lg">{noProgramsPlaceholder}</div>
78+
)}
79+
</div>
80+
</div>
81+
);
82+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useMemo } from "react";
2+
3+
import { ProgramCardProps } from "@/features/program";
4+
5+
interface UseFilteredAndOrderedProgramsProps {
6+
programs: ProgramCardProps[];
7+
selectedFilters: Record<string, string[]>;
8+
order: Record<string, string[]>;
9+
}
10+
11+
export const useFilteredAndOrderedPrograms = ({
12+
programs,
13+
selectedFilters,
14+
order,
15+
}: UseFilteredAndOrderedProgramsProps) => {
16+
return useMemo(() => {
17+
let result = [...programs];
18+
19+
// Apply filters
20+
const ungrouped = selectedFilters["ungrouped"];
21+
const hasAllFilter = ungrouped?.includes("All");
22+
23+
// Apply network filters if not "All"
24+
const selectedNetworks = selectedFilters["Network"] || [];
25+
if (!hasAllFilter && selectedNetworks.length > 0) {
26+
result = result.filter((program) => selectedNetworks.includes(program.chainId.toString()));
27+
}
28+
29+
// Apply ordering
30+
const orderValue = order["ORDER BY TIME"]?.[0] || order["ORDER BY NAME"]?.[0] || "Recent";
31+
switch (orderValue) {
32+
case "Recent":
33+
result.sort((a, b) => b.createdAtBlock - a.createdAtBlock);
34+
break;
35+
case "Oldest":
36+
result.sort((a, b) => a.createdAtBlock - b.createdAtBlock);
37+
break;
38+
case "A-Z":
39+
result.sort((a, b) => a.title.localeCompare(b.title));
40+
break;
41+
case "Z-A":
42+
result.sort((a, b) => b.title.localeCompare(a.title));
43+
break;
44+
}
45+
46+
return result;
47+
}, [programs, order, selectedFilters]);
48+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./ProgramList";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { MultipleSelectGroup } from "@/components/MultipleSelect";
2+
import { ProgramCardProps } from "@/features/program";
3+
import { getChainInfo } from "@/lib";
4+
5+
export const getOrderAndFilterOptions = (programs: ProgramCardProps[]) => {
6+
const orderOptions = [
7+
{
8+
groupLabel: "ORDER BY TIME",
9+
multiple: false,
10+
items: ["Recent", "Oldest"].map((value) => ({
11+
label: value,
12+
value,
13+
exclusive: true,
14+
exclusiveScope: "global",
15+
})),
16+
},
17+
{
18+
groupLabel: "ORDER BY NAME",
19+
multiple: false,
20+
items: ["A-Z", "Z-A"].map((value) => ({
21+
label: value,
22+
value,
23+
exclusive: true,
24+
exclusiveScope: "global",
25+
})),
26+
},
27+
] satisfies MultipleSelectGroup[];
28+
29+
const filterOptions = [
30+
{
31+
multiple: false,
32+
items: [
33+
{
34+
label: "All",
35+
value: "All",
36+
exclusive: true,
37+
exclusiveScope: "global",
38+
},
39+
],
40+
},
41+
{
42+
groupLabel: "Network",
43+
multiple: true,
44+
collapsible: true,
45+
items: [...new Set(programs.map((program) => program.chainId))].map((chainId) => ({
46+
label: `Rounds on ${getChainInfo(chainId).name}`,
47+
value: chainId.toString(),
48+
})),
49+
},
50+
] satisfies MultipleSelectGroup[];
51+
52+
return { orderOptions, filterOptions };
53+
};

‎src/features/program/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./components/ProgramCard";
22
export * from "./components/ProgramCardGroup";
3+
export * from "./components/ProgramList";

‎src/types/pool.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ export interface PoolData {
3030
votingEndDate: Date;
3131
poolStatus: PoolStatus;
3232
operatorsCount: number;
33+
createdAtBlock: number;
3334
logoImg?: string;
34-
onClick?: () => void;
35+
onClick?: (pool?: { chainId: number; roundId: string }) => void;
3536
}
3637

3738
// Type guard for PoolData

0 commit comments

Comments
 (0)
This repository has been archived.