import { serializePB, resetGachaGameAndFormConfig, useEditor, } from '@tempi/core-editor'; import { HEADER, Page as PageSetting, ROOT, WebsiteMenu, } from '@tempi/core-renderer'; import { DesignMode, DeviceEnum, encodePageToFile } from '@tempi/editor-site'; import debounce from 'lodash.debounce'; import { Website } from 'modules/website'; import { useEffect, useState } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; import { pageApi } from 'services/page'; import { buildTree, FlattenedItem, flattenTree, TreeItem, TreeItems, } from '../utils'; import { useWebsiteDetail } from './useWebsiteDetail'; export interface UseWebsiteMenuProps { autoAddHeader?: boolean; onGet: (id: number) => Promise; onUpdate: ( body: Partial< Partial< Omit & { domainId?: number; } > >, ) => Promise; } export const useWebsiteMenu = ({ autoAddHeader, onGet, onUpdate, }: UseWebsiteMenuProps) => { const [loading, setLoading] = useState(false); const { id } = useParams<{ id: string }>(); const [searchParams, setSearchParams] = useSearchParams(); const pageId = +searchParams.get('pageId'); const { website, mutateWebsite, mutatePage } = useWebsiteDetail({ revalidateWebsiteOnMount: false, revalidatePageOnMount: false, onGet, }); const [openModalSettingPage, setOpenModalSettingPage] = useState(false); const [currMenuModify, setCurrMenuModify] = useState(); const [currMenuType, setCurrMenuType] = useState<'LINK' | 'PAGE'>(); const [defaultActiveTab, setDefaultActiveTab] = useState< 'GENERAL' | 'SEO' | 'SOCIAL_IMAGE' | 'PASSWORD' >(); const [searchText, setSearchText] = useState(''); const [searchResult, setSearchResult] = useState([]); const { draftPageContent, query, actions } = useEditor((_, query) => ({ draftPageContent: serializePB(JSON.parse(query.serialize()), { isEditingBlock: false, isEditingPopup: false, }), })); const onOpenSettingModal = ( pageType: 'PAGE' | 'LINK', defaultActiveTab?: 'GENERAL' | 'SEO' | 'PASSWORD', data?: any, ) => { setCurrMenuType(pageType); setCurrMenuModify(data); setDefaultActiveTab(defaultActiveTab || 'GENERAL'); setOpenModalSettingPage(true); }; const [items, setItems] = useState([]); useEffect(() => { const oldCollapse = new Map( flattenTree(items).map(({ id, collapsed }) => [id, collapsed]), ); setItems( buildTree( website?.menu?.map((menuItem) => { const { id, page, name, parentId, ...rest } = menuItem; const oldCollapseState = oldCollapse.get(id); return { ...rest, title: name, id: id, children: [], isHomepage: page?.isHomepage, slug: page?.slug, isActive: rest?.linkPageId === pageId, parentId: parentId || null, collapsed: oldCollapseState ?? true, }; }) || [], ), ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(website?.menu), pageId]); useEffect(() => { if (searchText) { const searchResult = flattenTree(items).filter( (item) => item?.slug?.toLowerCase().includes(searchText?.toLowerCase()) || item?.title?.toLowerCase().includes(searchText?.toLowerCase()), ); setSearchResult(searchResult); } else { setSearchResult([]); } }, [searchText, items]); const onUpdateWebsiteMenus = async ( websiteMenus: WebsiteMenu[], pageUpdate?: WebsiteMenu['page'], ) => { setLoading(true); try { if (pageUpdate) await pageApi.updatePage(pageUpdate.id, pageUpdate); await onUpdate({ id: +id, menu: websiteMenus, }); pageUpdate?.id === pageId && mutatePage(); await mutateWebsite(); } finally { setLoading(false); } }; const onDeleteWebsiteMenu = async ( websiteMenus: WebsiteMenu[], idPageDelete: number, ) => { setLoading(true); try { !!idPageDelete && (await pageApi.deletePage(idPageDelete)); await onUpdate({ id: +id, menu: websiteMenus, }); if (idPageDelete === +pageId) { setSearchParams((prev) => { prev.delete('pageId'); return prev; }); } mutateWebsite(); } finally { setLoading(false); } }; const handleAutoAddHeader = async () => { if (!autoAddHeader) return; const isWebsiteContainGlobalHeader = Object.keys(draftPageContent).some( (key) => key === HEADER, ); const isWebsiteSinglePage = (website?.menu || []).filter((menu) => menu.linkType === 'PAGE') .length === 1; if (!isWebsiteContainGlobalHeader && isWebsiteSinglePage) { const { WebsiteHeaderEditor, DEFAULT_WEBSITE_HEADER } = await import( '@tempi/core-elements' ); const newNodeTree = query .parseReactElement() .toNodeTree((node) => { node.id = HEADER; }); actions.history.throttle().addNodeTree(newNodeTree, ROOT, 0); } }; const onCreateWebsiteMenu = async ( websiteMenuCreate: WebsiteMenu, pageDuplicateId?: number, cb?: (id: number) => void, ) => { setLoading(true); const currentWebsiteMenus = convertWebsiteMenu([...flattenTree(items)]); let menusUpdate = currentWebsiteMenus ? [...currentWebsiteMenus] : []; try { const { page, ...menu } = websiteMenuCreate; if (menu.linkType === 'PAGE') { let pageData = { ...page }; if (pageDuplicateId) { const pageDuplicate = await pageApi .getPage(pageDuplicateId, { device: DeviceEnum.DESKTOP, }) .then((res) => res?.data?.page); const { createdAt, createdBy, id, updatedAt, updatedBy, ...rest } = pageDuplicate; resetGachaGameAndFormConfig(rest.content); pageData = { ...rest, ...page }; } const data = await pageApi.createPage({ ...pageData, designMode: DesignMode.RESPONSIVE, name: menu.name, websiteId: +id, }); // handle add header handleAutoAddHeader(); menu.linkPageId = data.id; } menusUpdate = [...menusUpdate, menu]; await onUpdate({ id: +id, menu: menusUpdate, }); mutateWebsite(); cb?.(menu.linkPageId); } catch (err) { menusUpdate = currentWebsiteMenus || []; } finally { setLoading(false); return menusUpdate; } }; const convertWebsiteMenu = (websiteMenus: FlattenedItem[]): WebsiteMenu[] => websiteMenus.map((item) => { const { slug, isActive, children, collapsed, title, isHomepage, index, depth, parentId, ...rest } = item; return { name: title, parentId: parentId?.toString(), ...rest }; }); const handleDeleteTreeNode = (nodeId: string) => { let menuUpdate = [...flattenTree(items)]; const indexOfNode = menuUpdate.findIndex((f) => f.id === nodeId); const menuItemDeleted = menuUpdate.splice(indexOfNode, 1)?.[0]; menuUpdate = menuUpdate.map((item) => { if (item.parentId === menuItemDeleted.id) { return { ...item, parentId: menuItemDeleted.parentId }; } return { ...item }; }); onDeleteWebsiteMenu( convertWebsiteMenu(menuUpdate), menuItemDeleted.linkPageId, ); }; const handleClickMenuItem = (pageId: number) => { !!pageId && setSearchParams((prev) => ({ ...prev, pageId, })); }; const handleUpdateMenuItem = ( updateData: TreeItem & { page?: PageSetting; }, ) => { const { page, ...rest } = updateData; const menuUpdate = [...flattenTree(items)]; const indexOfMenuUpdate = menuUpdate.findIndex( (f) => f.id === updateData.id, ); if (indexOfMenuUpdate >= 0) menuUpdate[indexOfMenuUpdate] = { ...menuUpdate[indexOfMenuUpdate], ...(rest || {}), }; onUpdateWebsiteMenus(convertWebsiteMenu(menuUpdate), page); }; const debounceUpdateMenu = debounce(async (websiteMenus: FlattenedItem[]) => { try { setLoading(true); await onUpdate({ id: +id, menu: convertWebsiteMenu(websiteMenus), }); } finally { mutateWebsite(); setLoading(false); } }, 200); const handleExportPage = async (item: TreeItem) => { const pageId = item.linkPageId; const result = await pageApi.getPage(pageId).then((res) => res?.data?.page); const { name, content, mobileContent, designMode = DesignMode.RESPONSIVE, } = result; if (!content && !mobileContent) return; // remove lucky draw and form config. resetGachaGameAndFormConfig(content); encodePageToFile(name, { content, mobileContent, designMode }); }; return { loading, onCreateWebsiteMenu, items, handleDeleteTreeNode, handleClickMenuItem, handleUpdateMenuItem, handleExportPage, openModalSettingPage, setOpenModalSettingPage, currMenuModify, currMenuType, defaultActiveTab, convertWebsiteMenu, onOpenSettingModal, setItems, debounceUpdateMenu, searchText, setSearchText, searchResult, website, }; };