feat: Add UnlockProOverlay component and its styles for subscription upgrades.
This commit is contained in:
@@ -0,0 +1,734 @@
|
|||||||
|
/* ── Outer shell ── */
|
||||||
|
.modal {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 820px;
|
||||||
|
max-height: calc(100dvh - 2.5rem);
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 28px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
|
background: linear-gradient(160deg, #111827 0%, #0d1525 40%, #090f1c 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||||
|
0 32px 80px -12px rgba(0, 0, 0, 0.8),
|
||||||
|
0 0 120px -40px rgba(79, 109, 230, 0.28);
|
||||||
|
padding: 28px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.modal {
|
||||||
|
padding: 36px 36px 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Ambient glows (not clipped by modal) ── */
|
||||||
|
.glowLeft {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
left: -80px;
|
||||||
|
top: -80px;
|
||||||
|
width: 280px;
|
||||||
|
height: 280px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(59, 130, 246, 0.18) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glowRight {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
right: -60px;
|
||||||
|
bottom: -60px;
|
||||||
|
width: 240px;
|
||||||
|
height: 240px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(99, 102, 241, 0.14) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topLine {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 60%;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(96, 165, 250, 0.35),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Close button ── */
|
||||||
|
.closeBtn {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 20;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeBtn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeBtn:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ── */
|
||||||
|
.header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 5px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.28);
|
||||||
|
background: rgba(251, 191, 36, 0.1);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeText {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: clamp(1.9rem, 5vw, 3rem);
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.05;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
background: linear-gradient(175deg, #fff 20%, #94a3b8 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #64748b;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Card grid ── */
|
||||||
|
.grid {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Plan card ── */
|
||||||
|
.planCard {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(30, 58, 110, 0.55) 0%,
|
||||||
|
rgba(15, 28, 58, 0.4) 100%
|
||||||
|
);
|
||||||
|
padding: 22px;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planCard::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(59, 130, 246, 0.12) 0%,
|
||||||
|
transparent 60%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Points card (eligible) ── */
|
||||||
|
.pointsCard {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
padding: 22px;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition:
|
||||||
|
border-color 0.3s,
|
||||||
|
background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsCardActive {
|
||||||
|
border-color: rgba(52, 211, 153, 0.3);
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(16, 60, 45, 0.5) 0%,
|
||||||
|
rgba(5, 30, 22, 0.35) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsCardActive::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(52, 211, 153, 0.1) 0%,
|
||||||
|
transparent 60%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Points not-eligible card ── */
|
||||||
|
.pointsUnavailableCard {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px dashed rgba(255, 255, 255, 0.12);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
padding: 22px;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsUnavailableCard:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Plan label chip ── */
|
||||||
|
.planChip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(59, 130, 246, 0.18);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.13em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #93c5fd;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Price ── */
|
||||||
|
.price {
|
||||||
|
font-size: 50px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #fff;
|
||||||
|
margin: 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceSuffix {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Feature list ── */
|
||||||
|
.featureList {
|
||||||
|
list-style: none;
|
||||||
|
margin: 16px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(59, 130, 246, 0.18);
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Points card header row ── */
|
||||||
|
.pointsHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsLabelRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsIconBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1e293b;
|
||||||
|
color: #475569;
|
||||||
|
transition:
|
||||||
|
background 0.2s,
|
||||||
|
color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsIconBoxActive {
|
||||||
|
background: #10b981;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsLabel {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.13em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #475569;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsLabelActive {
|
||||||
|
color: #6ee7b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Toggle ── */
|
||||||
|
.toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #1e293b;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleActive {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleThumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleThumbActive {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Discount amount ── */
|
||||||
|
.discount {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #334155;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discountActive {
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discountSuffix {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #334155;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsNote {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsBalance {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balanceNum {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #475569;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balanceNumActive {
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Unavailable card content ── */
|
||||||
|
.unavailTitle {
|
||||||
|
font-size: 14.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailDesc {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #475569;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressWrap {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #334155;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressTrack {
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressFill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, #3b82f6, #6366f1);
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailCta {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 16px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3b82f6;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailCta:hover {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Payment summary box ── */
|
||||||
|
.summaryBox {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
margin-top: 18px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryLabel {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #475569;
|
||||||
|
margin: 0 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryOriginal {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #334155;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryAmount {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryPrice {
|
||||||
|
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryUnit {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryDivider {
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
margin: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaWrap {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── CTA Button ── */
|
||||||
|
.ctaBtn {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: none;
|
||||||
|
background: linear-gradient(90deg, #2563eb 0%, #4f46e5 100%);
|
||||||
|
padding: 14px 24px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(99, 102, 241, 0.5),
|
||||||
|
0 8px 32px -6px rgba(79, 86, 230, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaBtn:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(90deg, #3b82f6 0%, #6366f1 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(99, 102, 241, 0.7),
|
||||||
|
0 12px 40px -6px rgba(99, 102, 241, 0.65);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaBtn:active:not(:disabled) {
|
||||||
|
transform: scale(0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaBtn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shimmer on hover */
|
||||||
|
.ctaBtn::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
105deg,
|
||||||
|
transparent 35%,
|
||||||
|
rgba(255, 255, 255, 0.12) 50%,
|
||||||
|
transparent 65%
|
||||||
|
);
|
||||||
|
transform: translateX(-100%) skewX(-15deg);
|
||||||
|
transition: transform 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaBtn:hover::after {
|
||||||
|
transform: translateX(100%) skewX(-15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Footer ── */
|
||||||
|
.footer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
margin-top: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerLink {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerLink:hover {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Alert messages ── */
|
||||||
|
.alertError {
|
||||||
|
margin-top: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
|
background: rgba(239, 68, 68, 0.07);
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertInfo {
|
||||||
|
margin-top: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(52, 211, 153, 0.2);
|
||||||
|
background: rgba(52, 211, 153, 0.07);
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #6ee7b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertIconBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertIconError {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertIconInfo {
|
||||||
|
background: rgba(52, 211, 153, 0.2);
|
||||||
|
color: #6ee7b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Chip for unavail ── */
|
||||||
|
.unavailChip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailChipIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #334155;
|
||||||
|
transition:
|
||||||
|
background 0.2s,
|
||||||
|
color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsUnavailableCard:hover .unavailChipIcon {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailChipLabel {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.13em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import s from "./UnlockProOverlay.module.css";
|
||||||
|
|
||||||
export type UnlockProBilling = {
|
export type UnlockProBilling = {
|
||||||
pointsEnabled: boolean;
|
pointsEnabled: boolean;
|
||||||
@@ -46,16 +47,12 @@ type UnlockProOverlayProps = {
|
|||||||
telegramGroupUrl?: string;
|
telegramGroupUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const features = {
|
const FEATURES = {
|
||||||
"zh-CN": [
|
"zh-CN": ["15天高精度趋势预报", "实时多源雷达图", "全平台智能气象推送"],
|
||||||
{ icon: TrendingUp, label: "15天高精度趋势预报" },
|
|
||||||
{ icon: Radar, label: "实时多源雷达图" },
|
|
||||||
{ icon: Send, label: "全平台智能气象推送" },
|
|
||||||
],
|
|
||||||
"en-US": [
|
"en-US": [
|
||||||
{ icon: TrendingUp, label: "15-day precision forecast" },
|
"15-day precision forecast",
|
||||||
{ icon: Radar, label: "Real-time radar panel" },
|
"Real-time radar panel",
|
||||||
{ icon: Send, label: "Cross-platform alert push" },
|
"Cross-platform alerts",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,358 +73,284 @@ export function UnlockProOverlay({
|
|||||||
telegramGroupUrl,
|
telegramGroupUrl,
|
||||||
}: UnlockProOverlayProps) {
|
}: UnlockProOverlayProps) {
|
||||||
const isEn = locale === "en-US";
|
const isEn = locale === "en-US";
|
||||||
|
const canUsePoints = billing.pointsEnabled && billing.isEligible;
|
||||||
const title = isEn ? "Unlock PolyWeather Pro" : "解锁 PolyWeather Pro";
|
const featureList = FEATURES[locale] ?? FEATURES["zh-CN"];
|
||||||
const subtitle = isEn
|
|
||||||
? "High-precision weather intelligence, delivered everywhere."
|
|
||||||
: "全球最精准的高精度气象推送,全平台覆盖";
|
|
||||||
|
|
||||||
const finalPayLabel =
|
const finalPayLabel =
|
||||||
payLabel || (isEn ? "Subscribe & Activate" : "立即订阅并激活服务");
|
payLabel || (isEn ? "Subscribe & Activate" : "立即订阅并激活服务");
|
||||||
|
|
||||||
const featureList = features[locale] ?? features["zh-CN"];
|
const progressPct = billing.pointsEnabled
|
||||||
|
? Math.min(100, Math.round((points / billing.pointsPerUsd) * 100))
|
||||||
const canUsePoints = billing.pointsEnabled && billing.isEligible;
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full max-w-[820px] overflow-hidden rounded-[32px] border border-white/[0.08] bg-[#0c1220] shadow-[0_0_0_1px_rgba(255,255,255,0.04),0_32px_80px_-16px_rgba(0,0,0,0.7),0_0_120px_-40px_rgba(99,130,246,0.25)]">
|
<div className={s.modal}>
|
||||||
{/* Ambient background */}
|
{/* Ambient glows */}
|
||||||
<div className="pointer-events-none absolute inset-0">
|
<div className={s.glowLeft} />
|
||||||
<div className="absolute -left-32 -top-32 h-72 w-72 rounded-full bg-blue-600/20 blur-[80px]" />
|
<div className={s.glowRight} />
|
||||||
<div className="absolute -bottom-24 -right-16 h-64 w-64 rounded-full bg-indigo-600/15 blur-[80px]" />
|
<div className={s.topLine} />
|
||||||
<div className="absolute left-1/2 top-0 h-px w-3/4 -translate-x-1/2 bg-gradient-to-r from-transparent via-blue-400/30 to-transparent" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Close button */}
|
{/* Close button */}
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute right-4 top-4 z-20 flex h-9 w-9 items-center justify-center rounded-full border border-white/10 bg-white/5 text-slate-500 backdrop-blur-sm transition-all hover:border-white/20 hover:bg-white/10 hover:text-white active:scale-90"
|
className={s.closeBtn}
|
||||||
title={isEn ? "Close" : "关闭"}
|
title={isEn ? "Close" : "关闭"}
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={15} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative z-10 p-6 md:p-8 lg:p-10">
|
{/* ── Header ── */}
|
||||||
{/* Header */}
|
<div className={s.header}>
|
||||||
<div className="mb-8 flex flex-col items-center text-center">
|
<div className={s.badge}>
|
||||||
{/* Badge */}
|
<Crown size={12} style={{ color: "#fbbf24" }} />
|
||||||
<div className="mb-5 inline-flex items-center gap-2 rounded-full border border-amber-400/25 bg-amber-400/10 px-3.5 py-1.5 backdrop-blur-sm">
|
<span className={s.badgeText}>Pro</span>
|
||||||
<Crown size={13} className="text-amber-400" />
|
|
||||||
<span className="text-[11px] font-bold uppercase tracking-[0.14em] text-amber-300">
|
|
||||||
Pro
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="bg-gradient-to-b from-white via-white to-slate-300 bg-clip-text text-4xl font-black leading-[1.05] tracking-tight text-transparent md:text-5xl lg:text-[3.25rem]">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-3 max-w-[440px] text-sm leading-relaxed text-slate-400 md:text-[15px]">
|
|
||||||
{subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h2 className={s.title}>
|
||||||
|
{isEn ? "Unlock PolyWeather Pro" : "解锁 PolyWeather Pro"}
|
||||||
|
</h2>
|
||||||
|
<p className={s.subtitle}>
|
||||||
|
{isEn
|
||||||
|
? "High-precision weather intelligence, delivered everywhere."
|
||||||
|
: "全球最精准的高精度气象推送,全平台覆盖"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Main content grid */}
|
{/* ── Cards ── */}
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-[1fr_1fr]">
|
<div className={s.grid}>
|
||||||
{/* Left: Plan card */}
|
{/* Left: Plan card */}
|
||||||
<div className="relative overflow-hidden rounded-2xl border border-blue-500/20 bg-gradient-to-br from-blue-600/10 via-blue-500/5 to-transparent p-5 md:p-6">
|
<div className={s.planCard}>
|
||||||
{/* Inner glow */}
|
<span className={s.planChip}>
|
||||||
<div className="pointer-events-none absolute -right-8 -top-8 h-32 w-32 rounded-full bg-blue-500/20 blur-2xl" />
|
<Zap size={10} />
|
||||||
|
Standard Pro
|
||||||
<div className="relative">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="inline-flex items-center gap-1.5 rounded-md bg-blue-500/15 px-2 py-1 text-[10px] font-bold uppercase tracking-[0.14em] text-blue-300">
|
|
||||||
<Zap size={10} />
|
|
||||||
Standard Pro
|
|
||||||
</span>
|
|
||||||
<span className="text-[11px] text-slate-500">
|
|
||||||
/ {isEn ? "mo" : "月"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 flex items-baseline gap-1">
|
|
||||||
<span className="text-[52px] font-black leading-none tracking-tight text-white">
|
|
||||||
${planPriceUsd.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 space-y-2.5">
|
|
||||||
{featureList.map(({ icon: Icon, label }, i) => (
|
|
||||||
<div key={i} className="flex items-center gap-2.5">
|
|
||||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-md bg-blue-500/20">
|
|
||||||
<CheckCircle2 size={12} className="text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<span className="text-[13px] text-slate-300">{label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right: Points card */}
|
|
||||||
{canUsePoints ? (
|
|
||||||
<div
|
|
||||||
className={`relative overflow-hidden rounded-2xl border p-5 transition-all duration-300 md:p-6 ${
|
|
||||||
usePoints
|
|
||||||
? "border-emerald-500/30 bg-gradient-to-br from-emerald-600/10 via-emerald-500/5 to-transparent"
|
|
||||||
: "border-white/[0.08] bg-white/[0.03]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`pointer-events-none absolute -right-8 -top-8 h-32 w-32 rounded-full blur-2xl transition-all duration-300 ${
|
|
||||||
usePoints ? "bg-emerald-500/15" : "bg-transparent"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
{/* Header row */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={`flex h-7 w-7 items-center justify-center rounded-lg transition-colors duration-200 ${
|
|
||||||
usePoints
|
|
||||||
? "bg-emerald-500 text-white"
|
|
||||||
: "bg-slate-800 text-slate-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Coins size={14} />
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`text-[11px] font-bold uppercase tracking-[0.12em] transition-colors duration-200 ${
|
|
||||||
usePoints ? "text-emerald-300" : "text-slate-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isEn ? "Points Credit" : "积分抵扣"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Toggle */}
|
|
||||||
<button
|
|
||||||
onClick={onToggleUsePoints}
|
|
||||||
className={`relative h-6 w-11 flex-shrink-0 cursor-pointer rounded-full transition-all duration-300 ${
|
|
||||||
usePoints ? "bg-emerald-500" : "bg-slate-700"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`absolute top-1 h-4 w-4 rounded-full bg-white shadow transition-all duration-200 ${
|
|
||||||
usePoints ? "right-1" : "left-1"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Discount display */}
|
|
||||||
<div className="mt-4 flex items-baseline gap-1.5">
|
|
||||||
<span
|
|
||||||
className={`text-[48px] font-black leading-none tracking-tight transition-colors duration-300 ${
|
|
||||||
usePoints ? "text-emerald-400" : "text-slate-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
-${billing.discountAmount.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
<span className="mb-1 text-[10px] font-bold uppercase tracking-[0.1em] text-slate-600">
|
|
||||||
off
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="mt-3 text-[12px] leading-5 text-slate-400">
|
|
||||||
{usePoints
|
|
||||||
? isEn
|
|
||||||
? `Using ${billing.pointsUsed} pts · saves $${billing.discountAmount.toFixed(2)}`
|
|
||||||
: `已消耗 ${billing.pointsUsed} 积分 · 省 $${billing.discountAmount.toFixed(2)}`
|
|
||||||
: isEn
|
|
||||||
? `Toggle to save up to $${billing.maxDiscountUsd.toFixed(2)}`
|
|
||||||
: `开启可最多抵扣 $${billing.maxDiscountUsd.toFixed(2)}`}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Points balance */}
|
|
||||||
<div className="mt-4 flex items-center gap-1.5">
|
|
||||||
<Sparkles size={11} className="text-slate-600" />
|
|
||||||
<span className="text-[11px] text-slate-500">
|
|
||||||
{isEn ? "Your balance:" : "当前积分:"}{" "}
|
|
||||||
<span
|
|
||||||
className={`font-bold ${
|
|
||||||
usePoints ? "text-emerald-400" : "text-slate-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{points}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
/* Not eligible / points disabled */
|
|
||||||
<div className="group relative overflow-hidden rounded-2xl border border-dashed border-white/10 bg-white/[0.025] p-5 transition-all duration-300 hover:border-white/20 md:p-6">
|
|
||||||
<div className="relative flex h-full flex-col">
|
|
||||||
{/* Top */}
|
|
||||||
<div>
|
|
||||||
<div className="mb-3 flex items-center gap-2">
|
|
||||||
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-slate-800 text-slate-600 transition-colors duration-200 group-hover:bg-slate-700 group-hover:text-slate-400">
|
|
||||||
<Coins size={14} />
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-600">
|
|
||||||
{!billing.pointsEnabled
|
|
||||||
? isEn
|
|
||||||
? "Points Disabled"
|
|
||||||
: "积分未开启"
|
|
||||||
: isEn
|
|
||||||
? "Insufficient Points"
|
|
||||||
: "积分不足"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 className="text-base font-bold text-slate-200">
|
|
||||||
{isEn ? "Earn Points & Save" : "赚取积分,抵扣订阅"}
|
|
||||||
</h4>
|
|
||||||
<p className="mt-1.5 text-[12px] leading-5 text-slate-500">
|
|
||||||
{!billing.pointsEnabled
|
|
||||||
? isEn
|
|
||||||
? "Points redemption is unavailable for this plan."
|
|
||||||
: "当前套餐暂不支持积分抵扣。"
|
|
||||||
: isEn
|
|
||||||
? `Need ${billing.pointsPerUsd} pts minimum. You have: ${points}`
|
|
||||||
: `至少需要 ${billing.pointsPerUsd} 积分,当前仅有 ${points}`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress bar */}
|
|
||||||
{billing.pointsEnabled && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="mb-1.5 flex items-center justify-between text-[10px] text-slate-600">
|
|
||||||
<span>
|
|
||||||
{points} / {billing.pointsPerUsd}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{Math.min(
|
|
||||||
100,
|
|
||||||
Math.round((points / billing.pointsPerUsd) * 100),
|
|
||||||
)}
|
|
||||||
%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-1 w-full overflow-hidden rounded-full bg-slate-800">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full bg-gradient-to-r from-blue-600 to-indigo-500 transition-all duration-500"
|
|
||||||
style={{
|
|
||||||
width: `${Math.min(100, (points / billing.pointsPerUsd) * 100)}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Bottom CTA */}
|
|
||||||
<div className="mt-auto pt-4">
|
|
||||||
{telegramGroupUrl ? (
|
|
||||||
<Link
|
|
||||||
href={telegramGroupUrl}
|
|
||||||
target="_blank"
|
|
||||||
className="inline-flex items-center gap-1.5 text-[11px] font-semibold text-blue-400 transition-colors hover:text-blue-300"
|
|
||||||
>
|
|
||||||
<MessageSquare size={12} />
|
|
||||||
{isEn ? "Join Telegram to earn" : "加入电报群赚取积分"}
|
|
||||||
<ArrowRight size={11} />
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center gap-1.5 text-[11px] text-slate-600">
|
|
||||||
<MessageSquare size={12} />
|
|
||||||
{isEn
|
|
||||||
? "Join community to earn points"
|
|
||||||
: "加入社群即可赚取积分"}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Payment summary */}
|
|
||||||
<div className="mt-5 overflow-hidden rounded-2xl border border-white/[0.07] bg-white/[0.03]">
|
|
||||||
{/* Summary row */}
|
|
||||||
<div className="flex items-center justify-between px-5 py-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-600">
|
|
||||||
{isEn ? "Total Due Today" : "今日应付"}
|
|
||||||
</p>
|
|
||||||
{billing.discountAmount > 0 && usePoints && (
|
|
||||||
<p className="text-[11px] text-slate-600 line-through">
|
|
||||||
${planPriceUsd.toFixed(2)} USD
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-baseline gap-1.5">
|
|
||||||
<span className="font-mono text-[36px] font-black leading-none tracking-tight text-white md:text-[42px]">
|
|
||||||
${billing.finalPrice.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-xs font-bold uppercase tracking-[0.1em] text-slate-500">
|
|
||||||
USD
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CTA Button */}
|
|
||||||
<div className="border-t border-white/[0.06] p-3">
|
|
||||||
<button
|
|
||||||
onClick={onPay}
|
|
||||||
disabled={payBusy}
|
|
||||||
className="group relative flex w-full items-center justify-center gap-3 overflow-hidden rounded-xl bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-3.5 text-[15px] font-bold text-white shadow-[0_0_0_1px_rgba(99,102,241,0.5),0_8px_32px_-8px_rgba(99,102,241,0.6)] transition-all duration-200 hover:from-blue-500 hover:to-indigo-500 hover:shadow-[0_0_0_1px_rgba(99,102,241,0.7),0_12px_40px_-8px_rgba(99,102,241,0.7)] active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-60"
|
|
||||||
>
|
|
||||||
{/* Shimmer */}
|
|
||||||
<div className="pointer-events-none absolute inset-0 -translate-x-full skew-x-12 bg-gradient-to-r from-transparent via-white/10 to-transparent transition-transform duration-700 group-hover:translate-x-full" />
|
|
||||||
|
|
||||||
{payBusy ? (
|
|
||||||
<Loader2 size={18} className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Wallet size={17} />
|
|
||||||
<span>{finalPayLabel}</span>
|
|
||||||
<ArrowRight
|
|
||||||
size={17}
|
|
||||||
className="transition-transform duration-200 group-hover:translate-x-0.5"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer links */}
|
|
||||||
<div className="mt-4 flex items-center justify-center gap-6 text-[10px] font-semibold uppercase tracking-[0.12em] text-slate-700">
|
|
||||||
<span className="flex items-center gap-1.5">
|
|
||||||
<Lock size={11} />
|
|
||||||
{isEn ? "Secure Payment" : "安全付款"}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="text-slate-800">·</span>
|
|
||||||
<Link
|
<div
|
||||||
href={faqHref}
|
style={{
|
||||||
className="flex items-center gap-1.5 transition-colors hover:text-slate-500"
|
display: "flex",
|
||||||
|
alignItems: "baseline",
|
||||||
|
gap: 4,
|
||||||
|
marginTop: 12,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<BellRing size={11} />
|
<span className={s.price}>${planPriceUsd.toFixed(2)}</span>
|
||||||
{isEn ? "Subscription FAQ" : "订阅说明"}
|
<span className={s.priceSuffix}>/ {isEn ? "mo" : "月"}</span>
|
||||||
</Link>
|
</div>
|
||||||
|
|
||||||
|
<ul className={s.featureList}>
|
||||||
|
{featureList.map((item, i) => (
|
||||||
|
<li key={i} className={s.featureItem}>
|
||||||
|
<span className={s.featureIcon}>
|
||||||
|
<CheckCircle2 size={11} />
|
||||||
|
</span>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error / Info messages */}
|
{/* Right: Points card */}
|
||||||
{errorText && (
|
{canUsePoints ? (
|
||||||
<div className="mt-4 flex items-start gap-2.5 rounded-xl border border-rose-500/20 bg-rose-500/8 p-3">
|
<div
|
||||||
<div className="mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-rose-500/20">
|
className={`${s.pointsCard} ${usePoints ? s.pointsCardActive : ""}`}
|
||||||
<X size={10} className="text-rose-400" />
|
>
|
||||||
|
{/* Header row */}
|
||||||
|
<div className={s.pointsHeader}>
|
||||||
|
<div className={s.pointsLabelRow}>
|
||||||
|
<div
|
||||||
|
className={`${s.pointsIconBox} ${usePoints ? s.pointsIconBoxActive : ""}`}
|
||||||
|
>
|
||||||
|
<Coins size={13} />
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`${s.pointsLabel} ${usePoints ? s.pointsLabelActive : ""}`}
|
||||||
|
>
|
||||||
|
{isEn ? "Points Credit" : "积分抵扣"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onToggleUsePoints}
|
||||||
|
className={`${s.toggle} ${usePoints ? s.toggleActive : ""}`}
|
||||||
|
title={isEn ? "Toggle points" : "切换积分"}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`${s.toggleThumb} ${usePoints ? s.toggleThumbActive : ""}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Discount amount */}
|
||||||
|
<div style={{ display: "flex", alignItems: "baseline" }}>
|
||||||
|
<span
|
||||||
|
className={`${s.discount} ${usePoints ? s.discountActive : ""}`}
|
||||||
|
>
|
||||||
|
-${billing.discountAmount.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<span className={s.discountSuffix}>off</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className={s.pointsNote}>
|
||||||
|
{usePoints
|
||||||
|
? isEn
|
||||||
|
? `Using ${billing.pointsUsed} pts · saves $${billing.discountAmount.toFixed(2)}`
|
||||||
|
: `已消耗 ${billing.pointsUsed} 积分 · 省 $${billing.discountAmount.toFixed(2)}`
|
||||||
|
: isEn
|
||||||
|
? `Toggle to save up to $${billing.maxDiscountUsd.toFixed(2)}`
|
||||||
|
: `开启可最多抵扣 $${billing.maxDiscountUsd.toFixed(2)}`}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={s.pointsBalance}>
|
||||||
|
<Sparkles size={11} />
|
||||||
|
<span>
|
||||||
|
{isEn ? "Balance:" : "当前积分:"}{" "}
|
||||||
|
<span
|
||||||
|
className={`${s.balanceNum} ${usePoints ? s.balanceNumActive : ""}`}
|
||||||
|
>
|
||||||
|
{points}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[12px] leading-5 text-rose-400">{errorText}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
{infoText && (
|
/* Not eligible */
|
||||||
<div className="mt-4 flex items-start gap-2.5 rounded-xl border border-emerald-500/20 bg-emerald-500/8 p-3">
|
<div className={s.pointsUnavailableCard}>
|
||||||
<div className="mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-emerald-500/20">
|
<div className={s.unavailChip}>
|
||||||
<CheckCircle2 size={10} className="text-emerald-400" />
|
<div className={s.unavailChipIcon}>
|
||||||
|
<Coins size={12} />
|
||||||
|
</div>
|
||||||
|
<span className={s.unavailChipLabel}>
|
||||||
|
{!billing.pointsEnabled
|
||||||
|
? isEn
|
||||||
|
? "Points Disabled"
|
||||||
|
: "积分未开启"
|
||||||
|
: isEn
|
||||||
|
? "Insufficient Points"
|
||||||
|
: "积分不足"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className={s.unavailTitle}>
|
||||||
|
{isEn ? "Earn Points & Save" : "赚取积分,抵扣订阅"}
|
||||||
|
</h4>
|
||||||
|
<p className={s.unavailDesc}>
|
||||||
|
{!billing.pointsEnabled
|
||||||
|
? isEn
|
||||||
|
? "Points redemption is unavailable for this plan."
|
||||||
|
: "当前套餐暂不支持积分抵扣。"
|
||||||
|
: isEn
|
||||||
|
? `Need ${billing.pointsPerUsd} pts minimum. You have: ${points}`
|
||||||
|
: `至少需要 ${billing.pointsPerUsd} 积分,当前仅有 ${points}`}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{billing.pointsEnabled && (
|
||||||
|
<div className={s.progressWrap}>
|
||||||
|
<div className={s.progressHeader}>
|
||||||
|
<span>
|
||||||
|
{points} / {billing.pointsPerUsd}
|
||||||
|
</span>
|
||||||
|
<span>{progressPct}%</span>
|
||||||
|
</div>
|
||||||
|
<div className={s.progressTrack}>
|
||||||
|
<div
|
||||||
|
className={s.progressFill}
|
||||||
|
style={{ width: `${progressPct}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ marginTop: "auto", paddingTop: 16 }}>
|
||||||
|
{telegramGroupUrl ? (
|
||||||
|
<Link
|
||||||
|
href={telegramGroupUrl}
|
||||||
|
target="_blank"
|
||||||
|
className={s.unavailCta}
|
||||||
|
>
|
||||||
|
<MessageSquare size={12} />
|
||||||
|
{isEn ? "Join Telegram to earn" : "加入电报群赚取积分"}
|
||||||
|
<ArrowRight size={11} />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className={s.unavailCta} style={{ cursor: "default" }}>
|
||||||
|
<MessageSquare size={12} />
|
||||||
|
{isEn
|
||||||
|
? "Join community to earn points"
|
||||||
|
: "加入社群即可赚取积分"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[12px] leading-5 text-emerald-400">{infoText}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Payment summary ── */}
|
||||||
|
<div className={s.summaryBox}>
|
||||||
|
<div className={s.summaryRow}>
|
||||||
|
<div>
|
||||||
|
<p className={s.summaryLabel}>
|
||||||
|
{isEn ? "Total Due Today" : "今日应付"}
|
||||||
|
</p>
|
||||||
|
{billing.discountAmount > 0 && usePoints && (
|
||||||
|
<p className={s.summaryOriginal}>
|
||||||
|
${planPriceUsd.toFixed(2)} USD
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={s.summaryAmount}>
|
||||||
|
<span className={s.summaryPrice}>
|
||||||
|
${billing.finalPrice.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<span className={s.summaryUnit}>USD</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={s.summaryDivider} />
|
||||||
|
|
||||||
|
<div className={s.ctaWrap}>
|
||||||
|
<button onClick={onPay} disabled={payBusy} className={s.ctaBtn}>
|
||||||
|
{payBusy ? (
|
||||||
|
<Loader2 size={18} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Wallet size={17} />
|
||||||
|
{finalPayLabel}
|
||||||
|
<ArrowRight size={17} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Footer ── */}
|
||||||
|
<div className={s.footer}>
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 5 }}>
|
||||||
|
<Lock size={11} />
|
||||||
|
{isEn ? "Secure Payment" : "安全付款"}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: "#0f172a" }}>·</span>
|
||||||
|
<Link href={faqHref} className={s.footerLink}>
|
||||||
|
<BellRing size={11} />
|
||||||
|
{isEn ? "Subscription FAQ" : "订阅说明"}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Error / Info ── */}
|
||||||
|
{errorText && (
|
||||||
|
<div className={s.alertError}>
|
||||||
|
<div className={`${s.alertIconBox} ${s.alertIconError}`}>
|
||||||
|
<X size={10} />
|
||||||
|
</div>
|
||||||
|
{errorText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{infoText && (
|
||||||
|
<div className={s.alertInfo}>
|
||||||
|
<div className={`${s.alertIconBox} ${s.alertIconInfo}`}>
|
||||||
|
<CheckCircle2 size={10} />
|
||||||
|
</div>
|
||||||
|
{infoText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user