Skip to content

Commit

Permalink
feat(earn): Market summary component (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
essj authored Feb 13, 2025
1 parent 9205e75 commit 28713e9
Show file tree
Hide file tree
Showing 13 changed files with 212 additions and 46 deletions.
10 changes: 10 additions & 0 deletions apps/flame-defi/app/earn/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { cn } from "@repo/ui/lib";

export const Card = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => {
return (
<div className={cn("rounded-lg bg-semi-white", className)} {...props} />
);
};
1 change: 1 addition & 0 deletions apps/flame-defi/app/earn/components/Card/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Card } from "./Card";
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useEffect, useState } from "react";
import { MarketSummaryCard } from "./MarketSummaryCard";

// TODO: Use fetched values.
const VALUE_DEPOSIT = 1000000;
const VALUE_BORROW = 75000;

export const MarketSummary = () => {
const [countDeposit, setCountDeposit] = useState<number | null>(null);
const [countBorrow, setCountBorrow] = useState<number | null>(null);

const [isAnimating, setIsAnimating] = useState(false);

useEffect(() => {
// Prevent counter animation when number is too low.
// May need to adjust based on the expected maximum value.
const multiplier = 0.1;
const countDepositMinimum = VALUE_DEPOSIT * multiplier;
const countBorrowMinimum = VALUE_BORROW * multiplier;

if (
countDeposit !== null &&
countBorrow !== null &&
countDeposit > countDepositMinimum &&
countBorrow > countBorrowMinimum
) {
setIsAnimating(true);
}
}, [countDeposit, countBorrow]);

return (
<div className="flex flex-col gap-2 md:flex-row">
<MarketSummaryCard
label="Total Deposits"
value={VALUE_DEPOSIT}
count={countDeposit}
setCount={setCountDeposit}
isAnimating={isAnimating}
/>
<MarketSummaryCard
label="Total Borrow"
value={VALUE_BORROW}
count={countBorrow}
setCount={setCountBorrow}
isAnimating={isAnimating}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Skeleton } from "@repo/ui/shadcn-primitives";
import Big from "big.js";
import React, { useEffect } from "react";
import { FormattedNumber } from "react-intl";
import { Card } from "../Card";

interface MarketSummaryCardProps {
label: React.ReactNode;
value: number;
count: number | null;
setCount: (value: number) => void;
/**
* Sync animation state between multiple cards.
*/
isAnimating: boolean;
}

export const MarketSummaryCard = ({
label,
value,
count,
setCount,
isAnimating,
}: MarketSummaryCardProps) => {
useEffect(() => {
let isMounted = true;

const counter = (minimum: number, maximum: number) => {
let i = minimum;

const updateCount = () => {
if (isMounted && i <= maximum) {
setCount(i);

// Increment counter based on proximity to maximum so multiple cards have synced animations.
const range = maximum - minimum;
const progress = (i - minimum) / range;
const step = Math.max(1, (Math.log10(1 + 9 * progress) * range) / 10);

i += Math.ceil(step);

setTimeout(updateCount, 10);
}

if (isMounted && i > maximum) {
setCount(maximum);
}
};

updateCount();

return () => {
isMounted = false;
};
};

counter(0, value);

return () => {
isMounted = false;
};
}, [value, setCount]);

return (
<Card className="flex flex-col rounded-xl p-5 space-y-1">
<span className="text-xs/3 text-grey-light">{label}</span>

<div className="relative">
{/* Reserve space for animation to prevent card size increasing with counter value. */}
<span className="text-3xl/8 font-dot opacity-0">
<FormattedNumber
value={+new Big(value).toFixed()}
style="currency"
currency="USD"
maximumFractionDigits={0}
/>
</span>
<span className="text-3xl/8 font-dot w-full absolute top-0 left-0">
<Skeleton isLoading={!count || !isAnimating}>
<span>
<FormattedNumber
value={+new Big(count ?? 0).toFixed()}
style="currency"
currency="USD"
maximumFractionDigits={0}
/>
</span>
</Skeleton>
</span>
</div>
</Card>
);
};
1 change: 1 addition & 0 deletions apps/flame-defi/app/earn/components/MarketSummary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MarketSummary } from "./MarketSummary";
4 changes: 2 additions & 2 deletions apps/flame-defi/app/earn/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export const Table = () => {
"md:last:table-cell",
)}
>
<div className="flex items-center space-x-2">
<div className="text-xs/3 text-grey-light font-mono font-medium uppercase">
<div className="flex items-end space-x-2">
<div className="text-xs/3 text-grey-light font-semibold tracking-widest uppercase">
{flexRender(
header.column.columnDef.header,
header.getContext(),
Expand Down
16 changes: 0 additions & 16 deletions apps/flame-defi/app/earn/components/Table/TableCard.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const TablePagination = () => {
}, [data?.vaults?.pageInfo?.countTotal]);

return (
<Skeleton isLoading={isPending} className="w-[200px]">
<Skeleton isLoading={isPending} className="w-52">
<Pagination>
<PaginationContent>
<PaginationItem>
Expand Down
43 changes: 34 additions & 9 deletions apps/flame-defi/app/earn/components/Table/TableSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { SearchIcon } from "@repo/ui/icons";
import { cn } from "@repo/ui/lib";
import { Input, Skeleton } from "@repo/ui/shadcn-primitives";
import { useTable } from "earn/hooks/useTable";
import { useRef, useState } from "react";

export const TableSearch = () => {
const inputRef = useRef<HTMLInputElement>(null);

const [isFocused, setIsFocused] = useState(false);

const {
search,
setSearch,
Expand All @@ -12,16 +18,35 @@ export const TableSearch = () => {
return (
<Skeleton
isLoading={isRefetching && !data?.vaults.items?.length}
className="w-[200px]"
className="w-full md:w-52"
>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
type="text"
placeholder="Search vaults"
startAdornment={<SearchIcon size={16} />}
className="w-[200px]"
/>
<div className="w-full *:w-full">
<Input
ref={inputRef}
value={search}
onChange={(e) => setSearch(e.target.value)}
onBlur={() => setIsFocused(false)}
onFocus={() => setIsFocused(true)}
type="text"
startAdornment={
<div
onClick={() => {
if (inputRef.current && !isFocused) {
setIsFocused(true);
inputRef.current.focus();
}
}}
>
<SearchIcon aria-label="Search" size={24} />
</div>
}
className={cn(
"transition-all duration-300 ease-in-out",
search ? "w-full md:w-52" : "w-0 border-transparent",
"focus-visible:w-full md:focus-visible:w-52 focus-visible:border-orange-soft",
)}
/>
</div>
</Skeleton>
);
};
1 change: 0 additions & 1 deletion apps/flame-defi/app/earn/components/Table/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { Table } from "./Table";
export { TableCard } from "./TableCard";
export { TablePagination } from "./TablePagination";
export { TableSearch } from "./TableSearch";
8 changes: 5 additions & 3 deletions apps/flame-defi/app/earn/contexts/TableContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,16 @@ export const TableContextProvider = ({ children }: PropsWithChildren) => {
cell: ({ row }) => {
return (
<div className="flex items-center space-x-2 md:space-x-4">
{row.original.asset.logoURI && (
{row.original.asset.logoURI ? (
<Image
src={row.original.asset.logoURI}
alt={row.original.name}
width={30}
height={30}
className="rounded-full"
className="rounded-full shrink-0"
/>
) : (
<div className="rounded-full shrink-0 w-[30px] h-[30px] bg-grey-dark" />
)}
<div className="flex flex-col space-y-1 overflow-hidden">
<span className="text-base/4 truncate max-w-[25vw] md:max-w-auto">
Expand Down Expand Up @@ -119,7 +121,7 @@ export const TableContextProvider = ({ children }: PropsWithChildren) => {
>
<span
className={cn(
"text-xs/3 truncate max-w-[35vw]",
"text-xs/3 truncate max-w-[25vw]",
"md:text-base/4 md:max-w-auto",
)}
>
Expand Down
28 changes: 15 additions & 13 deletions apps/flame-defi/app/earn/sections/TableSection/TableSection.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
import {
Table,
TableCard,
TablePagination,
TableSearch,
} from "earn/components/Table";
import { Card } from "earn/components/Card";
import { MarketSummary } from "earn/components/MarketSummary";
import { Table, TablePagination, TableSearch } from "earn/components/Table";
import { useTable } from "earn/hooks/useTable";

export const TableSection = () => {
const { status } = useTable();

return (
<section className="flex flex-col p-4 md:p-20">
<div className="flex justify-end w-full mb-6">
<div className="flex flex-col justify-between gap-4 mt-20 mb-6 md:flex-row md:mb-4 md:mt-0">
<h1 className="text-xl/6">Lend</h1>
<MarketSummary />
</div>

<div className="flex w-full mb-4">
<TableSearch />
</div>

{status === "error" && (
<TableCard className="h-[250px] text-lg text-grey-light flex items-center justify-center">
<Card className="h-[250px] text-lg text-grey-light flex items-center justify-center">
{`We couldn't fetch vault data. Please try again later.`}
</TableCard>
</Card>
)}
{status === "empty" && (
<TableCard className="h-[250px] text-lg text-grey-light flex items-center justify-center">
<Card className="h-[250px] text-lg text-grey-light flex items-center justify-center">
{`No vaults found.`}
</TableCard>
</Card>
)}
{status === "success" && (
<>
<TableCard>
<Card className="overflow-x-hidden md:overflow-x-auto">
<Table />
</TableCard>
</Card>

<div className="flex justify-center mt-10">
<TablePagination />
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/shadcn-primitives/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground md:text-sm",
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground md:text-sm outline-none",
"disabled:cursor-not-allowed disabled:opacity-50",
"file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-orange-soft",
Expand Down

0 comments on commit 28713e9

Please sign in to comment.