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/node_modules/
|
||||||
frontend/.next/
|
frontend/.next/
|
||||||
frontend/.vercel/
|
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,
|
Info,
|
||||||
Wallet,
|
Wallet,
|
||||||
Zap,
|
Zap,
|
||||||
ArrowRight,
|
|
||||||
Minus,
|
Minus,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Lock,
|
|
||||||
X,
|
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Loader2,
|
Loader2,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
@@ -43,6 +40,7 @@ import {
|
|||||||
getSupabaseBrowserClient,
|
getSupabaseBrowserClient,
|
||||||
hasSupabasePublicEnv,
|
hasSupabasePublicEnv,
|
||||||
} from "@/lib/supabase/client";
|
} from "@/lib/supabase/client";
|
||||||
|
import { UnlockProOverlay } from "@/components/subscription/UnlockProOverlay";
|
||||||
|
|
||||||
// --- Types ---
|
// --- Types ---
|
||||||
|
|
||||||
@@ -946,130 +944,28 @@ export function AccountCenter() {
|
|||||||
{/* Paywall Mask */}
|
{/* Paywall Mask */}
|
||||||
{!isSubscribed && showOverlay && (
|
{!isSubscribed && showOverlay && (
|
||||||
<div className="absolute inset-0 z-30 flex items-center justify-center p-4">
|
<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">
|
<UnlockProOverlay
|
||||||
{/* Close Button */}
|
points={totalPoints}
|
||||||
<button
|
planPriceUsd={billing.planAmount}
|
||||||
onClick={() => setShowOverlay(false)}
|
usePoints={usePoints}
|
||||||
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"
|
onToggleUsePoints={() => setUsePoints((prev) => !prev)}
|
||||||
title="稍后再说"
|
billing={{
|
||||||
>
|
pointsEnabled: billing.pointsEnabled,
|
||||||
<X size={20} />
|
isEligible: billing.canRedeem,
|
||||||
</button>
|
pointsUsed: billing.pointsUsed,
|
||||||
|
discountAmount: billing.discountAmount,
|
||||||
<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">
|
finalPrice: billing.payAmount,
|
||||||
<Crown className="text-white w-10 h-10" fill="currentColor" />
|
maxDiscountUsd: billing.maxDiscountUsdc,
|
||||||
</div>
|
pointsPerUsd: billing.pointsPerUsdc,
|
||||||
|
}}
|
||||||
<h2 className="text-3xl font-bold text-white mb-4 mt-4">
|
onPay={() => void handleOverlayCheckout()}
|
||||||
开启 PolyWeather Pro
|
onClose={() => setShowOverlay(false)}
|
||||||
</h2>
|
payBusy={paymentBusy}
|
||||||
<p className="text-slate-400 mb-10 max-w-md mx-auto">
|
payLabel={hasPayingWallet ? "立即订阅并激活服务" : "连接钱包并支付"}
|
||||||
解锁 15
|
errorText={paymentError || undefined}
|
||||||
天高精度趋势分析、实时雷达与闪电追踪。尊享全平台无广告体验。
|
infoText={paymentInfo || undefined}
|
||||||
</p>
|
faqHref="/account"
|
||||||
|
/>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1666,6 +1666,7 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid rgba(34, 211, 238, 0.34);
|
border: 1px solid rgba(34, 211, 238, 0.34);
|
||||||
|
|||||||
@@ -1,13 +1,57 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { LogIn, UserRound } from "lucide-react";
|
||||||
import { useDashboardStore } from "@/hooks/useDashboardStore";
|
import { useDashboardStore } from "@/hooks/useDashboardStore";
|
||||||
import { useI18n } from "@/hooks/useI18n";
|
import { useI18n } from "@/hooks/useI18n";
|
||||||
|
import {
|
||||||
|
getSupabaseBrowserClient,
|
||||||
|
hasSupabasePublicEnv,
|
||||||
|
} from "@/lib/supabase/client";
|
||||||
|
|
||||||
export function HeaderBar() {
|
export function HeaderBar() {
|
||||||
const store = useDashboardStore();
|
const store = useDashboardStore();
|
||||||
const { locale, setLocale, t } = useI18n();
|
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 (
|
return (
|
||||||
<header className="header">
|
<header className="header">
|
||||||
@@ -35,12 +79,13 @@ export function HeaderBar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/account"
|
href={accountHref}
|
||||||
className="account-btn"
|
className="account-btn"
|
||||||
title={t("header.accountAria")}
|
title={accountAria}
|
||||||
aria-label={t("header.accountAria")}
|
aria-label={accountAria}
|
||||||
>
|
>
|
||||||
{t("header.account")}
|
{isAuthenticated ? <UserRound size={14} /> : <LogIn size={14} />}
|
||||||
|
<span>{accountLabel}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -70,4 +115,4 @@ export function HeaderBar() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,180 +1,76 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import { useRouter } from "next/navigation";
|
||||||
ArrowRight,
|
|
||||||
BarChart3,
|
|
||||||
Crown,
|
|
||||||
Lock,
|
|
||||||
ShieldCheck,
|
|
||||||
Sparkles,
|
|
||||||
X,
|
|
||||||
Zap,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useI18n } from "@/hooks/useI18n";
|
import { useI18n } from "@/hooks/useI18n";
|
||||||
import { useDashboardStore } from "@/hooks/useDashboardStore";
|
import { useDashboardStore } from "@/hooks/useDashboardStore";
|
||||||
import { useMemo, useState } from "react";
|
import { UnlockProOverlay } from "@/components/subscription/UnlockProOverlay";
|
||||||
|
|
||||||
type ProFeaturePaywallProps = {
|
type ProFeaturePaywallProps = {
|
||||||
feature: "today" | "history" | "future";
|
feature: "today" | "history" | "future";
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ProFeaturePaywall({
|
export function ProFeaturePaywall({ feature, onClose }: ProFeaturePaywallProps) {
|
||||||
feature,
|
const router = useRouter();
|
||||||
onClose,
|
|
||||||
}: ProFeaturePaywallProps) {
|
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const { proAccess } = useDashboardStore();
|
const { proAccess } = useDashboardStore();
|
||||||
const [usePoints, setUsePoints] = useState(true);
|
const [usePoints, setUsePoints] = useState(true);
|
||||||
|
|
||||||
const isEn = locale === "en-US";
|
const isEn = locale === "en-US";
|
||||||
|
const pointsAvailable = Number(proAccess.points || 0);
|
||||||
|
|
||||||
// Redemption logic: 500 points = $1 discount
|
const PRO_PRICE_USD = 5;
|
||||||
// Max discount $3 (requires 1500 points)
|
const POINTS_PER_USD = 500;
|
||||||
const PRO_PRICE = 5;
|
const MAX_DISCOUNT_USD = 3;
|
||||||
const POINTS_VAL = 500;
|
|
||||||
const MAX_DISCOUNT = 3;
|
|
||||||
|
|
||||||
const pointsAvailable = proAccess.points || 0;
|
|
||||||
|
|
||||||
const billing = useMemo(() => {
|
const billing = useMemo(() => {
|
||||||
const maxRedeemable = MAX_DISCOUNT * POINTS_VAL;
|
const isEligible = pointsAvailable >= POINTS_PER_USD;
|
||||||
const actualRedeem = Math.min(pointsAvailable, maxRedeemable);
|
const maxRedeemablePoints = MAX_DISCOUNT_USD * POINTS_PER_USD;
|
||||||
const discount = Math.floor(actualRedeem / POINTS_VAL);
|
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 {
|
return {
|
||||||
pointsUsed: discount * POINTS_VAL,
|
pointsEnabled: true,
|
||||||
discountAmount: discount,
|
isEligible,
|
||||||
payAmount: PRO_PRICE - (usePoints ? discount : 0),
|
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 (
|
return (
|
||||||
<div className="flex w-full flex-col items-center justify-center py-6 md:py-10 z-30 p-4">
|
<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">
|
<UnlockProOverlay
|
||||||
{/* 关闭按钮 */}
|
locale={locale}
|
||||||
{onClose && (
|
points={pointsAvailable}
|
||||||
<button
|
planPriceUsd={PRO_PRICE_USD}
|
||||||
onClick={onClose}
|
usePoints={usePoints}
|
||||||
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"
|
onToggleUsePoints={() => setUsePoints((prev) => !prev)}
|
||||||
title={isEn ? "Close" : "稍后再说"}
|
billing={billing}
|
||||||
>
|
onClose={onClose}
|
||||||
<X size={20} />
|
onPay={() => router.push("/account")}
|
||||||
</button>
|
payLabel={payLabel}
|
||||||
)}
|
faqHref="/account"
|
||||||
|
/>
|
||||||
{/* 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>
|
|
||||||
</div>
|
</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.infoAria": "查看系统技术说明",
|
||||||
"header.account": "账户",
|
"header.account": "账户",
|
||||||
"header.accountAria": "打开账户页",
|
"header.accountAria": "打开账户页",
|
||||||
|
"header.signIn": "登录 / 注册",
|
||||||
|
"header.signInAria": "打开登录注册页",
|
||||||
"header.live": "实时",
|
"header.live": "实时",
|
||||||
"header.refreshAria": "刷新所有数据",
|
"header.refreshAria": "刷新所有数据",
|
||||||
"header.langAria": "切换语言",
|
"header.langAria": "切换语言",
|
||||||
@@ -164,6 +166,8 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
|
|||||||
"header.infoAria": "Open system technical notes",
|
"header.infoAria": "Open system technical notes",
|
||||||
"header.account": "Account",
|
"header.account": "Account",
|
||||||
"header.accountAria": "Open account center",
|
"header.accountAria": "Open account center",
|
||||||
|
"header.signIn": "Sign In / Register",
|
||||||
|
"header.signInAria": "Open sign-in page",
|
||||||
"header.live": "LIVE",
|
"header.live": "LIVE",
|
||||||
"header.refreshAria": "Refresh all data",
|
"header.refreshAria": "Refresh all data",
|
||||||
"header.langAria": "Switch language",
|
"header.langAria": "Switch language",
|
||||||
|
|||||||
Reference in New Issue
Block a user