Skip to content

Commit

Permalink
add explorer projects tab (#3524)
Browse files Browse the repository at this point in the history
* feat: add getPaginatedProjects query

* feat: getPaginatedProjects to data layer

* feat: add useProjets hook

* feat: add projects route

* feat: add projects tab in explorer home page

* chore: rename project page and card to application page and card

* fix: tests

* feat: move CardSkeleton

* chore: rm file

* fix: query

* feat: add projectPath

* feat: add project card

* feat: update explore projects page

* feat: add getProjectsBySearchTerm to datalayer

* feat: add useProjectsBySearchTerm hook

* feat: search functionality

* feat: update getProjects queries

* fix: ui issues
  • Loading branch information
bhargavaparoksham authored Jun 21, 2024
1 parent 0ccd4d0 commit 853e81e
Show file tree
Hide file tree
Showing 17 changed files with 736 additions and 369 deletions.
4 changes: 4 additions & 0 deletions packages/common/src/routes/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ export function applicationPath(p: {
export function collectionPath(collectionCid: string): string {
return `/#/collections/${collectionCid}`;
}

export function projectPath(p: { projectId: string }): string {
return `/#/projects/${p.projectId}`;
}
70 changes: 70 additions & 0 deletions packages/data-layer/src/data-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import {
getApplicationsForExplorer,
getPayoutsByChainIdRoundIdProjectId,
getApprovedApplicationsByProjectIds,
getPaginatedProjects,
getProjectsBySearchTerm,
} from "./queries";
import { mergeCanonicalAndLinkedProjects } from "./utils";

Expand Down Expand Up @@ -313,6 +315,74 @@ export class DataLayer {
return projects;
}

/**
* Gets all active projects in the given range.
* @param first // number of projects to return
* @param offset // number of projects to skip
*
* @returns v2Project[]
*/
async getPaginatedProjects({
first,
offset,
}: {
first: number;
offset: number;
}): Promise<v2Project[]> {
const requestVariables = {
first,
offset,
};

const response: { projects: v2Project[] } = await request(
this.gsIndexerEndpoint,
getPaginatedProjects,
requestVariables,
);

const projects: v2Project[] = mergeCanonicalAndLinkedProjects(
response.projects,
);

return projects;
}

/**
* Gets all projects that match the search term.
* @param searchTerm // search term to filter projects
* @param first // number of projects to return
* @param offset // number of projects to skip
*
* @returns v2Project[]
*/
async getProjectsBySearchTerm({
searchTerm,
first,
offset,
}: {
searchTerm: string;
first: number;
offset: number;
}): Promise<v2Project[]> {
const requestVariables = {
searchTerm,
first,
offset,
};

const response: { searchProjects: v2Project[] } = await request(
this.gsIndexerEndpoint,
getProjectsBySearchTerm,
requestVariables,
);

const projects: v2Project[] = mergeCanonicalAndLinkedProjects(
response.searchProjects,
);

return projects;
}

/**
* getApplicationsByProjectIds() returns a list of projects by address.
* @param projectIds
Expand Down
80 changes: 80 additions & 0 deletions packages/data-layer/src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,86 @@ export const getProjectsByAddress = gql`
}
`;

/**
* Get active projects in the given range
`* @param $first - The number of projects to return
* @param $offset - The offset of the projects
*
* @returns The v2Projects
*/
export const getPaginatedProjects = gql`
query getPaginatedProjects($first: Int!, $offset: Int!) {
projects(
filter: {
tags: { equalTo: "allo-v2" }
not: { tags: { contains: "program" } }
chainId: {
in: [1, 137, 10, 324, 42161, 42220, 43114, 534352, 8453, 1329]
}
rounds: { every: { applicationsExist: true } }
}
first: $first
offset: $offset
) {
id
chainId
metadata
metadataCid
name
nodeId
projectNumber
registryAddress
tags
nonce
anchorAddress
projectType
}
}
`;

/**
* Get projects by search term
* @param $searchTerm - The search term
`* @param $first - The number of projects to return
* @param $offset - The offset of the projects
*
* @returns The v2Projects
*/
export const getProjectsBySearchTerm = gql`
query getProjectsBySearchTerm(
$searchTerm: String!
$first: Int!
$offset: Int!
) {
searchProjects(
searchTerm: $searchTerm
filter: {
tags: { equalTo: "allo-v2" }
not: { tags: { contains: "program" } }
chainId: {
in: [1, 137, 10, 324, 42161, 42220, 43114, 534352, 8453, 1329]
}
rounds: { every: { applicationsExist: true } }
}
first: $first
offset: $offset
) {
id
chainId
metadata
metadataCid
name
nodeId
projectNumber
registryAddress
tags
nonce
anchorAddress
projectType
}
}
`;

export const getProjectsAndRolesByAddress = gql`
query getProjectsAndRolesByAddressQuery(
$address: String!
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { vi } from "vitest";
import { render, fireEvent, screen } from "@testing-library/react";
import { ProjectCard, ProjectCardSkeleton } from "./ProjectCard";
import { ApplicationCard } from "./ApplicationCard";
import { ApplicationSummary } from "data-layer";
import { zeroAddress } from "viem";
import { ChakraProvider } from "@chakra-ui/react";
import { CardSkeleton } from "./ProjectBanner";

vi.mock("common/src/config", async () => {
return {
Expand All @@ -13,7 +14,7 @@ vi.mock("common/src/config", async () => {
};
});

describe("ProjectCard", () => {
describe("ApplicationCard", () => {
const mockApplication: ApplicationSummary = {
roundName: "This is a round name!",
applicationRef: "1",
Expand All @@ -37,7 +38,7 @@ describe("ProjectCard", () => {
const removeFromCart = vi.fn();

render(
<ProjectCard
<ApplicationCard
application={mockApplication}
inCart={false}
onAddToCart={addToCart}
Expand All @@ -55,7 +56,7 @@ describe("ProjectCard", () => {
const removeFromCart = vi.fn();

render(
<ProjectCard
<ApplicationCard
application={mockApplication}
inCart={false}
onAddToCart={addToCart}
Expand All @@ -72,7 +73,7 @@ describe("ProjectCard", () => {
const removeFromCart = vi.fn();

render(
<ProjectCard
<ApplicationCard
application={mockApplication}
inCart={true}
onAddToCart={addToCart}
Expand All @@ -87,7 +88,7 @@ describe("ProjectCard", () => {
it("renders ProjectCardSkeleton correctly", () => {
render(
<ChakraProvider>
<ProjectCardSkeleton />
<CardSkeleton />
</ChakraProvider>
);
});
Expand Down
104 changes: 104 additions & 0 deletions packages/grant-explorer/src/features/common/ApplicationCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { ReactComponent as CartCircleIcon } from "../../assets/icons/cart-circle.svg";
import { ReactComponent as CheckedCircleIcon } from "../../assets/icons/checked-circle.svg";
import { ApplicationSummary } from "data-layer";
import {
Badge,
BasicCard,
CardContent,
CardDescription,
CardHeader,
} from "./styles";
import { applicationPath } from "common/src/routes/explorer";
import { ProjectBanner, ProjectLogo } from "./ProjectBanner";
import { usePostHog } from "posthog-js/react";

export function ApplicationCard(props: {
application: ApplicationSummary;
inCart: boolean;
onAddToCart: (app: ApplicationSummary) => void;
onRemoveFromCart: (app: ApplicationSummary) => void;
}): JSX.Element {
const {
application,
inCart,
onAddToCart: addToCart,
onRemoveFromCart: removeFromCart,
} = props;

const posthog = usePostHog();
const roundId = application.roundId.toLowerCase();

return (
<BasicCard className="w-full hover:opacity-90 transition hover:shadow-none">
<a
target="_blank"
href={applicationPath({
chainId: application.chainId,
roundId,
applicationId: application.roundApplicationId,
})}
data-track-event="project-card"
>
<CardHeader className="relative">
<ProjectBanner
bannerImgCid={application.bannerImageCid}
classNameOverride={
"bg-black h-[120px] w-full object-cover rounded-t"
}
resizeHeight={120}
/>
</CardHeader>
<CardContent className="relative">
{application.logoImageCid !== null && (
<ProjectLogo
className="border-solid border-2 border-white absolute -top-[24px] "
imageCid={application.logoImageCid}
size={48}
/>
)}
<div className="truncate mt-4">{application.name}</div>
<CardDescription className=" min-h-[96px]">
<div className="text-sm line-clamp-4">
{application.summaryText}
</div>
</CardDescription>

<Badge color="grey" rounded="3xl">
<span className="truncate">{application.roundName}</span>
</Badge>
</CardContent>
</a>
<div className="p-2">
<div className="border-t pt-2 flex justify-end">
{inCart ? (
<button
aria-label="Remove from cart"
onClick={() => {
posthog.capture("application_removed_from_cart", {
applicationRef: application.applicationRef,
});

removeFromCart(application);
}}
>
<CheckedCircleIcon className="w-10" />
</button>
) : (
<button
aria-label="Add to cart"
onClick={() => {
posthog.capture("application_added_to_cart", {
applicationRef: application.applicationRef,
});

addToCart(application);
}}
>
<CartCircleIcon className="w-10" />
</button>
)}
</div>
</div>
</BasicCard>
);
}
37 changes: 37 additions & 0 deletions packages/grant-explorer/src/features/common/ProjectBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Skeleton, SkeletonCircle, SkeletonText } from "@chakra-ui/react";
import DefaultBannerImage from "../../assets/default_banner.jpg";
import { createIpfsImageUrl } from "common/src/ipfs";
import { getConfig } from "common/src/config";
Expand Down Expand Up @@ -31,3 +32,39 @@ export function ProjectBanner(props: {
</div>
);
}

export function ProjectLogo(props: {
className?: string;
imageCid: string;
size: number;
}): JSX.Element {
const {
ipfs: { baseUrl: ipfsBaseUrl },
} = getConfig();

const projectLogoImageUrl = createIpfsImageUrl({
baseUrl: ipfsBaseUrl,
cid: props.imageCid,
height: props.size * 2,
});

return (
<img
className={`object-cover rounded-full ${props.className ?? ""}`}
style={{ height: props.size, width: props.size }}
src={projectLogoImageUrl}
alt="Project Banner"
/>
);
}

export function CardSkeleton(): JSX.Element {
return (
<div className="bg-white rounded-3xl overflow-hidden p-4 pb-10">
<Skeleton height="110px" />
<SkeletonCircle size="48px" mt="-24px" ml="10px" />
<SkeletonText mt="3" noOfLines={1} spacing="4" skeletonHeight="7" />
<SkeletonText mt="10" noOfLines={4} spacing="4" skeletonHeight="2" />
</div>
);
}
Loading

0 comments on commit 853e81e

Please sign in to comment.