2
0

feat(editor): Custom icon on typebot

This commit is contained in:
Baptiste Arnaud
2022-04-01 16:28:09 +02:00
parent 3585e63c48
commit 525887a32c
22 changed files with 2113 additions and 56 deletions

View File

@ -4,6 +4,7 @@ import {
Flex,
IconButton,
MenuItem,
Tag,
Text,
useDisclosure,
useToast,
@ -14,14 +15,15 @@ import { useRouter } from 'next/router'
import { isMobile } from 'services/utils'
import { MoreButton } from 'components/dashboard/FolderContent/MoreButton'
import { ConfirmModal } from 'components/modals/ConfirmModal'
import { GlobeIcon, GripIcon, ToolIcon } from 'assets/icons'
import { GripIcon } from 'assets/icons'
import { deleteTypebot, duplicateTypebot } from 'services/typebots'
import { Typebot } from 'models'
import { useTypebotDnd } from 'contexts/TypebotDndContext'
import { useDebounce } from 'use-debounce'
import { TypebotIcon } from 'components/shared/TypebotHeader/TypebotIcon'
type ChatbotCardProps = {
typebot: Pick<Typebot, 'id' | 'publishedTypebotId' | 'name'>
typebot: Pick<Typebot, 'id' | 'publishedTypebotId' | 'name' | 'icon'>
isReadOnly?: boolean
onTypebotDeleted?: () => void
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement>) => void
@ -101,6 +103,18 @@ export const TypebotButton = ({
onMouseDown={onMouseDown}
cursor="pointer"
>
{typebot.publishedTypebotId && (
<Tag
colorScheme="blue"
variant="solid"
rounded="full"
pos="absolute"
top="27px"
size="sm"
>
Live
</Tag>
)}
{!isReadOnly && (
<>
<IconButton
@ -129,18 +143,12 @@ export const TypebotButton = ({
)}
<VStack spacing="4">
<Flex
boxSize="45px"
rounded="full"
justifyContent="center"
alignItems="center"
bgColor={typebot.publishedTypebotId ? 'blue.500' : 'gray.400'}
color="white"
fontSize={'4xl'}
>
{typebot.publishedTypebotId ? (
<GlobeIcon fontSize="20px" />
) : (
<ToolIcon fill="white" fontSize="20px" />
)}
{<TypebotIcon icon={typebot.icon} boxSize={'35px'} />}
</Flex>
<Text>{typebot.name}</Text>
</VStack>

View File

@ -1,31 +1,59 @@
import { useEffect, useState } from 'react'
import { Button, Flex, HStack, Stack, Text } from '@chakra-ui/react'
import { ChangeEvent, useEffect, useState } from 'react'
import {
Button,
Flex,
HStack,
Stack,
Text,
Input as ClassicInput,
SimpleGrid,
GridItem,
} from '@chakra-ui/react'
import { SearchContextManager } from '@giphy/react-components'
import { UploadButton } from '../buttons/UploadButton'
import { GiphySearch } from './GiphySearch'
import { useTypebot } from 'contexts/TypebotContext'
import { useDebounce } from 'use-debounce'
import { Input } from '../Textbox'
import { BaseEmoji, emojiIndex } from 'emoji-mart'
import { emojis } from './emojis'
type Props = {
url?: string
onSubmit: (url: string) => void
isEmojiEnabled?: boolean
isGiphyEnabled?: boolean
onSubmit: (url: string) => void
onClose?: () => void
}
export const ImageUploadContent = ({
url,
onSubmit,
isEmojiEnabled = false,
isGiphyEnabled = true,
onClose,
}: Props) => {
const [currentTab, setCurrentTab] = useState<'link' | 'upload' | 'giphy'>(
'upload'
)
const [currentTab, setCurrentTab] = useState<
'link' | 'upload' | 'giphy' | 'emoji'
>(isEmojiEnabled ? 'emoji' : 'upload')
const handleSubmit = (url: string) => {
onSubmit(url)
onClose && onClose()
}
const handleSubmit = (url: string) => onSubmit(url)
return (
<Stack>
<HStack>
{isEmojiEnabled && (
<Button
variant={currentTab === 'emoji' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('emoji')}
size="sm"
>
Emoji
</Button>
)}
<Button
variant={currentTab === 'upload' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('upload')}
@ -61,7 +89,7 @@ const BodyContent = ({
url,
onSubmit,
}: {
tab: 'upload' | 'link' | 'giphy'
tab: 'upload' | 'link' | 'giphy' | 'emoji'
url?: string
onSubmit: (url: string) => void
}) => {
@ -72,6 +100,8 @@ const BodyContent = ({
return <EmbedLinkContent initialUrl={url} onNewUrl={onSubmit} />
case 'giphy':
return <GiphyContent onNewUrl={onSubmit} />
case 'emoji':
return <EmojiContent onEmojiSelected={onSubmit} />
}
}
@ -114,6 +144,55 @@ const EmbedLinkContent = ({ initialUrl, onNewUrl }: ContentProps) => {
)
}
const EmojiContent = ({
onEmojiSelected,
}: {
onEmojiSelected: (emoji: string) => void
}) => {
const [searchValue, setSearchValue] = useState('')
const [filteredEmojis, setFilteredEmojis] = useState<string[]>(emojis)
const handleEmojiClick = (emoji: string) => () => onEmojiSelected(emoji)
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value)
setFilteredEmojis(
emojiIndex.search(e.target.value)?.map((o) => (o as BaseEmoji).native) ??
emojis
)
}
return (
<Stack>
<ClassicInput
placeholder="Search..."
value={searchValue}
onChange={handleSearchChange}
/>
<SimpleGrid
maxH="350px"
overflowY="scroll"
overflowX="hidden"
spacing={0}
columns={7}
>
{filteredEmojis.map((emoji) => (
<GridItem key={emoji}>
<Button
onClick={handleEmojiClick(emoji)}
variant="ghost"
size="sm"
fontSize="xl"
>
{emoji}
</Button>
</GridItem>
))}
</SimpleGrid>
</Stack>
)
}
const GiphyContent = ({ onNewUrl }: ContentProps) => {
if (!process.env.NEXT_PUBLIC_GIPHY_API_KEY)
return <Text>NEXT_PUBLIC_GIPHY_API_KEY is missing in environment</Text>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,48 @@
import {
Popover,
Tooltip,
chakra,
PopoverTrigger,
PopoverContent,
} from '@chakra-ui/react'
import React from 'react'
import { ImageUploadContent } from '../ImageUploadContent'
import { TypebotIcon } from './TypebotIcon'
type Props = { icon?: string | null; onChangeIcon: (icon: string) => void }
export const EditableTypebotIcon = ({ icon, onChangeIcon }: Props) => {
return (
<Popover isLazy>
{({ onClose }) => (
<>
<Tooltip label="Change icon">
<chakra.span
cursor="pointer"
px="2"
rounded="md"
_hover={{ bgColor: 'gray.100' }}
transition="background-color 0.2s"
data-testid="editable-icon"
>
<PopoverTrigger>
<chakra.span>
<TypebotIcon icon={icon} emojiFontSize="2xl" />
</chakra.span>
</PopoverTrigger>
</chakra.span>
</Tooltip>
<PopoverContent p="2">
<ImageUploadContent
url={icon ?? ''}
onSubmit={onChangeIcon}
isGiphyEnabled={false}
isEmojiEnabled={true}
onClose={onClose}
/>
</PopoverContent>
</>
)}
</Popover>
)
}

View File

@ -24,7 +24,6 @@ export const EditableTypebotName = ({ name, onNewName }: EditableProps) => {
overflow="hidden"
display="flex"
alignItems="center"
minW="100px"
/>
<EditableInput />
</Editable>

View File

@ -16,6 +16,7 @@ import React from 'react'
import { isNotDefined } from 'utils'
import { PublishButton } from '../buttons/PublishButton'
import { CollaborationMenuButton } from './CollaborationMenuButton'
import { EditableTypebotIcon } from './EditableTypebotIcons'
import { EditableTypebotName } from './EditableTypebotName'
export const headerHeight = 56
@ -26,6 +27,7 @@ export const TypebotHeader = () => {
const {
typebot,
updateOnBothTypebots,
updateTypebot,
save,
undo,
redo,
@ -37,6 +39,8 @@ export const TypebotHeader = () => {
const handleNameSubmit = (name: string) => updateOnBothTypebots({ name })
const handleChangeIcon = (icon: string) => updateTypebot({ icon })
const handlePreviewClick = async () => {
save().then()
setRightPanel(RightPanel.PREVIEW)
@ -50,7 +54,7 @@ export const TypebotHeader = () => {
align="center"
pos="relative"
h={`${headerHeight}px`}
zIndex={2}
zIndex={100}
bgColor="white"
flexShrink={0}
>
@ -105,7 +109,7 @@ export const TypebotHeader = () => {
align="center"
spacing="6"
>
<HStack alignItems="center">
<HStack alignItems="center" spacing={4}>
<IconButton
as={NextChakraLink}
aria-label="Navigate back"
@ -118,33 +122,42 @@ export const TypebotHeader = () => {
: '/typebots'
}
/>
{typebot?.name && (
<EditableTypebotName
name={typebot?.name}
onNewName={handleNameSubmit}
<HStack spacing={1}>
<EditableTypebotIcon
icon={typebot?.icon}
onChangeIcon={handleChangeIcon}
/>
)}
<Tooltip label="Undo">
<IconButton
display={['none', 'flex']}
icon={<UndoIcon />}
size="sm"
aria-label="Undo"
onClick={undo}
isDisabled={!canUndo}
/>
</Tooltip>
{typebot?.name && (
<EditableTypebotName
name={typebot?.name}
onNewName={handleNameSubmit}
/>
)}
</HStack>
<Tooltip label="Redo">
<IconButton
display={['none', 'flex']}
icon={<RedoIcon />}
size="sm"
aria-label="Redo"
onClick={redo}
isDisabled={!canRedo}
/>
</Tooltip>
<HStack>
<Tooltip label="Undo">
<IconButton
display={['none', 'flex']}
icon={<UndoIcon />}
size="sm"
aria-label="Undo"
onClick={undo}
isDisabled={!canUndo}
/>
</Tooltip>
<Tooltip label="Redo">
<IconButton
display={['none', 'flex']}
icon={<RedoIcon />}
size="sm"
aria-label="Redo"
onClick={redo}
isDisabled={!canRedo}
/>
</Tooltip>
</HStack>
</HStack>
{isSavingLoading && (
<HStack>

View File

@ -0,0 +1,37 @@
import { ToolIcon } from 'assets/icons'
import React from 'react'
import { chakra, Image } from '@chakra-ui/react'
type Props = {
icon?: string | null
emojiFontSize?: string
boxSize?: string
}
export const TypebotIcon = ({
icon,
boxSize = '25px',
emojiFontSize,
}: Props) => {
return (
<>
{icon ? (
icon.startsWith('http') ? (
<Image
src={icon}
boxSize={boxSize}
objectFit={icon.endsWith('.svg') ? undefined : 'cover'}
alt="typebot icon"
rounded="md"
/>
) : (
<chakra.span role="img" fontSize={emojiFontSize}>
{icon}
</chakra.span>
)
) : (
<ToolIcon boxSize={boxSize} />
)}
</>
)
}

View File

@ -36,7 +36,7 @@ export const UploadButton = ({
id="file-input"
display="none"
onChange={handleInputChange}
accept=".jpg, .jpeg, .png"
accept=".jpg, .jpeg, .png, .svg"
/>
<Button
as="label"