@turtleclub/opportunities 0.1.0-beta.20 → 0.1.0-beta.22

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.
@@ -1,267 +1,342 @@
1
- "use client";
2
-
3
- import { ChangeEvent, FormEvent, useMemo, useState } from "react";
4
- import { Button, Checkbox, Input } from "@turtleclub/ui";
5
- import { ShieldAlert, Send, ChevronDown, ChevronUp, type LucideProps, Shield } from "lucide-react";
6
- import type { ComponentType } from "react";
7
- import type { CoverRequestData, CoverRequestFormProps } from "../types";
8
- const ShieldAlertIcon = ShieldAlert as ComponentType<LucideProps>;
9
- const SendIcon = Send as ComponentType<LucideProps>;
10
- const ChevronUpIcon = ChevronUp as ComponentType<LucideProps>;
11
- const ChevronDownIcon = ChevronDown as ComponentType<LucideProps>;
12
- const ShieldIcon = Shield as ComponentType<LucideProps>;
13
-
14
- const PERIOD_OPTIONS = [
15
- { label: "28d", days: 28 },
16
- { label: "3m", days: 90 },
17
- { label: "6m", days: 180 },
18
- { label: "1y", days: 365 },
19
- ] as const;
20
-
21
- export function CoverRequestForm({
22
- protocolName,
23
- depositedAmount,
24
- baseApyLabel,
25
- onSuccess,
26
- onError,
27
- startExpanded,
28
- }: CoverRequestFormProps) {
29
- const [coverageAmount, setCoverageAmount] = useState("");
30
- const [useDepositedAmount, setUseDepositedAmount] = useState(false);
31
- const [desiredApySacrifice, setDesiredApySacrifice] = useState("");
32
- const [selectedPeriodDays, setSelectedPeriodDays] = useState(90);
33
- const [inputError, setInputError] = useState<string | null>(null);
34
- const [isExpanded, setIsExpanded] = useState(startExpanded ?? false);
35
-
36
- const parsedCoverageAmount = useMemo(() => {
37
- const amountStr = useDepositedAmount ? (depositedAmount ?? "") : coverageAmount;
38
- if (!amountStr.trim()) return null;
39
- const value = Number(amountStr);
40
- return Number.isFinite(value) && value > 0 ? value : null;
41
- }, [coverageAmount, useDepositedAmount, depositedAmount]);
42
-
43
- const parsedSacrifice = useMemo(() => {
44
- if (!desiredApySacrifice.trim()) return null;
45
- const value = Number(desiredApySacrifice);
46
- return Number.isFinite(value) ? value : null;
47
- }, [desiredApySacrifice]);
48
-
49
- const parsedBaseApy = useMemo(() => {
50
- if (!baseApyLabel) return null;
51
- const value = Number(baseApyLabel.replace(/[^0-9.]/g, ""));
52
- return Number.isFinite(value) ? value : null;
53
- }, [baseApyLabel]);
54
-
55
- const netApy = useMemo(() => {
56
- if (parsedBaseApy === null || parsedSacrifice === null) return null;
57
- return parsedBaseApy - parsedSacrifice;
58
- }, [parsedBaseApy, parsedSacrifice]);
59
-
60
- const estimatedPremium = useMemo(() => {
61
- if (parsedCoverageAmount === null || parsedSacrifice === null) return null;
62
- // Premium = Amount × (APY/100) × (Days/365)
63
- return parsedCoverageAmount * (parsedSacrifice / 100) * (selectedPeriodDays / 365);
64
- }, [parsedCoverageAmount, parsedSacrifice, selectedPeriodDays]);
65
-
66
- const resetForm = () => {
67
- setCoverageAmount("");
68
- setUseDepositedAmount(false);
69
- setDesiredApySacrifice("");
70
- setSelectedPeriodDays(90);
71
- setInputError(null);
72
- };
73
-
74
- const handleSubmit = (): CoverRequestData | undefined => {
75
- if (parsedCoverageAmount === null) {
76
- setInputError("Enter a valid coverage amount");
77
- return;
78
- }
79
- if (parsedSacrifice === null) {
80
- setInputError("Enter a valid APY sacrifice");
81
- return;
82
- }
83
- if (parsedSacrifice < 0) {
84
- setInputError("APY sacrifice cannot be negative");
85
- return;
86
- }
87
- setInputError(null);
88
-
89
- const requestData: CoverRequestData = {
90
- protocolName,
91
- coverageAmount: parsedCoverageAmount,
92
- periodDays: selectedPeriodDays,
93
- desiredApySacrifice: parsedSacrifice,
94
- estimatedPremium: estimatedPremium ?? 0,
95
- };
96
-
97
- onSuccess?.("Cover request submitted");
98
- console.info("Cover request submitted", requestData);
99
-
100
- return requestData;
101
- };
102
-
103
- const handleFormSubmit = (event: FormEvent<HTMLFormElement>) => {
104
- event.preventDefault();
105
- const requestData = handleSubmit();
106
- if (requestData) {
107
- console.log("Submitted data successfully", requestData);
108
- resetForm();
109
- }
110
- };
111
-
112
- return (
113
- <div className="rounded-xl bg-gradient-to-br from-primary/5 to-card overflow-hidden border border-primary/20">
114
- <button
115
- onClick={() => setIsExpanded(!isExpanded)}
116
- type="button"
117
- className="w-full p-4 flex items-center justify-between hover:bg-primary/5 transition-colors"
118
- >
119
- <div className="flex items-center gap-3 text-left">
120
- <div className="p-2 rounded-lg bg-primary/10 border border-primary/20">
121
- <ShieldAlertIcon className="w-5 h-5 text-primary" />
122
- </div>
123
- <div className="space-y-1">
124
- <p className="text-sm font-semibold text-foreground">
125
- Request coverage for this opportunity
126
- </p>
127
- <p className="text-xs text-muted-foreground leading-relaxed">
128
- We don&apos;t yet have Nexus Mutual cover available for{" "}
129
- <span className="font-medium">{protocolName}</span>.
130
- </p>
131
- </div>
132
- </div>
133
- <div className="flex items-center gap-2">
134
- {isExpanded ? (
135
- <ChevronUpIcon className="w-5 h-5 text-muted-foreground" />
136
- ) : (
137
- <ChevronDownIcon className="w-5 h-5 text-muted-foreground" />
138
- )}
139
- </div>
140
- </button>
141
-
142
- {isExpanded && (
143
- <form onSubmit={handleFormSubmit} className="p-4 space-y-4">
144
- <div className="flex items-center justify-between text-sm text-muted-foreground">
145
- <span className="font-medium text-foreground">{protocolName}</span>
146
- {baseApyLabel && <span>Current APR: {baseApyLabel}</span>}
147
- </div>
148
-
149
- {/* Coverage Amount */}
150
- <div className="space-y-2">
151
- <label className="text-xs text-foreground">Coverage Amount</label>
152
- <Input
153
- variant=""
154
- value={useDepositedAmount ? (depositedAmount ?? "") : coverageAmount}
155
- onChange={(e: ChangeEvent<HTMLInputElement>) => {
156
- const value = e.target.value;
157
- if (value === "" || /^\d*\.?\d*$/.test(value)) {
158
- setCoverageAmount(value);
159
- }
160
- }}
161
- placeholder="e.g. 1"
162
- className="w-full bg-secondary border border-border rounded-sm px-4 py-3 pr-12 text-foreground placeholder-muted-foreground focus:outline-none focus:border-primary/50 transition-colors"
163
- disabled={useDepositedAmount}
164
- />
165
- {depositedAmount && (
166
- <label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
167
- <Checkbox
168
- checked={useDepositedAmount}
169
- onCheckedChange={(checked) => setUseDepositedAmount(!useDepositedAmount)}
170
- className="rounded border-border"
171
- />
172
- Use my deposited amount ({depositedAmount})
173
- </label>
174
- )}
175
- </div>
176
-
177
- {/* Period Selector */}
178
- <div className="space-y-3">
179
- <label className="text-xs text-foreground text-center block">Coverage Period</label>
180
- <div className="flex gap-2">
181
- {PERIOD_OPTIONS.map((option) => (
182
- <Button
183
- key={option.days}
184
- type="button"
185
- onClick={() => setSelectedPeriodDays(option.days)}
186
- variant="none"
187
- className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-colors ${
188
- selectedPeriodDays === option.days
189
- ? "bg-primary text-primary-foreground border-primary hover:border-primary"
190
- : "bg-secondary text-foreground border-border hover:border-primary/50"
191
- }`}
192
- >
193
- {option.label}
194
- </Button>
195
- ))}
196
- </div>
197
- </div>
198
-
199
- {/* APY Sacrifice */}
200
- <div className="space-y-2">
201
- <label className="text-xs text-foreground">
202
- APY you&apos;re willing to sacrifice for cover (%)
203
- </label>
204
- <Input
205
- type="text"
206
- variant="bordered"
207
- value={desiredApySacrifice}
208
- onChange={(e: ChangeEvent<HTMLInputElement>) => {
209
- const value = e.target.value;
210
- if (value === "" || /^\d*\.?\d*$/.test(value)) {
211
- setDesiredApySacrifice(e.target.value);
212
- }
213
- }}
214
- placeholder="e.g. 2.5"
215
- className="w-full bg-secondary border border-border rounded-sm px-4 py-3 pr-12 text-foreground placeholder-muted-foreground focus:outline-none focus:border-primary/50 transition-colors"
216
- />
217
-
218
- <div className="flex items-center justify-between text-xs p-2 rounded-md bg-muted/50">
219
- <span className="text-muted-foreground">
220
- {parsedBaseApy ?? "—"}% − {parsedSacrifice ?? "—"}% =
221
- </span>
222
- <span
223
- className={`font-medium ${netApy !== null && netApy >= 0 ? "text-green-500" : "text-muted-foreground"}`}
224
- >
225
- {netApy !== null ? `${netApy.toFixed(2)}% Net APY` : "— Net APY"}
226
- </span>
227
- </div>
228
- </div>
229
-
230
- {/* Estimated Premium */}
231
- <div className="p-3 rounded-sm bg-primary/5 border border-primary/20 space-y-1">
232
- <p className="text-xs text-muted-foreground">Estimated Premium</p>
233
- <p className="text-lg font-semibold text-foreground">
234
- {estimatedPremium !== null
235
- ? estimatedPremium.toLocaleString(undefined, {
236
- minimumFractionDigits: 2,
237
- maximumFractionDigits: 2,
238
- })
239
- : ""}
240
- </p>
241
- <p className="text-[10px] text-muted-foreground">
242
- {parsedCoverageAmount?.toLocaleString() ?? "—"} × {parsedSacrifice ?? "—"}% ×{" "}
243
- {selectedPeriodDays} days
244
- </p>
245
- </div>
246
-
247
- {inputError && <p className="text-[11px] text-destructive">{inputError}</p>}
248
-
249
- <p className="text-base text-muted-foreground">
250
- We&apos;ll use this to gauge interest and bring cover to the protocols you care about.
251
- </p>
252
-
253
- <div className="flex justify-center gap-2 w-full ">
254
- <Button
255
- className=" flex w-full text-lg font-semibold rounded-sm"
256
- variant="green"
257
- type="submit"
258
- >
259
- <ShieldIcon className="w-5 h-5 text-primary" />
260
- Request Cover
261
- </Button>
262
- </div>
263
- </form>
264
- )}
265
- </div>
266
- );
267
- }
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { useForm, useStore } from "@tanstack/react-form";
5
+ import {
6
+ Button,
7
+ Input,
8
+ Collapsible,
9
+ CollapsibleTrigger,
10
+ CollapsibleContent,
11
+ Label,
12
+ Badge,
13
+ cn,
14
+ } from "@turtleclub/ui";
15
+ import { ShieldAlert, ChevronDown, ChevronUp, type LucideProps, Shield } from "lucide-react";
16
+ import type { ComponentType } from "react";
17
+ import type { CoverRequestFormProps } from "../types";
18
+ import { useSubmitCoverRequest } from "@turtleclub/hooks";
19
+
20
+ const ShieldAlertIcon = ShieldAlert as ComponentType<LucideProps>;
21
+ const ChevronUpIcon = ChevronUp as ComponentType<LucideProps>;
22
+ const ChevronDownIcon = ChevronDown as ComponentType<LucideProps>;
23
+ const ShieldIcon = Shield as ComponentType<LucideProps>;
24
+
25
+ type PeriodDays = 28 | 90 | 180 | 365;
26
+
27
+ const PERIOD_OPTIONS = [
28
+ { label: "28d", days: 28 },
29
+ { label: "3m", days: 90 },
30
+ { label: "6m", days: 180 },
31
+ { label: "1y", days: 365 },
32
+ ] as const;
33
+
34
+ const NUMERIC_INPUT_REGEX = /^\d*\.?\d*$/;
35
+
36
+ function parsePositiveNumber(value: string): number | null {
37
+ if (!value) return null;
38
+ const num = Number(value);
39
+ return Number.isFinite(num) && num > 0 ? num : null;
40
+ }
41
+
42
+ function handleNumericInputChange(
43
+ e: React.ChangeEvent<HTMLInputElement>,
44
+ onChange: (value: string) => void
45
+ ) {
46
+ const value = e.target.value;
47
+ if (value === "" || NUMERIC_INPUT_REGEX.test(value)) {
48
+ onChange(value);
49
+ }
50
+ }
51
+
52
+ const USDC_TOKEN = {
53
+ symbol: "USDC",
54
+ logoUrl: "https://storage.googleapis.com/turtle-assets/tokens/usdc.png",
55
+ };
56
+
57
+ export function CoverRequestForm({
58
+ protocolName,
59
+ baseApyLabel,
60
+ onSuccess,
61
+ onError,
62
+ startExpanded,
63
+ }: CoverRequestFormProps) {
64
+ const [isExpanded, setIsExpanded] = useState(startExpanded ?? false);
65
+
66
+ const form = useForm({
67
+ defaultValues: {
68
+ coverageAmount: "",
69
+ periodDays: 90 as PeriodDays,
70
+ desiredApySacrifice: "",
71
+ tokenSymbol: "USDC",
72
+ },
73
+ onSubmit: async ({ value }) => {
74
+ const coverageAmount = Number(value.coverageAmount);
75
+ const desiredApySacrifice = Number(value.desiredApySacrifice);
76
+ const periodDays = value.periodDays;
77
+
78
+ const calculatedEstimatedPremium =
79
+ coverageAmount * (desiredApySacrifice / 100) * (periodDays / 365);
80
+
81
+ submitCoverRequest({
82
+ protocolName,
83
+ coverageAmount: String(coverageAmount),
84
+ periodDays,
85
+ desiredApySacrifice: String(desiredApySacrifice),
86
+ calculatedEstimatedPremium: String(calculatedEstimatedPremium),
87
+ tokenSymbol: value.tokenSymbol,
88
+ });
89
+ },
90
+ });
91
+
92
+ const { mutate: submitCoverRequest, isPending } = useSubmitCoverRequest({
93
+ onSuccess: (msg) => {
94
+ form.reset();
95
+ onSuccess?.(msg);
96
+ setIsExpanded(false);
97
+ },
98
+ onError,
99
+ });
100
+
101
+ const parsedBaseApy = useMemo(() => {
102
+ if (!baseApyLabel) return null;
103
+ const value = Number(baseApyLabel.replace(/[^0-9.]/g, ""));
104
+ return Number.isFinite(value) ? value : null;
105
+ }, [baseApyLabel]);
106
+
107
+ const coverageAmountValue = useStore(form.store, (state) => state.values.coverageAmount);
108
+ const sacrificeValue = useStore(form.store, (state) => state.values.desiredApySacrifice);
109
+ const periodDaysValue = useStore(form.store, (state) => state.values.periodDays);
110
+
111
+ const parsedCoverageAmount = useMemo(() => {
112
+ return parsePositiveNumber(coverageAmountValue);
113
+ }, [coverageAmountValue]);
114
+
115
+ const parsedSacrifice = useMemo(() => {
116
+ if (!sacrificeValue) return null;
117
+ const num = Number(sacrificeValue);
118
+ return Number.isFinite(num) ? num : null;
119
+ }, [sacrificeValue]);
120
+
121
+ const netApy = useMemo(() => {
122
+ if (parsedBaseApy === null || parsedSacrifice === null) return null;
123
+ return parsedBaseApy - parsedSacrifice;
124
+ }, [parsedBaseApy, parsedSacrifice]);
125
+
126
+ const calculatedEstimatedPremium = useMemo(() => {
127
+ if (parsedCoverageAmount === null || parsedSacrifice === null) return null;
128
+ return parsedCoverageAmount * (parsedSacrifice / 100) * (periodDaysValue / 365);
129
+ }, [parsedCoverageAmount, parsedSacrifice, periodDaysValue]);
130
+
131
+ return (
132
+ <Collapsible
133
+ open={isExpanded}
134
+ onOpenChange={setIsExpanded}
135
+ className="rounded-xl overflow-hidden border"
136
+ >
137
+ <CollapsibleTrigger className="w-full p-4 flex items-center cursor-pointer justify-between hover:bg-primary/5 transition-colors">
138
+ <div className="flex items-center gap-3 text-left">
139
+ <div className="p-2 rounded-lg bg-primary/10 border border-primary/20">
140
+ <ShieldAlertIcon className="w-5 h-5 text-primary" />
141
+ </div>
142
+ <div className="space-y-1">
143
+ <p className="text-sm font-semibold text-foreground">
144
+ Request coverage for this opportunity
145
+ </p>
146
+ <p className="text-sm text-muted-foreground leading-relaxed">
147
+ We don&apos;t yet have Nexus Mutual cover available for{" "}
148
+ <span className="font-medium">{protocolName}</span>.
149
+ </p>
150
+ </div>
151
+ </div>
152
+ <div className="flex items-center gap-2">
153
+ {isExpanded ? (
154
+ <ChevronUpIcon className="w-5 h-5 text-muted-foreground" />
155
+ ) : (
156
+ <ChevronDownIcon className="w-5 h-5 text-muted-foreground" />
157
+ )}
158
+ </div>
159
+ </CollapsibleTrigger>
160
+
161
+ <CollapsibleContent>
162
+ <form
163
+ onSubmit={(e) => {
164
+ e.preventDefault();
165
+ e.stopPropagation();
166
+ form.handleSubmit();
167
+ }}
168
+ className="p-4 space-y-4"
169
+ >
170
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
171
+ <span className="font-medium text-foreground">{protocolName}</span>
172
+ {baseApyLabel && <span>Current APR: {baseApyLabel}</span>}
173
+ </div>
174
+
175
+ {/* Coverage Amount */}
176
+ <form.Field
177
+ name="coverageAmount"
178
+ validators={{
179
+ onSubmit: ({ value }) => {
180
+ if (!parsePositiveNumber(value)) {
181
+ return "Enter a valid coverage amount";
182
+ }
183
+ return undefined;
184
+ },
185
+ }}
186
+ >
187
+ {(field) => (
188
+ <div className="space-y-2">
189
+ <Label className="text-xs text-foreground">Coverage Amount</Label>
190
+ <div className="flex items-center gap-6">
191
+ {/* Amount input */}
192
+ <div className="flex-1">
193
+ <Input
194
+ value={field.state.value}
195
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
196
+ handleNumericInputChange(e, field.handleChange)
197
+ }
198
+ onBlur={field.handleBlur}
199
+ placeholder="e.g. 1000"
200
+ className="w-full bg-secondary border border-border rounded-sm px-4 py-3 pr-24 text-foreground placeholder-muted-foreground focus:outline-none focus:border-primary/50 transition-colors"
201
+ />
202
+ </div>
203
+ <Badge
204
+ className={cn(
205
+ "bg-muted hover:bg-muted/80 w-auto min-w-[80px] rounded-md font-medium",
206
+ "focus:ring-primary/20 focus:ring-2 focus:outline-none",
207
+ "min-w-[80px] gap-1.5"
208
+ )}
209
+ >
210
+ <img
211
+ src={USDC_TOKEN.logoUrl}
212
+ alt={USDC_TOKEN.symbol}
213
+ className="w-5 h-5 rounded-full"
214
+ />
215
+ <span className="text-sm font-medium text-foreground">{USDC_TOKEN.symbol}</span>
216
+ </Badge>
217
+ </div>
218
+ {field.state.meta.errors.length > 0 && (
219
+ <p className="text-[11px] text-destructive">{field.state.meta.errors[0]}</p>
220
+ )}
221
+ </div>
222
+ )}
223
+ </form.Field>
224
+
225
+ {/* Period Selector */}
226
+ <form.Field name="periodDays">
227
+ {(field) => (
228
+ <div className="space-y-3">
229
+ <Label className="text-sm text-foreground block">Coverage Period</Label>
230
+ <div className="flex gap-2">
231
+ {PERIOD_OPTIONS.map((option) => (
232
+ <Button
233
+ key={option.days}
234
+ type="button"
235
+ onClick={() => field.handleChange(option.days as PeriodDays)}
236
+ variant="none"
237
+ className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-colors ${
238
+ field.state.value === option.days
239
+ ? "bg-primary text-primary-foreground border-primary hover:border-primary"
240
+ : "bg-secondary text-foreground border-border hover:border-primary/50"
241
+ }`}
242
+ >
243
+ {option.label}
244
+ </Button>
245
+ ))}
246
+ </div>
247
+ </div>
248
+ )}
249
+ </form.Field>
250
+
251
+ {/* APY Sacrifice */}
252
+ <form.Field
253
+ name="desiredApySacrifice"
254
+ validators={{
255
+ onSubmit: ({ value }) => {
256
+ const num = parsePositiveNumber(value);
257
+ if (num === null) {
258
+ return "Enter a valid APY sacrifice percentage";
259
+ }
260
+ if (num > 100) {
261
+ return "Sacrifice cannot exceed 100%";
262
+ }
263
+ return undefined;
264
+ },
265
+ }}
266
+ >
267
+ {(field) => (
268
+ <div className="space-y-2">
269
+ <Label className="text-sm text-foreground">
270
+ APY you&apos;re willing to sacrifice for cover (%)
271
+ </Label>
272
+ <Input
273
+ type="text"
274
+ value={field.state.value}
275
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
276
+ handleNumericInputChange(e, field.handleChange)
277
+ }
278
+ onBlur={field.handleBlur}
279
+ placeholder="e.g. 2.5"
280
+ className="w-full bg-secondary border border-border rounded-sm px-4 py-3 pr-12 text-foreground placeholder-muted-foreground focus:outline-none focus:border-primary/50 transition-colors"
281
+ />
282
+ {field.state.meta.errors.length > 0 && (
283
+ <p className="text-[11px] text-destructive">{field.state.meta.errors[0]}</p>
284
+ )}
285
+
286
+ <div className="flex items-center justify-between text-xs p-2 rounded-md bg-muted/50">
287
+ <span className="text-muted-foreground">
288
+ {parsedBaseApy ?? "—"}% − {parsedSacrifice ?? "—"}% =
289
+ </span>
290
+ <span
291
+ className={`font-medium ${
292
+ netApy !== null
293
+ ? netApy >= 0
294
+ ? "text-green-500"
295
+ : "text-destructive"
296
+ : "text-muted-foreground"
297
+ }`}
298
+ >
299
+ {netApy !== null ? `${netApy.toFixed(2)}% Net APY` : "— Net APY"}
300
+ </span>
301
+ </div>
302
+ </div>
303
+ )}
304
+ </form.Field>
305
+
306
+ {/* Estimated Premium */}
307
+ <div className="p-3 rounded-sm bg-primary/5 border border-primary/20 space-y-1">
308
+ <p className="text-sm text-muted-foreground">Estimated Premium</p>
309
+ <p className="text-lg font-semibold text-foreground">
310
+ {calculatedEstimatedPremium !== null
311
+ ? calculatedEstimatedPremium.toLocaleString(undefined, {
312
+ minimumFractionDigits: 2,
313
+ maximumFractionDigits: 6,
314
+ })
315
+ : "—"}
316
+ </p>
317
+ <p className="text-[10px] text-muted-foreground">
318
+ {parsedCoverageAmount?.toLocaleString() ?? "—"} × {parsedSacrifice ?? "—"}% ×{" "}
319
+ {periodDaysValue} days
320
+ </p>
321
+ </div>
322
+
323
+ <p className="text-base text-muted-foreground">
324
+ We&apos;ll use this to gauge interest and bring cover to the protocols you care about.
325
+ </p>
326
+
327
+ <div className="flex justify-center gap-2 w-full">
328
+ <Button
329
+ className="flex w-full text-lg font-semibold rounded-sm"
330
+ variant="green"
331
+ type="submit"
332
+ disabled={isPending}
333
+ >
334
+ <ShieldIcon className="w-5 h-5 text-primary" />
335
+ {isPending ? "Submitting..." : "Request Cover"}
336
+ </Button>
337
+ </div>
338
+ </form>
339
+ </CollapsibleContent>
340
+ </Collapsible>
341
+ );
342
+ }