Skip to content

Commit de2c794

Browse files
committed
feat: Add impact section
1 parent f110370 commit de2c794

File tree

5 files changed

+278
-14
lines changed

5 files changed

+278
-14
lines changed

packages/grant-explorer/src/features/api/gap.ts

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,53 @@ export interface IGapGrant {
2929
updates: IGrantStatus[];
3030
}
3131

32+
export interface IGapImpact {
33+
id: string;
34+
uid: Hex;
35+
schemaUID: Hex;
36+
refUID: Hex;
37+
attester: Hex;
38+
recipient: Hex;
39+
revoked: boolean;
40+
revocationTime: number;
41+
createdAt: string;
42+
updatedAt: string;
43+
chainID: number;
44+
type: string;
45+
data: {
46+
work: string;
47+
impact: string;
48+
proof: string;
49+
startedAt: number;
50+
completedAt: number;
51+
type: string;
52+
};
53+
txid: string;
54+
verified: IGapVerified[];
55+
}
56+
57+
export interface IGapVerified {
58+
id: string;
59+
uid: Hex;
60+
schemaUID: Hex;
61+
refUID: Hex;
62+
attester: Hex;
63+
recipient: Hex;
64+
revoked: boolean;
65+
revocationTime: number;
66+
createdAt: string;
67+
updatedAt: string;
68+
chainID: number;
69+
type: string;
70+
data: {
71+
type: string;
72+
reason: string;
73+
};
74+
}
75+
3276
export function useGap(projectId?: string) {
3377
const [grants, setGrants] = useState<IGapGrant[]>([]);
78+
const [impacts, setImpacts] = useState<IGapImpact[]>([]);
3479

3580
const getGrantsFor = async (projectRegistryId: string) => {
3681
if (!indexerUrl) throw new Error("GAP Indexer url not set.");
@@ -76,30 +121,72 @@ export function useGap(projectId?: string) {
76121
}
77122
};
78123

79-
const { isLoading } = useSWR(
124+
const getImpactsFor = async (projectRegistryId: string) => {
125+
if (!indexerUrl) throw new Error("GAP Indexer url not set.");
126+
try {
127+
const items: IGapImpact[] = await fetch(
128+
`${indexerUrl}/grants/external-id/${projectRegistryId}/impacts`
129+
).then((res) => res.json());
130+
131+
if (!Array.isArray(items)) {
132+
setImpacts([]);
133+
return;
134+
}
135+
136+
setImpacts(items);
137+
} catch (e) {
138+
console.error(`No impacts found for project: ${projectRegistryId}`);
139+
console.error(e);
140+
setImpacts([]);
141+
}
142+
};
143+
144+
const { isLoading: isGrantsLoading } = useSWR(
80145
`${indexerUrl}/grants/external-id/${projectId}`,
81146
{
82147
fetcher: async () => projectId && getGrantsFor(projectId),
83148
}
84149
);
85150

151+
const { isLoading: isImpactsLoading } = useSWR(
152+
`${indexerUrl}/grants/external-id/${projectId}/impacts`,
153+
{
154+
fetcher: async () => projectId && getImpactsFor(projectId),
155+
}
156+
);
157+
86158
return {
87159
/**
88160
* Fetch GAP Indexer for grants for a project
89161
* @param projectRegistryId registryId
90162
*/
91163
getGrantsFor,
164+
/**
165+
* Fetch GAP Indexer for impacts for a project
166+
* @param projectRegistryId registryId
167+
*/
168+
getImpactsFor,
92169
/**
93170
* Grants for a project (loaded from GAP)
94171
*/
95172
grants,
96-
isGapLoading: isLoading,
173+
/**
174+
* Impacts for a project (loaded from GAP)
175+
*/
176+
impacts,
177+
/**
178+
* Loading state for grants and impacts
179+
*/
180+
isGapLoading: isGrantsLoading || isImpactsLoading,
97181
};
98182
}
99183

100184
export const gapAppUrl = `${process.env.REACT_APP_KARMA_GAP_APP_URL}`;
101185

102-
export const getGapProjectUrl = (projectUID: string, grantUID?: string) =>
186+
export const getGapProjectGrantUrl = (projectUID: string, grantUID?: string) =>
103187
`${gapAppUrl}/project/${projectUID}/${
104188
grantUID ? `?tab=grants&grant=${grantUID}` : ""
105189
}`;
190+
191+
export const getGapProjectImpactUrl = (projectUID: string) =>
192+
`${gapAppUrl}/project/${projectUID}/impact`;

packages/grant-explorer/src/features/round/KarmaGrant/GrantList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react";
22
import { GrantItem } from "./GrantItem";
3-
import { IGapGrant, gapAppUrl, getGapProjectUrl } from "../../api/gap";
3+
import { IGapGrant, gapAppUrl, getGapProjectGrantUrl } from "../../api/gap";
44
import { Flex, Link, Text } from "@chakra-ui/react";
55

66
interface GrantListProps {
@@ -18,7 +18,7 @@ export const GrantList: React.FC<GrantListProps> = ({ grants }) => {
1818
<GrantItem
1919
key={+index}
2020
grant={grant}
21-
url={getGapProjectUrl(grant.projectUID, grant.uid)}
21+
url={getGapProjectGrantUrl(grant.projectUID, grant.uid)}
2222
/>
2323
))}
2424
<Text fontFamily="DM Mono" textAlign="center" className={"text-xs"}>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {
2+
Box,
3+
Divider,
4+
Flex,
5+
Link,
6+
Text,
7+
useDisclosure,
8+
} from "@chakra-ui/react";
9+
import GitcoinLogo from "../../../assets/gitcoinlogo-white.svg";
10+
import { renderToHTML } from "common/src/markdown";
11+
import { ChevronDownIcon } from "@heroicons/react/24/outline";
12+
import { ExpandableGrid } from "../../common/ExpandableGrid";
13+
import { dateFromMs } from "../../api/utils";
14+
import { useEffect, useState, FC } from "react";
15+
import { IGapImpact, getGapProjectImpactUrl } from "../../api/gap";
16+
17+
import { ShieldCheckIcon } from "@heroicons/react/24/solid";
18+
import { useEnsName } from "wagmi";
19+
20+
interface ImpactItemProps {
21+
impact: IGapImpact;
22+
url: string;
23+
}
24+
25+
const EthereumAddressToENSName: FC<{
26+
address: `0x${string}`;
27+
shouldTruncate?: boolean;
28+
}> = ({ address, shouldTruncate = true }) => {
29+
const { data: ensName } = useEnsName({
30+
address: address,
31+
});
32+
const lowerCasedAddress = address.toLowerCase();
33+
const addressToDisplay = shouldTruncate
34+
? lowerCasedAddress?.slice(0, 6) + "..." + lowerCasedAddress?.slice(-6)
35+
: lowerCasedAddress;
36+
37+
return <span className="font-body">{ensName || addressToDisplay}</span>;
38+
};
39+
40+
export const ImpactItem: React.FC<ImpactItemProps> = ({ impact }) => {
41+
return (
42+
<tr className="" key={impact.uid}>
43+
<td
44+
className={
45+
"py-4 border-t border-t-black pr-6 px-6 max-w-[420px] max-sm:min-w-[200px] text-black "
46+
}
47+
>
48+
<Link
49+
target="_blank"
50+
href={getGapProjectImpactUrl(impact.refUID)}
51+
className="text-lg font-semibold align-top"
52+
>
53+
{impact.data?.work}
54+
</Link>
55+
</td>
56+
<td
57+
className={
58+
"py-4 border-t border-t-black pr-6 px-6 max-w-[420px] max-sm:min-w-[200px] align-top"
59+
}
60+
>
61+
<div className="mb-2">{impact.data?.impact}</div>
62+
<Link target="_blank" href={impact.data?.proof}>
63+
{impact.data?.proof}
64+
</Link>
65+
</td>
66+
<td
67+
className={
68+
"py-4 border-t border-t-black pr-6 px-6 max-w-[420px] max-sm:min-w-[200px] px-3 align-top pr-5"
69+
}
70+
>
71+
<div className="flex flex-row gap-3 items-center justify-start ml-3 min-w-max max-w-full">
72+
{impact.verified.length ? (
73+
<div className="w-max max-w-full">
74+
{impact.verified.map((verification) => (
75+
<div className="flex flex-row gap-2 items-center justify-start">
76+
<div className="bg-teal-100 flex gap-2 rounded-full px-2 text-xs items-center font-modern-era-medium text-teal-500">
77+
<ShieldCheckIcon className="w-4 h-4" />
78+
Verified
79+
</div>
80+
<span className="text-xs">
81+
by{" "}
82+
<EthereumAddressToENSName address={verification.attester} />
83+
</span>
84+
</div>
85+
))}
86+
</div>
87+
) : null}
88+
</div>
89+
</td>
90+
<td
91+
className={
92+
"py-4 border-t border-t-black pr-6 px-6 max-w-[420px] max-sm:min-w-[200px] border-l border-l-zinc-400"
93+
}
94+
>
95+
<p className="w-36 max-w-max text-gray-500 text-sm font-medium ">
96+
{impact.data?.startedAt
97+
? dateFromMs(impact.data?.startedAt * 1000)
98+
: "N/A"}
99+
{" → "}
100+
{impact.data?.completedAt
101+
? dateFromMs(impact.data?.completedAt * 1000)
102+
: "N/A"}
103+
</p>
104+
</td>
105+
</tr>
106+
);
107+
};
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { ImpactItem } from "./ImpactItem";
2+
import { IGapImpact, gapAppUrl, getGapProjectImpactUrl } from "../../api/gap";
3+
import { Flex, Link, Text } from "@chakra-ui/react";
4+
5+
interface ImpactListProps {
6+
impacts: IGapImpact[];
7+
}
8+
9+
export const ImpactList: React.FC<ImpactListProps> = ({ impacts }) => {
10+
return (
11+
<Flex gap={2} flexDir="column" py={6} px={3}>
12+
<h4 className="text-3xl">Project impacts</h4>
13+
{impacts.length > 0 ? (
14+
<table className="overflow-x-auto w-full my-5">
15+
<thead>
16+
<tr>
17+
<th className="text-black text-xs font-medium uppercase text-left px-6 py-3 font-body">
18+
Work
19+
</th>
20+
<th className="text-black text-xs font-medium uppercase text-left px-6 py-3 font-body">
21+
Impact & Proof
22+
</th>
23+
<th className="text-black text-xs font-medium uppercase text-left px-6 py-3 font-body">
24+
Verifications
25+
</th>
26+
<th className="text-black text-xs font-medium uppercase text-left px-6 py-3 font-body">
27+
Timeframe
28+
</th>
29+
</tr>
30+
</thead>
31+
<tbody className="">
32+
{impacts.map((item) => (
33+
<ImpactItem
34+
key={item.uid}
35+
impact={item}
36+
url={getGapProjectImpactUrl(item.refUID)}
37+
/>
38+
))}
39+
</tbody>
40+
</table>
41+
) : (
42+
<Text>
43+
No previous impacts are being tracked for this project. If you're the
44+
owner of this project, visit{" "}
45+
<Link target="_blank" href={gapAppUrl}>
46+
<Text as="span" className="text-gitcoin-violet-500">
47+
gap.karmahq.xyz
48+
</Text>
49+
</Link>{" "}
50+
to add your project and post updates.
51+
</Text>
52+
)}
53+
54+
<Text fontFamily="DM Mono" textAlign="center" className={"text-xs"}>
55+
Data provided by Karma via{" "}
56+
<Link href={gapAppUrl} target="_blank">
57+
<Text as="span" className="text-gitcoin-violet-500">
58+
gap.karmahq.xyz
59+
</Text>
60+
</Link>
61+
.
62+
</Text>
63+
</Flex>
64+
);
65+
};

0 commit comments

Comments
 (0)