Skip to content

Commit

Permalink
"see more" payment options
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewliu08 committed Sep 19, 2024
1 parent ca78129 commit 0db8d6b
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 90 deletions.
1 change: 1 addition & 0 deletions apps/crepe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@heroicons/react": "^2.1.5",
"@neynar/nodejs-sdk": "^1.12.0",
"@rainbow-me/rainbowkit": "^1.1.1",
"@tailwindcss/forms": "^0.5.9",
"@tanstack/react-query": "^5.51.11",
"@trpc/client": "^11.0.0-next-beta.318",
"@trpc/react-query": "^11.0.0-next-beta.318",
Expand Down
142 changes: 89 additions & 53 deletions apps/crepe/src/app/checkout/components/payment-method.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { usePaymentInfo } from "../payment-info-context";
import { trpc } from "../trpc";
import { Notification } from "./notification";
import RightPane from "./right-pane";
import { SearchPaletteItem, SearchPaletteWithGroups } from "./search-palette";
import { PaymentMethodsSkeleton } from "./skeletons";

import { formatAmountDecimals, truncateAddress } from "@/src/utils/format";
Expand Down Expand Up @@ -129,17 +130,14 @@ export default function PaymentMethods() {
<section aria-labelledby="payment-methods" className="w-5/6 mx-auto">
{(ensName || address) && (
<h2 className="text-lg font-medium text-gray-900 mb-8">
Pay with {ensName || truncateAddress(address!)}:
Pay with {ensName || truncateAddress(address!)}
</h2>
)}
<PaymentOptionList
tokenBalances={paymentOptions?.tokenBalances || []}
isLoading={isLoadingAmounts}
handleTransfer={handleTransfer}
/>
<div className="flex justify-center w-full mt-5">
<Button variant="tertiary">See more</Button>
</div>
</section>
{/* TODO: show this notification using a hook */}
<Notification
Expand All @@ -161,31 +159,79 @@ function PaymentOptionList({
isLoading: boolean;
handleTransfer: (tokenBalance: TokenBalance) => Promise<void>;
}) {
const [showSearchPalette, setShowSearchPalette] = useState(false);

const visibleOptions = tokenBalances.slice(0, 3);

return (
<ul className="flex flex-col gap-4">
{isLoading ? (
<PaymentMethodsSkeleton />
) : (
tokenBalances.map((tokenBalance, index) => (
<li key={index}>
<PaymentOption
tokenBalance={tokenBalance}
onClick={handleTransfer}
/>
</li>
))
<div>
<ul className="flex flex-col gap-4">
{isLoading ? (
<PaymentMethodsSkeleton />
) : (
visibleOptions.map((tokenBalance, index) => (
<li key={index}>
<PaymentOptionButton
tokenBalance={tokenBalance}
onClick={handleTransfer}
/>
</li>
))
)}
</ul>
<div className="flex justify-center w-full mt-5">
<Button variant="tertiary" onClick={() => setShowSearchPalette(true)}>
See more
</Button>
</div>
{showSearchPalette && (
<SearchPaletteWithGroups
items={tokenBalances.map((tokenBalance) =>
tokenBalanceToSearchPaletteItem(tokenBalance, handleTransfer)
)}
open={showSearchPalette}
onClose={() => setShowSearchPalette(false)}
/>
)}
</ul>
</div>
);
}

function PaymentOption({
function tokenBalanceToSearchPaletteItem(
tokenBalance: TokenBalance,
handleTransfer: (tokenBalance: TokenBalance) => Promise<void>
): SearchPaletteItem {
return {
filterString: `${tokenBalance.token.symbol} on ${getChainName(
tokenBalance.token.chainId
)}`,
group: getChainName(tokenBalance.token.chainId),
content: <PaymentOption tokenBalance={tokenBalance} />,
onClick: () => {
handleTransfer(tokenBalance);
},
};
}

function PaymentOptionButton({
tokenBalance,
onClick,
}: {
tokenBalance: TokenBalance;
onClick: (tokenBalance: TokenBalance) => Promise<void>;
}) {
const handleClick = async () => {
await onClick(tokenBalance);
};

return (
<Button variant="outline" onClick={handleClick} className="w-full p-0">
<PaymentOption tokenBalance={tokenBalance} />
</Button>
);
}

function PaymentOption({ tokenBalance }: { tokenBalance: TokenBalance }) {
const formattedBalance = formatAmountDecimals(
BigInt(tokenBalance.tokenBalance),
tokenBalance.token
Expand All @@ -196,48 +242,38 @@ function PaymentOption({
);
const chainName = getChainName(tokenBalance.token.chainId);

const handleClick = async () => {
await onClick(tokenBalance);
};

return (
<Button
variant="outline"
onClick={handleClick}
className="w-full p-0 text-left"
>
<div className="flex items-center w-full px-4 py-2">
<div className="flex items-center gap-5 w-full">
<div className="relative">
<div className="flex items-center w-full px-4 py-2 text-left">
<div className="flex items-center gap-5 w-full">
<div className="relative">
<Image
src={tokenBalance.token.logoURI || ""}
alt={tokenBalance.token.symbol}
width={32}
height={32}
className="w-8 h-8"
/>
<div className="absolute -bottom-0.5 -right-0.5 bg-white rounded-md p-[0.1rem]">
<Image
src={tokenBalance.token.logoURI || ""}
alt={tokenBalance.token.symbol}
width={32}
height={32}
className="w-8 h-8"
src={chainToLogo[tokenBalance.token.chainId]}
alt={tokenBalance.token.chainId.toString()}
width={16}
height={16}
className="h-3.5 w-3.5 rounded-md"
/>
<div className="absolute -bottom-0.5 -right-0.5 bg-white rounded-md p-[0.1rem]">
<Image
src={chainToLogo[tokenBalance.token.chainId]}
alt={tokenBalance.token.chainId.toString()}
width={16}
height={16}
className="h-3.5 w-3.5 rounded-md"
/>
</div>
</div>
</div>

<div className="flex flex-col flex-grow">
<div className="text-base font-medium">
{formattedAmount} {tokenBalance.token.symbol} on{" "}
<span className="capitalize">{chainName}</span>
</div>
<div className="text-sm text-gray-700 font-light">
Balance: {formattedBalance}
</div>
<div className="flex flex-col flex-grow">
<div className="text-base font-medium">
{formattedAmount} {tokenBalance.token.symbol} on{" "}
<span className="capitalize">{chainName}</span>
</div>
<div className="text-sm text-gray-700 font-light">
Balance: {formattedBalance}
</div>
</div>
</div>
</Button>
</div>
);
}
168 changes: 168 additions & 0 deletions apps/crepe/src/app/checkout/components/search-palette.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"use client";

import {
Combobox,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
Dialog,
DialogBackdrop,
DialogPanel,
} from "@headlessui/react";
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
import { useState } from "react";

export type SearchPaletteItem = {
filterString: string;
group: string;
content: React.ReactNode;
onClick: () => void;
};

export function SearchPaletteWithGroups({
items,
open,
onClose,
}: {
items: SearchPaletteItem[];
open: boolean;
onClose: () => void;
}) {
const [query, setQuery] = useState("");

const filteredItems =
query === ""
? []
: items.filter((item) => {
return item.filterString.toLowerCase().includes(query.toLowerCase());
});

const groups = items.reduce<Record<string, SearchPaletteItem[]>>(
(groups, item) => {
return {
...groups,
[item.group]: [...(groups[item.group] || []), item],
};
},
{}
);

const filteredGroups = filteredItems.reduce<
Record<string, SearchPaletteItem[]>
>((groups, item) => {
return {
...groups,
[item.group]: [...(groups[item.group] || []), item],
};
}, {});

return (
<Dialog
transition
className="relative z-10"
open={open}
onClose={() => {
onClose();
setQuery("");
}}
>
<DialogBackdrop
transition
className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in"
/>

<div className="fixed inset-0 z-10 w-screen overflow-y-auto p-4 sm:p-6 md:p-20">
<DialogPanel
transition
className="mx-auto max-w-xl transform overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all data-[closed]:scale-95 data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in"
>
<Combobox
onChange={(item: SearchPaletteItem) => {
if (item) item.onClick();
}}
>
<div className="relative">
<MagnifyingGlassIcon
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-gray-400"
aria-hidden="true"
/>
<ComboboxInput
autoFocus
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm"
placeholder="Search..."
onChange={(event) => setQuery(event.target.value)}
onBlur={() => setQuery("")}
/>
</div>

{/* Query is empty. Show all groups */}
{query === "" && (
<ComboboxOptions
static
as="ul"
className="max-h-80 scroll-pb-2 scroll-pt-11 space-y-2 overflow-y-auto pb-2"
>
{Object.entries(groups).map(([group, items]) => (
<SearchGroup key={group} group={group} items={items} />
))}
</ComboboxOptions>
)}

{/* Query is not empty and there are results. Show filtered groups */}
{filteredItems.length > 0 && (
<ComboboxOptions
static
as="ul"
className="max-h-80 scroll-pb-2 scroll-pt-11 space-y-2 overflow-y-auto pb-2"
>
{Object.entries(filteredGroups).map(([group, items]) => (
<SearchGroup key={group} group={group} items={items} />
))}
</ComboboxOptions>
)}

{/* Query is not empty and there are no results. Show no results message */}
{query !== "" && filteredItems.length === 0 && (
<div className="border-t border-gray-100 px-6 pt-8 pb-14 text-center text-sm sm:px-14">
<p className="mt-4 font-semibold text-gray-900">
No results found
</p>
<p className="mt-2 text-gray-500">
This address does not have sufficient balance for this search
result.
</p>
</div>
)}
</Combobox>
</DialogPanel>
</div>
</Dialog>
);
}

function SearchGroup({
group,
items,
}: {
group: string;
items: SearchPaletteItem[];
}) {
return (
<li key={group}>
<h2 className="bg-gray-100 px-4 py-2.5 text-xs font-semibold text-gray-900 capitalize">
{group}
</h2>
<ul>
{items.map((item, index) => (
<ComboboxOption
key={index}
value={item}
className="cursor-pointer select-none px-2 py-1 data-[focus]:bg-gray-100"
>
{item.content}
</ComboboxOption>
))}
</ul>
</li>
);
}
2 changes: 1 addition & 1 deletion apps/crepe/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ const config: Config = {
},
},
},
plugins: [],
plugins: [require("@tailwindcss/forms")],
};
export default config;
Loading

0 comments on commit 0db8d6b

Please sign in to comment.