Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, it, expect, afterEach, vi } from "vitest"
import { cleanup, render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { OpportunityCard } from "./opportunity-card"

afterEach(cleanup)

describe("OpportunityCard", () => {
it("renders APR, token pair, TVL, and action label for valid opportunities", () => {
const onAction = vi.fn()
render(
<OpportunityCard
name="WBTC/USDC Liquidity"
tokens={["WBTC", "USDC"]}
apy={0.125} // FormatPct multiplies by 100 or takes percent directly? Assuming it works like formatUsd
tvlUsd={1500000}
onAction={onAction}
actionLabel="Deposit"
/>
)

// Token Pair / Name
expect(screen.getByText("WBTC/USDC Liquidity")).toBeInTheDocument()

// Key metrics labels
expect(screen.getByText("APR")).toBeInTheDocument()
expect(screen.getByText("TVL")).toBeInTheDocument()

// Action button
const button = screen.getByRole("button", { name: "Deposit" })
expect(button).toBeInTheDocument()
expect(button).not.toBeDisabled()
})

it("correctly renders the unavailable/coming-soon state", () => {
render(
<OpportunityCard
name="ETH/USDC Liquidity"
tokens={["ETH", "USDC"]}
apy={0}
tvlUsd={0}
isAvailable={false}
/>
)

const button = screen.getByRole("button", { name: "Coming Soon" })
expect(button).toBeInTheDocument()
expect(button).toBeDisabled()
})

it("fires onAction when the action button is clicked", async () => {
const onAction = vi.fn()
const user = userEvent.setup()

render(
<OpportunityCard
name="GLV"
tokens={["GLV"]}
apy={0.085}
tvlUsd={500000}
onAction={onAction}
actionLabel="Earn"
/>
)

await user.click(screen.getByRole("button", { name: "Earn" }))
expect(onAction).toHaveBeenCalledOnce()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { cn } from "@workspace/ui/lib/utils"
import { Button } from "@workspace/ui/components/button"
import { formatPct, formatUsd } from "@/shared/lib/format"
import { TokenIcon } from "@/shared/components/TokenIcon"

export type OpportunityCardProps = {
name: string
tokens: string[]
apy: number
tvlUsd: number
isAvailable?: boolean
onAction?: () => void
actionLabel?: string
}

export function OpportunityCard({
name,
tokens,
apy,
tvlUsd,
isAvailable = true,
onAction,
actionLabel = "Earn",
}: OpportunityCardProps) {
return (
<div
className={cn(
"rounded-xl border border-border bg-card p-5 transition-colors",
!isAvailable && "opacity-60",
)}
>
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex -space-x-2">
{tokens.map((symbol, i) => (
<TokenIcon
key={`${symbol}-${i}`}
symbol={symbol}
size={28}
className="ring-card"
/>
))}
</div>
<span className="text-[15px] font-semibold">{name}</span>
</div>
</div>

<div className="mb-5 grid grid-cols-2 gap-4">
<div>
<p className="mb-1 text-[11px] text-muted-foreground">APR</p>
<p className="font-mono text-[14px] font-semibold text-green-400">
{formatPct(apy, { sign: false })}
</p>
</div>
<div>
<p className="mb-1 text-[11px] text-muted-foreground">TVL</p>
<p className="font-mono text-[14px] text-muted-foreground">
{formatUsd(tvlUsd, { compact: true })}
</p>
</div>
</div>

<Button
className="w-full text-[13px] font-medium"
disabled={!isAvailable}
onClick={() => {
if (isAvailable) onAction?.()
}}
>
{isAvailable ? actionLabel : "Coming Soon"}
</Button>
</div>
)
}