2
0

🚀 Init preview and typebot cotext in editor

This commit is contained in:
Baptiste Arnaud
2021-12-22 14:59:07 +01:00
parent a54e42f255
commit b7cdc0d14a
87 changed files with 4431 additions and 735 deletions

View 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>
)}
</>
)
}

View 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>
)
}
/>
)
}

View File

@@ -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>
)
}

View 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>
</>
)
}

View 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>
)
}

View File

@@ -0,0 +1 @@
export * from './TypebotHeader'

View File

@@ -0,0 +1,9 @@
import { Button } from '@chakra-ui/react'
export const PublishButton = () => {
return (
<Button ml={2} colorScheme={'blue'}>
Publish
</Button>
)
}