create-lightning-scaffold 1.0.2 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -1
- package/package.json +1 -1
- package/templates/base/.env.example.ejs +5 -0
- package/templates/mobile/components/History.tsx.ejs +57 -52
- package/templates/mobile/components/Recovery.tsx.ejs +78 -67
- package/templates/mobile/components/Swap.tsx.ejs +58 -152
- package/templates/state/redux/index.ts +2 -7
- package/templates/state/zustand/index.ts +0 -5
- package/templates/vite/src/components/History.tsx.ejs +16 -16
- package/templates/vite/src/components/Recovery.tsx.ejs +24 -18
- package/templates/vite/src/components/Swap.tsx.ejs +52 -91
- package/templates/web/components/History.tsx.ejs +17 -17
- package/templates/web/components/Recovery.tsx.ejs +27 -19
- package/templates/web/components/Swap.tsx.ejs +58 -134
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useState, useEffect } from "react";
|
|
3
3
|
import { useWallet } from "@lazorkit/wallet";
|
|
4
|
-
import { Connection, PublicKey
|
|
4
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
5
5
|
|
|
6
6
|
const TOKENS = [
|
|
7
7
|
{ symbol: "SOL", mint: "So11111111111111111111111111111111111111112", decimals: 9 },
|
|
@@ -11,9 +11,10 @@ const TOKENS = [
|
|
|
11
11
|
{ symbol: "JUP", mint: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", decimals: 6 },
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
-
const SLIPPAGE_OPTIONS = [50, 100, 300];
|
|
14
|
+
const SLIPPAGE_OPTIONS = [50, 100, 300];
|
|
15
15
|
|
|
16
16
|
const JUPITER_API = "https://api.jup.ag/swap/v1";
|
|
17
|
+
const JUP_API_KEY = process.env.NEXT_PUBLIC_JUPITER_API_KEY || "";
|
|
17
18
|
|
|
18
19
|
function useBalances(walletAddress: string | undefined) {
|
|
19
20
|
const [balances, setBalances] = useState<Record<string, number>>({});
|
|
@@ -22,11 +23,7 @@ function useBalances(walletAddress: string | undefined) {
|
|
|
22
23
|
const rpc = process.env.NEXT_PUBLIC_SOLANA_RPC || "https://api.devnet.solana.com";
|
|
23
24
|
const conn = new Connection(rpc);
|
|
24
25
|
const pk = new PublicKey(walletAddress);
|
|
25
|
-
|
|
26
|
-
// Fetch SOL balance
|
|
27
26
|
conn.getBalance(pk).then((bal) => setBalances((b) => ({ ...b, SOL: bal / 1e9 }))).catch(() => {});
|
|
28
|
-
|
|
29
|
-
// Fetch token balances
|
|
30
27
|
conn.getParsedTokenAccountsByOwner(pk, { programId: new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") })
|
|
31
28
|
.then((res) => {
|
|
32
29
|
const tokenBals: Record<string, number> = {};
|
|
@@ -75,85 +72,71 @@ export function Swap() {
|
|
|
75
72
|
const [signing, setSigning] = useState(false);
|
|
76
73
|
const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
77
74
|
|
|
78
|
-
const switchTokens = () => {
|
|
79
|
-
setFromToken(toToken);
|
|
80
|
-
setToToken(fromToken);
|
|
81
|
-
setAmount("");
|
|
82
|
-
setQuote(null);
|
|
83
|
-
};
|
|
75
|
+
const switchTokens = () => { setFromToken(toToken); setToToken(fromToken); setAmount(""); setQuote(null); };
|
|
84
76
|
|
|
85
|
-
// Fetch quote from Jupiter
|
|
86
77
|
useEffect(() => {
|
|
87
|
-
if (!amount || parseFloat(amount) <= 0) {
|
|
88
|
-
setQuote(null);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
78
|
+
if (!amount || parseFloat(amount) <= 0) { setQuote(null); return; }
|
|
91
79
|
const timeout = setTimeout(async () => {
|
|
92
80
|
try {
|
|
93
81
|
const inputAmount = Math.floor(parseFloat(amount) * 10 ** fromToken.decimals);
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
);
|
|
82
|
+
const headers: Record<string, string> = {};
|
|
83
|
+
if (JUP_API_KEY) headers["x-api-key"] = JUP_API_KEY;
|
|
84
|
+
const res = await fetch(`${JUPITER_API}/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${inputAmount}&slippageBps=${slippage}&restrictIntermediateTokens=true`, { headers });
|
|
97
85
|
const data = await res.json();
|
|
98
86
|
if (data.outAmount) setQuote(data);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
87
|
+
else setQuote(null);
|
|
88
|
+
} catch (e) { console.error("Quote error:", e); setQuote(null); }
|
|
102
89
|
}, 500);
|
|
103
90
|
return () => clearTimeout(timeout);
|
|
104
91
|
}, [amount, fromToken, toToken, slippage]);
|
|
105
92
|
|
|
106
|
-
const outputAmount = quote
|
|
107
|
-
? (parseInt(quote.outAmount) / 10 ** toToken.decimals).toFixed(toToken.decimals === 6 ? 2 : 4)
|
|
108
|
-
: "0.00";
|
|
93
|
+
const outputAmount = quote ? (parseInt(quote.outAmount) / 10 ** toToken.decimals).toFixed(toToken.decimals === 6 ? 2 : 4) : "0.00";
|
|
109
94
|
|
|
110
95
|
const handleSwap = async () => {
|
|
111
96
|
if (!smartWalletPubkey || !quote) return;
|
|
112
97
|
setLoading(true);
|
|
113
98
|
setResult(null);
|
|
114
99
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
body: JSON.stringify({
|
|
120
|
-
quoteResponse: quote,
|
|
121
|
-
userPublicKey: smartWalletPubkey.toBase58(),
|
|
122
|
-
dynamicComputeUnitLimit: true,
|
|
123
|
-
dynamicSlippage: true,
|
|
124
|
-
}),
|
|
100
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
101
|
+
if (JUP_API_KEY) headers["x-api-key"] = JUP_API_KEY;
|
|
102
|
+
const swapRes = await fetch(`${JUPITER_API}/swap-instructions`, {
|
|
103
|
+
method: "POST", headers,
|
|
104
|
+
body: JSON.stringify({ quoteResponse: quote, userPublicKey: smartWalletPubkey.toBase58(), wrapAndUnwrapSol: true, dynamicComputeUnitLimit: true }),
|
|
125
105
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
106
|
+
if (!swapRes.ok) throw new Error(`Jupiter API error: ${await swapRes.text()}`);
|
|
107
|
+
const { setupInstructions = [], swapInstruction, cleanupInstruction, addressLookupTableAddresses = [] } = await swapRes.json();
|
|
108
|
+
if (!swapInstruction) throw new Error("No swap instruction returned");
|
|
109
|
+
|
|
110
|
+
const toInstruction = (ix: any) => ({
|
|
111
|
+
programId: new PublicKey(ix.programId),
|
|
112
|
+
keys: ix.accounts.map((acc: any) => ({ pubkey: new PublicKey(acc.pubkey), isSigner: acc.isSigner, isWritable: acc.isWritable })),
|
|
113
|
+
data: Buffer.from(ix.data, "base64"),
|
|
133
114
|
});
|
|
134
|
-
|
|
115
|
+
const instructions = [...setupInstructions.map(toInstruction), toInstruction(swapInstruction), ...(cleanupInstruction ? [toInstruction(cleanupInstruction)] : [])];
|
|
116
|
+
|
|
117
|
+
let addressLookupTableAccounts: any[] = [];
|
|
118
|
+
if (addressLookupTableAddresses.length > 0) {
|
|
119
|
+
const rpc = process.env.NEXT_PUBLIC_SOLANA_RPC || "https://api.devnet.solana.com";
|
|
120
|
+
const conn = new Connection(rpc);
|
|
121
|
+
const alts = await Promise.all(addressLookupTableAddresses.map(async (addr: string) => (await conn.getAddressLookupTable(new PublicKey(addr))).value));
|
|
122
|
+
addressLookupTableAccounts = alts.filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const signature = await signAndSendTransaction({ instructions, transactionOptions: { feeToken: "USDC", addressLookupTableAccounts } });
|
|
135
126
|
setResult({ success: true, message: `Swapped! ${signature.slice(0, 8)}...` });
|
|
136
|
-
setAmount("");
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
setResult({ success: false, message: e instanceof Error ? e.message : "Swap failed" });
|
|
140
|
-
} finally {
|
|
141
|
-
setLoading(false);
|
|
142
|
-
}
|
|
127
|
+
setAmount(""); setQuote(null);
|
|
128
|
+
} catch (e) { console.error("Swap error:", e); setResult({ success: false, message: e instanceof Error ? e.message : "Swap failed" }); }
|
|
129
|
+
finally { setLoading(false); }
|
|
143
130
|
};
|
|
144
131
|
|
|
145
132
|
const handleSignMessage = async () => {
|
|
146
|
-
setSigning(true);
|
|
147
|
-
setResult(null);
|
|
133
|
+
setSigning(true); setResult(null);
|
|
148
134
|
try {
|
|
149
135
|
const message = `Verify wallet ownership\nTimestamp: ${Date.now()}`;
|
|
150
136
|
const { signature } = await signMessage(message);
|
|
151
137
|
setResult({ success: true, message: `Signed: ${signature.slice(0, 16)}...` });
|
|
152
|
-
} catch (e) {
|
|
153
|
-
|
|
154
|
-
} finally {
|
|
155
|
-
setSigning(false);
|
|
156
|
-
}
|
|
138
|
+
} catch (e) { console.error("Sign error:", e); setResult({ success: false, message: e instanceof Error ? e.message : "Sign failed" }); }
|
|
139
|
+
finally { setSigning(false); }
|
|
157
140
|
};
|
|
158
141
|
|
|
159
142
|
const addr = wallet?.smartWallet || "";
|
|
@@ -163,30 +146,20 @@ export function Swap() {
|
|
|
163
146
|
<div className="w-full max-w-sm bg-neutral-900 rounded-2xl p-6 text-white">
|
|
164
147
|
<div className="flex items-center justify-between mb-6">
|
|
165
148
|
<h1 className="text-lg font-semibold">Swap</h1>
|
|
166
|
-
<button onClick={() => disconnect()} className="text-xs text-neutral-500 hover:text-neutral-300">
|
|
167
|
-
{addr.slice(0, 6)}...{addr.slice(-4)}
|
|
168
|
-
</button>
|
|
149
|
+
<button onClick={() => disconnect()} className="text-xs text-neutral-500 hover:text-neutral-300">{addr.slice(0, 6)}...{addr.slice(-4)}</button>
|
|
169
150
|
</div>
|
|
170
151
|
<div className="space-y-2">
|
|
171
152
|
<div className="border border-neutral-700 rounded-xl p-4 bg-neutral-800">
|
|
172
|
-
<div className="flex items-center justify-between mb-2">
|
|
173
|
-
<span className="text-xs text-neutral-500">From</span>
|
|
174
|
-
<span className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span>
|
|
175
|
-
</div>
|
|
153
|
+
<div className="flex items-center justify-between mb-2"><span className="text-xs text-neutral-500">From</span><span className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span></div>
|
|
176
154
|
<div className="flex items-center gap-3">
|
|
177
155
|
<input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} className="flex-1 text-2xl font-medium bg-transparent w-full focus:outline-none text-white placeholder-neutral-600" />
|
|
178
156
|
<TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
|
|
179
157
|
</div>
|
|
180
158
|
<button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} className="text-xs text-neutral-500 hover:text-white mt-1">Max</button>
|
|
181
159
|
</div>
|
|
182
|
-
<div className="flex justify-center">
|
|
183
|
-
<button onClick={switchTokens} className="w-8 h-8 border border-neutral-700 rounded-lg flex items-center justify-center text-neutral-500 hover:bg-neutral-800 bg-neutral-800">↓</button>
|
|
184
|
-
</div>
|
|
160
|
+
<div className="flex justify-center"><button onClick={switchTokens} className="w-8 h-8 border border-neutral-700 rounded-lg flex items-center justify-center text-neutral-500 hover:bg-neutral-800 bg-neutral-800">↓</button></div>
|
|
185
161
|
<div className="border border-neutral-700 rounded-xl p-4 bg-neutral-800">
|
|
186
|
-
<div className="flex items-center justify-between mb-2">
|
|
187
|
-
<span className="text-xs text-neutral-500">To</span>
|
|
188
|
-
<span className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span>
|
|
189
|
-
</div>
|
|
162
|
+
<div className="flex items-center justify-between mb-2"><span className="text-xs text-neutral-500">To</span><span className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span></div>
|
|
190
163
|
<div className="flex items-center gap-3">
|
|
191
164
|
<span className="flex-1 text-2xl font-medium text-neutral-500">{outputAmount}</span>
|
|
192
165
|
<TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
|
|
@@ -194,64 +167,29 @@ export function Swap() {
|
|
|
194
167
|
{quote && <p className="text-xs text-neutral-500 mt-2">via Jupiter · {quote.routePlan?.[0]?.swapInfo?.label || "Best route"}</p>}
|
|
195
168
|
</div>
|
|
196
169
|
</div>
|
|
197
|
-
<div className="flex items-center justify-between mt-3 text-xs">
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
{SLIPPAGE_OPTIONS.map((s) => (
|
|
201
|
-
<button key={s} onClick={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? "bg-white text-black" : "bg-neutral-700 text-neutral-300 hover:bg-neutral-600"}`}>
|
|
202
|
-
{s / 100}%
|
|
203
|
-
</button>
|
|
204
|
-
))}
|
|
205
|
-
</div>
|
|
206
|
-
</div>
|
|
207
|
-
<button onClick={handleSwap} disabled={loading || !quote} className="mt-4 w-full py-3 bg-white text-black text-sm font-medium rounded-xl hover:opacity-90 disabled:opacity-50">
|
|
208
|
-
{loading ? "Swapping..." : "Swap"}
|
|
209
|
-
</button>
|
|
210
|
-
<button onClick={handleSignMessage} disabled={signing} className="mt-2 w-full py-3 bg-transparent text-white text-sm font-medium rounded-xl border border-neutral-700 hover:bg-neutral-800 disabled:opacity-50">
|
|
211
|
-
{signing ? "Signing..." : "Sign Message"}
|
|
212
|
-
</button>
|
|
170
|
+
<div className="flex items-center justify-between mt-3 text-xs"><span className="text-neutral-500">Slippage</span><div className="flex gap-1">{SLIPPAGE_OPTIONS.map((s) => (<button key={s} onClick={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? "bg-white text-black" : "bg-neutral-700 text-neutral-300 hover:bg-neutral-600"}`}>{s / 100}%</button>))}</div></div>
|
|
171
|
+
<button onClick={handleSwap} disabled={loading || !quote} className="mt-4 w-full py-3 bg-white text-black text-sm font-medium rounded-xl hover:opacity-90 disabled:opacity-50">{loading ? "Swapping..." : "Swap"}</button>
|
|
172
|
+
<button onClick={handleSignMessage} disabled={signing} className="mt-2 w-full py-3 bg-transparent text-white text-sm font-medium rounded-xl border border-neutral-700 hover:bg-neutral-800 disabled:opacity-50">{signing ? "Signing..." : "Sign Message"}</button>
|
|
213
173
|
{result && <p className={`mt-3 text-sm text-center ${result.success ? "text-neutral-400" : "text-red-400"}`}>{result.message}</p>}
|
|
214
174
|
<p className="mt-4 text-xs text-center text-neutral-500">Gas sponsored · Powered by LazorKit + Jupiter</p>
|
|
215
175
|
</div>
|
|
216
176
|
<% } else { %>
|
|
217
177
|
<div style={styles.container}>
|
|
218
|
-
<div style={styles.header}>
|
|
219
|
-
<h1 style={styles.title}>Swap</h1>
|
|
220
|
-
<button onClick={() => disconnect()} style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</button>
|
|
221
|
-
</div>
|
|
178
|
+
<div style={styles.header}><h1 style={styles.title}>Swap</h1><button onClick={() => disconnect()} style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</button></div>
|
|
222
179
|
<div style={styles.card}>
|
|
223
180
|
<div style={styles.label}><span>From</span><span>Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span></div>
|
|
224
|
-
<div style={styles.inputRow}>
|
|
225
|
-
<input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} style={styles.input} />
|
|
226
|
-
<TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
|
|
227
|
-
</div>
|
|
181
|
+
<div style={styles.inputRow}><input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} style={styles.input} /><TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} /></div>
|
|
228
182
|
<button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} style={{ ...styles.address, fontSize: 12, marginTop: 4 }}>Max</button>
|
|
229
183
|
</div>
|
|
230
|
-
<div style={styles.arrow}><button onClick={switchTokens} style={{ ...styles.arrowBox, cursor: 'pointer'
|
|
184
|
+
<div style={styles.arrow}><button onClick={switchTokens} style={{ ...styles.arrowBox, cursor: 'pointer' }}>↓</button></div>
|
|
231
185
|
<div style={styles.card}>
|
|
232
186
|
<div style={styles.label}><span>To</span><span>Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span></div>
|
|
233
|
-
<div style={styles.inputRow}>
|
|
234
|
-
<span style={{ ...styles.input, color: '#d4d4d4' }}>{outputAmount}</span>
|
|
235
|
-
<TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
|
|
236
|
-
</div>
|
|
187
|
+
<div style={styles.inputRow}><span style={{ ...styles.input, color: '#737373' }}>{outputAmount}</span><TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} /></div>
|
|
237
188
|
{quote && <p style={styles.quote}>via Jupiter · {quote.routePlan?.[0]?.swapInfo?.label || "Best route"}</p>}
|
|
238
189
|
</div>
|
|
239
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, fontSize: 12 }}>
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
{SLIPPAGE_OPTIONS.map((s) => (
|
|
243
|
-
<button key={s} onClick={() => setSlippage(s)} style={{ padding: '4px 8px', borderRadius: 4, border: 'none', cursor: 'pointer', background: slippage === s ? '#fafafa' : '#262626', color: slippage === s ? '#0a0a0a' : '#a3a3a3' }}>
|
|
244
|
-
{s / 100}%
|
|
245
|
-
</button>
|
|
246
|
-
))}
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
<button onClick={handleSwap} disabled={loading || !quote} style={{ ...styles.button, ...(loading || !quote ? styles.buttonDisabled : {}) }}>
|
|
250
|
-
{loading ? "Swapping..." : "Swap"}
|
|
251
|
-
</button>
|
|
252
|
-
<button onClick={handleSignMessage} disabled={signing} style={{ ...styles.buttonSecondary, ...(signing ? styles.buttonDisabled : {}) }}>
|
|
253
|
-
{signing ? "Signing..." : "Sign Message"}
|
|
254
|
-
</button>
|
|
190
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, fontSize: 12 }}><span style={{ color: '#737373' }}>Slippage</span><div style={{ display: 'flex', gap: 4 }}>{SLIPPAGE_OPTIONS.map((s) => (<button key={s} onClick={() => setSlippage(s)} style={{ padding: '4px 8px', borderRadius: 4, border: 'none', cursor: 'pointer', background: slippage === s ? '#fafafa' : '#262626', color: slippage === s ? '#0a0a0a' : '#a3a3a3' }}>{s / 100}%</button>))}</div></div>
|
|
191
|
+
<button onClick={handleSwap} disabled={loading || !quote} style={{ ...styles.button, ...(loading || !quote ? styles.buttonDisabled : {}) }}>{loading ? "Swapping..." : "Swap"}</button>
|
|
192
|
+
<button onClick={handleSignMessage} disabled={signing} style={{ ...styles.buttonSecondary, ...(signing ? styles.buttonDisabled : {}) }}>{signing ? "Signing..." : "Sign Message"}</button>
|
|
255
193
|
{result && <p style={{ ...styles.result, color: result.success ? '#737373' : '#ef4444' }}>{result.message}</p>}
|
|
256
194
|
<p style={styles.footer}>Gas sponsored · Powered by LazorKit + Jupiter</p>
|
|
257
195
|
</div>
|
|
@@ -264,25 +202,11 @@ function TokenSelect({ token, tokens, onChange, exclude }: { token: typeof TOKEN
|
|
|
264
202
|
return (
|
|
265
203
|
<div style={{ position: 'relative' }}>
|
|
266
204
|
<% if (styling === 'tailwind') { %>
|
|
267
|
-
<button onClick={() => setOpen(!open)} className="flex items-center gap-2 px-3 py-2 bg-neutral-700 rounded-lg text-sm font-medium text-white">
|
|
268
|
-
|
|
269
|
-
</button>
|
|
270
|
-
{open && (
|
|
271
|
-
<div className="absolute right-0 mt-1 bg-neutral-800 border border-neutral-700 rounded-lg shadow-lg z-10">
|
|
272
|
-
{tokens.filter((t) => t.symbol !== exclude).map((t) => (
|
|
273
|
-
<button key={t.symbol} onClick={() => { onChange(t); setOpen(false); }} className="block w-full px-4 py-2 text-left text-sm text-white hover:bg-neutral-700">{t.symbol}</button>
|
|
274
|
-
))}
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
205
|
+
<button onClick={() => setOpen(!open)} className="flex items-center gap-2 px-3 py-2 bg-neutral-700 rounded-lg text-sm font-medium text-white">{token.symbol}<span className="text-neutral-400">▼</span></button>
|
|
206
|
+
{open && (<div className="absolute right-0 mt-1 bg-neutral-800 border border-neutral-700 rounded-lg shadow-lg z-10">{tokens.filter((t) => t.symbol !== exclude).map((t) => (<button key={t.symbol} onClick={() => { onChange(t); setOpen(false); }} className="block w-full px-4 py-2 text-left text-sm text-white hover:bg-neutral-700">{t.symbol}</button>))}</div>)}
|
|
277
207
|
<% } else { %>
|
|
278
208
|
<button onClick={() => setOpen(!open)} style={styles.tokenBtn}>{token.symbol} <span style={{ color: '#a3a3a3' }}>▼</span></button>
|
|
279
|
-
{open && (
|
|
280
|
-
<div style={styles.dropdown as React.CSSProperties}>
|
|
281
|
-
{tokens.filter((t) => t.symbol !== exclude).map((t) => (
|
|
282
|
-
<button key={t.symbol} onClick={() => { onChange(t); setOpen(false); }} style={styles.dropdownItem}>{t.symbol}</button>
|
|
283
|
-
))}
|
|
284
|
-
</div>
|
|
285
|
-
)}
|
|
209
|
+
{open && (<div style={styles.dropdown as React.CSSProperties}>{tokens.filter((t) => t.symbol !== exclude).map((t) => (<button key={t.symbol} onClick={() => { onChange(t); setOpen(false); }} style={styles.dropdownItem}>{t.symbol}</button>))}</div>)}
|
|
286
210
|
<% } %>
|
|
287
211
|
</div>
|
|
288
212
|
);
|