import React, { useEffect, useState } from 'react'; // Import Polotno and third-party libraries import { PolotnoContainer, WorkspaceWrap } from 'polotno'; import { Toolbar } from 'polotno/toolbar/toolbar'; import { ZoomButtons } from 'polotno/toolbar/zoom-buttons'; import { Workspace } from 'polotno/canvas/workspace'; import { setGoogleFonts } from 'polotno/config'; // Hooks import { useDispatch, useSelector } from 'react-redux'; import { useParams, useNavigate } from 'react-router-dom'; import { RootState } from '../../redux/reducers'; import { AppDispatch } from '../../redux/store'; import { StoreType } from 'polotno/model/store'; // Actions import { CLEAR_REDUX, GET_ONE_TEMPLATE, SET_BLOCK_COLOR, SET_CUSTOM_FIELDS, SET_CUSTOM_FIELDS_V2, SET_PLATFORM_FIELDS, SET_ROS_OFFER_PERCENTAGE, TEMPLATE_LOADING, } from '../../redux/actions/action-types'; import { failure } from '../../redux/actions/snackbarActions'; import { searchAndAdvanceChange, selectProduct, setPageCount, } from '../../redux/actions/templateActions'; import { clearQrFields } from '../../redux/actions/customQRCodeActions'; // Utils import { changeColorOfBoxesForSnapPack, drawRestrictedAreaOnPage, extractCustomRistrictedBoxColor, getFileAsBlob, } from '../../utils/template-builder'; import { addIdentifiersForTemplates } from '../../utils/templateIdentifierArea'; import { addElementsforRealPennedLetters } from '../../utils/templateRestrictedArea/realPenned'; import { DEFAULT_RESTRICTED_AREA_BG, DPI, allowedImageTypes, multiPageLetters } from '../../utils/constants'; import { addSafetyBordersForTemplates } from '../../utils/templateSafetyBorders'; import { MESSAGES } from '../../utils/message'; import { removeItem } from '../../utils/local-storage'; // @ts-ignore import fonts from '../../utils/fonts.json'; // @ts-ignore import LexiRegularFont from '../../assets/Fonts/Lexi-Regular.ttf'; // Components import TopNavigation from '../TopNavigation'; import SidePanel from '../SidePanel'; import Typography from '../GenericUIBlocks/Typography'; import GenericSnackbar from '../GenericUIBlocks/GenericSnackbar/Toast'; import './styles.scss'; /** * This code defines a React functional component called `TemplateBuilder` that is responsible for rendering a template builder interface. * It includes various useEffect hooks to handle component lifecycle events and state updates. * The component uses the `polotno` library to create a canvas workspace where users can design templates. * It also includes a side panel with different sections for adding and customizing elements on the canvas. * * @param {Object} props - The component props. * @param {Object} props.store - The store object passed as a prop to the `TemplateBuilder` component. * @param {Object} props.styles - The styles contain CSS Properties passed as a prop to the `TemplateBuilder` component. * @returns {JSX.Element} The rendered template builder interface. */ interface TemplateBuilderProps { store: StoreType; platformName?: string | null; templateGalleryModal?: boolean; createTemplateRoute?: string | null; olcTemplate?: Record; designerTemplateQuery?: Record | null; allowSenderFields?: boolean; allowPropertyFields?: boolean; allowAdditionalPage?: boolean; excludedFields?: string[] | null; designerQueryAmount?: string | number; allowedAddOns?: any; allowedTemplateSections?: any; propertyOfferCost?: number; customPropertyOfferCost?: number; gsvCost?: number; selectedTemplateType?: string | null; currentTheme?: string | null | undefined; onReturnAndNavigate?: () => void; onGetCustomFields?: () => Promise; onDuplicateTemplate?: (payload: any) => Promise; onCreateCustomTemplateQuery?: (payload: any) => Promise; onGetOneTemplate?: (payload: any) => Promise; onGetTemplates?: (payload: any) => Promise; onSubmit?: (payload: any) => Promise; onGetBrandingImages?: (payload: any) => Promise; onDeleteBrandingImage?: (id: string | number) => Promise; onUploadBrandingImage?: (payload: any) => Promise; onGetQRCodes?: (payload: any) => Promise; onDeleteQRCodes?: (id: string | number) => Promise; onUploadQRCode?: (payload: any) => Promise; onEditQRCode?: (payload: any) => Promise; } const TemplateBuilder: React.FC = ({ store, platformName, templateGalleryModal, createTemplateRoute, designerTemplateQuery, olcTemplate, allowSenderFields, excludedFields, designerQueryAmount, allowPropertyFields, allowedAddOns, allowedTemplateSections, allowAdditionalPage, propertyOfferCost, customPropertyOfferCost, gsvCost, selectedTemplateType, currentTheme, onCreateCustomTemplateQuery, onReturnAndNavigate, onGetOneTemplate, onGetCustomFields, onDuplicateTemplate, onGetTemplates, onSubmit, onGetBrandingImages, onDeleteBrandingImage, onUploadBrandingImage, onGetQRCodes, onDeleteQRCodes, onUploadQRCode, onEditQRCode, }) => { const [isStoreUpdated, setIsStoreUpdated] = useState(false); const [isDuplication, setIsDuplication] = useState(false); const [switchTabCount, setSwitchTabCount] = useState(1); const [selectedSection, setSelectedSection] = useState('text'); const { id } = useParams(); const dispatch: AppDispatch = useDispatch(); const navigate = useNavigate(); const template = useSelector( (state: RootState) => state.templates.template ) as Record; const product = useSelector( (state: RootState) => state.templates.product ) as Record; const envelopeType = useSelector( (state: RootState) => state.templates.envelopeType ); const allProducts = useSelector((state: RootState) => state.templates.products); const currentTemplateType = product?.productType; const currentProductId = +product?.id; const containerStyle = { width: '100vw', height: currentTheme === 'v2' ? 'calc(100vh - 106px)' : '90vh', position: 'relative', }; useEffect(() => { if (olcTemplate) { handleLoadTemplate(); } }, [olcTemplate]); useEffect(() => { if (designerTemplateQuery) { setTimeout(() => { handleLoadDesignerTemplate(); }, 100); } }, [designerTemplateQuery]); // Event listener for visibility change useEffect(() => { const handleVisibilityChange = () => { if (document.hidden) { setSwitchTabCount((prev) => prev + 1); } }; const handleClick = (event: MouseEvent) => { if ( event.target instanceof Element && event.target.closest('.polotno-side-tabs-container') && store?.openedSidePanel !== 'Templates' ) { setSelectedSection(store?.openedSidePanel); } }; document.addEventListener('click', handleClick); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener('click', handleClick); }; }, []); useEffect(() => { removeItem('hireDesignerFormState'); removeItem('queryFiles'); }, []) useEffect(() => { if (!product && !olcTemplate && !id && !designerTemplateQuery) { navigate(createTemplateRoute || '/create-template'); } }, []); // @ts-ignore useEffect(() => { if (product || (id && onGetOneTemplate)) { setGoogleFonts(fonts); if (id && onGetOneTemplate) { try { onGetOneTemplate(id).then((template) => { if (template) { dispatch({ type: GET_ONE_TEMPLATE, payload: { data: template } }); addSafetyBordersForTemplates(template?.product?.id, store); dispatch({ type: TEMPLATE_LOADING, payload: true }); } else { dispatch(failure('Unable to load the Template')); setTimeout(() => { navigate(createTemplateRoute || '/create-template'); }, 1000); } }); } catch (error) { return error; } finally { dispatch({ type: TEMPLATE_LOADING, payload: false }); } // @ts-ignore } else if (store.pages.length === 0 && !olcTemplate) { createInitialPage(); } fetchCustomFields(); const handleChange = () => { if (!isStoreUpdated) { setIsStoreUpdated(true); setIsDuplication(false); } }; const off = store.on('change', handleChange); return () => { store.history.clear(); store.clear(); dispatch(clearQrFields()); dispatch({ type: SET_ROS_OFFER_PERCENTAGE, payload: '' }); dispatch({ type: SET_BLOCK_COLOR, payload: DEFAULT_RESTRICTED_AREA_BG }); dispatch(setPageCount(store?.pages?.length)); off(); }; } }, []); useEffect(() => { if (!id && !olcTemplate && product && store.pages.length === 0) { createInitialPage(); } }, [product]); useEffect(() => { const handleBeforeUnload = (event: { returnValue: string }) => { const message = 'Are you sure you want to leave?'; event.returnValue = message; return message; }; const addBeforeUnloadListener = () => { window.removeEventListener('beforeunload', handleBeforeUnload); window.addEventListener('beforeunload', handleBeforeUnload); }; const removeBeforeUnloadListener = () => { window.removeEventListener('beforeunload', handleBeforeUnload); }; if (isStoreUpdated && !isDuplication) { addBeforeUnloadListener(); } return () => { removeBeforeUnloadListener(); }; }, [isStoreUpdated, isDuplication]); const fetchCustomFields = async () => { if (onGetCustomFields) { const allCustomFields: any = await onGetCustomFields(); const platformFields: any = []; const customFields: any = []; if (allCustomFields?.version === 'v2') { const flattenedFields = allCustomFields.customFields.flatMap((section: { fields: any; }) => section.fields); for (const field of flattenedFields) { (field.isPlatformField ? platformFields : customFields).push(field); } const filteredCustomFields = allCustomFields?.customFields .map((customField: any) => ({ ...customField, fields: customField.fields.filter((field: any) => customFields.includes(field)) })) .filter((section: { fields: any[] }) => section.fields.length > 0); if (allCustomFields?.customFields?.length) { dispatch({ type: SET_CUSTOM_FIELDS, payload: [] }); dispatch({ type: SET_CUSTOM_FIELDS_V2, payload: filteredCustomFields }); } else { dispatch({ type: SET_CUSTOM_FIELDS, payload: [] }); dispatch({ type: SET_CUSTOM_FIELDS_V2, payload: [] }); } if (platformFields.length) { dispatch({ type: SET_PLATFORM_FIELDS, payload: platformFields }); } else { dispatch({ type: SET_PLATFORM_FIELDS, payload: [] }); } } else { for (const field of allCustomFields) { (field.isPlatformField ? platformFields : customFields).push(field); } if (customFields.length) { dispatch({ type: SET_CUSTOM_FIELDS, payload: customFields }); } else { dispatch({ type: SET_CUSTOM_FIELDS, payload: [] }); } if (platformFields.length) { dispatch({ type: SET_PLATFORM_FIELDS, payload: platformFields }); } else { dispatch({ type: SET_PLATFORM_FIELDS, payload: [] }); } } } }; const createInitialPage = async () => { if (product) { const activePanel = selectedTemplateType === 'prebuilt' ? 'Templates' : 'text'; activePanel === 'text' && store.openSidePanel(activePanel); store.addPage(); const paperSize = product?.selectedSize?.split('x'); store.setUnit({ unit: 'in', dpi: DPI, }); store.setSize(+paperSize[1] * DPI, +paperSize[0] * DPI); if (multiPageLetters.includes(product.productType)) { store.addPage(); store.selectPage(store.pages[0].id); } //@ts-ignore drawRestrictedAreaOnPage(store, product, envelopeType); addIdentifiersForTemplates(product.id, store); if (currentTemplateType === 'Real Penned Letter' && +product?.id === 16) { handleRealPennedLetters(); } dispatch(clearQrFields()); dispatch({ type: SET_ROS_OFFER_PERCENTAGE, payload: '' }); dispatch({ type: SET_BLOCK_COLOR, payload: DEFAULT_RESTRICTED_AREA_BG }); dispatch(setPageCount(store?.pages?.length)); if (activePanel === 'Templates') { setTimeout(() => { store.openSidePanel(activePanel); }, 1500) } store.history.clear(); } setIsStoreUpdated(false); }; const handleRealPennedLetters = async () => { try { // Load Lexi Regular Fonts Into Store store.addFont({ fontFamily: 'lexi Regular', url: LexiRegularFont, }); addElementsforRealPennedLetters(store); const response = await fetch(LexiRegularFont); const blob = await response.blob(); const reader = new FileReader(); // Load Lexi Regular Base64 into JSON reader.onloadend = () => { store.addFont({ fontFamily: 'lexi Regular', url: reader.result, } as any); }; reader.readAsDataURL(blob); } catch (error) { console.error('Error loading the font file:', error); } }; const handleLoadDesignerTemplate = async () => { if (designerTemplateQuery?.title && designerTemplateQuery?.product) { const flatProducts = allProducts.flatMap(product => product.size.map((sizeItem: any) => ({ id: sizeItem.id || product.id, title: product.title, productType: product.productType, size: [{ id: sizeItem.id || product.id, size: sizeItem.size }], ...(product.windowed !== undefined && { windowed: product.windowed }) })) ); const selectedProduct = flatProducts.find((product) => product.id === designerTemplateQuery?.product?.id); if (selectedProduct) { dispatch({ type: CLEAR_REDUX }); dispatch({ type: SET_ROS_OFFER_PERCENTAGE, payload: '' }); dispatch({ type: SET_BLOCK_COLOR, payload: DEFAULT_RESTRICTED_AREA_BG }); dispatch(setPageCount(store?.pages?.length)); store.clear(); store.history.clear(); dispatch(searchAndAdvanceChange('title', designerTemplateQuery?.title)); if (selectedProduct.productType === 'Professional Letters') { let selectedEnvelope = selectedProduct.id == 2 ? '#10 Double-Window' : '#10 Grey' dispatch(searchAndAdvanceChange('envelopeType', selectedEnvelope)); } dispatch(selectProduct(selectedProduct)); return; } } onReturnAndNavigate ? onReturnAndNavigate() : navigate(createTemplateRoute || '/'); }; const handleLoadTemplate = async () => { const existingTemplate = olcTemplate; if (existingTemplate) { dispatch({ type: GET_ONE_TEMPLATE, payload: { data: existingTemplate } }); const workspaceElement = document.querySelector( '.polotno-workspace-container' ); if (workspaceElement) { workspaceElement.classList.add('show-loader'); } // @ts-ignore const paperDimensions = existingTemplate?.product?.paperSize.split('x'); store.setUnit({ unit: 'in', dpi: 96, }); store.setSize(+paperDimensions[1] * DPI, +paperDimensions[0] * DPI); let jsonData = await getFileAsBlob(existingTemplate?.templateUrl); if (existingTemplate?.product?.productType === 'Snap Pack Mailers') { const updatedBoxes = changeColorOfBoxesForSnapPack(jsonData, true, true); jsonData = updatedBoxes; } if (existingTemplate?.product?.productType === 'Real Penned Letter' && +existingTemplate?.product?.id === 16) { let clonedJson = JSON.stringify(jsonData) .replace(/{{/g, '((') .replace(/}}/g, '))'); jsonData = JSON.parse(clonedJson); } const RistrictedBoxColor = extractCustomRistrictedBoxColor(jsonData); dispatch({ type: SET_BLOCK_COLOR, payload: RistrictedBoxColor ? RistrictedBoxColor : DEFAULT_RESTRICTED_AREA_BG }); dispatch(setPageCount(store?.pages?.length)); store.loadJSON(jsonData); await store.waitLoading(); dispatch(clearQrFields()); if (allowedAddOns?.includes('custom_property_offer') && existingTemplate?.meta?.rosOfferPercentage) { dispatch({ type: SET_ROS_OFFER_PERCENTAGE, payload: existingTemplate?.meta?.rosOfferPercentage }); } else { dispatch({ type: SET_ROS_OFFER_PERCENTAGE, payload: '' }); } setIsStoreUpdated(false); addSafetyBordersForTemplates(existingTemplate?.product?.id, store); dispatch({ type: TEMPLATE_LOADING, payload: false }); if (workspaceElement) { workspaceElement.classList.add('hide-loader'); } } }; // Incase of Real Penned Letters hide tooltip const Tooltip = () => null; return ( <> {MESSAGES.TEMPLATE_MESSAGE_ON_SMALL_SCREEN}
{switchTabCount > 0 && (
{+product?.id !== 16 && ( )} {+product?.id !== 16 ? ( ) : ( )}
)}
); }; export default TemplateBuilder;