Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(earn): Market summary component #61

Merged
merged 9 commits into from
Feb 13, 2025
Merged
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
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>
);
};
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