@turtleclub/opportunities 0.1.0-beta.14 → 0.1.0-beta.15
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 +6 -0
- package/package.json +5 -5
- package/src/cover-offer/components/CoverOfferCard.tsx +419 -291
- package/src/cover-offer/components/CoverRequestForm.tsx +188 -73
- package/src/cover-offer/components/MembershipRequestCard.tsx +77 -0
- package/src/cover-offer/components/NexusCoverSection.tsx +87 -65
- package/src/cover-offer/constants.ts +8 -6
- package/src/cover-offer/hooks/useCheckNexusMembership.ts +54 -0
- package/src/cover-offer/hooks/useCoverQuote.ts +4 -6
- package/src/cover-offer/hooks/useExistingCovers.ts +126 -0
- package/src/cover-offer/hooks/useNexusPurchase.ts +65 -94
- package/src/cover-offer/hooks/useUserCoverNfts.ts +83 -0
- package/src/cover-offer/index.ts +0 -2
- package/src/cover-offer/types/index.ts +25 -9
- package/src/cover-offer/utils/index.ts +46 -9
|
@@ -1,291 +1,419 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { Shield, ChevronDown, ChevronUp, type LucideProps } from "lucide-react";
|
|
5
|
-
import type { ComponentType } from "react";
|
|
6
|
-
import {
|
|
7
|
-
Button,
|
|
8
|
-
Slider,
|
|
9
|
-
Skeleton,
|
|
10
|
-
Checkbox,
|
|
11
|
-
Tooltip,
|
|
12
|
-
TooltipTrigger,
|
|
13
|
-
TooltipContent,
|
|
14
|
-
} from "@turtleclub/ui";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
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
|
-
const
|
|
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
|
-
<p className="text-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<div>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Shield, ChevronDown, ChevronUp, CheckCircle, type LucideProps } from "lucide-react";
|
|
5
|
+
import type { ComponentType } from "react";
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
Slider,
|
|
9
|
+
Skeleton,
|
|
10
|
+
Checkbox,
|
|
11
|
+
Tooltip,
|
|
12
|
+
TooltipTrigger,
|
|
13
|
+
TooltipContent,
|
|
14
|
+
} from "@turtleclub/ui";
|
|
15
|
+
import { formatEther, parseEther } from "viem";
|
|
16
|
+
import { useBalance } from "wagmi";
|
|
17
|
+
import { useCoverQuote } from "../hooks/useCoverQuote";
|
|
18
|
+
import { useNexusPurchase } from "../hooks/useNexusPurchase";
|
|
19
|
+
import { useExistingCovers } from "../hooks/useExistingCovers";
|
|
20
|
+
import {
|
|
21
|
+
getOpportunityAPY,
|
|
22
|
+
calculateAdjustedAPY,
|
|
23
|
+
formatAPY,
|
|
24
|
+
formatEthAmount,
|
|
25
|
+
parseCoverTiming,
|
|
26
|
+
} from "../utils";
|
|
27
|
+
import { NEXUS_KYC_REQUIREMENTS_URL, NEXUS_PROTOCOL_COVER_TERMS_URL } from "../constants";
|
|
28
|
+
import type { CoverOfferCardProps } from "../types";
|
|
29
|
+
|
|
30
|
+
const ShieldIcon = Shield as ComponentType<LucideProps>;
|
|
31
|
+
const ChevronDownIcon = ChevronDown as ComponentType<LucideProps>;
|
|
32
|
+
const ChevronUpIcon = ChevronUp as ComponentType<LucideProps>;
|
|
33
|
+
const CheckCircleIcon = CheckCircle as ComponentType<LucideProps>;
|
|
34
|
+
|
|
35
|
+
export function CoverOfferCard({
|
|
36
|
+
productId,
|
|
37
|
+
opportunity,
|
|
38
|
+
amountToCover,
|
|
39
|
+
buyerAddress,
|
|
40
|
+
protocolName,
|
|
41
|
+
coverProductName,
|
|
42
|
+
onSuccess,
|
|
43
|
+
onError,
|
|
44
|
+
startExpanded,
|
|
45
|
+
}: CoverOfferCardProps) {
|
|
46
|
+
const [isExpanded, setIsExpanded] = useState(startExpanded ?? false);
|
|
47
|
+
const [hasAgreedToTerms, setHasAgreedToTerms] = useState(false);
|
|
48
|
+
|
|
49
|
+
const { coverPeriod, setCoverPeriod, isLoading, error, quoteResult, premiumEth, yearlyCostPerc } =
|
|
50
|
+
useCoverQuote({
|
|
51
|
+
productId,
|
|
52
|
+
buyerAddress,
|
|
53
|
+
amountToCover,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { purchase, isPurchasing } = useNexusPurchase({
|
|
57
|
+
onSuccess,
|
|
58
|
+
onError,
|
|
59
|
+
});
|
|
60
|
+
const { hasCoverForProduct, isLoading: isCoversLoading } = useExistingCovers(buyerAddress);
|
|
61
|
+
const { data: balanceData, isLoading: isBalanceLoading } = useBalance({
|
|
62
|
+
address: buyerAddress as `0x${string}` | undefined,
|
|
63
|
+
query: { enabled: !!buyerAddress },
|
|
64
|
+
});
|
|
65
|
+
const userBalanceEth = balanceData?.value;
|
|
66
|
+
const premiumWei = premiumEth ? parseEther(premiumEth) : 0n;
|
|
67
|
+
const insufficientBalance =
|
|
68
|
+
isBalanceLoading ||
|
|
69
|
+
(userBalanceEth !== undefined && premiumWei > 0n && userBalanceEth < premiumWei);
|
|
70
|
+
|
|
71
|
+
const existingCover = hasCoverForProduct(productId);
|
|
72
|
+
const hasExistingCover = !!existingCover;
|
|
73
|
+
|
|
74
|
+
const originalAPY = getOpportunityAPY(opportunity);
|
|
75
|
+
const adjustedAPY =
|
|
76
|
+
yearlyCostPerc !== null ? calculateAdjustedAPY(originalAPY, yearlyCostPerc) : null;
|
|
77
|
+
const coverCostPerc = yearlyCostPerc !== null ? yearlyCostPerc * 100 : null;
|
|
78
|
+
|
|
79
|
+
const handlePurchase = async () => {
|
|
80
|
+
if (isPurchasing || !quoteResult) return;
|
|
81
|
+
await purchase(quoteResult);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const disabledReason = (() => {
|
|
85
|
+
if (!hasAgreedToTerms) return "Checkbox not marked";
|
|
86
|
+
if (!quoteResult) return "Quote not ready yet";
|
|
87
|
+
if (isLoading) return "Fetching quote...";
|
|
88
|
+
if (error) return error;
|
|
89
|
+
if (insufficientBalance) return "Insufficient balance for premium";
|
|
90
|
+
if (isPurchasing) return "Purchasing cover...";
|
|
91
|
+
return null;
|
|
92
|
+
})();
|
|
93
|
+
const isPurchaseDisabled =
|
|
94
|
+
!quoteResult ||
|
|
95
|
+
isLoading ||
|
|
96
|
+
!!error ||
|
|
97
|
+
!hasAgreedToTerms ||
|
|
98
|
+
isPurchasing ||
|
|
99
|
+
insufficientBalance;
|
|
100
|
+
|
|
101
|
+
const purchaseButton = (
|
|
102
|
+
<Button
|
|
103
|
+
onClick={handlePurchase}
|
|
104
|
+
disabled={isPurchaseDisabled}
|
|
105
|
+
className="w-full"
|
|
106
|
+
variant="green"
|
|
107
|
+
>
|
|
108
|
+
{isLoading ? "Getting Quote..." : isPurchasing ? "Purchasing Cover..." : "Purchase Cover"}
|
|
109
|
+
</Button>
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="rounded-xl bg-gradient-to-br from-primary/10 to-card border border-primary/20 overflow-hidden">
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
116
|
+
className="w-full p-4 flex items-center justify-between hover:bg-primary/5 transition-colors"
|
|
117
|
+
>
|
|
118
|
+
<div className="flex items-center gap-3">
|
|
119
|
+
<div className="p-2 rounded-lg bg-primary/10 border border-primary/20">
|
|
120
|
+
<ShieldIcon className="w-5 h-5 text-primary" />
|
|
121
|
+
</div>
|
|
122
|
+
<div className="text-left">
|
|
123
|
+
<div className="flex items-center gap-2">
|
|
124
|
+
<span className="text-sm font-semibold text-foreground">Protect Your Deposit</span>
|
|
125
|
+
<span className="px-2 py-0.5 text-[10px] font-medium text-primary bg-primary/10 rounded-full border border-primary/20">
|
|
126
|
+
NEW
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
<p className="text-xs text-muted-foreground">Nexus Mutual Single Protocol Cover</p>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="flex items-center gap-2">
|
|
133
|
+
{isExpanded ? (
|
|
134
|
+
<ChevronUpIcon className="w-5 h-5 text-muted-foreground" />
|
|
135
|
+
) : (
|
|
136
|
+
<ChevronDownIcon className="w-5 h-5 text-muted-foreground" />
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</button>
|
|
140
|
+
|
|
141
|
+
{/* Expandable Content */}
|
|
142
|
+
{isExpanded && (
|
|
143
|
+
<div className="px-4 pb-4 space-y-4">
|
|
144
|
+
{/* Protocol & Cover Info */}
|
|
145
|
+
<div className="flex items-center justify-between text-sm">
|
|
146
|
+
<span className="text-muted-foreground">
|
|
147
|
+
Protocol: <span className="font-medium text-foreground">{protocolName}</span>
|
|
148
|
+
</span>
|
|
149
|
+
<span className="text-[10px] text-muted-foreground px-2 py-1 rounded bg-muted/50 border border-border">
|
|
150
|
+
{coverProductName}
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Loading state for existing covers */}
|
|
155
|
+
{isCoversLoading && (
|
|
156
|
+
<div className="p-3 rounded-lg bg-background/50 border border-border">
|
|
157
|
+
<Skeleton className="h-4 w-48 mx-auto bg-neutral-alpha-10" />
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* Existing Cover Info */}
|
|
162
|
+
{hasExistingCover && existingCover && (
|
|
163
|
+
<>
|
|
164
|
+
<div className="p-3 rounded-lg bg-green-500/10 border border-green-500/20">
|
|
165
|
+
<div className="flex items-center gap-2 mb-2">
|
|
166
|
+
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
|
167
|
+
<span className="text-sm font-medium text-green-500">
|
|
168
|
+
You already have cover for this protocol
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
172
|
+
Your deposit is protected against smart contract bugs, oracle failures, and
|
|
173
|
+
governance attacks on this protocol. See{" "}
|
|
174
|
+
<a
|
|
175
|
+
href={NEXUS_PROTOCOL_COVER_TERMS_URL}
|
|
176
|
+
target="_blank"
|
|
177
|
+
rel="noopener noreferrer"
|
|
178
|
+
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
179
|
+
>
|
|
180
|
+
terms
|
|
181
|
+
</a>{" "}
|
|
182
|
+
for details.
|
|
183
|
+
</p>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div className="grid grid-cols-2 gap-3">
|
|
187
|
+
<div className="p-3 rounded-lg bg-background border border-border text-center">
|
|
188
|
+
<p className="text-[10px] uppercase tracking-wide text-muted-foreground mb-1">
|
|
189
|
+
Cover Amount
|
|
190
|
+
</p>
|
|
191
|
+
<p className="text-lg font-bold text-foreground">
|
|
192
|
+
{formatEthAmount(formatEther(existingCover.amount))} ETH
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="p-3 rounded-lg bg-background border border-border text-center">
|
|
196
|
+
<p className="text-[10px] uppercase tracking-wide text-muted-foreground mb-1">
|
|
197
|
+
Period
|
|
198
|
+
</p>
|
|
199
|
+
<p className="text-lg font-bold text-foreground">
|
|
200
|
+
{
|
|
201
|
+
parseCoverTiming(
|
|
202
|
+
existingCover.start,
|
|
203
|
+
existingCover.period,
|
|
204
|
+
existingCover.gracePeriod
|
|
205
|
+
).periodDays
|
|
206
|
+
}{" "}
|
|
207
|
+
days
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="p-3 rounded-lg bg-background border border-border">
|
|
213
|
+
<div className="flex items-center justify-between">
|
|
214
|
+
<span className="text-xs text-muted-foreground">Cover ID</span>
|
|
215
|
+
<span className="text-sm font-medium text-foreground">
|
|
216
|
+
#{existingCover.coverId.toString()}
|
|
217
|
+
</span>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
{/* Purchase Form - only show if no existing cover */}
|
|
224
|
+
{!hasExistingCover && !isCoversLoading && (
|
|
225
|
+
<>
|
|
226
|
+
{/* Value Proposition */}
|
|
227
|
+
<div className="p-3 rounded-lg bg-background/50 border border-border space-y-3">
|
|
228
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
229
|
+
<span className="text-primary font-medium">Safeguard your yield</span> against
|
|
230
|
+
protocol-specific risks. Deposit with confidence.
|
|
231
|
+
</p>
|
|
232
|
+
|
|
233
|
+
{/* Covered Events */}
|
|
234
|
+
<div className="space-y-2">
|
|
235
|
+
<p className="text-[10px] uppercase tracking-wide text-muted-foreground font-medium">
|
|
236
|
+
Covered Events
|
|
237
|
+
</p>
|
|
238
|
+
<ul className="text-[11px] text-muted-foreground space-y-1.5">
|
|
239
|
+
<li className="flex items-start gap-2">
|
|
240
|
+
<span className="text-primary mt-0.5">•</span>
|
|
241
|
+
<span>Smart contract bugs or exploits in the protocol</span>
|
|
242
|
+
</li>
|
|
243
|
+
<li className="flex items-start gap-2">
|
|
244
|
+
<span className="text-primary mt-0.5">•</span>
|
|
245
|
+
<span>Oracle manipulation or failure affecting the protocol</span>
|
|
246
|
+
</li>
|
|
247
|
+
<li className="flex items-start gap-2">
|
|
248
|
+
<span className="text-primary mt-0.5">•</span>
|
|
249
|
+
<span>Governance attacks on the protocol</span>
|
|
250
|
+
</li>
|
|
251
|
+
</ul>
|
|
252
|
+
<p className="text-[10px] text-muted-foreground/70 leading-relaxed">
|
|
253
|
+
Cover applies only to events directly related to this protocol. See full{" "}
|
|
254
|
+
<a
|
|
255
|
+
href={NEXUS_PROTOCOL_COVER_TERMS_URL}
|
|
256
|
+
target="_blank"
|
|
257
|
+
rel="noopener noreferrer"
|
|
258
|
+
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
259
|
+
>
|
|
260
|
+
terms and conditions
|
|
261
|
+
</a>{" "}
|
|
262
|
+
for exclusions.
|
|
263
|
+
</p>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* APY Comparison */}
|
|
268
|
+
<div className="grid grid-cols-3 gap-3">
|
|
269
|
+
<div className="p-3 rounded-lg bg-background border border-border text-center">
|
|
270
|
+
<p className="text-[10px] uppercase tracking-wide text-muted-foreground mb-1">
|
|
271
|
+
Base APR
|
|
272
|
+
</p>
|
|
273
|
+
<p className="text-lg font-bold text-foreground">{formatAPY(originalAPY)}</p>
|
|
274
|
+
</div>
|
|
275
|
+
<div className="p-3 rounded-lg bg-background/80 border border-border text-center">
|
|
276
|
+
<p className="text-[10px] uppercase tracking-wide text-muted-foreground mb-1"></p>
|
|
277
|
+
{isLoading ? (
|
|
278
|
+
<>
|
|
279
|
+
<Skeleton className="h-6 w-16 mx-auto bg-neutral-alpha-10" />
|
|
280
|
+
<Skeleton className="h-4 w-12 mx-auto mt-1 bg-neutral-alpha-10" />
|
|
281
|
+
</>
|
|
282
|
+
) : coverCostPerc !== null ? (
|
|
283
|
+
<>
|
|
284
|
+
<p className="text-lg font-bold text-orange-400">
|
|
285
|
+
-{coverCostPerc.toFixed(2)}%
|
|
286
|
+
</p>
|
|
287
|
+
<p className="text-[10px] text-muted-foreground">per year</p>
|
|
288
|
+
</>
|
|
289
|
+
) : (
|
|
290
|
+
<p className="text-lg font-bold text-muted-foreground">—</p>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
<div className="p-3 rounded-lg bg-primary/5 border border-primary/20 text-center">
|
|
294
|
+
<p className="text-[10px] uppercase tracking-wide text-primary/70 mb-1">
|
|
295
|
+
Net APY
|
|
296
|
+
</p>
|
|
297
|
+
{isLoading ? (
|
|
298
|
+
<>
|
|
299
|
+
<Skeleton className="h-6 w-14 mx-auto bg-neutral-alpha-10 " />
|
|
300
|
+
<Skeleton className="h-3 w-14 mx-auto mt-1 bg-neutral-alpha-10" />
|
|
301
|
+
</>
|
|
302
|
+
) : adjustedAPY !== null ? (
|
|
303
|
+
<>
|
|
304
|
+
<p className="text-lg font-bold text-primary">{formatAPY(adjustedAPY)}</p>
|
|
305
|
+
<p className="text-[10px] text-primary/60">protected</p>
|
|
306
|
+
</>
|
|
307
|
+
) : (
|
|
308
|
+
<p className="text-lg font-bold text-muted-foreground">—</p>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{/* Coverage Period Slider */}
|
|
314
|
+
<div className="space-y-3">
|
|
315
|
+
<div className="flex items-center justify-between">
|
|
316
|
+
<span className="text-xs text-muted-foreground">Coverage Period</span>
|
|
317
|
+
<div className="flex items-center gap-1">
|
|
318
|
+
<span className="text-sm font-medium text-foreground">{coverPeriod} days</span>
|
|
319
|
+
{isLoading ? (
|
|
320
|
+
<Skeleton className="h-4 w-20 bg-neutral-alpha-10" />
|
|
321
|
+
) : premiumEth ? (
|
|
322
|
+
<span className="text-sm text-muted-foreground">
|
|
323
|
+
({formatEthAmount(premiumEth)} ETH)
|
|
324
|
+
</span>
|
|
325
|
+
) : null}
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
<div className="w-full">
|
|
329
|
+
<Slider
|
|
330
|
+
value={[coverPeriod]}
|
|
331
|
+
onValueChange={(value) => setCoverPeriod(value[0])}
|
|
332
|
+
min={28}
|
|
333
|
+
max={365}
|
|
334
|
+
step={1}
|
|
335
|
+
/>
|
|
336
|
+
</div>
|
|
337
|
+
<div className="flex justify-between text-[10px] text-muted-foreground">
|
|
338
|
+
<span>28 days</span>
|
|
339
|
+
<span>365 days</span>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{/* Premium Details */}
|
|
344
|
+
<div className="flex items-center justify-between p-3 rounded-lg bg-background border border-border">
|
|
345
|
+
<div className="flex items-center gap-2">
|
|
346
|
+
<ShieldIcon className="w-4 h-4 text-primary" />
|
|
347
|
+
<span className="text-sm text-foreground">Premium</span>
|
|
348
|
+
</div>
|
|
349
|
+
{isLoading ? (
|
|
350
|
+
<Skeleton className="h-5 w-20 bg-neutral-alpha-10" />
|
|
351
|
+
) : premiumEth ? (
|
|
352
|
+
<span className="text-sm font-semibold text-foreground">
|
|
353
|
+
{formatEthAmount(premiumEth)} ETH
|
|
354
|
+
</span>
|
|
355
|
+
) : error ? (
|
|
356
|
+
<span className="text-sm text-destructive">No cover available</span>
|
|
357
|
+
) : (
|
|
358
|
+
<span className="text-sm text-muted-foreground">—</span>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
{/* Terms Agreement */}
|
|
363
|
+
<div className="space-y-3 p-3 rounded-lg bg-muted/20 border border-border ">
|
|
364
|
+
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
|
365
|
+
By buying Nexus Mutual Bundled Protocol Cover, you agree to the{" "}
|
|
366
|
+
<a
|
|
367
|
+
href={NEXUS_PROTOCOL_COVER_TERMS_URL}
|
|
368
|
+
target="_blank"
|
|
369
|
+
rel="noopener noreferrer"
|
|
370
|
+
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
371
|
+
>
|
|
372
|
+
terms and conditions
|
|
373
|
+
</a>
|
|
374
|
+
</p>
|
|
375
|
+
|
|
376
|
+
<label className="flex items-start gap-2 cursor-pointer group">
|
|
377
|
+
<div className="relative flex-shrink-0 mt-0.5">
|
|
378
|
+
<Checkbox
|
|
379
|
+
checked={hasAgreedToTerms}
|
|
380
|
+
onCheckedChange={(hasAgreedToTerms) =>
|
|
381
|
+
setHasAgreedToTerms(hasAgreedToTerms === true)
|
|
382
|
+
}
|
|
383
|
+
/>
|
|
384
|
+
</div>
|
|
385
|
+
<span className="text-[10px] text-muted-foreground leading-relaxed group-hover:text-foreground/70 transition-colors">
|
|
386
|
+
I confirm that I do not reside in the{" "}
|
|
387
|
+
<a
|
|
388
|
+
href={NEXUS_KYC_REQUIREMENTS_URL}
|
|
389
|
+
target="_blank"
|
|
390
|
+
rel="noopener noreferrer"
|
|
391
|
+
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
392
|
+
onClick={(e) => e.stopPropagation()}
|
|
393
|
+
>
|
|
394
|
+
countries listed here
|
|
395
|
+
</a>
|
|
396
|
+
, and acknowledge that in the event of a loss, I will be required to join as a
|
|
397
|
+
member of Nexus Mutual to file my claim.
|
|
398
|
+
</span>
|
|
399
|
+
</label>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
{/* Purchase Button */}
|
|
403
|
+
{disabledReason ? (
|
|
404
|
+
<Tooltip>
|
|
405
|
+
<TooltipTrigger asChild>
|
|
406
|
+
<div>{purchaseButton}</div>
|
|
407
|
+
</TooltipTrigger>
|
|
408
|
+
<TooltipContent side="top">{disabledReason}</TooltipContent>
|
|
409
|
+
</Tooltip>
|
|
410
|
+
) : (
|
|
411
|
+
purchaseButton
|
|
412
|
+
)}
|
|
413
|
+
</>
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
}
|