Update local testing script and resolve errors
This commit is contained in:
@@ -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,3 +1,2 @@
|
|||||||
export { default } from "./AskOrganizer.container";
|
export { default } from "./AskOrganizer.container";
|
||||||
export * from "./AskOrganizer.types";
|
export * from "./AskOrganizer.types";
|
||||||
|
|
||||||
|
|||||||
@@ -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,3 +1,2 @@
|
|||||||
export { default } from "./FeatureGrid.container";
|
export { default } from "./FeatureGrid.container";
|
||||||
export * from "./FeatureGrid.types";
|
export * from "./FeatureGrid.types";
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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) => {
|
||||||
|
|||||||
@@ -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
@@ -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."
|
||||||
|
|||||||
Reference in New Issue
Block a user