Files
assetx/webapp/components/points/PointsDashboard.tsx

195 lines
9.7 KiB
TypeScript
Raw Normal View History

"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import VipCard from "./VipCard";
import { useApp } from "@/contexts/AppContext";
import { fetchDashboard, type DashboardData } from "@/lib/api/points";
export default function PointsDashboard() {
const { t } = useApp();
const [dashboard, setDashboard] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadDashboard() {
setLoading(true);
const data = await fetchDashboard();
setDashboard(data);
setLoading(false);
}
loadDashboard();
}, []);
if (loading) {
return (
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-8 animate-pulse">
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded-xl" />
</div>
);
}
if (!dashboard) {
return (
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-8 animate-fade-in">
<p className="text-text-tertiary dark:text-gray-400">{t("points.failedToLoadDashboard")}</p>
</div>
);
}
return (
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-4 md:p-8 flex flex-col gap-6 animate-fade-in">
{/* Header Row */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="bg-[#f9fafb] dark:bg-gray-700 border border-border-normal dark:border-gray-600 rounded-full px-3 py-1 flex items-center justify-center">
<span className="text-caption-tiny font-bold text-text-primary dark:text-white leading-[150%] tracking-[0.01em]">
{dashboard.season.seasonName}
</span>
</div>
{dashboard.season.isLive && (
<div className="bg-[#e1f8ec] dark:bg-green-900/30 border border-[#b8ecd2] dark:border-green-700 rounded-full px-3 py-1 flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-[#10b981] opacity-55" />
<span className="text-caption-tiny font-bold text-[#10b981] dark:text-green-400 leading-[150%] tracking-[0.01em]">
{t("points.live")}
</span>
</div>
)}
</div>
<div className="rounded-[14px] px-4 py-2 flex items-center gap-2 h-[34.78px] shadow-lg bg-gradient-to-r from-[#fee685] to-[#fdc700] rotate-1">
<Image src="/components/points/icon-star.svg" alt="" width={17} height={17} />
<span className="text-caption-tiny font-bold text-text-primary leading-[150%] tracking-[0.01em]">
{t("points.xPoints")}
</span>
</div>
</div>
{/* ── 移动端布局 ── */}
<div className="md:hidden flex flex-col gap-4">
{/* Title */}
<div className="flex flex-col gap-1">
<h2 className="text-heading-h3 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em]">
{t("points.pointsDashboard")}
</h2>
<p className="text-body-small font-regular text-text-tertiary dark:text-gray-400 leading-[150%]">
<span>{t("points.unlockUpTo")} </span>
<span className="font-bold text-[#fdc700]">{t("points.xPoints")}</span>
<span> {t("points.withEcosystemMultipliers")}</span>
</p>
</div>
{/* Stats Row - 3 items in one line */}
<div className="bg-bg-subtle dark:bg-gray-700 rounded-2xl border border-white/5 dark:border-gray-600 px-4 py-3 flex items-center justify-between gap-2">
<div className="flex flex-col gap-0.5 min-w-0">
<span className="text-[10px] font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
{t("points.yourTotalPoints")}
</span>
<span className="text-[22px] font-extrabold leading-[110%] tracking-[-0.01em] bg-gradient-to-b from-[#111827] to-[#4b5563] bg-clip-text text-transparent dark:from-white dark:to-gray-400">
{dashboard.totalPoints.toLocaleString()}
</span>
</div>
<div className="w-px h-8 bg-border-normal dark:bg-gray-600 flex-shrink-0" />
<div className="flex flex-col gap-0.5">
<span className="text-[10px] font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
{t("points.globalRank")}
</span>
<span className="text-body-large font-bold text-text-primary dark:text-white leading-[130%]">
#{dashboard.globalRank.toLocaleString()}
</span>
<span className="text-[10px] font-regular text-text-secondary dark:text-gray-300 leading-[150%] tracking-[0.01em]">
Top {dashboard.topPercentage}
</span>
</div>
<div className="w-px h-8 bg-border-normal dark:bg-gray-600 flex-shrink-0" />
<div className="flex flex-col gap-0.5">
<span className="text-[10px] font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
{t("points.endsIn")}
</span>
<span className="text-body-large font-bold text-text-primary dark:text-white leading-[130%]">
{dashboard.season.daysRemaining}d {dashboard.season.hoursRemaining}h
</span>
<span className="text-[10px] font-regular text-text-secondary dark:text-gray-300 leading-[150%] tracking-[0.01em]">
{new Date(dashboard.season.endTime).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
</div>
</div>
{/* VipCard - full width, own row */}
<VipCard
memberTier={dashboard.memberTier}
level={dashboard.vipLevel}
currentPoints={dashboard.totalPoints}
totalPoints={dashboard.totalPoints + dashboard.pointsToNextTier}
nextTier={dashboard.nextTier}
pointsToNextTier={dashboard.pointsToNextTier}
/>
</div>
{/* ── 桌面端布局 ── */}
<div className="hidden md:flex gap-6">
{/* Left Section - Points Info */}
<div className="flex-1 flex flex-col gap-6">
{/* Title */}
<div className="flex flex-col gap-1">
<h2 className="text-heading-h3 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em]">
{t("points.pointsDashboard")}
</h2>
<p className="text-body-small font-regular text-text-tertiary dark:text-gray-400 leading-[150%]">
<span>{t("points.unlockUpTo")} </span>
<span className="font-bold text-[#fdc700]">{t("points.xPoints")}</span>
<span> {t("points.withEcosystemMultipliers")}</span>
</p>
</div>
{/* Stats Grid */}
<div className="flex items-center justify-between gap-6">
<div className="flex flex-col gap-1">
<span className="text-body-small font-bold text-text-secondary dark:text-gray-300 leading-[150%]">
{t("points.yourTotalPoints")}
</span>
<span className="text-[48px] font-extrabold leading-[110%] tracking-[-0.01em] font-inter bg-gradient-to-b from-[#111827] to-[#4b5563] bg-clip-text text-transparent dark:from-white dark:to-gray-400">
{dashboard.totalPoints.toLocaleString()}
</span>
</div>
<div className="bg-bg-subtle dark:bg-gray-700 rounded-2xl border border-white/5 dark:border-gray-600 px-6 h-28 flex items-center gap-12">
<div className="flex flex-col gap-1">
<span className="text-[10px] font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
{t("points.globalRank")}
</span>
<span className="text-heading-h3 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em]">
#{dashboard.globalRank.toLocaleString()}
</span>
<span className="text-[10px] font-regular text-text-secondary dark:text-gray-300 leading-[150%] tracking-[0.01em]">
Top {dashboard.topPercentage}
</span>
</div>
<div className="w-px h-8 bg-border-normal dark:bg-gray-600" />
<div className="flex flex-col gap-1">
<span className="text-[10px] font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
{t("points.endsIn")}
</span>
<span className="text-heading-h3 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em]">
{dashboard.season.daysRemaining}d {dashboard.season.hoursRemaining}h
</span>
<span className="text-[10px] font-regular text-text-secondary dark:text-gray-300 leading-[150%] tracking-[0.01em]">
{new Date(dashboard.season.endTime).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
</div>
</div>
</div>
</div>
{/* Right Section - VIP Card */}
<VipCard
memberTier={dashboard.memberTier}
level={dashboard.vipLevel}
currentPoints={dashboard.totalPoints}
totalPoints={dashboard.totalPoints + dashboard.pointsToNextTier}
nextTier={dashboard.nextTier}
pointsToNextTier={dashboard.pointsToNextTier}
/>
</div>
</div>
);
}