diff --git a/frontend/components/subscription/UnlockProOverlay.module.css b/frontend/components/subscription/UnlockProOverlay.module.css new file mode 100644 index 00000000..16f323df --- /dev/null +++ b/frontend/components/subscription/UnlockProOverlay.module.css @@ -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; +} diff --git a/frontend/components/subscription/UnlockProOverlay.tsx b/frontend/components/subscription/UnlockProOverlay.tsx index 45c3d8cf..114f9c62 100644 --- a/frontend/components/subscription/UnlockProOverlay.tsx +++ b/frontend/components/subscription/UnlockProOverlay.tsx @@ -18,6 +18,7 @@ import { X, Zap, } from "lucide-react"; +import s from "./UnlockProOverlay.module.css"; export type UnlockProBilling = { pointsEnabled: boolean; @@ -46,16 +47,12 @@ type UnlockProOverlayProps = { telegramGroupUrl?: string; }; -const features = { - "zh-CN": [ - { icon: TrendingUp, label: "15天高精度趋势预报" }, - { icon: Radar, label: "实时多源雷达图" }, - { icon: Send, label: "全平台智能气象推送" }, - ], +const FEATURES = { + "zh-CN": ["15天高精度趋势预报", "实时多源雷达图", "全平台智能气象推送"], "en-US": [ - { icon: TrendingUp, label: "15-day precision forecast" }, - { icon: Radar, label: "Real-time radar panel" }, - { icon: Send, label: "Cross-platform alert push" }, + "15-day precision forecast", + "Real-time radar panel", + "Cross-platform alerts", ], }; @@ -76,358 +73,284 @@ export function UnlockProOverlay({ telegramGroupUrl, }: UnlockProOverlayProps) { const isEn = locale === "en-US"; - - const title = isEn ? "Unlock PolyWeather Pro" : "解锁 PolyWeather Pro"; - const subtitle = isEn - ? "High-precision weather intelligence, delivered everywhere." - : "全球最精准的高精度气象推送,全平台覆盖"; - + const canUsePoints = billing.pointsEnabled && billing.isEligible; + const featureList = FEATURES[locale] ?? FEATURES["zh-CN"]; const finalPayLabel = payLabel || (isEn ? "Subscribe & Activate" : "立即订阅并激活服务"); - const featureList = features[locale] ?? features["zh-CN"]; - - const canUsePoints = billing.pointsEnabled && billing.isEligible; + const progressPct = billing.pointsEnabled + ? Math.min(100, Math.round((points / billing.pointsPerUsd) * 100)) + : 0; return ( -
- {subtitle} -
+ {/* ── Header ── */} ++ {isEn + ? "High-precision weather intelligence, delivered everywhere." + : "全球最精准的高精度气象推送,全平台覆盖"} +
+- {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)}`} -
- - {/* Points balance */} -- {!billing.pointsEnabled - ? isEn - ? "Points redemption is unavailable for this plan." - : "当前套餐暂不支持积分抵扣。" - : isEn - ? `Need ${billing.pointsPerUsd} pts minimum. You have: ${points}` - : `至少需要 ${billing.pointsPerUsd} 积分,当前仅有 ${points}`} -
-- {isEn ? "Total Due Today" : "今日应付"} -
- {billing.discountAmount > 0 && usePoints && ( -- ${planPriceUsd.toFixed(2)} USD -
- )} -+ {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)}`} +
+ +{errorText}
+ {!billing.pointsEnabled + ? isEn + ? "Points redemption is unavailable for this plan." + : "当前套餐暂不支持积分抵扣。" + : isEn + ? `Need ${billing.pointsPerUsd} pts minimum. You have: ${points}` + : `至少需要 ${billing.pointsPerUsd} 积分,当前仅有 ${points}`} +
+ + {billing.pointsEnabled && ( +{infoText}
+ {isEn ? "Total Due Today" : "今日应付"} +
+ {billing.discountAmount > 0 && usePoints && ( ++ ${planPriceUsd.toFixed(2)} USD +
+ )} +