ReceiveToSelect.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 { APP_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 ReceiveToOption = {
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 ReceiveToSelect({ appId, onChange }: Props) {
59 const anchorRef = useComboboxAnchor();
60 const [search, setSearch] = React.useState("");
61 const { data: appsData } = useApps(APP_SELECT_APPS_LIMIT, undefined, {
62 name: search,
63 });
64
65 const apps = React.useMemo(
66 () =>
67 [...(appsData?.apps || [])].sort((a, b) =>
68 getAppDisplayName(a.name).localeCompare(getAppDisplayName(b.name))
69 ),
70 [appsData?.apps]
71 );
72
73 const options = React.useMemo<ReceiveToOption[]>(
74 () => [
75 { value: LIGHTNING_BALANCE, label: LIGHTNING_BALANCE_LABEL },
76 ...apps.map((app) => ({
77 value: app.id.toString(),
78 label: getAppDisplayName(app.name),
79 app,
80 })),
81 ],
82 [apps]
83 );
84
85 const selectedOption = options.find((opt) =>
86 appId ? opt.value === appId.toString() : undefined
87 );
88
89 return (
90 <div className="grid w-full gap-1.5">
91 <Label>Receive to</Label>
92 <Combobox
93 items={options}
94 value={selectedOption}
95 itemToStringValue={(option) => option.value}
96 onInputValueChange={setSearch}
97 onValueChange={(option) =>
98 onChange(
99 !option || option.value === LIGHTNING_BALANCE
100 ? undefined
101 : Number(option.value)
102 )
103 }
104 >
105 <div ref={anchorRef} className="w-full">
106 <ComboboxInput placeholder={LIGHTNING_BALANCE_LABEL}>
107 <InputGroupAddon>
108 {selectedOption?.app ? (
109 <AppAvatar
110 app={selectedOption.app}
111 className="size-6 rounded-full"
112 />
113 ) : (
114 <div className="flex size-6 items-center justify-center rounded-sm bg-muted">
115 <WalletIcon className="size-3 text-muted-foreground" />
116 </div>
117 )}
118 </InputGroupAddon>
119 </ComboboxInput>
120 </div>
121 <ComboboxContent anchor={anchorRef}>
122 <ComboboxEmpty>No connections found.</ComboboxEmpty>
123 <ComboboxList>
124 {(option: ReceiveToOption) => (
125 <ComboboxItem key={option.value} value={option}>
126 {option.app ? (
127 <AppOption app={option.app} />
128 ) : (
129 <LightningOption />
130 )}
131 </ComboboxItem>
132 )}
133 </ComboboxList>
134 </ComboboxContent>
135 </Combobox>
136 </div>
137 );
138 }
139