Skip to content
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
44 changes: 44 additions & 0 deletions frontend/src/components/AmountTypeToggle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { ArrowRightLeft } from 'lucide-react';

export const AmountTypeToggle = ({ value, onChange, className = "" }) => {
return (
<div className={`inline-flex items-center gap-1 p-1 bg-gray-100 rounded-lg border border-gray-200 ${className}`}>
<button
type="button"
onClick={() => onChange("amount")}
aria-pressed={value === "amount"}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all duration-200 ${
value === "amount"
? "bg-white text-gray-900 shadow-sm border border-gray-200"
: "text-gray-500 hover:text-gray-700"
}`}
>
Flat
</button>

<button
type="button"
onClick={() => onChange(value === "amount" ? "percentage" : "amount")}
aria-label={`Toggle type, currently ${value}`}
className="p-1 text-slate-800 bg-gray-200 hover:bg-gray-300 hover:text-black transition-colors rounded-full flex-shrink-0"
title="Toggle type"
>
<ArrowRightLeft className="w-4 h-4" />
</button>

<button
type="button"
onClick={() => onChange("percentage")}
aria-pressed={value === "percentage"}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all duration-200 ${
Comment thread
coderabbitai[bot] marked this conversation as resolved.
value === "percentage"
? "bg-white text-gray-900 shadow-sm border border-gray-200"
: "text-gray-500 hover:text-gray-700"
}`}
>
%
</button>
</div>
);
};
67 changes: 55 additions & 12 deletions frontend/src/components/TokenPicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,24 @@ const TokenItem = memo(function TokenItem({
query,
isSelected,
onSelect,
disabled = false,
isLoading = false,
}) {
const handleClick = useCallback(() => {
if (disabled) return;
onSelect(token);
}, [onSelect, token]);
}, [disabled, onSelect, token]);

