diff --git a/apps/flame-defi/app/earn/components/Card/Card.tsx b/apps/flame-defi/app/earn/components/Card/Card.tsx new file mode 100644 index 0000000..447fa83 --- /dev/null +++ b/apps/flame-defi/app/earn/components/Card/Card.tsx @@ -0,0 +1,10 @@ +import { cn } from "@repo/ui/lib"; + +export const Card = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( +
+ ); +}; diff --git a/apps/flame-defi/app/earn/components/Card/index.ts b/apps/flame-defi/app/earn/components/Card/index.ts new file mode 100644 index 0000000..fbaf43c --- /dev/null +++ b/apps/flame-defi/app/earn/components/Card/index.ts @@ -0,0 +1 @@ +export { Card } from "./Card"; diff --git a/apps/flame-defi/app/earn/components/MarketSummary/MarketSummary.tsx b/apps/flame-defi/app/earn/components/MarketSummary/MarketSummary.tsx new file mode 100644 index 0000000..0b68396 --- /dev/null +++ b/apps/flame-defi/app/earn/components/MarketSummary/MarketSummary.tsx @@ -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(null); + const [countBorrow, setCountBorrow] = useState(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 ( +
+ + +
+ ); +}; diff --git a/apps/flame-defi/app/earn/components/MarketSummary/MarketSummaryCard.tsx b/apps/flame-defi/app/earn/components/MarketSummary/MarketSummaryCard.tsx new file mode 100644 index 0000000..f6e0bee --- /dev/null +++ b/apps/flame-defi/app/earn/components/MarketSummary/MarketSummaryCard.tsx @@ -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 ( + + {label} + +
+ {/* Reserve space for animation to prevent card size increasing with counter value. */} + + + + + + + + + + +
+
+ ); +}; diff --git a/apps/flame-defi/app/earn/components/MarketSummary/index.ts b/apps/flame-defi/app/earn/components/MarketSummary/index.ts new file mode 100644 index 0000000..bae18eb --- /dev/null +++ b/apps/flame-defi/app/earn/components/MarketSummary/index.ts @@ -0,0 +1 @@ +export { MarketSummary } from "./MarketSummary"; diff --git a/apps/flame-defi/app/earn/components/Table/Table.tsx b/apps/flame-defi/app/earn/components/Table/Table.tsx index c85d3ca..34af8f8 100644 --- a/apps/flame-defi/app/earn/components/Table/Table.tsx +++ b/apps/flame-defi/app/earn/components/Table/Table.tsx @@ -26,8 +26,8 @@ export const Table = () => { "md:last:table-cell", )} > -
-
+
+
{flexRender( header.column.columnDef.header, header.getContext(), diff --git a/apps/flame-defi/app/earn/components/Table/TableCard.tsx b/apps/flame-defi/app/earn/components/Table/TableCard.tsx deleted file mode 100644 index d87420e..0000000 --- a/apps/flame-defi/app/earn/components/Table/TableCard.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { cn } from "@repo/ui/lib"; - -export const TableCard = ({ - className, - ...props -}: React.HTMLAttributes) => { - return ( -
- ); -}; diff --git a/apps/flame-defi/app/earn/components/Table/TablePagination.tsx b/apps/flame-defi/app/earn/components/Table/TablePagination.tsx index 428b4f1..edbabb8 100644 --- a/apps/flame-defi/app/earn/components/Table/TablePagination.tsx +++ b/apps/flame-defi/app/earn/components/Table/TablePagination.tsx @@ -23,7 +23,7 @@ export const TablePagination = () => { }, [data?.vaults?.pageInfo?.countTotal]); return ( - + diff --git a/apps/flame-defi/app/earn/components/Table/TableSearch.tsx b/apps/flame-defi/app/earn/components/Table/TableSearch.tsx index 07d2c08..f23ef0b 100644 --- a/apps/flame-defi/app/earn/components/Table/TableSearch.tsx +++ b/apps/flame-defi/app/earn/components/Table/TableSearch.tsx @@ -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(null); + + const [isFocused, setIsFocused] = useState(false); + const { search, setSearch, @@ -12,16 +18,35 @@ export const TableSearch = () => { return ( - setSearch(e.target.value)} - type="text" - placeholder="Search vaults" - startAdornment={} - className="w-[200px]" - /> +
+ setSearch(e.target.value)} + onBlur={() => setIsFocused(false)} + onFocus={() => setIsFocused(true)} + type="text" + startAdornment={ +
{ + if (inputRef.current && !isFocused) { + setIsFocused(true); + inputRef.current.focus(); + } + }} + > + +
+ } + 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", + )} + /> +
); }; diff --git a/apps/flame-defi/app/earn/components/Table/index.ts b/apps/flame-defi/app/earn/components/Table/index.ts index 3ce795e..3f88dcf 100644 --- a/apps/flame-defi/app/earn/components/Table/index.ts +++ b/apps/flame-defi/app/earn/components/Table/index.ts @@ -1,4 +1,3 @@ export { Table } from "./Table"; -export { TableCard } from "./TableCard"; export { TablePagination } from "./TablePagination"; export { TableSearch } from "./TableSearch"; diff --git a/apps/flame-defi/app/earn/contexts/TableContext.tsx b/apps/flame-defi/app/earn/contexts/TableContext.tsx index 1cf5681..bb8ccea 100644 --- a/apps/flame-defi/app/earn/contexts/TableContext.tsx +++ b/apps/flame-defi/app/earn/contexts/TableContext.tsx @@ -77,14 +77,16 @@ export const TableContextProvider = ({ children }: PropsWithChildren) => { cell: ({ row }) => { return (
- {row.original.asset.logoURI && ( + {row.original.asset.logoURI ? ( {row.original.name} + ) : ( +
)}
@@ -119,7 +121,7 @@ export const TableContextProvider = ({ children }: PropsWithChildren) => { > diff --git a/apps/flame-defi/app/earn/sections/TableSection/TableSection.tsx b/apps/flame-defi/app/earn/sections/TableSection/TableSection.tsx index 866310e..cd91ffa 100644 --- a/apps/flame-defi/app/earn/sections/TableSection/TableSection.tsx +++ b/apps/flame-defi/app/earn/sections/TableSection/TableSection.tsx @@ -1,9 +1,6 @@ -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 = () => { @@ -11,25 +8,30 @@ export const TableSection = () => { return (
-
+
+

Lend

+ +
+ +
{status === "error" && ( - + {`We couldn't fetch vault data. Please try again later.`} - + )} {status === "empty" && ( - + {`No vaults found.`} - + )} {status === "success" && ( <> - + - +
diff --git a/packages/ui/src/shadcn-primitives/input.tsx b/packages/ui/src/shadcn-primitives/input.tsx index 244aa50..12edfce 100644 --- a/packages/ui/src/shadcn-primitives/input.tsx +++ b/packages/ui/src/shadcn-primitives/input.tsx @@ -19,7 +19,7 @@ const Input = React.forwardRef(