Skip to content

Commit

Permalink
Merge pull request #31 from AngleProtocol/feat/opportunity-page
Browse files Browse the repository at this point in the history
Feat/opportunity page
  • Loading branch information
hugolxt authored Dec 3, 2024
2 parents c319815 + 69bb034 commit 05f6c3f
Show file tree
Hide file tree
Showing 27 changed files with 454 additions and 173 deletions.
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"noParameterAssign": "off"
},
"complexity": {
"noStaticOnlyClass": "off",
"noBannedTypes": "error",
"noExcessiveCognitiveComplexity": "off",
"noExtraBooleanCast": "off",
Expand Down
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion merkl.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,5 @@ export default createConfig({
[zksync.id]: http(),
[optimism.id]: http(),
},
}
},
});
5 changes: 3 additions & 2 deletions src/api/opportunity/opportunity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ function getQueryParams(
const action = new URL(request.url).searchParams.get("action");
const chainId = new URL(request.url).searchParams.get("chain");
const page = new URL(request.url).searchParams.get("page");
console.log("PAGE", page);

const items = new URL(request.url).searchParams.get("items");
const search = new URL(request.url).searchParams.get("search");
Expand All @@ -35,7 +34,9 @@ export async function fetchOpportunities(
const query = getQueryParams(request, overrideQuery);

const { data: count } = await api.v4.opportunities.count.get({ query });
const { data: opportunities } = await api.v4.opportunities.index.get({ query });
const { data: opportunities } = await api.v4.opportunities.index.get({
query,
});

if (count === null || !opportunities) throw "Cannot fetch opportunities";
return { opportunities, count };
Expand Down
49 changes: 43 additions & 6 deletions src/api/services/campaign.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,54 @@
import type { Campaign } from "@angleprotocol/merkl-api";
import { api } from "../index.server";

class CampaignService {
export abstract class CampaignService {
/**
* Retrieves opportunities query params from page request
* @param request request containing query params such as chains, status, pagination...
* @param override params for which to override value
* @returns query
*/
static #getQueryFromRequest(
request: Request,
override?: Parameters<typeof api.v4.opportunities.index.get>[0]["query"],
) {
const status = new URL(request.url).searchParams.get("status");
const action = new URL(request.url).searchParams.get("action");
const chainId = new URL(request.url).searchParams.get("chain");
const page = new URL(request.url).searchParams.get("page");

const items = new URL(request.url).searchParams.get("items");
const search = new URL(request.url).searchParams.get("search");
const [sort, order] = new URL(request.url).searchParams.get("sort")?.split("-") ?? [];

const filters = Object.assign(
{ status, action, chainId, items, sort, order, name: search, page },
override ?? {},
page !== null && { page: Number(page) - 1 },
);

const query = Object.entries(filters).reduce(
(_query, [key, filter]) => Object.assign(_query, filter == null ? {} : { [key]: filter }),
{},
);

return query;
}

// ------ Fetch all campaigns
async get(): Promise<Campaign[]> {
static async get(): Promise<Campaign[]> {
const { data } = await api.v4.campaigns.index.get({ query: {} });

return data;
}

static async getByParams(query: Parameters<typeof api.v4.campaigns.index.get>[0]["query"]): Promise<Campaign[]> {
const { data } = await api.v4.campaigns.index.get({ query });
return data;
}

// ------ Fetch a campaign by ID
async getByID(Id: string): Promise<Campaign> {
return "To implements";
static async getByID(Id: string): Promise<Campaign | null> {
return null;
}
}

export const campaignService = new CampaignService();
1 change: 0 additions & 1 deletion src/api/services/chain.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Chain } from "@angleprotocol/merkl-api";
import { api } from "../index.server";

// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
export abstract class ChainService {
static async #fetch<R, T extends { data: R; status: number }>(
call: () => Promise<T>,
Expand Down
35 changes: 22 additions & 13 deletions src/api/services/opportunity.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { Campaign, Opportunity } from "@angleprotocol/merkl-api";
import type { Opportunity } from "@angleprotocol/merkl-api";
import config from "merkl.config";
import { api } from "../index.server";

// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
export abstract class OpportunityService {
static async #fetch<R, T extends { data: R; status: number }>(
call: () => Promise<T>,
Expand Down Expand Up @@ -67,27 +66,37 @@ export abstract class OpportunityService {
return { opportunities: opportunities.filter(o => o !== null), count };
}

static async getCampaigns(query: { chainId: number; type: string; identifier: string }): Promise<Campaign[]> {
static async get(query: {
chainId: number;
type: string;
identifier: string;
}): Promise<Opportunity> {
const { chainId, type, identifier } = query;

type T = Parameters<typeof api.v4.campaigns.index.get>[0]["query"]["type"];
const campaigns = await OpportunityService.#fetch(async () =>
api.v4.campaigns.index.get({ query: { chainId, type: type as T, identifier } }),
const opportunity = await OpportunityService.#fetch(async () =>
api.v4.opportunities({ id: `${chainId}-${type}-${identifier}` }).get(),
);

return campaigns.filter(c => c !== null);
//TODO: updates tags to take an array
if (config.tags && !opportunity.tags.includes(config.tags?.[0]))
throw new Response("Opportunity inacessible", { status: 403 });

return opportunity;
}

static async get(query: { chainId: number; type: string; identifier: string }): Promise<Opportunity> {
static async getCampaignsByParams(query: {
chainId: number;
type: string;
identifier: string;
}) {
const { chainId, type, identifier } = query;
const opportunity = await OpportunityService.#fetch(async () =>
api.v4.opportunities({ id: `${chainId}-${type}-${identifier}` }).get(),
const opportunityWithCampaigns = await OpportunityService.#fetch(async () =>
api.v4.opportunities({ id: `${chainId}-${type}-${identifier}` }).campaigns.get(),
);

//TODO: updates tags to take an array
if (config.tags && !opportunity.tags.includes(config.tags?.[0]))
if (config.tags && !opportunityWithCampaigns.tags.includes(config.tags?.[0]))
throw new Response("Opportunity inacessible", { status: 403 });

return opportunity;
return opportunityWithCampaigns;
}
}
1 change: 0 additions & 1 deletion src/api/services/protocol.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Protocol } from "@angleprotocol/merkl-api";
import { api } from "../index.server";

// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
export abstract class ProtocolService {
static async #fetch<R, T extends { data: R; status: number }>(
call: () => Promise<T>,
Expand Down
17 changes: 16 additions & 1 deletion src/api/services/reward.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Reward } from "@angleprotocol/merkl-api";
import { api } from "../index.server";

// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
export abstract class RewardService {
static async #fetch<R, T extends { data: R; status: number }>(
call: () => Promise<T>,
Expand All @@ -21,4 +20,20 @@ export abstract class RewardService {
//TODO: add some cache here
return rewards;
}

static async getByParams(query: {
items?: number;
page?: number;
chainId: number;
campaignIdentifiers: string[];
}) {
return RewardService.#fetch(async () =>
api.v4.rewards.index.get({
query: {
...query,
campaignIdentifiers: query.campaignIdentifiers.join(","),
},
}),
);
}
}
1 change: 0 additions & 1 deletion src/api/services/token.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Token } from "@angleprotocol/merkl-api";
import { api } from "../index.server";

// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
export abstract class TokenService {
static async #fetch<R, T extends { data: R; status: number }>(
call: () => Promise<T>,
Expand Down
57 changes: 33 additions & 24 deletions src/components/composite/Hero.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Campaign } from "@angleprotocol/merkl-api";
import type { Campaign, Opportunity } from "@angleprotocol/merkl-api";
import { useLocation } from "@remix-run/react";
import { Container, Divider, Group, Icon, type IconProps, Icons, Text, Title, Value } from "dappkit";
import { Box, Container, Divider, Group, Icon, type IconProps, Icons, Text, Title, Value } from "dappkit";
import { Button } from "dappkit";
import config from "merkl.config";
import moment from "moment";
import { type PropsWithChildren, type ReactNode, useMemo } from "react";
import { formatUnits, parseUnits } from "viem";

Expand All @@ -13,14 +14,20 @@ export type HeroProps = PropsWithChildren<{
description: ReactNode;
tags?: ReactNode[];
tabs?: { label: ReactNode; link: string }[];
campaigns?: Campaign[];
opportunity?: Opportunity;
}>;

export default function Hero({ navigation, icons, title, description, tags, tabs, children, campaigns }: HeroProps) {
export default function Hero({ navigation, icons, title, description, tags, children, opportunity, tabs }: HeroProps) {
const location = useLocation();

const filteredCampaigns = useMemo(() => {
if (!opportunity?.campaigns) return null;
const now = moment().unix();
return opportunity.campaigns?.filter((c: Campaign) => Number(c.endTimestamp) > now);
}, [opportunity]);

const totalRewards = useMemo(() => {
const amounts = campaigns?.map(campaign => {
const amounts = filteredCampaigns?.map(campaign => {
const duration = campaign.endTimestamp - campaign.startTimestamp;
const dayspan = BigInt(duration) / BigInt(3600 * 24);

Expand All @@ -30,7 +37,7 @@ export default function Hero({ navigation, icons, title, description, tags, tabs
const sum = amounts?.reduce((accumulator, currentValue) => accumulator + currentValue, 0n);
if (!sum) return "0.0";
return formatUnits(sum, 18);
}, [campaigns]);
}, [filteredCampaigns]);

return (
<>
Expand Down Expand Up @@ -116,45 +123,47 @@ export default function Hero({ navigation, icons, title, description, tags, tabs
{!location?.pathname.includes("user") && (
<Group className="w-full lg:w-auto lg:flex-col mr-xl*2" size="xl">
<Group className="flex-col">
<Value look={totalRewards === "0" ? "soft" : "base"} format="$0,0" size={3}>
{totalRewards}
</Value>
<Text size={3}>
<Value look={totalRewards === "0" ? "soft" : "base"} format="$0,0" size={"md"}>
{totalRewards}
</Value>
</Text>

<Text size="xl" className="font-bold">
Daily rewards
</Text>
</Group>
<Group className="flex-col">
<Text size={3}>todo</Text>
<Text size={3}>
<Value value format="0a%">
{(opportunity?.apr ?? 0) / 100}
</Value>
</Text>
<Text size={"xl"} className="font-bold">
APR
</Text>
</Group>
<Group className="flex-col">
<Text size={3}>{campaigns?.length}</Text>
<Text size={3}>{filteredCampaigns?.length}</Text>
<Text size={"xl"} className="font-bold">
Active campaigns
</Text>
</Group>
</Group>
)}
{/* {!!tabs && (
<Box size="sm" look="base" className="flex-row mt-xl*2 w-min">
{tabs?.map((tab) => (
<Button
look={location.pathname === tab.link ? "hype" : "soft"}
to={tab.link}
key={tab.link}
>
{tab.label}
</Button>
))}
</Box>
)} */}
</Group>
</Group>
</Container>
</Group>
{!!tabs && (
<Box size="sm" look="base" className="flex-row mt-xl*2 w-min">
{tabs?.map(tab => (
<Button look={location.pathname === tab.link ? "hype" : "soft"} to={tab.link} key={tab.link}>
{tab.label}
</Button>
))}
</Box>
)}
<div>{children}</div>
</>
);
Expand Down
13 changes: 0 additions & 13 deletions src/components/element/Tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export default function Tag<T extends keyof TagTypes>({ type, value, ...props }:
switch (type) {
case "status": {
const status = statuses[value as TagTypes["status"]] ?? statuses.LIVE;

return (
<Dropdown
size="lg"
Expand Down Expand Up @@ -61,7 +60,6 @@ export default function Tag<T extends keyof TagTypes>({ type, value, ...props }:
}
case "chain": {
const chain = value as TagTypes["chain"];

return (
<Dropdown
size="lg"
Expand Down Expand Up @@ -95,9 +93,7 @@ export default function Tag<T extends keyof TagTypes>({ type, value, ...props }:
}
case "action": {
const action = actions[value as TagTypes["action"]];

if (!action) return <Button {...props}>{value}</Button>;

return (
<Dropdown
size="lg"
Expand Down Expand Up @@ -127,12 +123,9 @@ export default function Tag<T extends keyof TagTypes>({ type, value, ...props }:
</Dropdown>
);
}

case "token": {
const token = value as TagTypes["token"];

if (!token) return <Button {...props}>{value}</Button>;

return (
<Dropdown
size="lg"
Expand Down Expand Up @@ -173,12 +166,9 @@ export default function Tag<T extends keyof TagTypes>({ type, value, ...props }:
</Dropdown>
);
}

case "tokenChain": {
const token = value as TagTypes["tokenChain"];

if (!token) return <Button {...props}>{value}</Button>;

return (
<Dropdown
size="lg"
Expand Down Expand Up @@ -224,12 +214,9 @@ export default function Tag<T extends keyof TagTypes>({ type, value, ...props }:
</Dropdown>
);
}

case "protocol": {
const protocol = value;

if (!protocol) return <Button {...props}>{value}</Button>;

return (
<Dropdown
size="lg"
Expand Down
Loading

0 comments on commit 05f6c3f

Please sign in to comment.