import * as React from 'react'; import { Component, Dispatch, ErrorInfo, useState, useEffect, useRef, useCallback, ReactNode, SetStateAction, createContext, useContext, FC, } from 'react'; import { Config, IData, IMedia, IWidgetEvents } from '../types'; import { Debug } from '../debug'; export const DEFAULT_CONFIG: Config = { classPrefix: 'adl-wdgt', shopThisLookText: 'SHOP THE LOOK', moreLikeThisText: 'More like this', }; export interface StoreState { exportState: (getState: () => StoreState, setter: StateSetter) => void; post: IMedia | null; data: IData; forceMobile: boolean; isMobile: boolean; sliderPosition: number; root: Element | null; config: Config; triggerEvent: IWidgetEvents['triggerEvent']; hasSubscribed: IWidgetEvents['hasSubscribed']; setStoreState: Dispatch>; canSlideLeft: boolean; canSlideRight: boolean; currentPosition: number; widgetWidth: number; productImageWidths: Record; setProductImageWidth: (productId: string, width: number) => void; getProductImageWidth: (productId: string) => number; unlockedShouldersForCarouselPostId: string | null; productListUniformMode: boolean; // In product list, when nbItems < content size, all items should be the same size } export interface StoreProps { state: Partial; children: ReactNode; } export const initial: StoreState = { exportState: () => {}, post: null, data: {} as IData, forceMobile: false, isMobile: false, root: null, sliderPosition: 0, config: DEFAULT_CONFIG, triggerEvent: () => {}, hasSubscribed: () => false, setStoreState: () => {}, canSlideLeft: false, canSlideRight: true, currentPosition: 0, widgetWidth: 0, // will be set by the Widget component productImageWidths: {}, setProductImageWidth: () => {}, getProductImageWidth: () => 0, unlockedShouldersForCarouselPostId: null, productListUniformMode: false, }; export const StoreContext = createContext(initial); export type StateSetter = Dispatch>; export const useStore = () => useContext(StoreContext); interface ErrorBoundaryProps { children: ReactNode; } interface ErrorBoundaryState { hasError: boolean; } /** * React error boundary scoped to the widget component tree. */ class ErrorBoundary extends Component { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(): ErrorBoundaryState { return { hasError: true }; } componentDidCatch(error: Error, info: ErrorInfo): void { Debug.error(error, info.componentStack); } render(): ReactNode { if (this.state.hasError) { return null; } return this.props.children; } } export const StoreProvider: FC = ({ state, children }) => { const [storeState, setStoreState] = useState(() => { // Create initial state first const initialState = { ...initial, ...state, }; // Then add setter function return { ...initialState, setStoreState: (newState: SetStateAction) => { setStoreState( typeof newState === 'function' ? newState : (current) => ({ ...current, ...newState }), ); }, }; }); const productImageWidthsRef = useRef>(new Map()); // Create product image width management functions that reference current state const setProductImageWidth = useCallback((productId: string, width: number) => { productImageWidthsRef.current.set(productId, width); }, []); const getProductImageWidth = (productId: string) => { return productImageWidthsRef.current.get(productId) || 0; }; // Update store state with the functions const contextValue = { ...storeState, setProductImageWidth, getProductImageWidth, }; // Move effects after state initialization useEffect(() => { if (state.exportState) { state.exportState(() => contextValue, setStoreState); } }, [state.exportState, contextValue]); useEffect(() => { setStoreState((current) => ({ ...current, ...state, })); }, [state]); return ( {children} ); }; // Hook for components to consume store export const useStoreComponent =

(props: P): [P, StoreState, StateSetter] => { const store = useStore(); return [props, store, store.setStoreState]; };