feat: Introduce premium map dashboard with dark theme, pro features, and payment processing.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -549,6 +549,25 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.root :global(.pro-locked) {
|
||||
filter: grayscale(0.8) opacity(0.7);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.root :global(.pro-locked::after) {
|
||||
content: "PRO";
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
background: var(--accent-blue);
|
||||
color: white;
|
||||
font-size: 7px;
|
||||
padding: 1px 3px;
|
||||
border-radius: 3px;
|
||||
font-weight: 900;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.root :global(.risk-badge) {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -14,103 +14,102 @@ import {
|
||||
getRiskBadgeLabel,
|
||||
getTemperatureChartData,
|
||||
} from "@/lib/dashboard-utils";
|
||||
import { ChevronRight, Crown } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
||||
const { locale, t } = useI18n();
|
||||
const chartData = getTemperatureChartData(detail, locale);
|
||||
|
||||
const canvasRef = useChart(
|
||||
() => {
|
||||
if (!chartData) {
|
||||
return {
|
||||
data: { datasets: [], labels: [] },
|
||||
type: "line",
|
||||
} satisfies ChartConfiguration<"line">;
|
||||
}
|
||||
|
||||
const forecastPoints = chartData.datasets.hasMgmHourly
|
||||
? chartData.datasets.mgmHourlyPoints
|
||||
: chartData.datasets.debPast.map(
|
||||
(value, index) => value ?? chartData.datasets.debFuture[index],
|
||||
);
|
||||
|
||||
const canvasRef = useChart(() => {
|
||||
if (!chartData) {
|
||||
return {
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
borderColor: chartData.datasets.hasMgmHourly
|
||||
? "rgba(250, 204, 21, 0.92)"
|
||||
: "rgba(52, 211, 153, 0.86)",
|
||||
borderWidth: 1.8,
|
||||
data: forecastPoints,
|
||||
fill: false,
|
||||
label: chartData.datasets.hasMgmHourly
|
||||
? locale === "en-US"
|
||||
? "MGM Forecast"
|
||||
: "MGM 预测"
|
||||
: locale === "en-US"
|
||||
? "DEB Forecast"
|
||||
: "DEB 预测",
|
||||
pointRadius: 0,
|
||||
spanGaps: true,
|
||||
tension: 0.28,
|
||||
},
|
||||
{
|
||||
backgroundColor: "#22d3ee",
|
||||
borderColor: "#22d3ee",
|
||||
borderWidth: 0,
|
||||
data: chartData.datasets.metarPoints,
|
||||
fill: false,
|
||||
label: locale === "en-US" ? "METAR Observation" : "METAR 实测",
|
||||
pointHoverRadius: 6,
|
||||
pointRadius: 3.8,
|
||||
showLine: false,
|
||||
},
|
||||
],
|
||||
labels: chartData.times,
|
||||
},
|
||||
options: {
|
||||
interaction: { intersect: false, mode: "index" },
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: "rgba(15, 23, 42, 0.95)",
|
||||
borderColor: "rgba(34, 211, 238, 0.25)",
|
||||
borderWidth: 1,
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: "rgba(255,255,255,0.03)" },
|
||||
ticks: {
|
||||
callback: (_value, index) =>
|
||||
typeof index === "number" && index % 4 === 0
|
||||
? chartData.times[index]
|
||||
: "",
|
||||
color: "#64748b",
|
||||
font: { size: 10 },
|
||||
maxRotation: 0,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
grid: { color: "rgba(255,255,255,0.03)" },
|
||||
max: chartData.max,
|
||||
min: chartData.min,
|
||||
ticks: {
|
||||
callback: (value) => `${value}${detail.temp_symbol || "°C"}`,
|
||||
color: "#64748b",
|
||||
font: { size: 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data: { datasets: [], labels: [] },
|
||||
type: "line",
|
||||
} satisfies ChartConfiguration<"line">;
|
||||
},
|
||||
[chartData, detail.temp_symbol, locale],
|
||||
);
|
||||
}
|
||||
|
||||
const forecastPoints = chartData.datasets.hasMgmHourly
|
||||
? chartData.datasets.mgmHourlyPoints
|
||||
: chartData.datasets.debPast.map(
|
||||
(value, index) => value ?? chartData.datasets.debFuture[index],
|
||||
);
|
||||
|
||||
return {
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
borderColor: chartData.datasets.hasMgmHourly
|
||||
? "rgba(250, 204, 21, 0.92)"
|
||||
: "rgba(52, 211, 153, 0.86)",
|
||||
borderWidth: 1.8,
|
||||
data: forecastPoints,
|
||||
fill: false,
|
||||
label: chartData.datasets.hasMgmHourly
|
||||
? locale === "en-US"
|
||||
? "MGM Forecast"
|
||||
: "MGM 预测"
|
||||
: locale === "en-US"
|
||||
? "DEB Forecast"
|
||||
: "DEB 预测",
|
||||
pointRadius: 0,
|
||||
spanGaps: true,
|
||||
tension: 0.28,
|
||||
},
|
||||
{
|
||||
backgroundColor: "#22d3ee",
|
||||
borderColor: "#22d3ee",
|
||||
borderWidth: 0,
|
||||
data: chartData.datasets.metarPoints,
|
||||
fill: false,
|
||||
label: locale === "en-US" ? "METAR Observation" : "METAR 实测",
|
||||
pointHoverRadius: 6,
|
||||
pointRadius: 3.8,
|
||||
showLine: false,
|
||||
},
|
||||
],
|
||||
labels: chartData.times,
|
||||
},
|
||||
options: {
|
||||
interaction: { intersect: false, mode: "index" },
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: "rgba(15, 23, 42, 0.95)",
|
||||
borderColor: "rgba(34, 211, 238, 0.25)",
|
||||
borderWidth: 1,
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: "rgba(255,255,255,0.03)" },
|
||||
ticks: {
|
||||
callback: (_value, index) =>
|
||||
typeof index === "number" && index % 4 === 0
|
||||
? chartData.times[index]
|
||||
: "",
|
||||
color: "#64748b",
|
||||
font: { size: 10 },
|
||||
maxRotation: 0,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
grid: { color: "rgba(255,255,255,0.03)" },
|
||||
max: chartData.max,
|
||||
min: chartData.min,
|
||||
ticks: {
|
||||
callback: (value) => `${value}${detail.temp_symbol || "°C"}`,
|
||||
color: "#64748b",
|
||||
font: { size: 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "line",
|
||||
} satisfies ChartConfiguration<"line">;
|
||||
}, [chartData, detail.temp_symbol, locale]);
|
||||
|
||||
return (
|
||||
<div className="detail-mini-chart-wrap">
|
||||
@@ -126,6 +125,7 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
||||
|
||||
export function DetailPanel() {
|
||||
const store = useDashboardStore();
|
||||
const router = useRouter();
|
||||
const { locale, t } = useI18n();
|
||||
const detail = store.selectedDetail;
|
||||
const isPro = store.proAccess.subscriptionActive;
|
||||
@@ -194,39 +194,40 @@ export function DetailPanel() {
|
||||
<span className={clsx("risk-badge", detail?.risk?.level || "low")}>
|
||||
{getRiskBadgeLabel(detail?.risk?.level, locale)}
|
||||
</span>
|
||||
<span className="local-time">
|
||||
{detail
|
||||
? `${detail.local_date} ${detail.local_time}`
|
||||
: t("detail.waitSelect")}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="history-btn"
|
||||
title={
|
||||
isPro
|
||||
<div className="relative group">
|
||||
<button
|
||||
type="button"
|
||||
className={clsx("history-btn", !isPro && "pro-locked")}
|
||||
title={
|
||||
isPro
|
||||
? t("detail.todayAnalysis")
|
||||
: `${t("detail.todayAnalysis")} (Pro)`
|
||||
}
|
||||
onClick={() => {
|
||||
blurActiveElement();
|
||||
void store.openTodayModal();
|
||||
}}
|
||||
disabled={!detail}
|
||||
>
|
||||
{isPro
|
||||
? t("detail.todayAnalysis")
|
||||
: `${t("detail.todayAnalysis")} (Pro)`
|
||||
}
|
||||
onClick={() => {
|
||||
blurActiveElement();
|
||||
void store.openTodayModal();
|
||||
}}
|
||||
disabled={!detail}
|
||||
>
|
||||
{isPro ? t("detail.todayAnalysis") : `${t("detail.todayAnalysis")} · Pro`}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="history-btn"
|
||||
title={isPro ? t("detail.history") : `${t("detail.history")} (Pro)`}
|
||||
onClick={() => {
|
||||
blurActiveElement();
|
||||
void store.openHistory();
|
||||
}}
|
||||
disabled={!detail}
|
||||
>
|
||||
{isPro ? t("detail.history") : `${t("detail.history")} · Pro`}
|
||||
</button>
|
||||
: `${t("detail.todayAnalysis")} · Pro`}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx("history-btn", !isPro && "pro-locked")}
|
||||
title={
|
||||
isPro ? t("detail.history") : `${t("detail.history")} (Pro)`
|
||||
}
|
||||
onClick={() => {
|
||||
blurActiveElement();
|
||||
void store.openHistory();
|
||||
}}
|
||||
disabled={!detail}
|
||||
>
|
||||
{isPro ? t("detail.history") : `${t("detail.history")} · Pro`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,7 +269,9 @@ export function DetailPanel() {
|
||||
</>
|
||||
) : (
|
||||
<div className="detail-scenery-fallback">
|
||||
<span className="detail-scenery-kicker">{detail.display_name}</span>
|
||||
<span className="detail-scenery-kicker">
|
||||
{detail.display_name}
|
||||
</span>
|
||||
<strong className="detail-scenery-title">
|
||||
{t("detail.sceneryTitle")}
|
||||
</strong>
|
||||
@@ -291,9 +294,33 @@ export function DetailPanel() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="detail-section">
|
||||
<section className="detail-section relative overflow-hidden rounded-2xl">
|
||||
<h3>{t("detail.todayMiniTrend")}</h3>
|
||||
<DetailMiniTemperatureChart detail={detail} />
|
||||
<div
|
||||
className={clsx(
|
||||
"transition-all duration-500",
|
||||
!isPro &&
|
||||
"blur-md opacity-30 select-none pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<DetailMiniTemperatureChart detail={detail} />
|
||||
</div>
|
||||
{!isPro && (
|
||||
<div className="absolute inset-x-0 bottom-0 top-[3rem] z-10 flex flex-col items-center justify-center p-4 bg-gradient-to-t from-[#0b0f1a] via-transparent to-transparent">
|
||||
<div className="bg-blue-600/90 text-white text-[10px] font-bold px-3 py-1.5 rounded-full shadow-lg shadow-blue-600/40 flex items-center gap-1.5 mb-2">
|
||||
<Crown size={10} fill="currentColor" /> Pro Feature
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 text-center mb-3">
|
||||
解锁日内高精趋势图
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push("/account")}
|
||||
className="text-[10px] font-bold text-blue-400 hover:text-blue-300 transition-colors flex items-center gap-1"
|
||||
>
|
||||
立即升级 <ChevronRight size={10} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<ForecastTable />
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useI18n } from "@/hooks/useI18n";
|
||||
import { useDashboardStore } from "@/hooks/useDashboardStore";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type ProFeaturePaywallProps = {
|
||||
feature: "today" | "history" | "future";
|
||||
@@ -32,148 +32,143 @@ export function ProFeaturePaywall({
|
||||
|
||||
// Redemption logic: 500 points = $1 discount
|
||||
// Max discount $3 (requires 1500 points)
|
||||
const maxRedeemablePoints = 1500;
|
||||
const PRO_PRICE = 5;
|
||||
const POINTS_VAL = 500;
|
||||
const MAX_DISCOUNT = 3;
|
||||
|
||||
const pointsAvailable = proAccess.points || 0;
|
||||
const effectivePoints = Math.min(pointsAvailable, maxRedeemablePoints);
|
||||
const discountAmount = usePoints ? Math.floor(effectivePoints / 500) : 0;
|
||||
const originalPrice = 5.0;
|
||||
const finalPrice = originalPrice - discountAmount;
|
||||
const pointsToConsume = discountAmount * 500;
|
||||
|
||||
const billing = useMemo(() => {
|
||||
const maxRedeemable = MAX_DISCOUNT * POINTS_VAL;
|
||||
const actualRedeem = Math.min(pointsAvailable, maxRedeemable);
|
||||
const discount = Math.floor(actualRedeem / POINTS_VAL);
|
||||
return {
|
||||
pointsUsed: discount * POINTS_VAL,
|
||||
discountAmount: discount,
|
||||
payAmount: PRO_PRICE - (usePoints ? discount : 0),
|
||||
};
|
||||
}, [usePoints, pointsAvailable]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center py-6 md:py-10">
|
||||
<div className="relative w-full max-w-xl rounded-[2.5rem] border border-white/10 bg-slate-900/80 p-8 shadow-3xl backdrop-blur-3xl md:p-12">
|
||||
<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 right-6 top-6 flex h-10 w-10 items-center justify-center rounded-full bg-white/5 text-slate-400 transition hover:bg-white/10 hover:text-white z-10"
|
||||
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 left-1/2 top-0 flex h-20 w-20 -translate-x-1/2 -translate-y-1/2 rotate-[-6deg] items-center justify-center rounded-[1.5rem] border-4 border-slate-950 bg-gradient-to-tr from-amber-400 via-orange-500 to-yellow-500 text-white shadow-2xl shadow-orange-500/30">
|
||||
<Crown size={32} fill="white" />
|
||||
<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>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<h3 className="text-3xl font-extrabold tracking-tight text-white md:text-4xl">
|
||||
{isEn ? "Unlock PolyWeather Pro" : "开启 PolyWeather Pro"}
|
||||
</h3>
|
||||
<p className="mx-auto mt-4 max-w-sm text-base leading-relaxed text-slate-400">
|
||||
{isEn
|
||||
? "Unlock 15-day precision trends, real-time radar, and ad-free experience across all platforms."
|
||||
: "解锁 15 天高精度趋势分析、实时雷达与闪电追踪。尊享全平台无广告体验。"}
|
||||
</p>
|
||||
</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>
|
||||
|
||||
{/* Pricing Cards */}
|
||||
<div className="mt-10 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{/* Plan Card */}
|
||||
<div className="relative overflow-hidden rounded-3xl border border-blue-500/30 bg-blue-500/5 p-6 transition hover:bg-blue-500/10">
|
||||
<div className="absolute -right-2 -top-1 rotate-12 rounded-lg bg-blue-500 px-2 py-0.5 text-[10px] font-bold text-white uppercase tracking-tighter shadow-lg">
|
||||
<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>
|
||||
<div className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">
|
||||
<p className="text-slate-500 text-[10px] font-bold uppercase tracking-widest mb-1">
|
||||
PRO PLAN
|
||||
</div>
|
||||
<div className="mt-2 flex items-baseline">
|
||||
</p>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-3xl font-black text-white">$5.00</span>
|
||||
<span className="ml-1 text-sm font-medium text-slate-500">
|
||||
<span className="text-slate-500 text-sm">
|
||||
/ {isEn ? "mo" : "月"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Points Card */}
|
||||
{/* 积分抵扣卡片 */}
|
||||
<div
|
||||
className={`relative rounded-3xl border p-6 transition ${
|
||||
usePoints && pointsAvailable >= 500
|
||||
? "border-indigo-500/30 bg-indigo-500/5"
|
||||
: "border-white/5 bg-white/5 opacity-60"
|
||||
}`}
|
||||
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">
|
||||
<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"
|
||||
}`}
|
||||
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={`relative h-6 w-11 shrink-0 rounded-full transition-colors ${
|
||||
usePoints && pointsAvailable >= 500
|
||||
? "bg-indigo-600"
|
||||
: "bg-slate-700"
|
||||
} ${pointsAvailable < 500 ? "opacity-30 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
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 left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow-sm transition-transform ${
|
||||
usePoints && pointsAvailable >= 500
|
||||
? "translate-x-5"
|
||||
: "translate-x-0"
|
||||
}`}
|
||||
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="mt-2 flex items-baseline">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span
|
||||
className={`text-3xl font-black ${usePoints && pointsAvailable >= 500 ? "text-emerald-400" : "text-slate-500"}`}
|
||||
>
|
||||
-${discountAmount.toFixed(2)}
|
||||
-${billing.discountAmount.toFixed(2)}
|
||||
</span>
|
||||
<span className="ml-1 text-[10px] font-bold text-slate-500 uppercase">
|
||||
<span className="text-slate-500 text-[10px] font-bold uppercase">
|
||||
OFF
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[10px] font-medium text-slate-500">
|
||||
<p className="text-[10px] text-slate-500 mt-1 italic">
|
||||
{pointsAvailable < 500
|
||||
? isEn
|
||||
? "Need 500+ points"
|
||||
? `Need 500+ points (Current: ${pointsAvailable})`
|
||||
: `积分不足 (当前 ${pointsAvailable})`
|
||||
: isEn
|
||||
? `Auto-consume ${pointsToConsume} points`
|
||||
: `已自动消耗 ${pointsToConsume} 积分`}
|
||||
</div>
|
||||
: usePoints
|
||||
? isEn
|
||||
? `Consumed ${billing.pointsUsed} points`
|
||||
: `已自动消耗 ${billing.pointsUsed} 积分`
|
||||
: isEn
|
||||
? "Up to $3.00 off"
|
||||
: "开启后最多抵扣 $3.00"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total & Action */}
|
||||
<div className="mt-10 space-y-6">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<span className="text-sm font-medium text-slate-400">
|
||||
<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">
|
||||
${finalPrice.toFixed(2)}
|
||||
${billing.payAmount.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/account"
|
||||
className="group flex w-full items-center justify-center rounded-2xl bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-5 text-lg font-bold text-white shadow-2xl shadow-blue-600/30 transition hover:from-blue-500 hover:to-indigo-500 active:scale-[0.98]"
|
||||
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="ml-2 transition-transform group-hover:translate-x-1"
|
||||
className="group-hover:translate-x-1 transition-transform"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
<span className="flex items-center gap-1.5 text-[10px] font-medium text-slate-500">
|
||||
<Lock size={12} className="opacity-50" />
|
||||
<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="text-[10px] font-medium text-slate-500 transition hover:text-slate-300"
|
||||
className="hover:text-white cursor-pointer transition-colors"
|
||||
>
|
||||
{isEn ? "FAQ" : "常见问题 (FAQ)"}
|
||||
</Link>
|
||||
|
||||
Reference in New Issue
Block a user