return (
<button
type="button"
onClick={handleClick}
disabled={disabled}
className={cn(
"w-full flex items-center gap-3 p-3 rounded-lg text-left",
"hover:bg-gray-50 focus:bg-gray-50 focus:outline-none transition-colors duration-200",
"border border-transparent hover:border-gray-200",
disabled && "opacity-60 cursor-not-allowed",
isSelected && "bg-blue-50 border-blue-200 ring-1 ring-blue-200"
)}
>
Expand Down Expand Up @@ -162,9 +167,11 @@ const TokenItem = memo(function TokenItem({
</div>
</div>

{isSelected && (
{isLoading ? (
<Loader2 className="w-4 h-4 animate-spin text-blue-600 flex-shrink-0" />
) : isSelected ? (
<div className="w-2 h-2 rounded-full bg-blue-600 flex-shrink-0" />
)}
) : null}
</button>
);
});
Expand All @@ -181,6 +188,8 @@ export function TokenPicker({
onCustomTokenClick,
}) {
const [open, setOpen] = useState(false);
const [isSelecting, setIsSelecting] = useState(false);
const [selectingTokenAddress, setSelectingTokenAddress] = useState(null);
const inputRef = useRef(null);
const {
tokens,
Expand All @@ -202,26 +211,43 @@ export function TokenPicker({
}
}, [open, setQuery]);

const handleSelect = (token) => {
onSelect(token);
setOpen(false);
const isOnTestnet = isTestnet(chainId);
const isInteractionDisabled = disabled || isOnTestnet || isSelecting;

const handleSelect = async (token) => {
if (isInteractionDisabled) return;

const tokenAddress = token.contract_address || token.address;
setIsSelecting(true);
setSelectingTokenAddress(tokenAddress);

try {
const shouldClose = await Promise.resolve(onSelect(token));
if (shouldClose !== false) {
setOpen(false);
}
} catch {
// Parent handler is responsible for surfacing a user-facing error.
} finally {
setIsSelecting(false);
setSelectingTokenAddress(null);
}
};

const handleCustomTokenClick = () => {
if (isInteractionDisabled) return;
if (onCustomTokenClick) {
onCustomTokenClick();
}
setOpen(false);
};

const isOnTestnet = isTestnet(chainId);

return (
<>
<Button
type="button"
variant="outline"
disabled={disabled || isOnTestnet}
disabled={isInteractionDisabled}
onClick={() => setOpen(true)}
className={cn(
"h-12 px-4 justify-between bg-white hover:bg-gray-50 border border-gray-300 text-gray-900",
Expand Down Expand Up @@ -271,7 +297,12 @@ export function TokenPicker({
</div>
)}

<Modal isOpen={open} onClose={() => setOpen(false)}>
<Modal
isOpen={open}
onClose={() => {
if (!isSelecting) setOpen(false);
}}
>
<div className="p-6">
<div className="flex items-center gap-2 mb-4">
<Coins className="w-5 h-5 text-blue-600" />
Expand All @@ -280,7 +311,10 @@ export function TokenPicker({
</h2>
<button
type="button"
onClick={() => setOpen(false)}
onClick={() => {
if (!isSelecting) setOpen(false);
}}
disabled={isSelecting}
className="ml-auto p-1 hover:bg-gray-100 rounded-full transition-colors"
>
<X className="w-5 h-5 text-gray-500" />
Expand All @@ -294,14 +328,16 @@ export function TokenPicker({
ref={inputRef}
placeholder={placeholder}
value={query}
disabled={isInteractionDisabled}
onChange={(e) => setQuery(e.target.value)}
className="pl-10 pr-10 h-12 border-gray-300 text-black"
/>
{query && (
<button
type="button"
disabled={isInteractionDisabled}
onClick={() => setQuery("")}
className="absolute right-3 top-1/2 transform -translate-y-1/2 p-1 hover:bg-gray-100 rounded-full transition-colors"
className="absolute right-3 top-1/2 transform -translate-y-1/2 p-1 hover:bg-gray-100 rounded-full transition-colors disabled:opacity-50"
>
<X className="w-4 h-4 text-gray-400" />
</button>
Expand Down Expand Up @@ -376,6 +412,12 @@ export function TokenPicker({
key={token.contract_address}
token={token}
query={query}
disabled={isSelecting}
isLoading={
isSelecting &&
selectingTokenAddress ===
(token.contract_address || token.address)
}
isSelected={
selected?.contract_address === token.contract_address ||
selected?.address === token.contract_address
Expand All @@ -392,6 +434,7 @@ export function TokenPicker({
<button
type="button"
onClick={handleCustomTokenClick}
disabled={isSelecting}
className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors border-2 border-dashed border-gray-200 hover:border-gray-300"
>
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
Expand Down
17 changes: 13 additions & 4 deletions frontend/src/components/ui/copyButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const CopyButton = ({ textToCopy, className = "" }) => {

const handleCopy = async (e) => {
e.stopPropagation();
e.preventDefault();
try {
await navigator.clipboard.writeText(textToCopy);
setCopied(true);
Expand All @@ -16,11 +17,19 @@ const CopyButton = ({ textToCopy, className = "" }) => {
}
};

const handleKeyDown = (e) => {
if (e.key === "Enter" || e.key === " ") {
handleCopy(e);
}
};

return (
<button
type="button"
<span
role="button"
tabIndex={0}
onClick={handleCopy}
className={`inline-flex items-center gap-1 px-2 py-1 text-xs rounded hover:bg-gray-100 transition-colors ${className}`}
onKeyDown={handleKeyDown}
className={`inline-flex cursor-pointer items-center gap-1 px-2 py-1 text-xs rounded hover:bg-gray-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 ${className}`}
title="Copy address"
>
{copied ? (
Expand All @@ -34,7 +43,7 @@ const CopyButton = ({ textToCopy, className = "" }) => {
<span className="text-gray-500">Copy</span>
</>
)}
</button>
</span>
);
};

Expand Down
Loading
Loading