Component cleanup

This commit is contained in:
adilallo
2026-04-29 07:20:16 -06:00
parent 252848eba9
commit e6127f1a3f
267 changed files with 2087 additions and 2196 deletions
@@ -0,0 +1,40 @@
"use client";
import { memo } from "react";
import { useSchemaData } from "../../../hooks";
import CardStepsView from "./CardSteps.view";
import type { CardStepsProps } from "./CardSteps.types";
/**
* Figma: "Community Rule System" → Sections → SectionCardSteps ([17434:19695](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=17434-19695)).
* Composes **`cards/Step`** (Figma Card / Step), not **`progress/Stepper`**.
*/
const CardStepsContainer = memo<CardStepsProps>(
({ title, subtitle, steps, headingDesktopLines }) => {
const schemaData = useSchemaData({
type: "HowTo",
name: title,
description: subtitle,
steps: steps.map((item) => ({
name: item.text,
text: item.text,
})),
});
const schemaJson = JSON.stringify(schemaData);
return (
<CardStepsView
title={title}
subtitle={subtitle}
steps={steps}
headingDesktopLines={headingDesktopLines}
schemaJson={schemaJson}
/>
);
},
);
CardStepsContainer.displayName = "CardSteps";
export default CardStepsContainer;
@@ -0,0 +1,18 @@
/** One row in the section grid; rendered with `cards/Step`. */
export interface CardStepsItem {
text: string;
iconShape?: string;
iconColor?: string;
}
export interface CardStepsProps {
title: string;
subtitle: string;
steps: CardStepsItem[];
/** Large-screen heading split: line 13 (e.g. How / CommunityRule / helps). */
headingDesktopLines?: readonly [string, string, string];
}
export interface CardStepsViewProps extends CardStepsProps {
schemaJson: string;
}
@@ -0,0 +1,61 @@
"use client";
import { useTranslation } from "../../../contexts/MessagesContext";
import SectionHeader from "../../type/SectionHeader";
import Step from "../../cards/Step";
import Button from "../../buttons/Button";
import type { CardStepsViewProps } from "./CardSteps.types";
function CardStepsView({
title,
subtitle,
steps,
headingDesktopLines,
schemaJson,
}: CardStepsViewProps) {
const t = useTranslation();
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: schemaJson }}
/>
<section className="bg-transparent py-[var(--spacing-scale-032)] px-[var(--spacing-scale-020)] sm:py-[var(--spacing-scale-048)] sm:px-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)] lg:px-[var(--spacing-scale-064)] xl:py-[var(--spacing-scale-076)] xl:px-[var(--spacing-scale-064)]">
<div className="max-w-[var(--spacing-measures-max-width-lg)] mx-auto">
<div className="grid grid-cols-1 gap-y-[var(--spacing-scale-032)] sm:gap-y-[var(--spacing-scale-048)] lg:gap-y-[var(--spacing-scale-056)]">
<div>
<SectionHeader
variant="multi-line"
title={title}
subtitle={subtitle}
titleLg={t("cardSteps.titleLg")}
stackedDesktopLines={headingDesktopLines}
/>
</div>
<div className="grid grid-cols-1 gap-y-[var(--spacing-scale-024)] lg:grid-cols-3 lg:gap-[var(--spacing-scale-024)]">
{steps.map((item, index) => (
<Step
key={index}
number={index + 1}
text={item.text}
iconShape={item.iconShape}
iconColor={item.iconColor}
/>
))}
</div>
<div className="text-center">
<Button buttonType="outline" palette="default" size="large">
{t("cardSteps.buttons.seeHowItWorks")}
</Button>
</div>
</div>
</div>
</section>
</>
);
}
export default CardStepsView;
@@ -0,0 +1,2 @@
export { default } from "./CardSteps.container";
export * from "./CardSteps.types";
@@ -1,16 +0,0 @@
export interface CommunityRuleDocumentEntry {
title: string;
body: string;
}
export interface CommunityRuleDocumentSection {
categoryName: string;
entries: CommunityRuleDocumentEntry[];
}
export interface CommunityRuleDocumentProps {
sections: CommunityRuleDocumentSection[];
className?: string;
/** When true, wrap in white background with left teal bar (small breakpoint). */
useCardStyle?: boolean;
}
@@ -1,65 +0,0 @@
"use client";
import { memo } from "react";
import type { CommunityRuleDocumentProps } from "./CommunityRuleDocument.types";
const SECTION_GAP = "var(--measures-spacing-1200, 64px)";
const TEAL_BG = "var(--color-teal-teal50, #c9fef9)";
const SECTION_LINE_COLOR = "var(--color-border-default-tertiary, #464646)";
function CommunityRuleDocumentView({
sections,
className = "",
useCardStyle = false,
}: CommunityRuleDocumentProps) {
const rootClass = useCardStyle
? `rounded-[12px] bg-white pl-3 border-l-4 ${className}`
: className;
const rootStyle = useCardStyle ? { borderLeftColor: TEAL_BG } : undefined;
const sectionLineStyle = useCardStyle
? undefined
: { borderLeftColor: SECTION_LINE_COLOR };
return (
<div
className={`flex flex-col min-w-0 ${rootClass}`}
style={{ gap: SECTION_GAP, ...rootStyle }}
>
{sections.map((section, sectionIndex) => (
<div
key={sectionIndex}
className={`flex flex-col min-w-0 ${!useCardStyle ? "border-l pl-3" : ""}`}
style={sectionLineStyle}
>
{/* Section content: line runs full height of this block via border-left */}
<div className="flex flex-1 flex-col gap-4 min-w-0">
<p className="font-inter font-medium text-[16px] leading-[20px] text-[var(--color-content-invert-secondary,#1f1f1f)] shrink-0">
{section.categoryName}
</p>
<div className="flex flex-col min-w-0" style={{ gap: "24px" }}>
{section.entries.map((entry, entryIndex) => (
<div
key={entryIndex}
className="flex flex-col min-w-0"
style={{ gap: "6px" }}
>
<p className="font-inter font-bold text-[20px] leading-[28px] text-[var(--color-content-invert-primary)] shrink-0">
{entry.title}
</p>
<p className="font-inter font-normal text-[14px] leading-[20px] text-[var(--color-content-invert-primary)] shrink-0">
{entry.body}
</p>
</div>
))}
</div>
</div>
</div>
))}
</div>
);
}
CommunityRuleDocumentView.displayName = "CommunityRuleDocumentView";
export default memo(CommunityRuleDocumentView);
@@ -1,2 +0,0 @@
export { default } from "./CommunityRuleDocument.view";
export type { CommunityRuleDocumentProps } from "./CommunityRuleDocument.types";
@@ -2,7 +2,7 @@
import { useTranslation } from "../../../contexts/MessagesContext";
import ContentLockup from "../../type/ContentLockup";
import MiniCard from "../../cards/MiniCard";
import Mini from "../../cards/Mini";
import type { FeatureGridViewProps } from "./FeatureGrid.types";
function FeatureGridView({
@@ -37,10 +37,10 @@ function FeatureGridView({
/>
</div>
{/* MiniCard Grid */}
{/* Mini grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-[var(--spacing-scale-012)] mt-[var(--spacing-scale-048)] lg:mt-0 lg:flex-grow lg:shrink-0">
{features.map((feature, index) => (
<MiniCard
<Mini
key={index}
backgroundColor={feature.backgroundColor}
labelLine1={feature.labelLine1}
@@ -3,7 +3,7 @@
import { useState, useEffect } from "react";
import Image from "next/image";
import { useMediaQuery } from "../../../hooks/useMediaQuery";
import RuleCard from "../../cards/RuleCard";
import Rule from "../../cards/Rule";
import { getAssetPath } from "../../../../lib/assetUtils";
import type { GovernanceTemplateCatalogEntry } from "../../../../lib/templates/governanceTemplateCatalog";
@@ -53,7 +53,7 @@ export function GovernanceTemplateGrid({
`}
>
{entries.map((card) => (
<RuleCard
<Rule
key={card.slug}
title={card.title}
description={card.description}
@@ -1,35 +0,0 @@
"use client";
import { memo } from "react";
import { useSchemaData } from "../../../hooks";
import NumberedCardsView from "./NumberedCards.view";
import type { NumberedCardsProps } from "./NumberedCards.types";
const NumberedCardsContainer = memo<NumberedCardsProps>(
({ title, subtitle, cards }) => {
const schemaData = useSchemaData({
type: "HowTo",
name: title,
description: subtitle,
steps: cards.map((card) => ({
name: card.text,
text: card.text,
})),
});
const schemaJson = JSON.stringify(schemaData);
return (
<NumberedCardsView
title={title}
subtitle={subtitle}
cards={cards}
schemaJson={schemaJson}
/>
);
},
);
NumberedCardsContainer.displayName = "NumberedCards";
export default NumberedCardsContainer;
@@ -1,15 +0,0 @@
export interface Card {
text: string;
iconShape?: string;
iconColor?: string;
}
export interface NumberedCardsProps {
title: string;
subtitle: string;
cards: Card[];
}
export interface NumberedCardsViewProps extends NumberedCardsProps {
schemaJson: string;
}
@@ -1,70 +0,0 @@
"use client";
import { useTranslation } from "../../../contexts/MessagesContext";
import SectionHeader from "../SectionHeader";
import NumberCard from "../../cards/NumberCard";
import Button from "../../buttons/Button";
import type { NumberedCardsViewProps } from "./NumberedCards.types";
function NumberedCardsView({
title,
subtitle,
cards,
schemaJson,
}: NumberedCardsViewProps) {
const t = useTranslation();
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: schemaJson }}
/>
<section className="bg-transparent py-[var(--spacing-scale-032)] px-[var(--spacing-scale-020)] sm:py-[var(--spacing-scale-048)] sm:px-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)] lg:px-[var(--spacing-scale-064)] xl:py-[var(--spacing-scale-076)] xl:px-[var(--spacing-scale-064)]">
<div className="max-w-[var(--spacing-measures-max-width-lg)] mx-auto">
<div className="grid grid-cols-1 gap-y-[var(--spacing-scale-032)] lg:gap-y-[var(--spacing-scale-056)]">
{/* Section Header */}
<div>
<SectionHeader
title={title}
subtitle={subtitle}
titleLg={t("numberedCards.titleLg")}
/>
</div>
{/* Cards Container */}
<div className="grid grid-cols-1 gap-y-[var(--spacing-scale-024)] lg:grid-cols-3 lg:gap-[var(--spacing-scale-024)]">
{cards.map((card, index) => (
<NumberCard
key={index}
number={index + 1}
text={card.text}
iconShape={card.iconShape}
iconColor={card.iconColor}
/>
))}
</div>
{/* Call to Action Button */}
<div className="text-center sm:text-left lg:text-center">
{/* Filled button for xsm and sm breakpoints */}
<div className="block lg:hidden">
<Button buttonType="filled" palette="default" size="large">
{t("numberedCards.buttons.createCommunityRule")}
</Button>
</div>
{/* Outline button for lg and xlg breakpoints */}
<div className="hidden lg:block">
<Button buttonType="outline" palette="default" size="large">
{t("numberedCards.buttons.seeHowItWorks")}
</Button>
</div>
</div>
</div>
</div>
</section>
</>
);
}
export default NumberedCardsView;
@@ -1,2 +0,0 @@
export { default } from "./NumberedCards.container";
export * from "./NumberedCards.types";
@@ -1,7 +1,7 @@
"use client";
import { useTranslation } from "../../../contexts/MessagesContext";
import SectionHeader from "../SectionHeader";
import SectionHeader from "../../type/SectionHeader";
import Button from "../../buttons/Button";
import { GovernanceTemplateGrid } from "../GovernanceTemplateGrid";
import { GovernanceTemplateGridSkeleton } from "../GovernanceTemplateGrid/GovernanceTemplateGridSkeleton";
-70
View File
@@ -1,70 +0,0 @@
"use client";
import { memo } from "react";
export type SectionHeaderVariantValue = "default" | "multi-line";
interface SectionHeaderProps {
title: string;
subtitle: string;
titleLg?: string;
variant?: SectionHeaderVariantValue;
}
const SectionHeader = memo<SectionHeaderProps>(
({ title, subtitle, titleLg, variant: variantProp = "default" }) => {
const variant = variantProp;
return (
<div
className={
variant === "multi-line"
? "flex flex-col gap-[var(--spacing-scale-004)] w-full lg:flex-row lg:justify-between lg:items-start xl:gap-[var(--spacing-scale-024)]"
: "flex flex-col gap-[var(--spacing-scale-004)] w-full lg:flex-row lg:justify-between lg:items-start xl:gap-[var(--spacing-scale-024)]"
}
>
{/* Title Container - Left side (lg breakpoint) */}
<div
className={
variant === "multi-line"
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[50%] xl:h-[156px] xl:flex xl:items-center"
: "lg:w-[369px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[452px] xl:h-[156px] xl:flex xl:items-center"
}
>
<h2
className={
variant === "multi-line"
? "font-bricolage-grotesque font-bold text-[28px] leading-[36px] md:font-bold md:text-[32px] md:leading-[40px] lg:w-[410px] lg:text-left xl:text-[40px] xl:leading-[52px] text-[var(--color-content-default-primary)]"
: "font-bricolage-grotesque font-bold text-[28px] leading-[36px] sm:text-[32px] sm:leading-[40px] lg:text-[32px] lg:leading-[40px] lg:w-[369px] lg:pr-[var(--spacing-scale-096)] xl:text-[40px] xl:leading-[52px] xl:w-[452px] xl:pr-[var(--spacing-scale-096)] text-[var(--color-content-default-primary)]"
}
>
<span className="block lg:hidden">{title}</span>
<span className="hidden lg:block">{titleLg || title}</span>
</h2>
</div>
{/* Subtitle Container */}
<div
className={
variant === "multi-line"
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end lg:ml-[var(--spacing-scale-016)] xl:ml-[0px] xl:w-[50%] xl:h-[156px] xl:flex xl:items-center xl:justify-end"
: "lg:w-[928px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end xl:h-[156px] xl:flex xl:items-center xl:justify-end"
}
>
<p
className={
variant === "multi-line"
? "font-inter font-normal text-[14px] leading-[20px] md:font-normal md:text-[18px] md:leading-[130%] xl:text-[24px] xl:leading-[32px] text-[var(--color-content-default-tertiary)]"
: "font-inter font-normal text-[18px] leading-[130%] sm:text-[18px] sm:leading-[32px] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] xl:text-right text-[#484848] sm:text-[var(--color-content-default-tertiary)] lg:text-[var(--color-content-default-tertiary)] xl:text-[var(--color-content-default-tertiary)] tracking-[0px]"
}
>
{subtitle}
</p>
</div>
</div>
);
},
);
SectionHeader.displayName = "SectionHeader";
export default SectionHeader;
@@ -1,163 +0,0 @@
"use client";
import { memo, useEffect, useState } from "react";
import { useMessages } from "../../../contexts/MessagesContext";
import { logger } from "../../../../lib/logger";
import WebVitalsDashboardView from "./WebVitalsDashboard.view";
import type { Metrics, Vitals, VitalData } from "./WebVitalsDashboard.types";
const createInitialVital = (): VitalData => ({
value: 0,
rating: "unknown",
});
const createInitialVitals = (): Vitals => ({
lcp: createInitialVital(),
fid: createInitialVital(),
cls: createInitialVital(),
fcp: createInitialVital(),
ttfb: createInitialVital(),
});
function reportWebVitalToApi(
metric: keyof Vitals,
value: number,
rating: VitalData["rating"],
): void {
if (typeof window === "undefined") return;
if (rating === "unknown") return;
const body = {
metric,
data: { value, rating },
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
};
void fetch("/api/web-vitals", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).catch((err: unknown) => {
logger.error("Web vitals ingest failed:", err);
});
}
const WebVitalsDashboardContainer = memo(() => {
const m = useMessages();
const copy = m.webVitalsDashboard;
const [vitals, setVitals] = useState<Vitals>(createInitialVitals);
const [metrics, setMetrics] = useState<Metrics>({});
const [loading, setLoading] = useState(true);
const [storage, setStorage] = useState<"external" | "local">("local");
const rumDashboardUrl =
typeof process.env.NEXT_PUBLIC_RUM_DASHBOARD_URL === "string" &&
process.env.NEXT_PUBLIC_RUM_DASHBOARD_URL.trim() !== ""
? process.env.NEXT_PUBLIC_RUM_DASHBOARD_URL.trim()
: null;
useEffect(() => {
const fetchVitals = async () => {
try {
const response = await fetch("/api/web-vitals");
const data = (await response.json()) as {
metrics?: Metrics;
storage?: "external" | "local";
};
setMetrics(data.metrics || {});
setStorage(data.storage === "external" ? "external" : "local");
} catch (error) {
logger.error("Error fetching web vitals:", error);
} finally {
setLoading(false);
}
};
fetchVitals();
if (typeof window !== "undefined") {
// web-vitals v4+ exposes onLCP / onCLS / … — legacy getLCP was removed.
import("web-vitals").then(
({ onCLS, onFID, onFCP, onLCP, onTTFB }) => {
onLCP((metric) => {
const rating = metric.rating as VitalData["rating"];
setVitals((prev) => ({
...prev,
lcp: {
value: Math.round(metric.value),
rating,
},
}));
reportWebVitalToApi("lcp", Math.round(metric.value), rating);
});
onFID((metric) => {
const rating = metric.rating as VitalData["rating"];
setVitals((prev) => ({
...prev,
fid: {
value: Math.round(metric.value),
rating,
},
}));
reportWebVitalToApi("fid", Math.round(metric.value), rating);
});
onCLS((metric) => {
const rounded = Math.round(metric.value * 1000) / 1000;
const rating = metric.rating as VitalData["rating"];
setVitals((prev) => ({
...prev,
cls: {
value: rounded,
rating,
},
}));
reportWebVitalToApi("cls", rounded, rating);
});
onFCP((metric) => {
const rating = metric.rating as VitalData["rating"];
setVitals((prev) => ({
...prev,
fcp: {
value: Math.round(metric.value),
rating,
},
}));
reportWebVitalToApi("fcp", Math.round(metric.value), rating);
});
onTTFB((metric) => {
const rating = metric.rating as VitalData["rating"];
setVitals((prev) => ({
...prev,
ttfb: {
value: Math.round(metric.value),
rating,
},
}));
reportWebVitalToApi("ttfb", Math.round(metric.value), rating);
});
},
);
}
}, []);
return (
<WebVitalsDashboardView
vitals={vitals}
metrics={metrics}
loading={loading}
storage={storage}
copy={copy}
rumDashboardUrl={rumDashboardUrl}
/>
);
});
WebVitalsDashboardContainer.displayName = "WebVitalsDashboard";
export default WebVitalsDashboardContainer;
@@ -1,40 +0,0 @@
import type messages from "../../../../messages/en/index";
export interface VitalData {
value: number;
rating: "good" | "needs-improvement" | "poor" | "unknown";
}
export interface Vitals {
lcp: VitalData;
fid: VitalData;
cls: VitalData;
fcp: VitalData;
ttfb: VitalData;
}
export interface MetricData {
count: number;
average: number;
min: number;
max: number;
goodCount: number;
needsImprovementCount: number;
poorCount: number;
lastUpdated?: string;
}
export interface Metrics {
[key: string]: MetricData;
}
export type WebVitalsDashboardCopy = typeof messages.webVitalsDashboard;
export interface WebVitalsDashboardViewProps {
vitals: Vitals;
metrics: Metrics;
loading: boolean;
storage: "external" | "local";
copy: WebVitalsDashboardCopy;
rumDashboardUrl: string | null;
}
@@ -1,169 +0,0 @@
import type { WebVitalsDashboardViewProps } from "./WebVitalsDashboard.types";
const getRatingColor = (rating: string): string => {
switch (rating) {
case "good":
return "text-green-600 bg-green-50";
case "needs-improvement":
return "text-yellow-600 bg-yellow-50";
case "poor":
return "text-red-600 bg-red-50";
default:
return "text-gray-600 bg-gray-50";
}
};
const getRatingIcon = (rating: string): string => {
switch (rating) {
case "good":
return "✅";
case "needs-improvement":
return "⚠️";
case "poor":
return "❌";
default:
return "❓";
}
};
function formatValue(metric: string, value: number): string {
if (metric === "cls") {
return value.toFixed(3);
}
return `${value}ms`;
}
function WebVitalsDashboardView({
vitals,
metrics,
loading,
storage,
copy,
rumDashboardUrl,
}: WebVitalsDashboardViewProps) {
if (loading) {
return (
<div className="p-6 bg-white rounded-lg shadow-lg">
<div className="animate-pulse">
<div className="h-6 bg-gray-200 rounded w-1/3 mb-4"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="p-4 border rounded-lg">
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-3/4"></div>
</div>
))}
</div>
</div>
</div>
);
}
return (
<div className="p-6 bg-white rounded-lg shadow-lg">
<h2 className="text-2xl font-bold mb-6 text-[var(--color-content-default-primary)]">
{copy.title}
</h2>
{storage === "external" && (
<div
className="mb-6 p-4 rounded-lg border border-[var(--color-border-default-primary)] bg-[var(--color-surface-default-secondary)] text-[var(--font-size-body-medium)] text-[var(--color-content-default-secondary)]"
role="status"
>
<p className="font-semibold text-[var(--color-content-default-primary)] mb-2">
{copy.externalNoticeTitle}
</p>
<p className="mb-3">{copy.externalNoticeBody}</p>
{rumDashboardUrl ? (
<a
href={rumDashboardUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--color-content-default-primary)] underline font-medium"
>
{copy.externalDashboardLinkLabel}
</a>
) : null}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
{Object.entries(vitals).map(([metric, data]) => (
<div
key={metric}
className={`p-4 border rounded-lg ${getRatingColor(data.rating)}`}
>
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-lg">{metric.toUpperCase()}</h3>
<span className="text-2xl">{getRatingIcon(data.rating)}</span>
</div>
<div className="text-sm">
<div className="font-medium">
{copy.valueLabel}: {formatValue(metric, data.value)}
</div>
<div className="capitalize">
{copy.ratingLabel}: {data.rating.replace("-", " ")}
</div>
</div>
</div>
))}
</div>
{Object.keys(metrics).length > 0 && (
<div className="mb-6">
<h3 className="text-lg font-semibold mb-4 text-[var(--color-content-default-primary)]">
{copy.historicalMetricsTitle}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(metrics).map(([metric, data]) => (
<div
key={metric}
className="p-4 border rounded-lg bg-[var(--color-surface-default-secondary)]"
>
<h4 className="font-semibold mb-2">{metric.toUpperCase()}</h4>
<div className="text-sm space-y-1">
<div>
{copy.countLabel}: {data.count}
</div>
<div>
{copy.averageLabel}: {formatValue(metric, data.average)}
</div>
<div>
{copy.rangeLabel}: {formatValue(metric, data.min)} -{" "}
{formatValue(metric, data.max)}
</div>
<div className="flex gap-2 text-xs">
<span className="text-green-600">
{copy.goodLabel}: {data.goodCount}
</span>
<span className="text-yellow-600">
{copy.needsImprovementLabel}: {data.needsImprovementCount}
</span>
<span className="text-red-600">
{copy.poorLabel}: {data.poorCount}
</span>
</div>
</div>
</div>
))}
</div>
</div>
)}
<div className="p-4 bg-[var(--color-surface-default-secondary)] rounded-lg">
<h3 className="font-semibold mb-2 text-[var(--color-content-default-primary)]">
{copy.performanceGuidelinesTitle}
</h3>
<ul className="text-sm space-y-1 text-[var(--color-content-default-secondary)]">
<li> {copy.guidelines.lcp}</li>
<li> {copy.guidelines.fid}</li>
<li> {copy.guidelines.cls}</li>
<li> {copy.guidelines.fcp}</li>
<li> {copy.guidelines.ttfb}</li>
</ul>
</div>
</div>
);
}
export default WebVitalsDashboardView;
@@ -1,2 +0,0 @@
export { default } from "./WebVitalsDashboard.container";
export * from "./WebVitalsDashboard.types";