import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import { Box, BoxProps, ButtonGroup, CloseIcon, ConfirmModal, EditIcon, Flex, MetaButton, RepeatClockIcon, ResponsiveText, useBreakpointValue, useToast, } from '@metafam/ds'; import { Maybe } from '@metafam/utils'; import deepEquals from 'deep-equal'; import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { Layout, Layouts, Responsive, WidthProvider } from 'react-grid-layout'; import { BoxMetadata, BoxType, BoxTypes, createBoxKey, DisplayOutput, gridSX, LayoutData, } from 'utils/boxTypes'; import { addBoxToLayouts, disableAddBox, enableAddBox, GRID_ROW_HEIGHT, isSameLayouts, MULTIPLE_ALLOWED_BOXES, removeBoxFromLayouts, updatedLayouts, } from 'utils/layoutHelpers'; import { AddBoxSection } from '#components/Section/AddBoxSection'; import { GuildFragment, Player } from '#graphql/autogen/hasura-sdk'; import { useBoxHeights } from '#lib/hooks/useBoxHeights'; import { errorHandler } from '#utils/errorHandler'; const ResponsiveGridLayout = WidthProvider(Responsive); type Props = React.PropsWithChildren<{ player?: Player; guild?: GuildFragment; savedLayoutData: LayoutData; defaultLayoutData: LayoutData; persisting: boolean; persistLayoutData: (layoutData: LayoutData) => Promise; showEditButton: boolean; allBoxOptions: BoxType[]; ens?: string; displayComponent: DisplayOutput; }> & BoxProps; export const EditableGridLayout: React.FC = ({ children, player, guild, persisting, defaultLayoutData, savedLayoutData, showEditButton, persistLayoutData, allBoxOptions, displayComponent: DisplaySection, ens, ...props }) => { const [saving, setSaving] = useState(false); const [exitAlertCancel, setExitAlertCancel] = useState(false); const [exitAlertReset, setExitAlertReset] = useState(false); const [changed, setChanged] = useState(false); const [editing, setEditing] = useState(false); const itemsRef = useRef>>([]); const heights = useBoxHeights(itemsRef.current); const mobile = useBreakpointValue({ base: true, sm: false }); const toast = useToast(); const [currentLayoutData, setCurrentLayoutData] = useState(savedLayoutData); const { layoutItems: currentLayoutItems, layouts: currentLayouts } = currentLayoutData; useEffect(() => { itemsRef.current = itemsRef.current.slice(0, currentLayoutItems.length); }, [currentLayoutItems]); useEffect(() => { const layouts = updatedLayouts(currentLayouts, heights, editing); if (!deepEquals(layouts, currentLayouts)) { setCurrentLayoutData(({ layoutItems }) => ({ layouts, layoutItems, })); } }, [currentLayouts, heights, editing]); const handleReset = useCallback(() => { setCurrentLayoutData(enableAddBox(defaultLayoutData)); setChanged(true); setExitAlertReset(false); }, [defaultLayoutData]); const handleCancel = useCallback(() => { setCurrentLayoutData(savedLayoutData); setEditing(false); setExitAlertCancel(false); }, [savedLayoutData]); const isDefaultLayout = useMemo( () => isSameLayouts(defaultLayoutData, currentLayoutData), [currentLayoutData, defaultLayoutData], ); const toggleEditLayout = useCallback(async () => { try { let layoutData = defaultLayoutData; if (editing) { setSaving(true); layoutData = disableAddBox(currentLayoutData); await persistLayoutData(layoutData); } else { layoutData = enableAddBox(currentLayoutData); } setCurrentLayoutData(layoutData); setEditing((e) => !e); setChanged(false); } catch (err) { toast({ title: 'Error', description: `Unable to save layout. Error: ${(err as Error).message}`, status: 'error', isClosable: true, }); errorHandler(err as Error); } finally { setSaving(false); } }, [editing, currentLayoutData, persistLayoutData, defaultLayoutData, toast]); const handleLayoutChange = useCallback( (_items: Array, layouts: Layouts) => { const newData = { layouts, layoutItems: currentLayoutItems }; // automatic height adjustments dirty `changed` setChanged( (already) => already || (editing && !isSameLayouts(currentLayoutData, newData)), ); setCurrentLayoutData(newData); }, [currentLayoutData, currentLayoutItems, editing], ); const onRemoveBox = useCallback( (boxKey: string): void => { const layoutData = { layouts: removeBoxFromLayouts(currentLayouts, boxKey), layoutItems: currentLayoutItems.filter((item) => item.key !== boxKey), }; setCurrentLayoutData(layoutData); setChanged(true); }, [currentLayouts, currentLayoutItems], ); const onAddBox = useCallback( (type: BoxType, metadata: BoxMetadata): void => { const key = createBoxKey(type, metadata); if (currentLayoutItems.find((item) => item.key === key)) { return; } const layoutData = { layouts: addBoxToLayouts(currentLayouts, type, metadata), layoutItems: [...currentLayoutItems, { type, metadata, key }], }; setCurrentLayoutData(layoutData); setChanged(true); }, [currentLayouts, currentLayoutItems], ); const availableBoxes = useMemo( () => allBoxOptions.filter( (box) => !currentLayoutItems.map(({ type }) => type).includes(box) || MULTIPLE_ALLOWED_BOXES.includes(box), ), [currentLayoutItems, allBoxOptions], ); return ( {showEditButton && ( {children} {editing && !isDefaultLayout && ( setExitAlertReset(true)} leftIcon={mobile ? undefined : } > Reset )} {editing && ( setExitAlertCancel(true)} leftIcon={mobile ? undefined : } > Cancel )} setExitAlertReset(false)} onYep={handleReset} header="Reset the layout to its default?" /> setExitAlertCancel(false)} onYep={handleCancel} header="Cancel editing the layout?" /> {(!editing || changed) && ( } transition="color 0.2s ease" isLoading={saving || persisting} onClick={toggleEditLayout} > )} )} {currentLayoutItems.map(({ key, type, metadata }, i) => ( {type === BoxTypes.ADD_NEW_BOX ? ( ) => { itemsRef.current[i] = e; }} /> ) : ( ) => { itemsRef.current[i] = e; }} /> )} ))} ); };