PayFromSelect.tsx raw

   1  "use client";
   2  
   3  import { WalletIcon } from "lucide-react";
   4  import React from "react";
   5  import AppAvatar from "src/components/AppAvatar";
   6  import {
   7    Combobox,
   8    ComboboxContent,
   9    ComboboxEmpty,
  10    ComboboxInput,
  11    ComboboxItem,
  12    ComboboxList,
  13    useComboboxAnchor,
  14  } from "src/components/ui/combobox";
  15  import { InputGroupAddon } from "src/components/ui/input-group";
  16  import { Label } from "src/components/ui/label";
  17  import { PAY_FROM_SELECT_APPS_LIMIT } from "src/constants";
  18  import { useApps } from "src/hooks/useApps";
  19  import { getAppDisplayName } from "src/lib/utils";
  20  import { App } from "src/types";
  21  
  22  const LIGHTNING_BALANCE = "lightning-balance";
  23  const LIGHTNING_BALANCE_LABEL = "Lightning Balance";
  24  
  25  type PayFromOption = {
  26    value: string;
  27    label: string;
  28    app?: App;
  29  };
  30  
  31  type Props = {
  32    appId?: number;
  33    onChange(appId: number | undefined): void;
  34  };
  35  
  36  function LightningOption() {
  37    return (
  38      <div className="flex items-center gap-3">
  39        <div className="flex size-6 items-center justify-center rounded-lg bg-muted">
  40          <WalletIcon className="size-3 text-muted-foreground" />
  41        </div>
  42        <div>{LIGHTNING_BALANCE_LABEL}</div>
  43      </div>
  44    );
  45  }
  46  
  47  function AppOption({ app }: { app: App }) {
  48    return (
  49      <div className="flex items-center gap-3">
  50        <AppAvatar app={app} className="size-6 rounded-lg" />
  51        <div className="min-w-0">
  52          <div>{getAppDisplayName(app.name)}</div>
  53        </div>
  54      </div>
  55    );
  56  }
  57  
  58  export default function PayFromSelect({ appId, onChange }: Props) {
  59    const anchorRef = useComboboxAnchor();
  60    const [search, setSearch] = React.useState("");
  61    const { data: appsData } = useApps(PAY_FROM_SELECT_APPS_LIMIT, undefined, {
  62      name: search,
  63    });
  64  
  65    const apps = React.useMemo(
  66      () =>
  67        [...(appsData?.apps || [])]
  68          .filter((app) => app.scopes.includes("pay_invoice"))
  69          .sort((a, b) =>
  70            getAppDisplayName(a.name).localeCompare(getAppDisplayName(b.name))
  71          ),
  72      [appsData?.apps]
  73    );
  74  
  75    const options = React.useMemo<PayFromOption[]>(
  76      () => [
  77        { value: LIGHTNING_BALANCE, label: LIGHTNING_BALANCE_LABEL },
  78        ...apps.map((app) => ({
  79          value: app.id.toString(),
  80          label: getAppDisplayName(app.name),
  81          app,
  82        })),
  83      ],
  84      [apps]
  85    );
  86  
  87    const selectedOption = options.find((opt) =>
  88      appId ? opt.value === appId.toString() : undefined
  89    );
  90  
  91    return (
  92      <div className="grid w-full gap-1.5">
  93        <Label>Pay from</Label>
  94        <Combobox
  95          items={options}
  96          value={selectedOption}
  97          itemToStringValue={(option) => option.value}
  98          onInputValueChange={setSearch}
  99          onValueChange={(option) =>
 100            onChange(
 101              option?.value === LIGHTNING_BALANCE
 102                ? undefined
 103                : Number(option?.value)
 104            )
 105          }
 106        >
 107          <div ref={anchorRef} className="w-full">
 108            <ComboboxInput placeholder={LIGHTNING_BALANCE_LABEL}>
 109              <InputGroupAddon>
 110                {selectedOption?.app ? (
 111                  <AppAvatar
 112                    app={selectedOption.app}
 113                    className="size-6 rounded-full"
 114                  />
 115                ) : (
 116                  <div className="flex size-6 items-center justify-center rounded-sm bg-muted">
 117                    <WalletIcon className="size-3 text-muted-foreground" />
 118                  </div>
 119                )}
 120              </InputGroupAddon>
 121            </ComboboxInput>
 122          </div>
 123          <ComboboxContent anchor={anchorRef}>
 124            <ComboboxEmpty>No connections found.</ComboboxEmpty>
 125            <ComboboxList>
 126              {(option: PayFromOption) => (
 127                <ComboboxItem key={option.value} value={option}>
 128                  {option.app ? (
 129                    <AppOption app={option.app} />
 130                  ) : (
 131                    <LightningOption />
 132                  )}
 133                </ComboboxItem>
 134              )}
 135            </ComboboxList>
 136          </ComboboxContent>
 137        </Combobox>
 138      </div>
 139    );
 140  }
 141