From 8f7c69056f149e7e4cb5169b48a6b32510b38c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sat, 11 Apr 2026 23:36:34 +0200 Subject: [PATCH] refactor(web): remove unnecessary "use client" from 6 pure-render components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BenchResourceCard, MobileProjectCard, MobileCapacityCard, DynamicFieldRenderer, BudgetStatusBar, and TimelineHeader use no hooks, event handlers, or browser APIs — they can be server components, reducing client bundle size. Co-Authored-By: Claude Opus 4.6 --- .../components/bench/BenchResourceCard.tsx | 16 +++-- .../dynamic-fields/DynamicFieldRenderer.tsx | 10 +-- .../components/mobile/MobileCapacityCard.tsx | 29 ++++++--- .../components/mobile/MobileProjectCard.tsx | 28 ++++++--- .../components/projects/BudgetStatusBar.tsx | 61 +++++++++++++------ .../components/timeline/TimelineHeader.tsx | 43 ++++++++----- 6 files changed, 122 insertions(+), 65 deletions(-) diff --git a/apps/web/src/components/bench/BenchResourceCard.tsx b/apps/web/src/components/bench/BenchResourceCard.tsx index d308a1e..f0b2ae1 100644 --- a/apps/web/src/components/bench/BenchResourceCard.tsx +++ b/apps/web/src/components/bench/BenchResourceCard.tsx @@ -1,5 +1,3 @@ -"use client"; - import Link from "next/link"; interface BenchResourceCardProps { @@ -29,11 +27,7 @@ export function BenchResourceCard({ .join(""); const availabilityLevel = - availableHoursPerDay >= 6 - ? "high" - : availableHoursPerDay >= 3 - ? "medium" - : "low"; + availableHoursPerDay >= 6 ? "high" : availableHoursPerDay >= 3 ? "medium" : "low"; const levelClass = availabilityLevel === "high" @@ -55,10 +49,14 @@ export function BenchResourceCard({
- {initials} + + {initials} +
-
{name}
+
+ {name} +
{eid}
diff --git a/apps/web/src/components/dynamic-fields/DynamicFieldRenderer.tsx b/apps/web/src/components/dynamic-fields/DynamicFieldRenderer.tsx index 305da95..3e5399b 100644 --- a/apps/web/src/components/dynamic-fields/DynamicFieldRenderer.tsx +++ b/apps/web/src/components/dynamic-fields/DynamicFieldRenderer.tsx @@ -1,5 +1,3 @@ -"use client"; - import { clsx } from "clsx"; import { formatDateLong } from "~/lib/format.js"; import { FieldType } from "@capakraken/shared"; @@ -36,9 +34,7 @@ function renderValue(fieldDef: BlueprintFieldDefinition, value: unknown): React. {bool ? "Yes" : "No"} @@ -100,9 +96,7 @@ function FieldRow({ fieldDef, value }: { fieldDef: BlueprintFieldDefinition; val {fieldDef.label}
{renderValue(fieldDef, value)}
- {fieldDef.description && ( -

{fieldDef.description}

- )} + {fieldDef.description &&

{fieldDef.description}

}
); } diff --git a/apps/web/src/components/mobile/MobileCapacityCard.tsx b/apps/web/src/components/mobile/MobileCapacityCard.tsx index 0d594ac..dd274dd 100644 --- a/apps/web/src/components/mobile/MobileCapacityCard.tsx +++ b/apps/web/src/components/mobile/MobileCapacityCard.tsx @@ -1,5 +1,3 @@ -"use client"; - interface MobileCapacityCardProps { totalResources: number; activeResources: number; @@ -16,8 +14,7 @@ export function MobileCapacityCard({ const pct = Math.min(100, Math.max(0, avgUtilizationPct)); const circumference = 2 * Math.PI * 34; // radius = 34 const dashOffset = circumference * (1 - pct / 100); - const color = - pct >= 90 ? "#d97706" : pct >= 70 ? "#059669" : "#6b7280"; + const color = pct >= 90 ? "#d97706" : pct >= 70 ? "#059669" : "#6b7280"; return (
@@ -27,7 +24,15 @@ export function MobileCapacityCard({
{/* CSS-only donut */} - + - + {Math.round(pct)}% @@ -54,7 +67,9 @@ export function MobileCapacityCard({ {overbookedCount > 0 && (
Overbooked - {overbookedCount} + + {overbookedCount} +
)}
diff --git a/apps/web/src/components/mobile/MobileProjectCard.tsx b/apps/web/src/components/mobile/MobileProjectCard.tsx index f6b57ed..e2b34cd 100644 --- a/apps/web/src/components/mobile/MobileProjectCard.tsx +++ b/apps/web/src/components/mobile/MobileProjectCard.tsx @@ -1,11 +1,9 @@ -"use client"; - import Link from "next/link"; const STATUS_BADGE: Record = { - ACTIVE: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300", - DRAFT: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400", - ON_HOLD: "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300", + ACTIVE: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300", + DRAFT: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400", + ON_HOLD: "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300", COMPLETED: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300", CANCELLED: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300", }; @@ -18,20 +16,32 @@ interface MobileProjectCardProps { allocationsCount?: number; } -export function MobileProjectCard({ id, shortCode, name, status, allocationsCount }: MobileProjectCardProps) { +export function MobileProjectCard({ + id, + shortCode, + name, + status, + allocationsCount, +}: MobileProjectCardProps) { return ( -
{shortCode}
+
+ {shortCode} +
{name}
{allocationsCount !== undefined && ( -
{allocationsCount} allocation{allocationsCount !== 1 ? "s" : ""}
+
+ {allocationsCount} allocation{allocationsCount !== 1 ? "s" : ""} +
)}
- + {status.charAt(0) + status.slice(1).toLowerCase().replace("_", " ")} diff --git a/apps/web/src/components/projects/BudgetStatusBar.tsx b/apps/web/src/components/projects/BudgetStatusBar.tsx index b614eef..8b3945d 100644 --- a/apps/web/src/components/projects/BudgetStatusBar.tsx +++ b/apps/web/src/components/projects/BudgetStatusBar.tsx @@ -1,5 +1,3 @@ -"use client"; - import { clsx } from "clsx"; import { formatMoney } from "~/lib/format.js"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; @@ -55,14 +53,18 @@ export function BudgetStatusBar({ // Cap visual bar segments at 100% total const cappedConfirmedPercent = Math.min(confirmedPercent, 100); - const cappedProposedPercent = Math.min(proposedPercent, Math.max(0, 100 - cappedConfirmedPercent)); + const cappedProposedPercent = Math.min( + proposedPercent, + Math.max(0, 100 - cappedConfirmedPercent), + ); - const highestWarning = warnings.length > 0 - ? warnings.reduce((prev, curr) => { - const levels: Record = { info: 0, warning: 1, critical: 2 }; - return (levels[curr.level] ?? 0) > (levels[prev.level] ?? 0) ? curr : prev; - }) - : null; + const highestWarning = + warnings.length > 0 + ? warnings.reduce((prev, curr) => { + const levels: Record = { info: 0, warning: 1, critical: 2 }; + return (levels[curr.level] ?? 0) > (levels[prev.level] ?? 0) ? curr : prev; + }) + : null; return (
@@ -74,12 +76,18 @@ export function BudgetStatusBar({
{/* Confirmed segment */}
{/* Proposed segment */}
@@ -89,8 +97,7 @@ export function BudgetStatusBar({ {formatEur(allocatedCents)} {" / "} - {formatEur(budgetCents)} - {" "} + {formatEur(budgetCents)}{" "} ({utilizationPercent.toFixed(1)}%) @@ -102,12 +109,20 @@ export function BudgetStatusBar({ getWarningBadgeStyle(highestWarning.level), )} > - {highestWarning.level === "critical" ? "⚠" : highestWarning.level === "warning" ? "!" : "i"} + {highestWarning.level === "critical" + ? "⚠" + : highestWarning.level === "warning" + ? "!" + : "i"} {warnings.length > 1 ? `${warnings.length} warnings` : "Warning"} )} - - {remainingCents >= 0 ? `${formatEur(remainingCents)} left` : `${formatEur(Math.abs(remainingCents))} over`} + + {remainingCents >= 0 + ? `${formatEur(remainingCents)} left` + : `${formatEur(Math.abs(remainingCents))} over`}
@@ -115,11 +130,21 @@ export function BudgetStatusBar({ {/* Legend */}
- + Confirmed {formatEur(confirmedCents)} - + Proposed {formatEur(proposedCents)}
diff --git a/apps/web/src/components/timeline/TimelineHeader.tsx b/apps/web/src/components/timeline/TimelineHeader.tsx index 02d2097..9f98294 100644 --- a/apps/web/src/components/timeline/TimelineHeader.tsx +++ b/apps/web/src/components/timeline/TimelineHeader.tsx @@ -1,5 +1,3 @@ -"use client"; - import { clsx } from "clsx"; import { MONTHS_SHORT } from "./timelineConstants.js"; @@ -33,7 +31,10 @@ export function TimelineHeader({ className="sticky top-0 z-40 flex bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800" style={{ height: HEADER_MONTH_HEIGHT }} > -
+
{monthGroups.map((m, i) => (
{showLabel && ( <> - + {zoom === "week" ? `${date.getDate()} ${MONTHS_SHORT[date.getMonth()]}` : date.getDate()} {zoom === "day" && ( - + {["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"][dow]} )}