feat: Add dashboard, subscription, and account UI components, and update .gitignore to exclude npm cache files.
This commit is contained in:
@@ -30,3 +30,9 @@ Thumbs.db
|
||||
frontend/node_modules/
|
||||
frontend/.next/
|
||||
frontend/.vercel/
|
||||
|
||||
# Local npm cache
|
||||
.npm-cache/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
-1
File diff suppressed because one or more lines are too long
-2
@@ -1,2 +0,0 @@
|
||||
|
||||
8cb93e59a06d811632ae336df0f6be7e74a10664 {"key":"make-fetch-happen:request-cache:https://registry.npmmirror.com/npm","integrity":"sha512-xePHdYHkp4kEac1FHhITRlDVqNl3EHt4DwARUbax5MhBVMcQQ3IdoY/AuuAcKi0T8tBoPPVPKQ1zbdRcWmyZmw==","time":1773356698813,"size":2200583,"metadata":{"time":1773356698656,"url":"https://registry.npmmirror.com/npm","reqHeaders":{"accept":"application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*"},"resHeaders":{"cache-control":"public, max-age=300","content-encoding":"gzip","content-type":"application/json; charset=utf-8","date":"Thu, 12 Mar 2026 23:04:01 GMT","etag":"W/\"42af5864cc03e9bad6ddc5c3f6fff0e916e8b4fc\"","vary":"Origin, Accept, Accept-Encoding"},"options":{"compress":true}}}
|
||||
@@ -1,50 +0,0 @@
|
||||
0 verbose cli C:\Program Files\nodejs\node.exe D:\Program Files\nodejs\node_global\node_modules\npm\bin\npm-cli.js
|
||||
1 info using npm@8.16.0
|
||||
2 info using node@v24.11.0
|
||||
3 timing npm:load:whichnode Completed in 0ms
|
||||
4 timing config:load:defaults Completed in 2ms
|
||||
5 timing config:load:file:D:\Program Files\nodejs\node_global\node_modules\npm\npmrc Completed in 2ms
|
||||
6 timing config:load:builtin Completed in 2ms
|
||||
7 timing config:load:cli Completed in 1ms
|
||||
8 timing config:load:env Completed in 1ms
|
||||
9 timing config:load:file:E:\web\PolyWeather\frontend\.npmrc Completed in 1ms
|
||||
10 timing config:load:project Completed in 3ms
|
||||
11 timing config:load:file:C:\Users\Yang\.npmrc Completed in 0ms
|
||||
12 timing config:load:user Completed in 0ms
|
||||
13 verbose config error loading global config [Error: EPERM: operation not permitted, open 'C:\Users\Yang\AppData\Roaming\npm\etc\npmrc'] {
|
||||
13 verbose config errno: -4048,
|
||||
13 verbose config code: 'EPERM',
|
||||
13 verbose config syscall: 'open',
|
||||
13 verbose config path: 'C:\\Users\\Yang\\AppData\\Roaming\\npm\\etc\\npmrc'
|
||||
13 verbose config }
|
||||
14 timing config:load:file:C:\Users\Yang\AppData\Roaming\npm\etc\npmrc Completed in 1ms
|
||||
15 timing config:load:global Completed in 1ms
|
||||
16 timing config:load:validate Completed in 1ms
|
||||
17 timing config:load:credentials Completed in 1ms
|
||||
18 timing config:load:setEnvs Completed in 0ms
|
||||
19 timing config:load Completed in 12ms
|
||||
20 timing npm:load:configload Completed in 13ms
|
||||
21 timing npm:load:mkdirpcache Completed in 1ms
|
||||
22 timing npm:load:mkdirplogs Completed in 1ms
|
||||
23 verbose title npm run build
|
||||
24 verbose argv "run" "build"
|
||||
25 timing npm:load:setTitle Completed in 0ms
|
||||
26 timing config:load:flatten Completed in 2ms
|
||||
27 timing npm:load:display Completed in 2ms
|
||||
28 verbose logfile logs-max:10 dir:E:\web\PolyWeather\.npm-cache\_logs
|
||||
29 verbose logfile E:\web\PolyWeather\.npm-cache\_logs\2026-03-12T23_04_58_382Z-debug-0.log
|
||||
30 timing npm:load:logFile Completed in 6ms
|
||||
31 timing npm:load:timers Completed in 0ms
|
||||
32 timing npm:load:configScope Completed in 0ms
|
||||
33 timing npm:load Completed in 23ms
|
||||
34 silly logfile done cleaning log files
|
||||
35 http fetch GET 200 https://registry.npmmirror.com/npm 354ms (cache miss)
|
||||
36 timing command:run Completed in 1340ms
|
||||
37 notice
|
||||
37 notice New major version of npm available! 8.16.0 -> 11.11.1
|
||||
37 notice Changelog: <https://github.com/npm/cli/releases/tag/v11.11.1>
|
||||
37 notice Run `npm install -g npm@11.11.1` to update!
|
||||
37 notice
|
||||
38 verbose exit 1
|
||||
39 timing npm Completed in 1412ms
|
||||
40 verbose code 1
|
||||
@@ -1,38 +0,0 @@
|
||||
0 verbose cli C:\Program Files\nodejs\node.exe D:\Program Files\nodejs\node_global\node_modules\npm\bin\npm-cli.js
|
||||
1 info using npm@8.16.0
|
||||
2 info using node@v24.11.0
|
||||
3 timing npm:load:whichnode Completed in 0ms
|
||||
4 timing config:load:defaults Completed in 2ms
|
||||
5 timing config:load:file:D:\Program Files\nodejs\node_global\node_modules\npm\npmrc Completed in 2ms
|
||||
6 timing config:load:builtin Completed in 2ms
|
||||
7 timing config:load:cli Completed in 1ms
|
||||
8 timing config:load:env Completed in 1ms
|
||||
9 timing config:load:file:E:\web\PolyWeather\frontend\.npmrc Completed in 0ms
|
||||
10 timing config:load:project Completed in 2ms
|
||||
11 timing config:load:file:C:\Users\Yang\.npmrc Completed in 1ms
|
||||
12 timing config:load:user Completed in 1ms
|
||||
13 timing config:load:file:C:\Users\Yang\AppData\Roaming\npm\etc\npmrc Completed in 0ms
|
||||
14 timing config:load:global Completed in 0ms
|
||||
15 timing config:load:validate Completed in 1ms
|
||||
16 timing config:load:credentials Completed in 1ms
|
||||
17 timing config:load:setEnvs Completed in 1ms
|
||||
18 timing config:load Completed in 12ms
|
||||
19 timing npm:load:configload Completed in 13ms
|
||||
20 timing npm:load:mkdirpcache Completed in 1ms
|
||||
21 timing npm:load:mkdirplogs Completed in 0ms
|
||||
22 verbose title npm run build
|
||||
23 verbose argv "run" "build"
|
||||
24 timing npm:load:setTitle Completed in 1ms
|
||||
25 timing config:load:flatten Completed in 1ms
|
||||
26 timing npm:load:display Completed in 2ms
|
||||
27 verbose logfile logs-max:10 dir:E:\web\PolyWeather\.npm-cache\_logs
|
||||
28 verbose logfile E:\web\PolyWeather\.npm-cache\_logs\2026-03-12T23_05_44_607Z-debug-0.log
|
||||
29 timing npm:load:logFile Completed in 5ms
|
||||
30 timing npm:load:timers Completed in 0ms
|
||||
31 timing npm:load:configScope Completed in 0ms
|
||||
32 timing npm:load Completed in 22ms
|
||||
33 silly logfile done cleaning log files
|
||||
34 timing command:run Completed in 5592ms
|
||||
35 verbose exit 1
|
||||
36 timing npm Completed in 5625ms
|
||||
37 verbose code 1
|
||||
@@ -1,38 +0,0 @@
|
||||
0 verbose cli C:\Program Files\nodejs\node.exe D:\Program Files\nodejs\node_global\node_modules\npm\bin\npm-cli.js
|
||||
1 info using npm@8.16.0
|
||||
2 info using node@v24.11.0
|
||||
3 timing npm:load:whichnode Completed in 1ms
|
||||
4 timing config:load:defaults Completed in 2ms
|
||||
5 timing config:load:file:D:\Program Files\nodejs\node_global\node_modules\npm\npmrc Completed in 2ms
|
||||
6 timing config:load:builtin Completed in 2ms
|
||||
7 timing config:load:cli Completed in 1ms
|
||||
8 timing config:load:env Completed in 1ms
|
||||
9 timing config:load:file:E:\web\PolyWeather\frontend\.npmrc Completed in 0ms
|
||||
10 timing config:load:project Completed in 3ms
|
||||
11 timing config:load:file:C:\Users\Yang\.npmrc Completed in 0ms
|
||||
12 timing config:load:user Completed in 0ms
|
||||
13 timing config:load:file:C:\Users\Yang\AppData\Roaming\npm\etc\npmrc Completed in 1ms
|
||||
14 timing config:load:global Completed in 1ms
|
||||
15 timing config:load:validate Completed in 1ms
|
||||
16 timing config:load:credentials Completed in 0ms
|
||||
17 timing config:load:setEnvs Completed in 1ms
|
||||
18 timing config:load Completed in 12ms
|
||||
19 timing npm:load:configload Completed in 12ms
|
||||
20 timing npm:load:mkdirpcache Completed in 1ms
|
||||
21 timing npm:load:mkdirplogs Completed in 0ms
|
||||
22 verbose title npm run build
|
||||
23 verbose argv "run" "build"
|
||||
24 timing npm:load:setTitle Completed in 1ms
|
||||
25 timing config:load:flatten Completed in 2ms
|
||||
26 timing npm:load:display Completed in 2ms
|
||||
27 verbose logfile logs-max:10 dir:E:\web\PolyWeather\.npm-cache\_logs
|
||||
28 verbose logfile E:\web\PolyWeather\.npm-cache\_logs\2026-03-12T23_06_24_382Z-debug-0.log
|
||||
29 timing npm:load:logFile Completed in 5ms
|
||||
30 timing npm:load:timers Completed in 0ms
|
||||
31 timing npm:load:configScope Completed in 0ms
|
||||
32 timing npm:load Completed in 22ms
|
||||
33 silly logfile done cleaning log files
|
||||
34 timing command:run Completed in 28008ms
|
||||
35 verbose exit 0
|
||||
36 timing npm Completed in 28039ms
|
||||
37 info ok
|
||||
@@ -1,38 +0,0 @@
|
||||
0 verbose cli C:\Program Files\nodejs\node.exe D:\Program Files\nodejs\node_global\node_modules\npm\bin\npm-cli.js
|
||||
1 info using npm@8.16.0
|
||||
2 info using node@v24.11.0
|
||||
3 timing npm:load:whichnode Completed in 0ms
|
||||
4 timing config:load:defaults Completed in 2ms
|
||||
5 timing config:load:file:D:\Program Files\nodejs\node_global\node_modules\npm\npmrc Completed in 2ms
|
||||
6 timing config:load:builtin Completed in 2ms
|
||||
7 timing config:load:cli Completed in 1ms
|
||||
8 timing config:load:env Completed in 0ms
|
||||
9 timing config:load:file:E:\web\PolyWeather\frontend\.npmrc Completed in 0ms
|
||||
10 timing config:load:project Completed in 3ms
|
||||
11 timing config:load:file:C:\Users\Yang\.npmrc Completed in 1ms
|
||||
12 timing config:load:user Completed in 1ms
|
||||
13 timing config:load:file:C:\Users\Yang\AppData\Roaming\npm\etc\npmrc Completed in 0ms
|
||||
14 timing config:load:global Completed in 0ms
|
||||
15 timing config:load:validate Completed in 1ms
|
||||
16 timing config:load:credentials Completed in 1ms
|
||||
17 timing config:load:setEnvs Completed in 1ms
|
||||
18 timing config:load Completed in 13ms
|
||||
19 timing npm:load:configload Completed in 13ms
|
||||
20 timing npm:load:mkdirpcache Completed in 0ms
|
||||
21 timing npm:load:mkdirplogs Completed in 1ms
|
||||
22 verbose title npm run build
|
||||
23 verbose argv "run" "build"
|
||||
24 timing npm:load:setTitle Completed in 0ms
|
||||
25 timing config:load:flatten Completed in 3ms
|
||||
26 timing npm:load:display Completed in 4ms
|
||||
27 verbose logfile logs-max:10 dir:E:\web\PolyWeather\.npm-cache\_logs
|
||||
28 verbose logfile E:\web\PolyWeather\.npm-cache\_logs\2026-03-12T23_13_24_173Z-debug-0.log
|
||||
29 timing npm:load:logFile Completed in 5ms
|
||||
30 timing npm:load:timers Completed in 0ms
|
||||
31 timing npm:load:configScope Completed in 0ms
|
||||
32 timing npm:load Completed in 23ms
|
||||
33 silly logfile done cleaning log files
|
||||
34 timing command:run Completed in 15766ms
|
||||
35 verbose exit 1
|
||||
36 timing npm Completed in 15801ms
|
||||
37 verbose code 1
|
||||
@@ -1,38 +0,0 @@
|
||||
0 verbose cli C:\Program Files\nodejs\node.exe D:\Program Files\nodejs\node_global\node_modules\npm\bin\npm-cli.js
|
||||
1 info using npm@8.16.0
|
||||
2 info using node@v24.11.0
|
||||
3 timing npm:load:whichnode Completed in 1ms
|
||||
4 timing config:load:defaults Completed in 2ms
|
||||
5 timing config:load:file:D:\Program Files\nodejs\node_global\node_modules\npm\npmrc Completed in 2ms
|
||||
6 timing config:load:builtin Completed in 2ms
|
||||
7 timing config:load:cli Completed in 1ms
|
||||
8 timing config:load:env Completed in 1ms
|
||||
9 timing config:load:file:E:\web\PolyWeather\frontend\.npmrc Completed in 0ms
|
||||
10 timing config:load:project Completed in 3ms
|
||||
11 timing config:load:file:C:\Users\Yang\.npmrc Completed in 1ms
|
||||
12 timing config:load:user Completed in 1ms
|
||||
13 timing config:load:file:C:\Users\Yang\AppData\Roaming\npm\etc\npmrc Completed in 0ms
|
||||
14 timing config:load:global Completed in 0ms
|
||||
15 timing config:load:validate Completed in 1ms
|
||||
16 timing config:load:credentials Completed in 1ms
|
||||
17 timing config:load:setEnvs Completed in 0ms
|
||||
18 timing config:load Completed in 12ms
|
||||
19 timing npm:load:configload Completed in 12ms
|
||||
20 timing npm:load:mkdirpcache Completed in 1ms
|
||||
21 timing npm:load:mkdirplogs Completed in 1ms
|
||||
22 verbose title npm run build
|
||||
23 verbose argv "run" "build"
|
||||
24 timing npm:load:setTitle Completed in 0ms
|
||||
25 timing config:load:flatten Completed in 2ms
|
||||
26 timing npm:load:display Completed in 2ms
|
||||
27 verbose logfile logs-max:10 dir:E:\web\PolyWeather\.npm-cache\_logs
|
||||
28 verbose logfile E:\web\PolyWeather\.npm-cache\_logs\2026-03-12T23_24_16_712Z-debug-0.log
|
||||
29 timing npm:load:logFile Completed in 5ms
|
||||
30 timing npm:load:timers Completed in 0ms
|
||||
31 timing npm:load:configScope Completed in 0ms
|
||||
32 timing npm:load Completed in 22ms
|
||||
33 silly logfile done cleaning log files
|
||||
34 timing command:run Completed in 28946ms
|
||||
35 verbose exit 0
|
||||
36 timing npm Completed in 28976ms
|
||||
37 info ok
|
||||
@@ -1,38 +0,0 @@
|
||||
0 verbose cli C:\Program Files\nodejs\node.exe D:\Program Files\nodejs\node_global\node_modules\npm\bin\npm-cli.js
|
||||
1 info using npm@8.16.0
|
||||
2 info using node@v24.11.0
|
||||
3 timing npm:load:whichnode Completed in 1ms
|
||||
4 timing config:load:defaults Completed in 1ms
|
||||
5 timing config:load:file:D:\Program Files\nodejs\node_global\node_modules\npm\npmrc Completed in 2ms
|
||||
6 timing config:load:builtin Completed in 3ms
|
||||
7 timing config:load:cli Completed in 1ms
|
||||
8 timing config:load:env Completed in 1ms
|
||||
9 timing config:load:file:E:\web\PolyWeather\frontend\.npmrc Completed in 0ms
|
||||
10 timing config:load:project Completed in 2ms
|
||||
11 timing config:load:file:C:\Users\Yang\.npmrc Completed in 1ms
|
||||
12 timing config:load:user Completed in 1ms
|
||||
13 timing config:load:file:C:\Users\Yang\AppData\Roaming\npm\etc\npmrc Completed in 0ms
|
||||
14 timing config:load:global Completed in 1ms
|
||||
15 timing config:load:validate Completed in 1ms
|
||||
16 timing config:load:credentials Completed in 0ms
|
||||
17 timing config:load:setEnvs Completed in 1ms
|
||||
18 timing config:load Completed in 12ms
|
||||
19 timing npm:load:configload Completed in 12ms
|
||||
20 timing npm:load:mkdirpcache Completed in 1ms
|
||||
21 timing npm:load:mkdirplogs Completed in 0ms
|
||||
22 verbose title npm run build
|
||||
23 verbose argv "run" "build"
|
||||
24 timing npm:load:setTitle Completed in 0ms
|
||||
25 timing config:load:flatten Completed in 1ms
|
||||
26 timing npm:load:display Completed in 3ms
|
||||
27 verbose logfile logs-max:10 dir:E:\web\PolyWeather\.npm-cache\_logs
|
||||
28 verbose logfile E:\web\PolyWeather\.npm-cache\_logs\2026-03-13T00_15_22_381Z-debug-0.log
|
||||
29 timing npm:load:logFile Completed in 4ms
|
||||
30 timing npm:load:timers Completed in 0ms
|
||||
31 timing npm:load:configScope Completed in 0ms
|
||||
32 timing npm:load Completed in 21ms
|
||||
33 silly logfile done cleaning log files
|
||||
34 timing command:run Completed in 18606ms
|
||||
35 verbose exit 1
|
||||
36 timing npm Completed in 18699ms
|
||||
37 verbose code 1
|
||||
@@ -1,38 +0,0 @@
|
||||
0 verbose cli C:\Program Files\nodejs\node.exe D:\Program Files\nodejs\node_global\node_modules\npm\bin\npm-cli.js
|
||||
1 info using npm@8.16.0
|
||||
2 info using node@v24.11.0
|
||||
3 timing npm:load:whichnode Completed in 0ms
|
||||
4 timing config:load:defaults Completed in 1ms
|
||||
5 timing config:load:file:D:\Program Files\nodejs\node_global\node_modules\npm\npmrc Completed in 2ms
|
||||
6 timing config:load:builtin Completed in 2ms
|
||||
7 timing config:load:cli Completed in 2ms
|
||||
8 timing config:load:env Completed in 1ms
|
||||
9 timing config:load:file:E:\web\PolyWeather\frontend\.npmrc Completed in 0ms
|
||||
10 timing config:load:project Completed in 3ms
|
||||
11 timing config:load:file:C:\Users\Yang\.npmrc Completed in 0ms
|
||||
12 timing config:load:user Completed in 0ms
|
||||
13 timing config:load:file:C:\Users\Yang\AppData\Roaming\npm\etc\npmrc Completed in 1ms
|
||||
14 timing config:load:global Completed in 1ms
|
||||
15 timing config:load:validate Completed in 1ms
|
||||
16 timing config:load:credentials Completed in 1ms
|
||||
17 timing config:load:setEnvs Completed in 1ms
|
||||
18 timing config:load Completed in 13ms
|
||||
19 timing npm:load:configload Completed in 14ms
|
||||
20 timing npm:load:mkdirpcache Completed in 0ms
|
||||
21 timing npm:load:mkdirplogs Completed in 0ms
|
||||
22 verbose title npm run build
|
||||
23 verbose argv "run" "build"
|
||||
24 timing npm:load:setTitle Completed in 0ms
|
||||
25 timing config:load:flatten Completed in 2ms
|
||||
26 timing npm:load:display Completed in 2ms
|
||||
27 verbose logfile logs-max:10 dir:E:\web\PolyWeather\.npm-cache\_logs
|
||||
28 verbose logfile E:\web\PolyWeather\.npm-cache\_logs\2026-03-13T00_16_59_417Z-debug-0.log
|
||||
29 timing npm:load:logFile Completed in 12ms
|
||||
30 timing npm:load:timers Completed in 0ms
|
||||
31 timing npm:load:configScope Completed in 0ms
|
||||
32 timing npm:load Completed in 30ms
|
||||
33 silly logfile done cleaning log files
|
||||
34 timing command:run Completed in 24583ms
|
||||
35 verbose exit 0
|
||||
36 timing npm Completed in 24623ms
|
||||
37 info ok
|
||||
@@ -27,13 +27,10 @@ import {
|
||||
Info,
|
||||
Wallet,
|
||||
Zap,
|
||||
ArrowRight,
|
||||
Minus,
|
||||
ShieldCheck,
|
||||
BarChart3,
|
||||
Sparkles,
|
||||
Lock,
|
||||
X,
|
||||
ChevronRight,
|
||||
Loader2,
|
||||
CreditCard,
|
||||
@@ -43,6 +40,7 @@ import {
|
||||
getSupabaseBrowserClient,
|
||||
hasSupabasePublicEnv,
|
||||
} from "@/lib/supabase/client";
|
||||
import { UnlockProOverlay } from "@/components/subscription/UnlockProOverlay";
|
||||
|
||||
// --- Types ---
|
||||
|
||||
@@ -946,130 +944,28 @@ export function AccountCenter() {
|
||||
{/* Paywall Mask */}
|
||||
{!isSubscribed && showOverlay && (
|
||||
<div className="absolute inset-0 z-30 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-2xl bg-[#161b2a]/90 backdrop-blur-2xl border border-white/10 rounded-[2.5rem] p-8 md:p-12 shadow-[0_0_80px_-10px_rgba(79,70,229,0.3)] text-center relative">
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={() => setShowOverlay(false)}
|
||||
className="absolute top-6 right-6 p-2 bg-white/5 hover:bg-white/10 rounded-full text-slate-500 hover:text-white transition-all"
|
||||
title="稍后再说"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
||||
<div className="absolute -top-10 left-1/2 -translate-x-1/2 w-20 h-20 bg-gradient-to-tr from-yellow-500 to-amber-400 rounded-3xl flex items-center justify-center shadow-2xl rotate-12">
|
||||
<Crown className="text-white w-10 h-10" fill="currentColor" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl font-bold text-white mb-4 mt-4">
|
||||
开启 PolyWeather Pro
|
||||
</h2>
|
||||
<p className="text-slate-400 mb-10 max-w-md mx-auto">
|
||||
解锁 15
|
||||
天高精度趋势分析、实时雷达与闪电追踪。尊享全平台无广告体验。
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-left mb-10">
|
||||
{/* Plan Card */}
|
||||
<div className="p-6 bg-blue-600/10 border-2 border-blue-500/50 rounded-3xl relative">
|
||||
<div className="absolute -top-3 right-6 bg-blue-500 text-[10px] px-2 py-1 rounded font-bold text-white">
|
||||
月付套餐
|
||||
</div>
|
||||
<p className="text-slate-500 text-[10px] font-bold uppercase tracking-widest mb-1">
|
||||
PRO PLAN
|
||||
</p>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-3xl font-black text-white">
|
||||
${billing.planAmount.toFixed(2)}
|
||||
</span>
|
||||
<span className="text-slate-500 text-sm">/ 月</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Points Card */}
|
||||
<div
|
||||
className={`p-6 rounded-3xl border transition-all ${usePoints && billing.canRedeem ? "bg-indigo-600/20 border-indigo-500/50" : "bg-white/5 border-white/10"}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-bold text-indigo-400 uppercase tracking-widest">
|
||||
积分抵扣
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setUsePoints(!usePoints)}
|
||||
disabled={!billing.canRedeem}
|
||||
className={`w-10 h-5 rounded-full relative transition-all ${usePoints && billing.canRedeem ? "bg-indigo-500" : "bg-slate-700"} ${!billing.canRedeem ? "opacity-30 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute top-1 w-3 h-3 rounded-full bg-white transition-all ${usePoints && billing.canRedeem ? "right-1" : "left-1"}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span
|
||||
className={`text-2xl font-bold ${usePoints && billing.canRedeem ? "text-green-400" : "text-slate-500"}`}
|
||||
>
|
||||
-${billing.discountAmount.toFixed(2)}
|
||||
</span>
|
||||
<span className="text-slate-500 text-xs">OFF</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-500 mt-1 italic">
|
||||
{!billing.pointsEnabled
|
||||
? "积分抵扣未开启"
|
||||
: !billing.canRedeem
|
||||
? `积分不足 (当前 ${totalPoints},至少 ${billing.pointsPerUsdc})`
|
||||
: usePoints
|
||||
? `已自动消耗 ${billing.pointsUsed} 积分`
|
||||
: `最多抵扣 $${billing.maxDiscountUsdc.toFixed(2)}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between px-2 text-sm">
|
||||
<span className="text-slate-400">应付总计:</span>
|
||||
<span className="text-3xl font-black text-white">
|
||||
${billing.payAmount.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => void handleOverlayCheckout()}
|
||||
disabled={paymentBusy}
|
||||
className="w-full py-4 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold rounded-2xl shadow-xl shadow-blue-600/30 transition-all flex items-center justify-center gap-2 group active:scale-95 text-lg"
|
||||
>
|
||||
{paymentBusy ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
连接钱包并支付{" "}
|
||||
<ArrowRight
|
||||
size={20}
|
||||
className="group-hover:translate-x-1 transition-transform"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex justify-center items-center gap-6 text-[10px] uppercase tracking-widest text-slate-500">
|
||||
<span className="flex items-center gap-1.5 font-medium">
|
||||
<Lock size={12} className="opacity-50" /> 安全加密支付
|
||||
</span>
|
||||
<span className="hover:text-white cursor-pointer transition-colors font-medium">
|
||||
常见问题 (FAQ)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{paymentError && (
|
||||
<div className="mt-2 text-xs text-rose-400 bg-rose-500/10 p-2 rounded-lg border border-rose-500/20">
|
||||
{paymentError}
|
||||
</div>
|
||||
)}
|
||||
{paymentInfo && (
|
||||
<div className="mt-2 text-xs text-emerald-400 bg-emerald-500/10 p-2 rounded-lg border border-emerald-500/20">
|
||||
{paymentInfo}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<UnlockProOverlay
|
||||
points={totalPoints}
|
||||
planPriceUsd={billing.planAmount}
|
||||
usePoints={usePoints}
|
||||
onToggleUsePoints={() => setUsePoints((prev) => !prev)}
|
||||
billing={{
|
||||
pointsEnabled: billing.pointsEnabled,
|
||||
isEligible: billing.canRedeem,
|
||||
pointsUsed: billing.pointsUsed,
|
||||
discountAmount: billing.discountAmount,
|
||||
finalPrice: billing.payAmount,
|
||||
maxDiscountUsd: billing.maxDiscountUsdc,
|
||||
pointsPerUsd: billing.pointsPerUsdc,
|
||||
}}
|
||||
onPay={() => void handleOverlayCheckout()}
|
||||
onClose={() => setShowOverlay(false)}
|
||||
payBusy={paymentBusy}
|
||||
payLabel={hasPayingWallet ? "立即订阅并激活服务" : "连接钱包并支付"}
|
||||
errorText={paymentError || undefined}
|
||||
infoText={paymentInfo || undefined}
|
||||
faqHref="/account"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1666,6 +1666,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(34, 211, 238, 0.34);
|
||||
|
||||
@@ -1,13 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import clsx from "clsx";
|
||||
import { LogIn, UserRound } from "lucide-react";
|
||||
import { useDashboardStore } from "@/hooks/useDashboardStore";
|
||||
import { useI18n } from "@/hooks/useI18n";
|
||||
import {
|
||||
getSupabaseBrowserClient,
|
||||
hasSupabasePublicEnv,
|
||||
} from "@/lib/supabase/client";
|
||||
|
||||
export function HeaderBar() {
|
||||
const store = useDashboardStore();
|
||||
const { locale, setLocale, t } = useI18n();
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const supabaseReady = hasSupabasePublicEnv();
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
if (!supabaseReady) {
|
||||
setIsAuthenticated(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const supabase = getSupabaseBrowserClient();
|
||||
|
||||
void supabase.auth.getSession().then(({ data }) => {
|
||||
if (!mounted) return;
|
||||
setIsAuthenticated(Boolean(data.session?.user?.id));
|
||||
});
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
if (!mounted) return;
|
||||
setIsAuthenticated(Boolean(session?.user?.id));
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [supabaseReady]);
|
||||
|
||||
const accountHref = isAuthenticated
|
||||
? "/account"
|
||||
: "/auth/login?next=%2Faccount";
|
||||
const accountLabel = isAuthenticated ? t("header.account") : t("header.signIn");
|
||||
const accountAria = isAuthenticated
|
||||
? t("header.accountAria")
|
||||
: t("header.signInAria");
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
@@ -35,12 +79,13 @@ export function HeaderBar() {
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/account"
|
||||
href={accountHref}
|
||||
className="account-btn"
|
||||
title={t("header.accountAria")}
|
||||
aria-label={t("header.accountAria")}
|
||||
title={accountAria}
|
||||
aria-label={accountAria}
|
||||
>
|
||||
{t("header.account")}
|
||||
{isAuthenticated ? <UserRound size={14} /> : <LogIn size={14} />}
|
||||
<span>{accountLabel}</span>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
@@ -70,4 +115,4 @@ export function HeaderBar() {
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,180 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowRight,
|
||||
BarChart3,
|
||||
Crown,
|
||||
Lock,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
X,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useI18n } from "@/hooks/useI18n";
|
||||
import { useDashboardStore } from "@/hooks/useDashboardStore";
|
||||
import { useMemo, useState } from "react";
|
||||
import { UnlockProOverlay } from "@/components/subscription/UnlockProOverlay";
|
||||
|
||||
type ProFeaturePaywallProps = {
|
||||
feature: "today" | "history" | "future";
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export function ProFeaturePaywall({
|
||||
feature,
|
||||
onClose,
|
||||
}: ProFeaturePaywallProps) {
|
||||
export function ProFeaturePaywall({ feature, onClose }: ProFeaturePaywallProps) {
|
||||
const router = useRouter();
|
||||
const { locale } = useI18n();
|
||||
const { proAccess } = useDashboardStore();
|
||||
const [usePoints, setUsePoints] = useState(true);
|
||||
|
||||
const isEn = locale === "en-US";
|
||||
const pointsAvailable = Number(proAccess.points || 0);
|
||||
|
||||
// Redemption logic: 500 points = $1 discount
|
||||
// Max discount $3 (requires 1500 points)
|
||||
const PRO_PRICE = 5;
|
||||
const POINTS_VAL = 500;
|
||||
const MAX_DISCOUNT = 3;
|
||||
|
||||
const pointsAvailable = proAccess.points || 0;
|
||||
const PRO_PRICE_USD = 5;
|
||||
const POINTS_PER_USD = 500;
|
||||
const MAX_DISCOUNT_USD = 3;
|
||||
|
||||
const billing = useMemo(() => {
|
||||
const maxRedeemable = MAX_DISCOUNT * POINTS_VAL;
|
||||
const actualRedeem = Math.min(pointsAvailable, maxRedeemable);
|
||||
const discount = Math.floor(actualRedeem / POINTS_VAL);
|
||||
const isEligible = pointsAvailable >= POINTS_PER_USD;
|
||||
const maxRedeemablePoints = MAX_DISCOUNT_USD * POINTS_PER_USD;
|
||||
const boundedPoints = isEligible
|
||||
? Math.min(pointsAvailable, maxRedeemablePoints)
|
||||
: 0;
|
||||
const discountUnits = Math.floor(boundedPoints / POINTS_PER_USD);
|
||||
const pointsUsed = discountUnits * POINTS_PER_USD;
|
||||
const discountAmount = usePoints ? discountUnits : 0;
|
||||
|
||||
return {
|
||||
pointsUsed: discount * POINTS_VAL,
|
||||
discountAmount: discount,
|
||||
payAmount: PRO_PRICE - (usePoints ? discount : 0),
|
||||
pointsEnabled: true,
|
||||
isEligible,
|
||||
pointsPerUsd: POINTS_PER_USD,
|
||||
maxDiscountUsd: MAX_DISCOUNT_USD,
|
||||
pointsUsed: usePoints ? pointsUsed : 0,
|
||||
discountAmount,
|
||||
finalPrice: PRO_PRICE_USD - discountAmount,
|
||||
};
|
||||
}, [usePoints, pointsAvailable]);
|
||||
}, [pointsAvailable, usePoints]);
|
||||
|
||||
const payLabel = isEn
|
||||
? feature === "history"
|
||||
? "Unlock History in Account"
|
||||
: feature === "future"
|
||||
? "Unlock Forecast in Account"
|
||||
: "Unlock Intraday in Account"
|
||||
: feature === "history"
|
||||
? "去账户中心解锁历史功能"
|
||||
: feature === "future"
|
||||
? "去账户中心解锁未来分析"
|
||||
: "去账户中心解锁今日日内";
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center py-6 md:py-10 z-30 p-4">
|
||||
<div className="w-full max-w-2xl bg-[#161b2a]/90 backdrop-blur-2xl border border-white/10 rounded-[2.5rem] p-8 md:p-12 shadow-[0_0_80px_-10px_rgba(79,70,229,0.3)] text-center relative">
|
||||
{/* 关闭按钮 */}
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 p-2 bg-white/5 hover:bg-white/10 rounded-full text-slate-500 hover:text-white transition-all z-10"
|
||||
title={isEn ? "Close" : "稍后再说"}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Crown Badge */}
|
||||
<div className="absolute -top-10 left-1/2 -translate-x-1/2 w-20 h-20 bg-gradient-to-tr from-yellow-500 to-amber-400 rounded-3xl flex items-center justify-center shadow-2xl rotate-12">
|
||||
<Crown className="text-white w-10 h-10" fill="currentColor" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl font-bold text-white mb-4 mt-4">
|
||||
{isEn ? "Unlock PolyWeather Pro" : "开启 PolyWeather Pro"}
|
||||
</h2>
|
||||
<p className="text-slate-400 mb-10 max-w-md mx-auto">
|
||||
{isEn
|
||||
? "Unlock 15-day precision trends, real-time radar, and ad-free experience across all platforms."
|
||||
: "解锁 15 天高精度趋势分析、实时雷达与闪电追踪。尊享全平台无广告体验。"}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-left mb-10">
|
||||
{/* 订阅方案卡片 */}
|
||||
<div className="p-6 bg-blue-600/10 border-2 border-blue-500/50 rounded-3xl relative">
|
||||
<div className="absolute -top-3 right-6 bg-blue-500 text-[10px] px-2 py-1 rounded font-bold text-white uppercase tracking-tighter">
|
||||
{isEn ? "Monthly" : "月付套餐"}
|
||||
</div>
|
||||
<p className="text-slate-500 text-[10px] font-bold uppercase tracking-widest mb-1">
|
||||
PRO PLAN
|
||||
</p>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-3xl font-black text-white">$5.00</span>
|
||||
<span className="text-slate-500 text-sm">
|
||||
/ {isEn ? "mo" : "月"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 积分抵扣卡片 */}
|
||||
<div
|
||||
className={`p-6 rounded-3xl border transition-all ${usePoints && pointsAvailable >= 500 ? "bg-indigo-600/20 border-indigo-500/50" : "bg-white/5 border-white/10"}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span
|
||||
className={`text-[10px] font-bold uppercase tracking-widest ${usePoints && pointsAvailable >= 500 ? "text-indigo-400" : "text-slate-500"}`}
|
||||
>
|
||||
{isEn ? "Points Credit" : "积分抵扣"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setUsePoints(!usePoints)}
|
||||
disabled={pointsAvailable < 500}
|
||||
className={`w-10 h-5 rounded-full relative transition-all ${usePoints && pointsAvailable >= 500 ? "bg-indigo-500" : "bg-slate-700"} ${pointsAvailable < 500 ? "opacity-30 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute top-1 w-3 h-3 rounded-full bg-white transition-all ${usePoints && pointsAvailable >= 500 ? "right-1" : "left-1"}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span
|
||||
className={`text-3xl font-black ${usePoints && pointsAvailable >= 500 ? "text-emerald-400" : "text-slate-500"}`}
|
||||
>
|
||||
-${billing.discountAmount.toFixed(2)}
|
||||
</span>
|
||||
<span className="text-slate-500 text-[10px] font-bold uppercase">
|
||||
OFF
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-500 mt-1 italic">
|
||||
{pointsAvailable < 500
|
||||
? isEn
|
||||
? `Need 500+ points (Current: ${pointsAvailable})`
|
||||
: `积分不足 (当前 ${pointsAvailable})`
|
||||
: usePoints
|
||||
? isEn
|
||||
? `Consumed ${billing.pointsUsed} points`
|
||||
: `已自动消耗 ${billing.pointsUsed} 积分`
|
||||
: isEn
|
||||
? "Up to $3.00 off"
|
||||
: "开启后最多抵扣 $3.00"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between px-2 text-sm">
|
||||
<span className="text-slate-400">
|
||||
{isEn ? "Total Due:" : "应付总计:"}
|
||||
</span>
|
||||
<span className="text-3xl font-black text-white">
|
||||
${billing.payAmount.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/account"
|
||||
className="w-full py-4 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold rounded-2xl shadow-xl shadow-blue-600/30 transition-all flex items-center justify-center gap-2 group active:scale-95 text-lg"
|
||||
>
|
||||
{isEn ? "Connect Wallet & Pay" : "连接钱包并支付"}
|
||||
<ArrowRight
|
||||
size={20}
|
||||
className="group-hover:translate-x-1 transition-transform"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<div className="flex justify-center items-center gap-6 text-[10px] font-medium text-slate-500 uppercase tracking-widest">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Lock size={12} className="opacity-50" />{" "}
|
||||
{isEn ? "Secured Payment" : "安全加密支付"}
|
||||
</span>
|
||||
<Link
|
||||
href="/account"
|
||||
className="hover:text-white cursor-pointer transition-colors"
|
||||
>
|
||||
{isEn ? "FAQ" : "常见问题 (FAQ)"}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UnlockProOverlay
|
||||
locale={locale}
|
||||
points={pointsAvailable}
|
||||
planPriceUsd={PRO_PRICE_USD}
|
||||
usePoints={usePoints}
|
||||
onToggleUsePoints={() => setUsePoints((prev) => !prev)}
|
||||
billing={billing}
|
||||
onClose={onClose}
|
||||
onPay={() => router.push("/account")}
|
||||
payLabel={payLabel}
|
||||
faqHref="/account"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowRight,
|
||||
BellRing,
|
||||
CheckCircle2,
|
||||
Coins,
|
||||
Crown,
|
||||
Loader2,
|
||||
Lock,
|
||||
MessageSquare,
|
||||
TrendingUp,
|
||||
Wallet,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
export type UnlockProBilling = {
|
||||
pointsEnabled: boolean;
|
||||
isEligible: boolean;
|
||||
pointsUsed: number;
|
||||
discountAmount: number;
|
||||
finalPrice: number;
|
||||
maxDiscountUsd: number;
|
||||
pointsPerUsd: number;
|
||||
};
|
||||
|
||||
type UnlockProOverlayProps = {
|
||||
points: number;
|
||||
planPriceUsd: number;
|
||||
usePoints: boolean;
|
||||
billing: UnlockProBilling;
|
||||
onToggleUsePoints: () => void;
|
||||
onPay: () => void;
|
||||
onClose?: () => void;
|
||||
payBusy?: boolean;
|
||||
payLabel?: string;
|
||||
locale?: "zh-CN" | "en-US";
|
||||
errorText?: string;
|
||||
infoText?: string;
|
||||
faqHref?: string;
|
||||
telegramGroupUrl?: string;
|
||||
};
|
||||
|
||||
export function UnlockProOverlay({
|
||||
points,
|
||||
planPriceUsd,
|
||||
usePoints,
|
||||
billing,
|
||||
onToggleUsePoints,
|
||||
onPay,
|
||||
onClose,
|
||||
payBusy = false,
|
||||
payLabel,
|
||||
locale = "zh-CN",
|
||||
errorText,
|
||||
infoText,
|
||||
faqHref = "/account",
|
||||
telegramGroupUrl,
|
||||
}: UnlockProOverlayProps) {
|
||||
const isEn = locale === "en-US";
|
||||
|
||||
const title = isEn ? "Unlock PolyWeather Pro" : "解锁 PolyWeather Pro";
|
||||
const subtitle = isEn
|
||||
? "Get high-precision weather intelligence and full-platform alerts."
|
||||
: "获取全球最精准的高精度气象推送";
|
||||
|
||||
const finalPayLabel =
|
||||
payLabel || (isEn ? "Subscribe & Activate" : "立即订阅并激活服务");
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-2xl bg-gradient-to-b from-[#1e293b] to-[#0f172a] border border-white/10 rounded-[2.5rem] p-8 md:p-12 shadow-[0_0_100px_-20px_rgba(59,130,246,0.3)] overflow-hidden">
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-8 right-8 p-2 text-slate-500 hover:text-white bg-white/5 hover:bg-white/10 rounded-full transition-all active:scale-90"
|
||||
title={isEn ? "Close" : "关闭"}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="w-20 h-20 bg-gradient-to-tr from-yellow-500 via-amber-400 to-orange-500 rounded-[2rem] flex items-center justify-center shadow-2xl shadow-yellow-500/20 rotate-12 mb-6">
|
||||
<Crown className="text-white w-10 h-10 drop-shadow-lg" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-black text-white tracking-tight text-center">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-slate-400 text-sm mt-2 text-center">{subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-10 text-left">
|
||||
<div className="p-6 bg-blue-600/10 border-2 border-blue-500/40 rounded-3xl relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-blue-500/5 blur-2xl rounded-full translate-x-1/2 -translate-y-1/2" />
|
||||
<div className="relative z-10">
|
||||
<span className="text-[10px] font-bold text-blue-400 uppercase tracking-widest block mb-1">
|
||||
STANDARD PRO
|
||||
</span>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-4xl font-black text-white">
|
||||
${planPriceUsd.toFixed(2)}
|
||||
</span>
|
||||
<span className="text-slate-500 text-sm font-medium">
|
||||
/ {isEn ? "mo" : "月"}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="mt-4 space-y-2">
|
||||
{(isEn
|
||||
? [
|
||||
"15-day high precision trend",
|
||||
"Realtime radar panel",
|
||||
"Cross-platform alert push",
|
||||
]
|
||||
: ["15天趋势预报", "实时雷达图", "全平台推送"]
|
||||
).map((item, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="flex items-center gap-2 text-[11px] text-slate-400"
|
||||
>
|
||||
<CheckCircle2 size={12} className="text-blue-500" /> {item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{billing.pointsEnabled && billing.isEligible ? (
|
||||
<div
|
||||
className={`p-6 rounded-3xl border transition-all duration-300 flex flex-col justify-between ${usePoints ? "bg-indigo-500/10 border-indigo-500/50 shadow-inner" : "bg-white/5 border-white/10"}`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`p-1.5 rounded-lg ${usePoints ? "bg-indigo-500 text-white" : "bg-slate-700 text-slate-500"}`}
|
||||
>
|
||||
<Coins size={14} />
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-slate-300 uppercase tracking-widest">
|
||||
{isEn ? "Points Credit" : "积分抵扣"}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onToggleUsePoints}
|
||||
className={`w-10 h-5 rounded-full relative transition-all duration-300 ${usePoints ? "bg-indigo-500" : "bg-slate-700"}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute top-1 w-3 h-3 rounded-full bg-white shadow-sm transition-all duration-300 ${usePoints ? "right-1" : "left-1"}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span
|
||||
className={`text-3xl font-black ${usePoints ? "text-green-400" : "text-slate-600"}`}
|
||||
>
|
||||
-${billing.discountAmount.toFixed(2)}
|
||||
</span>
|
||||
<span className="text-slate-500 text-[10px] font-bold">DISCOUNT</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-500 leading-tight">
|
||||
{usePoints
|
||||
? isEn
|
||||
? `Using ${billing.pointsUsed} points`
|
||||
: `已自动消耗账户内 ${billing.pointsUsed} 积分`
|
||||
: isEn
|
||||
? `Up to $${billing.maxDiscountUsd.toFixed(2)} off`
|
||||
: `开启后最多可抵扣 $${billing.maxDiscountUsd.toFixed(2)}`}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 rounded-3xl border border-dashed border-white/10 bg-white/5 flex flex-col justify-between group hover:border-blue-500/30 transition-all">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="p-1.5 bg-slate-800 rounded-lg text-slate-500 group-hover:text-blue-400 transition-colors">
|
||||
<MessageSquare size={14} />
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">
|
||||
{!billing.pointsEnabled
|
||||
? isEn
|
||||
? "Points Disabled"
|
||||
: "积分抵扣未开启"
|
||||
: isEn
|
||||
? "Not Enough Points"
|
||||
: "积分不足"}
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="text-sm font-bold text-white mb-2">
|
||||
{isEn ? "Earn Credits in Community" : "活跃赚取抵扣"}
|
||||
</h4>
|
||||
<p className="text-[11px] text-slate-400 leading-normal">
|
||||
{!billing.pointsEnabled
|
||||
? isEn
|
||||
? "Points redemption is currently unavailable for this plan."
|
||||
: "当前套餐未开启积分抵扣。"
|
||||
: isEn
|
||||
? `Need at least ${billing.pointsPerUsd} points. Current: ${points}`
|
||||
: `需要至少 ${billing.pointsPerUsd} 积分,当前 ${points}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-center gap-1 text-[10px] text-blue-500/80 font-bold uppercase">
|
||||
{telegramGroupUrl ? (
|
||||
<Link href={telegramGroupUrl} target="_blank" className="inline-flex items-center gap-1">
|
||||
<span>{isEn ? "Open Telegram" : "前往电报群"}</span>
|
||||
<TrendingUp size={12} className="animate-bounce" />
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<span>{isEn ? "Open Telegram" : "前往电报群"}</span>
|
||||
<TrendingUp size={12} className="animate-bounce" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<span className="text-slate-400 text-sm font-medium">
|
||||
{isEn ? "Total Due:" : "应付总计:"}
|
||||
</span>
|
||||
<div className="text-right">
|
||||
<span className="text-4xl font-black text-white font-mono tracking-tighter">
|
||||
${billing.finalPrice.toFixed(2)}
|
||||
</span>
|
||||
<span className="text-slate-500 text-sm ml-2 font-mono uppercase tracking-widest">
|
||||
USD
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onPay}
|
||||
disabled={payBusy}
|
||||
className="w-full py-5 bg-gradient-to-r from-blue-600 via-indigo-600 to-indigo-700 hover:from-blue-500 hover:to-indigo-500 text-white font-black text-lg rounded-[1.5rem] shadow-2xl shadow-blue-600/30 transition-all active:scale-[0.98] flex items-center justify-center gap-3 group disabled:opacity-70"
|
||||
>
|
||||
{payBusy ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Wallet size={20} />
|
||||
{finalPayLabel}
|
||||
<ArrowRight
|
||||
size={20}
|
||||
className="group-hover:translate-x-1 transition-transform"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex justify-center items-center gap-6 pt-4 text-[10px] text-slate-600 uppercase tracking-widest font-bold">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Lock size={12} /> Secure Payment
|
||||
</span>
|
||||
<Link
|
||||
href={faqHref}
|
||||
className="flex items-center gap-1.5 hover:text-slate-400 transition-colors"
|
||||
>
|
||||
<BellRing size={12} /> Subscription FAQ
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{errorText ? (
|
||||
<div className="mt-2 text-xs text-rose-400 bg-rose-500/10 p-2 rounded-lg border border-rose-500/20">
|
||||
{errorText}
|
||||
</div>
|
||||
) : null}
|
||||
{infoText ? (
|
||||
<div className="mt-2 text-xs text-emerald-400 bg-emerald-500/10 p-2 rounded-lg border border-emerald-500/20">
|
||||
{infoText}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,8 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
|
||||
"header.infoAria": "查看系统技术说明",
|
||||
"header.account": "账户",
|
||||
"header.accountAria": "打开账户页",
|
||||
"header.signIn": "登录 / 注册",
|
||||
"header.signInAria": "打开登录注册页",
|
||||
"header.live": "实时",
|
||||
"header.refreshAria": "刷新所有数据",
|
||||
"header.langAria": "切换语言",
|
||||
@@ -164,6 +166,8 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
|
||||
"header.infoAria": "Open system technical notes",
|
||||
"header.account": "Account",
|
||||
"header.accountAria": "Open account center",
|
||||
"header.signIn": "Sign In / Register",
|
||||
"header.signInAria": "Open sign-in page",
|
||||
"header.live": "LIVE",
|
||||
"header.refreshAria": "Refresh all data",
|
||||
"header.langAria": "Switch language",
|
||||
|
||||
Reference in New Issue
Block a user