mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
fix(frontend): more wallet popover fixes (#11285)
## Changes 🏗️ <img width="800" height="547" alt="Screenshot 2025-10-29 at 22 11 35" src="https://github.com/user-attachments/assets/5c700ddc-d770-48ef-9847-7e652c5dedcb" /> <br /><br /> - Use [`react-currency-input-field`](https://www.npmjs.com/package/react-currency-input-field) for `<Input type="amount" />` under the hood - so it formats numbers nicely with `,` and `.` - Simplify form logic - Make the popover cover the trigger button when open - Re-organize imports - Show a `$` prefix in front of the amount inputs ## Checklist 📋 ### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Login - [x] Open the wallet with credits enabled - [x] Play with the inputs --------- Co-authored-by: Swifty <craigswift13@gmail.com>
This commit is contained in:
@@ -84,6 +84,7 @@
|
||||
"nuqs": "2.4.3",
|
||||
"party-js": "2.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-currency-input-field": "4.0.3",
|
||||
"react-day-picker": "9.8.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-drag-drop-files": "2.4.0",
|
||||
|
||||
12
autogpt_platform/frontend/pnpm-lock.yaml
generated
12
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -185,6 +185,9 @@ importers:
|
||||
react:
|
||||
specifier: 18.3.1
|
||||
version: 18.3.1
|
||||
react-currency-input-field:
|
||||
specifier: 4.0.3
|
||||
version: 4.0.3(react@18.3.1)
|
||||
react-day-picker:
|
||||
specifier: 9.8.1
|
||||
version: 9.8.1(react@18.3.1)
|
||||
@@ -6247,6 +6250,11 @@ packages:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
react-currency-input-field@4.0.3:
|
||||
resolution: {integrity: sha512-alimHDX5tplPsNB3jEAW7qjlJ76RfBAc/p8yru3cTiAYslj3oJ+KNnk788IZYe6ja3cAuH26v047lMzadh47ow==}
|
||||
peerDependencies:
|
||||
react: ^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
react-day-picker@9.8.1:
|
||||
resolution: {integrity: sha512-kMcLrp3PfN/asVJayVv82IjF3iLOOxuH5TNFWezX6lS/T8iVRFPTETpHl3TUSTH99IDMZLubdNPJr++rQctkEw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -14308,6 +14316,10 @@ snapshots:
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
react-currency-input-field@4.0.3(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
react-day-picker@9.8.1(react@18.3.1):
|
||||
dependencies:
|
||||
'@date-fns/tz': 1.2.0
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Eye, EyeSlash } from "@phosphor-icons/react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import CurrencyInput from "react-currency-input-field";
|
||||
import { Text } from "../Text/Text";
|
||||
import { useInput } from "./useInput";
|
||||
|
||||
@@ -30,6 +31,8 @@ export interface TextFieldProps extends Omit<InputProps, "size"> {
|
||||
| "datetime-local";
|
||||
// Textarea-specific props
|
||||
rows?: number;
|
||||
amountPrefix?: string;
|
||||
amountSuffix?: string;
|
||||
}
|
||||
|
||||
export function Input({
|
||||
@@ -42,13 +45,16 @@ export function Input({
|
||||
error,
|
||||
size = "medium",
|
||||
wrapperClassName,
|
||||
amountPrefix,
|
||||
amountSuffix,
|
||||
...props
|
||||
}: TextFieldProps) {
|
||||
const { handleInputChange, handleTextareaChange } = useInput({
|
||||
type: props.type,
|
||||
onChange: props.onChange,
|
||||
decimalCount,
|
||||
});
|
||||
const { handleInputChange, handleTextareaChange, handleAmountValueChange } =
|
||||
useInput({
|
||||
type: props.type,
|
||||
onChange: props.onChange,
|
||||
decimalCount,
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const isPasswordType = props.type === "password";
|
||||
@@ -110,6 +116,44 @@ export function Input({
|
||||
);
|
||||
}
|
||||
|
||||
if (props.type === "amount") {
|
||||
return (
|
||||
<CurrencyInput
|
||||
className={cn(
|
||||
baseStyles,
|
||||
errorStyles,
|
||||
// Size variants
|
||||
size === "small" && [
|
||||
"h-[2.25rem]",
|
||||
"py-2",
|
||||
"text-sm leading-[22px]",
|
||||
"placeholder:text-sm placeholder:leading-[22px]",
|
||||
],
|
||||
size === "medium" && ["h-[2.875rem]", "py-2.5"],
|
||||
)}
|
||||
placeholder={placeholder || label}
|
||||
// CurrencyInput gives unformatted numeric string in value param
|
||||
onValueChange={handleAmountValueChange}
|
||||
value={props.value as string | number | undefined}
|
||||
id={props.id}
|
||||
name={props.name}
|
||||
disabled={props.disabled}
|
||||
inputMode="decimal"
|
||||
decimalsLimit={decimalCount ?? 4}
|
||||
allowDecimals={decimalCount !== 0}
|
||||
groupSeparator=","
|
||||
decimalSeparator="."
|
||||
allowNegativeValue
|
||||
{...(hideLabel ? { "aria-label": label } : {})}
|
||||
// Pass through common handlers
|
||||
onBlur={props.onBlur as any}
|
||||
onFocus={props.onFocus as any}
|
||||
prefix={amountPrefix}
|
||||
suffix={amountSuffix}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseInput
|
||||
className={cn(
|
||||
|
||||
@@ -65,6 +65,20 @@ export function useInput(args: ExtendedInputProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleAmountValueChange(value?: string) {
|
||||
if (!args.onChange) return;
|
||||
const processedValue = value ?? "";
|
||||
|
||||
const syntheticEvent = {
|
||||
// We only need target.value for our consumers
|
||||
target: {
|
||||
value: processedValue,
|
||||
},
|
||||
} as unknown as React.ChangeEvent<HTMLInputElement>;
|
||||
|
||||
args.onChange(syntheticEvent);
|
||||
}
|
||||
|
||||
function handleTextareaChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
if (args.onChange) {
|
||||
// Create synthetic event with HTMLInputElement-like target for compatibility
|
||||
@@ -80,5 +94,5 @@ export function useInput(args: ExtendedInputProps) {
|
||||
}
|
||||
}
|
||||
|
||||
return { handleInputChange, handleTextareaChange };
|
||||
return { handleInputChange, handleTextareaChange, handleAmountValueChange };
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/__legacy__/ui/popover";
|
||||
import { ScrollArea } from "@/components/__legacy__/ui/scroll-area";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import useCredits from "@/hooks/useCredits";
|
||||
import { OnboardingStep } from "@/lib/autogpt-server-api";
|
||||
@@ -17,8 +18,7 @@ import { PopoverClose } from "@radix-ui/react-popover";
|
||||
import { X } from "lucide-react";
|
||||
import * as party from "party-js";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ScrollArea } from "../../../../../components/__legacy__/ui/scroll-area";
|
||||
import WalletRefill from "./components/WalletRefill";
|
||||
import { WalletRefill } from "./components/WalletRefill";
|
||||
import { TaskGroups } from "./components/WalletTaskGroups";
|
||||
|
||||
export interface Task {
|
||||
@@ -360,9 +360,8 @@ export function Wallet() {
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={12}
|
||||
collisionPadding={16}
|
||||
className={cn("z-50 w-[28.5rem] px-[0.625rem] py-2")}
|
||||
className={cn("relative -top-12 z-50 w-[28.5rem] px-[0.625rem] py-2")}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="mx-1 flex items-center justify-between border-b border-zinc-200 pb-3">
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/__legacy__/ui/form";
|
||||
import { Form, FormField } from "@/components/__legacy__/ui/form";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/__legacy__/ui/tabs";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import {
|
||||
useToast,
|
||||
useToastOnFail,
|
||||
} from "@/components/molecules/Toast/use-toast";
|
||||
import useCredits from "@/hooks/useCredits";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
@@ -49,12 +42,14 @@ const autoRefillSchema = z
|
||||
path: ["refillAmount"],
|
||||
});
|
||||
|
||||
export default function WalletRefill() {
|
||||
export function WalletRefill() {
|
||||
const { toast } = useToast();
|
||||
const toastOnFail = useToastOnFail();
|
||||
|
||||
const { requestTopUp, autoTopUpConfig, updateAutoTopUpConfig } = useCredits({
|
||||
fetchInitialAutoTopUpConfig: true,
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const topUpForm = useForm<z.infer<typeof topUpSchema>>({
|
||||
@@ -132,48 +127,29 @@ export default function WalletRefill() {
|
||||
Enter an amount (min. $5) and add credits instantly.
|
||||
</div>
|
||||
<Form {...topUpForm}>
|
||||
<form onSubmit={topUpForm.handleSubmit(submitTopUp)}>
|
||||
<form
|
||||
onSubmit={topUpForm.handleSubmit(submitTopUp)}
|
||||
className="my-4"
|
||||
>
|
||||
<FormField
|
||||
control={topUpForm.control}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6 mt-4">
|
||||
<FormLabel className="font-sans text-sm font-medium leading-snug text-zinc-800">
|
||||
Amount
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
<Input
|
||||
className={cn(
|
||||
"mt-2 rounded-3xl border-0 bg-white py-2 pl-6 pr-4 font-sans outline outline-1 outline-zinc-300",
|
||||
"focus:outline-2 focus:outline-offset-0 focus:outline-violet-700",
|
||||
)}
|
||||
label="Amount"
|
||||
id="amount"
|
||||
type="number"
|
||||
step="1"
|
||||
{...field}
|
||||
/>
|
||||
<span className="absolute left-10 -translate-y-9 text-sm text-zinc-500">
|
||||
$
|
||||
</span>
|
||||
</>
|
||||
</FormControl>
|
||||
<FormMessage className="mt-2 font-sans text-xs font-normal leading-tight" />
|
||||
</FormItem>
|
||||
<Input
|
||||
label="Amount"
|
||||
type="amount"
|
||||
size="small"
|
||||
decimalCount={0}
|
||||
id={field.name}
|
||||
error={topUpForm.formState.errors.amount?.message}
|
||||
amountPrefix="$"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
className={cn(
|
||||
"mb-2 inline-flex h-10 w-24 items-center justify-center rounded-3xl bg-zinc-800 px-4 py-2",
|
||||
"font-sans text-sm font-medium leading-snug text-white",
|
||||
"transition-colors duration-200 hover:bg-zinc-700 disabled:bg-zinc-500",
|
||||
)}
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Button type="submit" disabled={isLoading} size="small">
|
||||
Top up
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</TabsContent>
|
||||
@@ -188,78 +164,50 @@ export default function WalletRefill() {
|
||||
<Form {...autoRefillForm}>
|
||||
<form
|
||||
onSubmit={autoRefillForm.handleSubmit(submitAutoTopUpConfig)}
|
||||
className="my-6"
|
||||
>
|
||||
<FormField
|
||||
control={autoRefillForm.control}
|
||||
name="threshold"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6 mt-4">
|
||||
<FormLabel className="font-sans text-sm font-medium leading-snug text-zinc-800">
|
||||
Refill when balance drops below:
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
<Input
|
||||
className={cn(
|
||||
"mt-2 rounded-3xl border-0 bg-white py-2 pl-6 pr-4 font-sans outline outline-1 outline-zinc-300",
|
||||
"focus:outline-2 focus:outline-offset-0 focus:outline-violet-700",
|
||||
)}
|
||||
type="number"
|
||||
step="1"
|
||||
label="Refill when balance drops below:"
|
||||
id="threshold"
|
||||
{...field}
|
||||
/>
|
||||
<span className="absolute left-10 -translate-y-9 text-sm text-zinc-500">
|
||||
$
|
||||
</span>
|
||||
</>
|
||||
</FormControl>
|
||||
<FormMessage className="mt-2 font-sans text-xs font-normal leading-tight" />
|
||||
</FormItem>
|
||||
<Input
|
||||
type="amount"
|
||||
label="Refill when balance drops below:"
|
||||
id={field.name}
|
||||
size="small"
|
||||
decimalCount={0}
|
||||
error={autoRefillForm.formState.errors.threshold?.message}
|
||||
amountPrefix="$"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={autoRefillForm.control}
|
||||
name="refillAmount"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel className="font-sans text-sm font-medium leading-snug text-zinc-800">
|
||||
Add this amount:
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
<Input
|
||||
className={cn(
|
||||
"mt-2 rounded-3xl border-0 bg-white py-2 pl-6 pr-4 font-sans outline outline-1 outline-zinc-300",
|
||||
"focus:outline-2 focus:outline-offset-0 focus:outline-violet-700",
|
||||
)}
|
||||
type="number"
|
||||
step="1"
|
||||
label="Add this amount:"
|
||||
id="refillAmount"
|
||||
{...field}
|
||||
/>
|
||||
<span className="absolute left-10 -translate-y-9 text-sm text-zinc-500">
|
||||
$
|
||||
</span>
|
||||
</>
|
||||
</FormControl>
|
||||
<FormMessage className="mt-2 font-sans text-xs font-normal leading-tight" />
|
||||
</FormItem>
|
||||
<Input
|
||||
type="amount"
|
||||
label="Add this amount:"
|
||||
size="small"
|
||||
decimalCount={0}
|
||||
id={field.name}
|
||||
error={
|
||||
autoRefillForm.formState.errors.refillAmount?.message
|
||||
}
|
||||
amountPrefix="$"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
className={cn(
|
||||
"mb-4 inline-flex h-10 w-40 items-center justify-center rounded-3xl bg-zinc-800 px-4 py-2",
|
||||
"font-sans text-sm font-medium leading-snug text-white",
|
||||
"transition-colors duration-200 hover:bg-zinc-700 disabled:bg-zinc-500",
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
size="small"
|
||||
className="mt-5"
|
||||
>
|
||||
Enable Auto-refill
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</TabsContent>
|
||||
|
||||
Reference in New Issue
Block a user