Update local testing script and resolve errors

This commit is contained in:
adilallo
2026-01-29 20:57:39 -07:00
parent ca42982dea
commit adac7d0545
46 changed files with 209 additions and 136 deletions
@@ -78,9 +78,7 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
buttonHref, buttonHref,
}, },
onContactClick as onContactClick as
| (( | ((_data: Record<string, unknown>) => void)
_data: Record<string, unknown>,
) => void)
| undefined, | undefined,
); );
@@ -111,4 +109,3 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
AskOrganizerContainer.displayName = "AskOrganizer"; AskOrganizerContainer.displayName = "AskOrganizer";
export default AskOrganizerContainer; export default AskOrganizerContainer;
@@ -37,7 +37,6 @@ export interface AskOrganizerViewProps {
variant: AskOrganizerVariant; variant: AskOrganizerVariant;
labelledBy?: string; labelledBy?: string;
onContactClick: ( onContactClick: (
event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>, _event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
) => void; ) => void;
} }
@@ -53,4 +53,3 @@ function AskOrganizerView({
} }
export default AskOrganizerView; export default AskOrganizerView;
-1
View File
@@ -1,3 +1,2 @@
export { default } from "./AskOrganizer.container"; export { default } from "./AskOrganizer.container";
export * from "./AskOrganizer.types"; export * from "./AskOrganizer.types";
+2 -2
View File
@@ -34,6 +34,6 @@ export interface CheckboxViewProps {
checkGlyphColor: string; checkGlyphColor: string;
labelColor: string; labelColor: string;
accessibilityProps: React.HTMLAttributes<HTMLSpanElement>; accessibilityProps: React.HTMLAttributes<HTMLSpanElement>;
onToggle: (e: React.MouseEvent | React.KeyboardEvent) => void; onToggle: (_e: React.MouseEvent | React.KeyboardEvent) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLSpanElement>) => void; onKeyDown: (_e: React.KeyboardEvent<HTMLSpanElement>) => void;
} }
@@ -38,9 +38,7 @@ function ContentContainerView({
<h3 className={titleClasses}>{post.frontmatter.title}</h3> <h3 className={titleClasses}>{post.frontmatter.title}</h3>
{/* Description */} {/* Description */}
<p className={descriptionClasses}> <p className={descriptionClasses}>{post.frontmatter.description}</p>
{post.frontmatter.description}
</p>
</div> </div>
</div> </div>
@@ -63,9 +63,7 @@ function ContentLockupView({
</div> </div>
{/* Subtitle */} {/* Subtitle */}
{subtitle ? ( {subtitle ? <h2 className={styles.subtitle}>{subtitle}</h2> : null}
<h2 className={styles.subtitle}>{subtitle}</h2>
) : null}
</div> </div>
{/* Description */} {/* Description */}
@@ -94,11 +92,7 @@ function ContentLockupView({
</div> </div>
{/* Large button for md and lg breakpoints */} {/* Large button for md and lg breakpoints */}
<div className="hidden md:block xl:hidden"> <div className="hidden md:block xl:hidden">
<Button <Button variant="primary" size="large" className={buttonClassName}>
variant="primary"
size="large"
className={buttonClassName}
>
{ctaText} {ctaText}
</Button> </Button>
</div> </div>
@@ -1,5 +1,4 @@
export interface ContextMenuItemProps export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode; children?: React.ReactNode;
selected?: boolean; selected?: boolean;
hasSubmenu?: boolean; hasSubmenu?: boolean;
@@ -18,6 +17,6 @@ export interface ContextMenuItemViewProps {
disabled: boolean; disabled: boolean;
className: string; className: string;
itemClasses: string; itemClasses: string;
handleClick: (e: React.MouseEvent<HTMLDivElement>) => void; handleClick: (_e: React.MouseEvent<HTMLDivElement>) => void;
handleKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void; handleKeyDown: (_e: React.KeyboardEvent<HTMLDivElement>) => void;
} }
@@ -61,4 +61,3 @@ const FeatureGridContainer = memo<FeatureGridProps>(
FeatureGridContainer.displayName = "FeatureGrid"; FeatureGridContainer.displayName = "FeatureGrid";
export default FeatureGridContainer; export default FeatureGridContainer;
@@ -17,4 +17,3 @@ export interface FeatureGridViewProps extends FeatureGridProps {
features: Feature[]; features: Feature[];
labelledBy?: string; labelledBy?: string;
} }
@@ -50,4 +50,3 @@ function FeatureGridView({
} }
export default FeatureGridView; export default FeatureGridView;
-1
View File
@@ -1,3 +1,2 @@
export { default } from "./FeatureGrid.container"; export { default } from "./FeatureGrid.container";
export * from "./FeatureGrid.types"; export * from "./FeatureGrid.types";
+7 -7
View File
@@ -31,15 +31,15 @@ export interface HeaderViewProps {
| "footerLg"; | "footerLg";
showText: boolean; showText: boolean;
}>; }>;
renderNavigationItems: (size: NavSize) => React.ReactNode; renderNavigationItems: (_size: NavSize) => React.ReactNode;
renderLoginButton: (size: NavSize) => React.ReactNode; renderLoginButton: (_size: NavSize) => React.ReactNode;
renderCreateRuleButton: ( renderCreateRuleButton: (
buttonSize: "xsmall" | "small" | "medium" | "large" | "xlarge", _buttonSize: "xsmall" | "small" | "medium" | "large" | "xlarge",
containerSize: "small" | "medium" | "large" | "xlarge", _containerSize: "small" | "medium" | "large" | "xlarge",
avatarSize: "small" | "medium" | "large" | "xlarge", _avatarSize: "small" | "medium" | "large" | "xlarge",
) => React.ReactNode; ) => React.ReactNode;
renderLogo: ( renderLogo: (
size: _size:
| "default" | "default"
| "homeHeaderXsmall" | "homeHeaderXsmall"
| "homeHeaderSm" | "homeHeaderSm"
@@ -52,7 +52,7 @@ export interface HeaderViewProps {
| "headerXl" | "headerXl"
| "footer" | "footer"
| "footerLg", | "footerLg",
showText: boolean, _showText: boolean,
) => React.ReactNode; ) => React.ReactNode;
} }
@@ -35,15 +35,15 @@ export interface HomeHeaderViewProps {
| "footerLg"; | "footerLg";
showText: boolean; showText: boolean;
}>; }>;
renderNavigationItems: (size: NavSize) => React.ReactNode; renderNavigationItems: (_size: NavSize) => React.ReactNode;
renderLoginButton: (size: NavSize) => React.ReactNode; renderLoginButton: (_size: NavSize) => React.ReactNode;
renderCreateRuleButton: ( renderCreateRuleButton: (
buttonSize: "xsmall" | "small" | "medium" | "large" | "xlarge", _buttonSize: "xsmall" | "small" | "medium" | "large" | "xlarge",
containerSize: "small" | "medium" | "large" | "xlarge", _containerSize: "small" | "medium" | "large" | "xlarge",
avatarSize: "small" | "medium" | "large" | "xlarge", _avatarSize: "small" | "medium" | "large" | "xlarge",
) => React.ReactNode; ) => React.ReactNode;
renderLogo: ( renderLogo: (
size: _size:
| "default" | "default"
| "homeHeaderXsmall" | "homeHeaderXsmall"
| "homeHeaderSm" | "homeHeaderSm"
@@ -56,6 +56,6 @@ export interface HomeHeaderViewProps {
| "headerXl" | "headerXl"
| "footer" | "footer"
| "footerLg", | "footerLg",
showText: boolean, _showText: boolean,
) => React.ReactNode; ) => React.ReactNode;
} }
+7 -5
View File
@@ -1,5 +1,7 @@
export interface InputProps export interface InputProps extends Omit<
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "onChange" | "onFocus" | "onBlur"> { React.InputHTMLAttributes<HTMLInputElement>,
"size" | "onChange" | "onFocus" | "onBlur"
> {
size?: "small" | "medium" | "large"; size?: "small" | "medium" | "large";
labelVariant?: "default" | "horizontal"; labelVariant?: "default" | "horizontal";
state?: "default" | "active" | "hover" | "focus"; state?: "default" | "active" | "hover" | "focus";
@@ -32,7 +34,7 @@ export interface InputViewProps {
labelClasses: string; labelClasses: string;
inputClasses: string; inputClasses: string;
borderRadius: string; borderRadius: string;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void; handleChange: (_e: React.ChangeEvent<HTMLInputElement>) => void;
handleFocus: (e: React.FocusEvent<HTMLInputElement>) => void; handleFocus: (_e: React.FocusEvent<HTMLInputElement>) => void;
handleBlur: (e: React.FocusEvent<HTMLInputElement>) => void; handleBlur: (_e: React.FocusEvent<HTMLInputElement>) => void;
} }
@@ -1,5 +1,4 @@
export interface MenuBarItemProps export interface MenuBarItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href?: string; href?: string;
children?: React.ReactNode; children?: React.ReactNode;
variant?: "default" | "home"; variant?: "default" | "home";
@@ -1,5 +1,7 @@
export interface NavigationItemProps export interface NavigationItemProps extends Omit<
extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "isActive"> { React.AnchorHTMLAttributes<HTMLAnchorElement>,
"isActive"
> {
href?: string; href?: string;
children?: React.ReactNode; children?: React.ReactNode;
variant?: "default"; variant?: "default";
@@ -33,4 +33,3 @@ const NumberedCardsContainer = memo<NumberedCardsProps>(
NumberedCardsContainer.displayName = "NumberedCards"; NumberedCardsContainer.displayName = "NumberedCards";
export default NumberedCardsContainer; export default NumberedCardsContainer;
@@ -13,4 +13,3 @@ export interface NumberedCardsProps {
export interface NumberedCardsViewProps extends NumberedCardsProps { export interface NumberedCardsViewProps extends NumberedCardsProps {
schemaJson: string; schemaJson: string;
} }
@@ -63,4 +63,3 @@ function NumberedCardsView({
} }
export default NumberedCardsView; export default NumberedCardsView;
-1
View File
@@ -1,3 +1,2 @@
export { default } from "./NumberedCards.container"; export { default } from "./NumberedCards.container";
export * from "./NumberedCards.types"; export * from "./NumberedCards.types";
@@ -41,5 +41,5 @@ export interface QuoteBlockViewProps {
imageLoading: boolean; imageLoading: boolean;
currentAvatarSrc: string; currentAvatarSrc: string;
onImageLoad: () => void; onImageLoad: () => void;
onImageError: (error: unknown) => void; onImageError: (_error: unknown) => void;
} }
@@ -30,6 +30,6 @@ export interface RadioButtonViewProps {
backgroundWhenChecked: string; backgroundWhenChecked: string;
dotColor: string; dotColor: string;
labelColor: string; labelColor: string;
onToggle: (e: React.MouseEvent | React.KeyboardEvent) => void; onToggle: (_e: React.MouseEvent | React.KeyboardEvent) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLSpanElement>) => void; onKeyDown: (_e: React.KeyboardEvent<HTMLSpanElement>) => void;
} }
@@ -25,5 +25,5 @@ export interface RadioGroupViewProps {
options: RadioOption[]; options: RadioOption[];
className: string; className: string;
ariaLabel?: string; ariaLabel?: string;
onOptionChange: (optionValue: string) => void; onOptionChange: (_optionValue: string) => void;
} }
@@ -11,6 +11,6 @@ export interface RelatedArticlesViewProps {
slugOrder: string[]; slugOrder: string[];
isMobile: boolean; isMobile: boolean;
transformStyle: React.CSSProperties; transformStyle: React.CSSProperties;
getProgressStyle: (index: number) => React.CSSProperties; getProgressStyle: (_index: number) => React.CSSProperties;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void; onMouseDown?: (_e: React.MouseEvent<HTMLDivElement>) => void;
} }
+1 -1
View File
@@ -14,5 +14,5 @@ export interface RuleCardViewProps {
backgroundColor: string; backgroundColor: string;
className: string; className: string;
onClick: () => void; onClick: () => void;
onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void; onKeyDown: (_event: React.KeyboardEvent<HTMLDivElement>) => void;
} }
@@ -36,7 +36,12 @@ const RuleStackContainer = memo<RuleStackProps>(({ className = "" }) => {
logger.debug(`${templateName} template clicked`); logger.debug(`${templateName} template clicked`);
}; };
return <RuleStackView className={className} onTemplateClick={handleTemplateClick} />; return (
<RuleStackView
className={className}
onTemplateClick={handleTemplateClick}
/>
);
}); });
RuleStackContainer.displayName = "RuleStack"; RuleStackContainer.displayName = "RuleStack";
+1 -1
View File
@@ -4,5 +4,5 @@ export interface RuleStackProps {
export interface RuleStackViewProps { export interface RuleStackViewProps {
className: string; className: string;
onTemplateClick: (templateName: string) => void; onTemplateClick: (_templateName: string) => void;
} }
+2 -2
View File
@@ -47,9 +47,10 @@ const SelectContainer = forwardRef<HTMLButtonElement, SelectProps>(
// Sync internal state with external value prop // Sync internal state with external value prop
useEffect(() => { useEffect(() => {
if (value !== undefined) { if (value !== undefined && value !== selectedValue) {
setSelectedValue(value); setSelectedValue(value);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]); }, [value]);
useImperativeHandle( useImperativeHandle(
@@ -276,7 +277,6 @@ const SelectContainer = forwardRef<HTMLButtonElement, SelectProps>(
labelVariant={labelVariant} labelVariant={labelVariant}
className={className} className={className}
options={options} options={options}
children={children}
selectId={selectId} selectId={selectId}
labelId={labelId} labelId={labelId}
isOpen={isOpen} isOpen={isOpen}
+6 -8
View File
@@ -26,8 +26,8 @@ export interface SelectViewProps {
chevronClasses: string; chevronClasses: string;
// Callbacks // Callbacks
onButtonClick: () => void; onButtonClick: () => void;
onButtonKeyDown: (e: React.KeyboardEvent<HTMLButtonElement>) => void; onButtonKeyDown: (_e: React.KeyboardEvent<HTMLButtonElement>) => void;
onOptionClick: (value: string, text: string) => void; onOptionClick: (_value: string, _text: string) => void;
// Refs // Refs
selectRef: React.RefObject<HTMLButtonElement>; selectRef: React.RefObject<HTMLButtonElement>;
menuRef: React.RefObject<HTMLDivElement>; menuRef: React.RefObject<HTMLDivElement>;
@@ -38,11 +38,11 @@ export interface SelectViewProps {
export function SelectView({ export function SelectView({
label, label,
placeholder, placeholder: _placeholder,
size, size,
disabled, disabled,
error, error: _error,
labelVariant, labelVariant: _labelVariant,
options, options,
children, children,
selectId, selectId,
@@ -118,9 +118,7 @@ export function SelectView({
key={option.value} key={option.value}
selected={option.value === selectedValue} selected={option.value === selectedValue}
size={size} size={size}
onClick={() => onClick={() => onOptionClick(option.value, option.label)}
onOptionClick(option.value, option.label)
}
> >
{option.label} {option.label}
</SelectOption> </SelectOption>
@@ -15,6 +15,6 @@ export interface SelectOptionViewProps {
disabled: boolean; disabled: boolean;
className: string; className: string;
itemClasses: string; itemClasses: string;
handleClick: (e: React.MouseEvent<HTMLDivElement>) => void; handleClick: (_e: React.MouseEvent<HTMLDivElement>) => void;
handleKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void; handleKeyDown: (_e: React.KeyboardEvent<HTMLDivElement>) => void;
} }
@@ -1,7 +1,10 @@
import { forwardRef } from "react"; import { forwardRef } from "react";
import type { SelectOptionViewProps } from "./SelectOption.types"; import type { SelectOptionViewProps } from "./SelectOption.types";
export const SelectOptionView = forwardRef<HTMLDivElement, SelectOptionViewProps>( export const SelectOptionView = forwardRef<
HTMLDivElement,
SelectOptionViewProps
>(
( (
{ {
children, children,
+8 -6
View File
@@ -1,5 +1,7 @@
export interface SwitchProps export interface SwitchProps extends Omit<
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> { React.ButtonHTMLAttributes<HTMLButtonElement>,
"onChange"
> {
checked?: boolean; checked?: boolean;
onChange?: ( onChange?: (
_e: _e:
@@ -23,8 +25,8 @@ export interface SwitchViewProps {
trackClasses: string; trackClasses: string;
thumbClasses: string; thumbClasses: string;
labelClasses: string; labelClasses: string;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void; onClick: (_e: React.MouseEvent<HTMLButtonElement>) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLButtonElement>) => void; onKeyDown: (_e: React.KeyboardEvent<HTMLButtonElement>) => void;
onFocus: (e: React.FocusEvent<HTMLButtonElement>) => void; onFocus: (_e: React.FocusEvent<HTMLButtonElement>) => void;
onBlur: (e: React.FocusEvent<HTMLButtonElement>) => void; onBlur: (_e: React.FocusEvent<HTMLButtonElement>) => void;
} }
+7 -5
View File
@@ -1,5 +1,7 @@
export interface TextAreaProps export interface TextAreaProps extends Omit<
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size" | "onChange" | "onFocus" | "onBlur"> { React.TextareaHTMLAttributes<HTMLTextAreaElement>,
"size" | "onChange" | "onFocus" | "onBlur"
> {
size?: "small" | "medium" | "large"; size?: "small" | "medium" | "large";
labelVariant?: "default" | "horizontal"; labelVariant?: "default" | "horizontal";
state?: "default" | "active" | "hover" | "focus"; state?: "default" | "active" | "hover" | "focus";
@@ -33,7 +35,7 @@ export interface TextAreaViewProps {
labelClasses: string; labelClasses: string;
textareaClasses: string; textareaClasses: string;
borderRadius: string; borderRadius: string;
handleChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void; handleChange: (_e: React.ChangeEvent<HTMLTextAreaElement>) => void;
handleFocus: (e: React.FocusEvent<HTMLTextAreaElement>) => void; handleFocus: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
handleBlur: (e: React.FocusEvent<HTMLTextAreaElement>) => void; handleBlur: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
} }
+1 -1
View File
@@ -11,7 +11,7 @@ export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
value, value,
name, name,
disabled, disabled,
className, className: _className,
rows, rows,
containerClasses, containerClasses,
labelClasses, labelClasses,
+8 -6
View File
@@ -1,5 +1,7 @@
export interface ToggleProps export interface ToggleProps extends Omit<
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> { React.ButtonHTMLAttributes<HTMLButtonElement>,
"onChange"
> {
label?: string; label?: string;
checked?: boolean; checked?: boolean;
onChange?: ( onChange?: (
@@ -34,11 +36,11 @@ export interface ToggleViewProps {
labelClasses: string; labelClasses: string;
toggleClasses: string; toggleClasses: string;
onClick: ( onClick: (
e: _e:
| React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>, | React.KeyboardEvent<HTMLButtonElement>,
) => void; ) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLButtonElement>) => void; onKeyDown: (_e: React.KeyboardEvent<HTMLButtonElement>) => void;
onFocus: (e: React.FocusEvent<HTMLButtonElement>) => void; onFocus: (_e: React.FocusEvent<HTMLButtonElement>) => void;
onBlur: (e: React.FocusEvent<HTMLButtonElement>) => void; onBlur: (_e: React.FocusEvent<HTMLButtonElement>) => void;
} }
@@ -119,7 +119,6 @@ const ToggleGroupContainer = memo(
return ( return (
<ToggleGroupView <ToggleGroupView
groupId={groupId} groupId={groupId}
children={children}
className={className} className={className}
position={position} position={position}
state={state} state={state}
@@ -131,7 +130,9 @@ const ToggleGroupContainer = memo(
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
{...rest} {...rest}
/> >
{children}
</ToggleGroupView>
); );
}), }),
); );
@@ -1,5 +1,7 @@
export interface ToggleGroupProps export interface ToggleGroupProps extends Omit<
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> { React.ButtonHTMLAttributes<HTMLButtonElement>,
"onChange"
> {
children?: React.ReactNode; children?: React.ReactNode;
className?: string; className?: string;
position?: "left" | "middle" | "right"; position?: "left" | "middle" | "right";
@@ -24,8 +26,8 @@ export interface ToggleGroupViewProps {
showText: boolean; showText: boolean;
ariaLabel?: string; ariaLabel?: string;
toggleClasses: string; toggleClasses: string;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void; onClick: (_e: React.MouseEvent<HTMLButtonElement>) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLButtonElement>) => void; onKeyDown: (_e: React.KeyboardEvent<HTMLButtonElement>) => void;
onFocus: (e: React.FocusEvent<HTMLButtonElement>) => void; onFocus: (_e: React.FocusEvent<HTMLButtonElement>) => void;
onBlur: (e: React.FocusEvent<HTMLButtonElement>) => void; onBlur: (_e: React.FocusEvent<HTMLButtonElement>) => void;
} }
@@ -3,9 +3,9 @@ import type { ToggleGroupViewProps } from "./ToggleGroup.types";
export function ToggleGroupView({ export function ToggleGroupView({
groupId, groupId,
children, children,
className, className: _className,
position, position: _position,
state, state: _state,
showText, showText,
ariaLabel, ariaLabel,
toggleClasses, toggleClasses,
@@ -96,11 +96,14 @@ const WebVitalsDashboardContainer = memo(() => {
}, []); }, []);
return ( return (
<WebVitalsDashboardView vitals={vitals} metrics={metrics} loading={loading} /> <WebVitalsDashboardView
vitals={vitals}
metrics={metrics}
loading={loading}
/>
); );
}); });
WebVitalsDashboardContainer.displayName = "WebVitalsDashboard"; WebVitalsDashboardContainer.displayName = "WebVitalsDashboard";
export default WebVitalsDashboardContainer; export default WebVitalsDashboardContainer;
@@ -31,4 +31,3 @@ export interface WebVitalsDashboardViewProps {
metrics: Metrics; metrics: Metrics;
loading: boolean; loading: boolean;
} }
@@ -111,9 +111,7 @@ function WebVitalsDashboardView({
<span className="text-yellow-600"> <span className="text-yellow-600">
Needs Improvement: {data.needsImprovementCount} Needs Improvement: {data.needsImprovementCount}
</span> </span>
<span className="text-red-600"> <span className="text-red-600">Poor: {data.poorCount}</span>
Poor: {data.poorCount}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -155,4 +153,3 @@ function WebVitalsDashboardView({
} }
export default WebVitalsDashboardView; export default WebVitalsDashboardView;
@@ -1,3 +1,2 @@
export { default } from "./WebVitalsDashboard.container"; export { default } from "./WebVitalsDashboard.container";
export * from "./WebVitalsDashboard.types"; export * from "./WebVitalsDashboard.types";
+16 -13
View File
@@ -33,27 +33,30 @@ export function useMediaQuery(
query: string | keyof typeof BREAKPOINTS, query: string | keyof typeof BREAKPOINTS,
direction: "min" | "max" = "min", direction: "min" | "max" = "min",
): boolean { ): boolean {
const [matches, setMatches] = useState(false); // Convert breakpoint key to media query string
let mediaQuery: string;
if (query in BREAKPOINTS) {
const breakpoint = BREAKPOINTS[query as keyof typeof BREAKPOINTS];
mediaQuery = `(${direction}-width: ${breakpoint}px)`;
} else {
mediaQuery = query;
}
// Initialize state with current match if available (SSR safety)
const [matches, setMatches] = useState(() => {
if (typeof window === "undefined") {
return false;
}
return window.matchMedia(mediaQuery).matches;
});
useEffect(() => { useEffect(() => {
// Convert breakpoint key to media query string
let mediaQuery: string;
if (query in BREAKPOINTS) {
const breakpoint = BREAKPOINTS[query as keyof typeof BREAKPOINTS];
mediaQuery = `(${direction}-width: ${breakpoint}px)`;
} else {
mediaQuery = query;
}
// Check if window is available (SSR safety) // Check if window is available (SSR safety)
if (typeof window === "undefined") { if (typeof window === "undefined") {
return; return;
} }
const media = window.matchMedia(mediaQuery); const media = window.matchMedia(mediaQuery);
// Initialize matches synchronously - this is safe for media queries
// eslint-disable-next-line react-hooks/rules-of-hooks
setMatches(media.matches);
// Create listener for changes // Create listener for changes
const listener = (event: MediaQueryListEvent) => { const listener = (event: MediaQueryListEvent) => {
+25 -7
View File
@@ -16,6 +16,7 @@ The Container/Presentation pattern separates component logic from presentation,
### When to Use ### When to Use
Use this pattern for components that have: Use this pattern for components that have:
- Business logic or state management - Business logic or state management
- Data fetching or API calls - Data fetching or API calls
- Analytics tracking - Analytics tracking
@@ -40,6 +41,7 @@ app/components/[ComponentName]/
### File Responsibilities ### File Responsibilities
#### `index.tsx` #### `index.tsx`
- Exports the container component as the default export - Exports the container component as the default export
- Optionally exports types for external use - Optionally exports types for external use
- Maintains backward compatibility with existing import paths - Maintains backward compatibility with existing import paths
@@ -50,7 +52,9 @@ export type { AskOrganizerProps } from "./AskOrganizer.types";
``` ```
#### `[ComponentName].container.tsx` #### `[ComponentName].container.tsx`
**Contains all logic:** **Contains all logic:**
- React hooks (`useState`, `useEffect`, custom hooks) - React hooks (`useState`, `useEffect`, custom hooks)
- Event handlers and business logic - Event handlers and business logic
- Data fetching and API calls - Data fetching and API calls
@@ -60,6 +64,7 @@ export type { AskOrganizerProps } from "./AskOrganizer.types";
- Side effects - Side effects
**Should NOT contain:** **Should NOT contain:**
- JSX layout details (beyond composing the view) - JSX layout details (beyond composing the view)
- Inline styles or complex className logic (pass as props) - Inline styles or complex className logic (pass as props)
- Direct DOM manipulation - Direct DOM manipulation
@@ -74,7 +79,7 @@ import type { AskOrganizerProps } from "./AskOrganizer.types";
function AskOrganizerContainer(props: AskOrganizerProps) { function AskOrganizerContainer(props: AskOrganizerProps) {
const { trackEvent } = useAnalytics(); const { trackEvent } = useAnalytics();
const handleContactClick = () => { const handleContactClick = () => {
trackEvent({ trackEvent({
event: "contact_button_click", event: "contact_button_click",
@@ -83,10 +88,10 @@ function AskOrganizerContainer(props: AskOrganizerProps) {
}); });
// ... additional logic // ... additional logic
}; };
// Compute derived props // Compute derived props
const variantStyles = computeVariantStyles(props.variant); const variantStyles = computeVariantStyles(props.variant);
return ( return (
<AskOrganizerView <AskOrganizerView
{...props} {...props}
@@ -100,13 +105,16 @@ export default memo(AskOrganizerContainer);
``` ```
#### `[ComponentName].view.tsx` #### `[ComponentName].view.tsx`
**Pure presentation:** **Pure presentation:**
- Receives all data via props - Receives all data via props
- Renders JSX based on props - Renders JSX based on props
- No hooks, no state, no side effects - No hooks, no state, no side effects
- Only imports other presentational components - Only imports other presentational components
**Should NOT contain:** **Should NOT contain:**
- `useState`, `useEffect`, or any hooks - `useState`, `useEffect`, or any hooks
- Event handler implementations (receive as callbacks) - Event handler implementations (receive as callbacks)
- Data fetching or API calls - Data fetching or API calls
@@ -148,6 +156,7 @@ export function AskOrganizerView({
``` ```
#### `[ComponentName].types.ts` #### `[ComponentName].types.ts`
- Shared TypeScript interfaces and types - Shared TypeScript interfaces and types
- Public props interface (used by consumers) - Public props interface (used by consumers)
- Internal view props (used between container and view) - Internal view props (used between container and view)
@@ -177,6 +186,7 @@ export interface AskOrganizerViewProps extends AskOrganizerProps {
### Container Components ### Container Components
✅ **DO:** ✅ **DO:**
- Use React hooks (`useState`, `useEffect`, custom hooks) - Use React hooks (`useState`, `useEffect`, custom hooks)
- Handle all event handlers and business logic - Handle all event handlers and business logic
- Fetch data and manage loading states - Fetch data and manage loading states
@@ -185,6 +195,7 @@ export interface AskOrganizerViewProps extends AskOrganizerProps {
- Compose the view component with computed props - Compose the view component with computed props
❌ **DON'T:** ❌ **DON'T:**
- Include complex JSX layout (delegate to view) - Include complex JSX layout (delegate to view)
- Mix presentation logic with business logic - Mix presentation logic with business logic
- Access DOM directly (use refs when necessary) - Access DOM directly (use refs when necessary)
@@ -192,6 +203,7 @@ export interface AskOrganizerViewProps extends AskOrganizerProps {
### View Components ### View Components
✅ **DO:** ✅ **DO:**
- Receive all data via props - Receive all data via props
- Render JSX based on props - Render JSX based on props
- Import only presentational components - Import only presentational components
@@ -199,6 +211,7 @@ export interface AskOrganizerViewProps extends AskOrganizerProps {
- Accept callback props for user interactions - Accept callback props for user interactions
❌ **DON'T:** ❌ **DON'T:**
- Use any React hooks - Use any React hooks
- Manage state or side effects - Manage state or side effects
- Fetch data or make API calls - Fetch data or make API calls
@@ -220,11 +233,11 @@ import Button from "./Button";
const AskOrganizer = memo(({ title, variant, ...props }) => { const AskOrganizer = memo(({ title, variant, ...props }) => {
const { trackEvent } = useAnalytics(); const { trackEvent } = useAnalytics();
const handleContactClick = () => { const handleContactClick = () => {
trackEvent({ event: "contact_click", component: "AskOrganizer" }); trackEvent({ event: "contact_click", component: "AskOrganizer" });
}; };
return ( return (
<section> <section>
<ContentLockup title={title} /> <ContentLockup title={title} />
@@ -237,6 +250,7 @@ const AskOrganizer = memo(({ title, variant, ...props }) => {
### After (Container/Presentation) ### After (Container/Presentation)
**AskOrganizer.container.tsx:** **AskOrganizer.container.tsx:**
```typescript ```typescript
"use client"; "use client";
@@ -247,11 +261,11 @@ import type { AskOrganizerProps } from "./AskOrganizer.types";
function AskOrganizerContainer(props: AskOrganizerProps) { function AskOrganizerContainer(props: AskOrganizerProps) {
const { trackEvent } = useAnalytics(); const { trackEvent } = useAnalytics();
const handleContactClick = () => { const handleContactClick = () => {
trackEvent({ event: "contact_click", component: "AskOrganizer" }); trackEvent({ event: "contact_click", component: "AskOrganizer" });
}; };
return <AskOrganizerView {...props} onContactClick={handleContactClick} />; return <AskOrganizerView {...props} onContactClick={handleContactClick} />;
} }
@@ -259,6 +273,7 @@ export default memo(AskOrganizerContainer);
``` ```
**AskOrganizer.view.tsx:** **AskOrganizer.view.tsx:**
```typescript ```typescript
import ContentLockup from "../ContentLockup"; import ContentLockup from "../ContentLockup";
import Button from "../Button"; import Button from "../Button";
@@ -346,6 +361,7 @@ These components serve as reference implementations for the pattern.
The following components are candidates for future conversion: The following components are candidates for future conversion:
### High Priority (Complex Logic) ### High Priority (Complex Logic)
- `Header` / `HomeHeader` - Navigation state, conditional rendering logic - `Header` / `HomeHeader` - Navigation state, conditional rendering logic
- `MenuBar` - Menu state management, keyboard navigation - `MenuBar` - Menu state management, keyboard navigation
- `ContextMenu` - Positioning logic, click outside handling - `ContextMenu` - Positioning logic, click outside handling
@@ -353,12 +369,14 @@ The following components are candidates for future conversion:
- `ToggleGroup` - Group state management - `ToggleGroup` - Group state management
### Medium Priority (Some Logic) ### Medium Priority (Some Logic)
- `ContentContainer` - Data fetching or transformation - `ContentContainer` - Data fetching or transformation
- `RelatedArticles` - Data fetching, filtering logic - `RelatedArticles` - Data fetching, filtering logic
- `RuleStack` - Complex rendering logic - `RuleStack` - Complex rendering logic
- `LogoWall` - Animation or interaction logic - `LogoWall` - Animation or interaction logic
### Low Priority (Mostly Presentational) ### Low Priority (Mostly Presentational)
- `Button`, `Avatar`, `Checkbox`, `Input`, `TextArea` - Simple presentational components - `Button`, `Avatar`, `Checkbox`, `Input`, `TextArea` - Simple presentational components
- `Separator`, `SectionHeader`, `SectionNumber` - Pure presentation - `Separator`, `SectionHeader`, `SectionNumber` - Pure presentation
- `QuoteBlock`, `QuoteDecor`, `HeroDecor` - Decorative components - `QuoteBlock`, `QuoteDecor`, `HeroDecor` - Decorative components
+60 -1
View File
@@ -3,9 +3,37 @@
# Local testing script - run before committing/merging # Local testing script - run before committing/merging
# Usage: ./scripts/test-local.sh # Usage: ./scripts/test-local.sh
set -euo pipefail # Exit on error, undefined vars, pipe failures
echo "🧪 Running local tests before commit..." echo "🧪 Running local tests before commit..."
echo "" echo ""
# Cleanup function to ensure servers are killed
cleanup() {
echo ""
echo "🧹 Cleaning up any running servers..."
# Kill any Next.js servers on common test ports
for port in 3000 3010; do
if lsof -ti:$port >/dev/null 2>&1; then
echo " Killing process on port $port..."
lsof -ti:$port | xargs kill -9 2>/dev/null || true
fi
done
# Kill any processes from PID files if they exist
for pidfile in .next/runner.pid .next/performance-server.pid; do
if [ -f "$pidfile" ]; then
PID=$(cat "$pidfile" 2>/dev/null || echo "")
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
echo " Killing server PID: $PID (from $pidfile)..."
kill -9 "$PID" 2>/dev/null || true
fi
rm -f "$pidfile"
fi
done
}
trap cleanup EXIT INT TERM
echo "🔍 Linting..." echo "🔍 Linting..."
npm run lint || exit 1 npm run lint || exit 1
@@ -23,11 +51,42 @@ npm run test:e2e || exit 1
echo "" echo ""
echo "🖼️ Visual regression tests..." echo "🖼️ Visual regression tests..."
# Visual tests use Playwright's webServer config, should auto-start server
npm run visual:test || exit 1 npm run visual:test || exit 1
echo "" echo ""
echo "⚡ Performance tests (Lighthouse CI)..." echo "⚡ Performance tests (Lighthouse CI)..."
npm run performance:budget || exit 1 # Performance tests need a server running on port 3010
# Check if server is already running, if not start one
if ! lsof -ti:3010 >/dev/null 2>&1; then
echo " Starting server for performance tests..."
npm run build || exit 1
PORT=3010 HOST=127.0.0.1 node node_modules/next/dist/bin/next start -p 3010 -H 127.0.0.1 > .next/performance-server.log 2>&1 &
SERVER_PID=$!
echo "$SERVER_PID" > .next/performance-server.pid
echo " Server started (PID: $SERVER_PID), waiting for readiness..."
npx wait-on -t 120000 "tcp:127.0.0.1:3010" || { echo "❌ Server failed to start"; kill $SERVER_PID 2>/dev/null || true; exit 1; }
echo " ✅ Server ready"
fi
# Run performance tests
npm run performance:budget || EXIT_CODE=$?
# Cleanup performance server if we started it
if [ -f .next/performance-server.pid ]; then
PID=$(cat .next/performance-server.pid 2>/dev/null || echo "")
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
echo " Stopping performance test server..."
kill "$PID" 2>/dev/null || true
sleep 2
kill -9 "$PID" 2>/dev/null || true
fi
rm -f .next/performance-server.pid
fi
if [ -n "${EXIT_CODE:-}" ]; then
exit $EXIT_CODE
fi
echo "" echo ""
echo "✅ All tests passed! Safe to commit/merge." echo "✅ All tests passed! Safe to commit/merge."