@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.
- package/CHANGELOG.md +10 -0
- package/package.json +7 -4
- package/src/cover-offer/README.md +46 -0
- package/src/cover-offer/components/CoverOfferCard.tsx +312 -286
- package/src/cover-offer/components/CoverRequestForm.tsx +342 -267
- package/src/cover-offer/components/CoveredEventsInfo.tsx +120 -0
- package/src/cover-offer/components/ExistingCoverInfo.tsx +74 -0
- package/src/cover-offer/components/NexusCoverSection.tsx +17 -55
- package/src/cover-offer/components/PurchaseButtonSection.tsx +106 -0
- package/src/cover-offer/constants.ts +25 -1
- package/src/cover-offer/hooks/useCheckNexusMembership.ts +26 -48
- package/src/cover-offer/hooks/useCoverQuote.ts +80 -76
- package/src/cover-offer/hooks/useCoverTokenSelection.ts +84 -0
- package/src/cover-offer/hooks/useDebouncedValue.ts +12 -0
- package/src/cover-offer/hooks/useExistingCovers.ts +29 -54
- package/src/cover-offer/hooks/useNexusProduct.ts +6 -1
- package/src/cover-offer/hooks/useNexusPurchase.ts +42 -40
- package/src/cover-offer/hooks/useTokenApproval.ts +118 -0
- package/src/cover-offer/hooks/useUserCoverNfts.ts +10 -67
- package/src/cover-offer/index.ts +1 -1
- package/src/cover-offer/types/index.ts +6 -30
- package/src/cover-offer/utils/index.ts +9 -1
- package/src/cover-offer/components/MembershipRequestCard.tsx +0 -77
|
@@ -1,267 +1,342 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
</
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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'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'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'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
|
+
}
|