Просмотр исходного кода

refactor(dashboard): improve home layout alignment and structure

- Add max-w-7xl constraint to dashboard home container for consistent alignment with header
- Restructure dashboard-bento into 3 independent sections: metrics, chart, leaderboards
- Remove nested grid structure that caused misalignment on desktop
- Change admin layout from 2-column (content + sidebar) to 4-column grid (3 leaderboards + live sessions)
- Remove colSpan/rowSpan from StatisticsChartCard for full-width independent rendering
- Add min-h-[280px] to LeaderboardCard for consistent card heights
- Update tests to match new layout structure

Co-Authored-By: Claude Opus 4.5 <[email protected]>
ding113 2 месяцев назад
Родитель
Сommit
3c92731db1

+ 97 - 112
src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsx

@@ -9,6 +9,7 @@ import type { OverviewData } from "@/actions/overview";
 import { getOverviewData } from "@/actions/overview";
 import { getUserStatistics } from "@/actions/statistics";
 import type { CurrencyCode } from "@/lib/utils";
+import { cn } from "@/lib/utils";
 import { formatCurrency } from "@/lib/utils/currency";
 import type {
   LeaderboardEntry,
@@ -202,127 +203,111 @@ export function DashboardBento({
 
   return (
     <div className="space-y-6">
-      {/* Top Section: Metrics + Live Sessions */}
+      {/* Section 1: Metrics (Admin only) */}
       {isAdmin && (
-        <div className="mx-auto w-full max-w-7xl">
-          <BentoGrid>
-            {/* Metric Cards */}
-            <BentoMetricCard
-              title={t("metrics.concurrent")}
-              value={metrics.concurrentSessions}
-              icon={Activity}
-              accentColor="emerald"
-              className="min-h-[120px]"
-              comparisons={[
-                {
-                  value: metrics.recentMinuteRequests,
-                  label: t("metrics.rpm"),
-                  isPercentage: false,
-                },
-              ]}
-            />
-            <BentoMetricCard
-              title={t("metrics.todayRequests")}
-              value={metrics.todayRequests}
-              icon={TrendingUp}
-              accentColor="blue"
-              className="min-h-[120px]"
-              comparisons={[{ value: requestsChange, label: t("metrics.vsYesterday") }]}
-            />
-            <BentoMetricCard
-              title={t("metrics.todayCost")}
-              value={formatCurrency(metrics.todayCost, currencyCode)}
-              icon={DollarSign}
-              accentColor="amber"
-              className="min-h-[120px]"
-              comparisons={[{ value: costChange, label: t("metrics.vsYesterday") }]}
-            />
-            <BentoMetricCard
-              title={t("metrics.avgResponse")}
-              value={metrics.avgResponseTime}
-              icon={Clock}
-              formatter={formatResponseTime}
-              accentColor="purple"
-              className="min-h-[120px]"
-              comparisons={[{ value: -responseTimeChange, label: t("metrics.vsYesterday") }]}
-            />
-          </BentoGrid>
-        </div>
+        <BentoGrid>
+          <BentoMetricCard
+            title={t("metrics.concurrent")}
+            value={metrics.concurrentSessions}
+            icon={Activity}
+            accentColor="emerald"
+            className="min-h-[120px]"
+            comparisons={[
+              {
+                value: metrics.recentMinuteRequests,
+                label: t("metrics.rpm"),
+                isPercentage: false,
+              },
+            ]}
+          />
+          <BentoMetricCard
+            title={t("metrics.todayRequests")}
+            value={metrics.todayRequests}
+            icon={TrendingUp}
+            accentColor="blue"
+            className="min-h-[120px]"
+            comparisons={[{ value: requestsChange, label: t("metrics.vsYesterday") }]}
+          />
+          <BentoMetricCard
+            title={t("metrics.todayCost")}
+            value={formatCurrency(metrics.todayCost, currencyCode)}
+            icon={DollarSign}
+            accentColor="amber"
+            className="min-h-[120px]"
+            comparisons={[{ value: costChange, label: t("metrics.vsYesterday") }]}
+          />
+          <BentoMetricCard
+            title={t("metrics.avgResponse")}
+            value={metrics.avgResponseTime}
+            icon={Clock}
+            formatter={formatResponseTime}
+            accentColor="purple"
+            className="min-h-[120px]"
+            comparisons={[{ value: -responseTimeChange, label: t("metrics.vsYesterday") }]}
+          />
+        </BentoGrid>
       )}
 
-      {/* Middle Section: Statistics Chart + Leaderboards (+ Live Sessions sidebar for admin) */}
-      <div
-        data-testid={isAdmin ? "dashboard-home-layout" : undefined}
-        className={
-          isAdmin ? "grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_300px]" : undefined
-        }
-      >
-        <div className="min-w-0">
-          <div
-            className={[
-              "grid gap-4 md:gap-6",
-              "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
-              "auto-rows-[minmax(140px,auto)]",
-            ].join(" ")}
-          >
-            {statistics && (
-              <StatisticsChartCard
-                data={statistics}
-                onTimeRangeChange={setTimeRange}
-                currencyCode={currencyCode}
-                colSpan={3}
-              />
-            )}
+      {/* Section 2: Statistics Chart - Full width */}
+      {statistics && (
+        <StatisticsChartCard
+          data={statistics}
+          onTimeRangeChange={setTimeRange}
+          currencyCode={currencyCode}
+        />
+      )}
 
-            {canViewLeaderboard && (
-              <LeaderboardCard
-                title={tl("userRankings")}
-                entries={userLeaderboard}
-                currencyCode={currencyCode}
-                isLoading={userLeaderboardLoading}
-                emptyText={tl("noData")}
-                viewAllHref="/dashboard/leaderboard"
-                maxItems={3}
-                accentColor="primary"
-              />
-            )}
-            {canViewLeaderboard && (
-              <LeaderboardCard
-                title={tl("providerRankings")}
-                entries={providerLeaderboard}
-                currencyCode={currencyCode}
-                isLoading={providerLeaderboardLoading}
-                emptyText={tl("noData")}
-                viewAllHref="/dashboard/leaderboard"
-                maxItems={3}
-                accentColor="purple"
-              />
-            )}
-            {canViewLeaderboard && (
-              <LeaderboardCard
-                title={tl("modelRankings")}
-                entries={modelLeaderboard}
-                currencyCode={currencyCode}
-                isLoading={modelLeaderboardLoading}
-                emptyText={tl("noData")}
-                viewAllHref="/dashboard/leaderboard"
-                maxItems={3}
-                accentColor="blue"
-              />
-            )}
-          </div>
-        </div>
+      {/* Section 3: Leaderboards + Live Sessions */}
+      {canViewLeaderboard && (
+        <div
+          data-testid={isAdmin ? "dashboard-home-layout" : undefined}
+          className={cn(
+            "grid gap-6",
+            isAdmin
+              ? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-[1fr_1fr_1fr_280px]"
+              : "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
+          )}
+        >
+          <LeaderboardCard
+            title={tl("userRankings")}
+            entries={userLeaderboard}
+            currencyCode={currencyCode}
+            isLoading={userLeaderboardLoading}
+            emptyText={tl("noData")}
+            viewAllHref="/dashboard/leaderboard"
+            maxItems={3}
+            accentColor="primary"
+          />
+          <LeaderboardCard
+            title={tl("providerRankings")}
+            entries={providerLeaderboard}
+            currencyCode={currencyCode}
+            isLoading={providerLeaderboardLoading}
+            emptyText={tl("noData")}
+            viewAllHref="/dashboard/leaderboard"
+            maxItems={3}
+            accentColor="purple"
+          />
+          <LeaderboardCard
+            title={tl("modelRankings")}
+            entries={modelLeaderboard}
+            currencyCode={currencyCode}
+            isLoading={modelLeaderboardLoading}
+            emptyText={tl("noData")}
+            viewAllHref="/dashboard/leaderboard"
+            maxItems={3}
+            accentColor="blue"
+          />
 
-        {isAdmin && (
-          <aside data-testid="dashboard-home-sidebar" className="hidden lg:block">
+          {isAdmin && (
             <LiveSessionsPanel
+              data-testid="dashboard-home-sidebar"
               sessions={sessionsWithActivity}
               isLoading={sessionsLoading}
-              className="h-full"
             />
-          </aside>
-        )}
-      </div>
+          )}
+        </div>
+      )}
     </div>
   );
 }

+ 1 - 1
src/app/[locale]/dashboard/_components/bento/leaderboard-card.tsx

@@ -166,7 +166,7 @@ export function LeaderboardCard({
   const maxCost = Math.max(...entries.map((e) => e.totalCost), 0);
 
   return (
-    <BentoCard className={cn("flex flex-col", className)}>
+    <BentoCard className={cn("flex flex-col min-h-[280px]", className)}>
       {/* Header */}
       <div className="flex items-center justify-between mb-3">
         <h4 className="text-sm font-semibold">{title}</h4>

+ 1 - 7
src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx

@@ -29,7 +29,6 @@ export interface StatisticsChartCardProps {
   data: UserStatisticsData;
   onTimeRangeChange?: (timeRange: TimeRange) => void;
   currencyCode?: CurrencyCode;
-  colSpan?: 3 | 4;
   className?: string;
 }
 
@@ -37,7 +36,6 @@ export function StatisticsChartCard({
   data,
   onTimeRangeChange,
   currencyCode = "USD",
-  colSpan = 4,
   className,
 }: StatisticsChartCardProps) {
   const t = useTranslations("dashboard.statistics");
@@ -175,11 +173,7 @@ export function StatisticsChartCard({
   };
 
   return (
-    <BentoCard
-      colSpan={colSpan}
-      rowSpan={2}
-      className={cn("flex flex-col p-0 overflow-hidden", className)}
-    >
+    <BentoCard className={cn("flex flex-col p-0 overflow-hidden", className)}>
       {/* Header */}
       <div className="flex items-center justify-between border-b border-border/50 dark:border-white/[0.06]">
         <div className="flex items-center gap-4 p-4">

+ 1 - 1
src/app/[locale]/dashboard/_components/dashboard-main.tsx

@@ -27,7 +27,7 @@ export function DashboardMain({ children }: DashboardMainProps) {
   }
 
   if (isDashboardHomePage) {
-    return <main className="w-full px-6 py-8">{children}</main>;
+    return <main className="mx-auto w-full max-w-7xl px-6 py-8">{children}</main>;
   }
 
   return <main className="mx-auto w-full max-w-7xl px-6 py-8">{children}</main>;

+ 5 - 7
tests/unit/dashboard/dashboard-home-layout.test.tsx

@@ -171,7 +171,7 @@ afterEach(() => {
 });
 
 describe("DashboardMain layout classes", () => {
-  test("pathname /dashboard removes max-w-7xl but keeps px-6", () => {
+  test("pathname /dashboard has max-w-7xl and px-6", () => {
     routingMocks.usePathname.mockReturnValue("/dashboard");
     const { container, unmount } = renderSimple(
       <DashboardMain>
@@ -182,7 +182,7 @@ describe("DashboardMain layout classes", () => {
     const main = container.querySelector("main");
     expect(main).toBeTruthy();
     expect(main?.className).toContain("px-6");
-    expect(main?.className).not.toContain("max-w-7xl");
+    expect(main?.className).toContain("max-w-7xl");
 
     unmount();
   });
@@ -204,7 +204,7 @@ describe("DashboardMain layout classes", () => {
 });
 
 describe("DashboardBento admin layout", () => {
-  test("renders two-column layout with right sidebar LiveSessionsPanel", async () => {
+  test("renders four-column layout with LiveSessionsPanel in last column", async () => {
     const { container, unmount } = renderWithProviders(
       <DashboardBento
         isAdmin={true}
@@ -215,15 +215,13 @@ describe("DashboardBento admin layout", () => {
     );
     await flushPromises();
 
-    const grid = findByClassToken(container, "lg:grid-cols-[minmax(0,1fr)_300px]");
+    const grid = findByClassToken(container, "lg:grid-cols-[1fr_1fr_1fr_280px]");
     expect(grid).toBeTruthy();
 
     const livePanel = container.querySelector('[data-testid="live-sessions-panel"]');
     expect(livePanel).toBeTruthy();
 
-    const sidebar = findClosestWithClasses(livePanel, ["hidden", "lg:block"]);
-    expect(sidebar).toBeTruthy();
-    expect(grid?.contains(sidebar as HTMLElement)).toBe(true);
+    expect(grid?.contains(livePanel as HTMLElement)).toBe(true);
 
     unmount();
   });