🚀 Init preview and typebot cotext in editor
This commit is contained in:
106
apps/builder/components/shared/ContextMenu.tsx
Normal file
106
apps/builder/components/shared/ContextMenu.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
MutableRefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
useEventListener,
|
||||
Portal,
|
||||
Menu,
|
||||
MenuButton,
|
||||
PortalProps,
|
||||
MenuButtonProps,
|
||||
MenuProps,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
export interface ContextMenuProps<T extends HTMLElement> {
|
||||
renderMenu: () => JSX.Element | null
|
||||
children: (
|
||||
ref: MutableRefObject<T | null>,
|
||||
isOpened: boolean
|
||||
) => JSX.Element | null
|
||||
menuProps?: MenuProps
|
||||
portalProps?: PortalProps
|
||||
menuButtonProps?: MenuButtonProps
|
||||
}
|
||||
|
||||
export function ContextMenu<T extends HTMLElement = HTMLElement>(
|
||||
props: ContextMenuProps<T>
|
||||
) {
|
||||
const [isOpened, setIsOpened] = useState(false)
|
||||
const [isRendered, setIsRendered] = useState(false)
|
||||
const [isDeferredOpen, setIsDeferredOpen] = useState(false)
|
||||
const [position, setPosition] = useState<[number, number]>([0, 0])
|
||||
const targetRef = useRef<T>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpened) {
|
||||
setTimeout(() => {
|
||||
setIsRendered(true)
|
||||
setTimeout(() => {
|
||||
setIsDeferredOpen(true)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
setIsDeferredOpen(false)
|
||||
const timeout = setTimeout(() => {
|
||||
setIsRendered(isOpened)
|
||||
}, 1000)
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
}, [isOpened])
|
||||
|
||||
useEventListener(
|
||||
'contextmenu',
|
||||
(e) => {
|
||||
if (e.currentTarget === targetRef.current) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsOpened(true)
|
||||
setPosition([e.pageX, e.pageY])
|
||||
} else {
|
||||
setIsOpened(false)
|
||||
}
|
||||
},
|
||||
targetRef.current
|
||||
)
|
||||
|
||||
const onCloseHandler = useCallback(() => {
|
||||
props.menuProps?.onClose?.()
|
||||
setIsOpened(false)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.menuProps?.onClose, setIsOpened])
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.children(targetRef, isOpened)}
|
||||
{isRendered && (
|
||||
<Portal {...props.portalProps}>
|
||||
<Menu
|
||||
isOpen={isDeferredOpen}
|
||||
gutter={0}
|
||||
{...props.menuProps}
|
||||
onClose={onCloseHandler}
|
||||
>
|
||||
<MenuButton
|
||||
aria-hidden={true}
|
||||
w={1}
|
||||
h={1}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: position[0],
|
||||
top: position[1],
|
||||
cursor: 'default',
|
||||
}}
|
||||
{...props.menuButtonProps}
|
||||
/>
|
||||
{props.renderMenu()}
|
||||
</Menu>
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
71
apps/builder/components/shared/KBar.tsx
Normal file
71
apps/builder/components/shared/KBar.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
KBarPortal,
|
||||
KBarPositioner,
|
||||
KBarAnimator,
|
||||
KBarSearch,
|
||||
KBarResults,
|
||||
useMatches,
|
||||
} from 'kbar'
|
||||
import { chakra, Flex } from '@chakra-ui/react'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type KBarProps = {}
|
||||
|
||||
const KBarSearchChakra = chakra(KBarSearch)
|
||||
const KBarAnimatorChakra = chakra(KBarAnimator)
|
||||
const KBarResultsChakra = chakra(KBarResults)
|
||||
|
||||
export const KBar = ({}: KBarProps) => {
|
||||
return (
|
||||
<KBarPortal>
|
||||
<KBarPositioner>
|
||||
<KBarAnimatorChakra shadow="2xl" rounded="md">
|
||||
<KBarSearchChakra
|
||||
p={4}
|
||||
w="500px"
|
||||
roundedTop="md"
|
||||
_focus={{ outline: 'none' }}
|
||||
/>
|
||||
<RenderResults />
|
||||
</KBarAnimatorChakra>
|
||||
</KBarPositioner>
|
||||
</KBarPortal>
|
||||
)
|
||||
}
|
||||
|
||||
const RenderResults = () => {
|
||||
const { results } = useMatches()
|
||||
|
||||
return (
|
||||
<KBarResultsChakra
|
||||
items={results}
|
||||
onRender={({ item, active }) =>
|
||||
typeof item === 'string' ? (
|
||||
<Flex height="50px">{item}</Flex>
|
||||
) : (
|
||||
<Flex
|
||||
height="50px"
|
||||
roundedBottom="md"
|
||||
align="center"
|
||||
px="4"
|
||||
bgColor={active ? 'blue.50' : 'white'}
|
||||
_hover={{ bgColor: 'blue.50' }}
|
||||
>
|
||||
{active && (
|
||||
<Flex
|
||||
pos="absolute"
|
||||
left="0"
|
||||
h="full"
|
||||
w="3px"
|
||||
roundedRight="md"
|
||||
bgColor={'blue.500'}
|
||||
/>
|
||||
)}
|
||||
{item.name}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Editable, EditablePreview, EditableInput } from '@chakra-ui/editable'
|
||||
import { Tooltip } from '@chakra-ui/tooltip'
|
||||
import React from 'react'
|
||||
|
||||
type EditableProps = {
|
||||
name?: string
|
||||
onNewName: (newName: string) => void
|
||||
}
|
||||
export const EditableTypebotName = ({ name, onNewName }: EditableProps) => {
|
||||
return (
|
||||
<Tooltip label="Rename">
|
||||
<Editable defaultValue={name} onSubmit={onNewName}>
|
||||
<EditablePreview
|
||||
isTruncated
|
||||
cursor="pointer"
|
||||
maxW="200px"
|
||||
overflow="hidden"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
minW="100px"
|
||||
/>
|
||||
<EditableInput />
|
||||
</Editable>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
33
apps/builder/components/shared/TypebotHeader/SaveButton.tsx
Normal file
33
apps/builder/components/shared/TypebotHeader/SaveButton.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { IconButton, Text, Tooltip } from '@chakra-ui/react'
|
||||
import { CheckIcon, SaveIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import React from 'react'
|
||||
|
||||
export const SaveButton = () => {
|
||||
const { save, isSavingLoading, hasUnsavedChanges } = useTypebot()
|
||||
|
||||
const onSaveClick = () => {
|
||||
save()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasUnsavedChanges && (
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
Unsaved changes
|
||||
</Text>
|
||||
)}
|
||||
<Tooltip label="Save changes">
|
||||
<IconButton
|
||||
isDisabled={!hasUnsavedChanges}
|
||||
onClick={onSaveClick}
|
||||
isLoading={isSavingLoading}
|
||||
icon={
|
||||
hasUnsavedChanges ? <SaveIcon /> : <CheckIcon color="green.400" />
|
||||
}
|
||||
aria-label={hasUnsavedChanges ? 'Save' : 'Saved'}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
123
apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx
Normal file
123
apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { Flex, HStack, Button, IconButton } from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from 'assets/icons'
|
||||
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||
import { RightPanel, useEditor } from 'contexts/EditorContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { PublishButton } from '../buttons/PublishButton'
|
||||
import { EditableTypebotName } from './EditableTypebotName'
|
||||
import { SaveButton } from './SaveButton'
|
||||
|
||||
export const headerHeight = 56
|
||||
|
||||
export const TypebotHeader = () => {
|
||||
const router = useRouter()
|
||||
const { typebot } = useTypebot()
|
||||
const { setRightPanel } = useEditor()
|
||||
|
||||
const handleBackClick = () => {
|
||||
router.push({
|
||||
pathname: `/typebots`,
|
||||
query: { ...router.query, typebotId: [] },
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
w="full"
|
||||
borderBottomWidth="1px"
|
||||
justify="center"
|
||||
align="center"
|
||||
pos="fixed"
|
||||
h={`${headerHeight}px`}
|
||||
zIndex={2}
|
||||
bgColor="white"
|
||||
>
|
||||
<HStack>
|
||||
<Button
|
||||
as={NextChakraLink}
|
||||
href={{
|
||||
pathname: `/typebots/${typebot?.id}/edit`,
|
||||
query: { ...router.query, typebotId: [] },
|
||||
}}
|
||||
colorScheme={router.pathname.includes('/edit') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.includes('/edit') ? 'outline' : 'ghost'}
|
||||
>
|
||||
Flow
|
||||
</Button>
|
||||
<Button
|
||||
as={NextChakraLink}
|
||||
href={{
|
||||
pathname: `/typebots/${typebot?.id}/design`,
|
||||
query: { ...router.query, typebotId: [] },
|
||||
}}
|
||||
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
|
||||
>
|
||||
Theme
|
||||
</Button>
|
||||
<Button
|
||||
as={NextChakraLink}
|
||||
href={{
|
||||
pathname: `/typebots/${typebot?.id}/design`,
|
||||
query: { ...router.query, typebotId: [] },
|
||||
}}
|
||||
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
<Button
|
||||
as={NextChakraLink}
|
||||
href={{
|
||||
pathname: `/typebots/${typebot?.id}/share`,
|
||||
query: { ...router.query, typebotId: [] },
|
||||
}}
|
||||
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
<Button
|
||||
as={NextChakraLink}
|
||||
href={{
|
||||
pathname: `/typebots/${typebot?.id}/results/responses`,
|
||||
query: { ...router.query, typebotId: [] },
|
||||
}}
|
||||
colorScheme={router.pathname.includes('results') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.includes('results') ? 'outline' : 'ghost'}
|
||||
>
|
||||
Results
|
||||
</Button>
|
||||
</HStack>
|
||||
<Flex pos="absolute" left="1rem" justify="center" align="center">
|
||||
<Flex alignItems="center">
|
||||
<IconButton
|
||||
aria-label="Back"
|
||||
icon={<ChevronLeftIcon fontSize={30} />}
|
||||
mr={2}
|
||||
onClick={handleBackClick}
|
||||
/>
|
||||
<EditableTypebotName
|
||||
name={typebot?.name}
|
||||
onNewName={(newName) => console.log(newName)}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<HStack right="40px" pos="absolute">
|
||||
<SaveButton />
|
||||
<Button
|
||||
onClick={() => {
|
||||
setRightPanel(RightPanel.PREVIEW)
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
|
||||
<PublishButton />
|
||||
</HStack>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
1
apps/builder/components/shared/TypebotHeader/index.tsx
Normal file
1
apps/builder/components/shared/TypebotHeader/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './TypebotHeader'
|
||||
9
apps/builder/components/shared/buttons/PublishButton.tsx
Normal file
9
apps/builder/components/shared/buttons/PublishButton.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Button } from '@chakra-ui/react'
|
||||
|
||||
export const PublishButton = () => {
|
||||
return (
|
||||
<Button ml={2} colorScheme={'blue'}>
|
||||
Publish
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user