feat(editor): ✨ Custom icon on typebot
This commit is contained in:
@ -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>
|
||||
|
@ -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>
|
||||
|
1816
apps/builder/components/shared/ImageUploadContent/emojis.ts
Normal file
1816
apps/builder/components/shared/ImageUploadContent/emojis.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
)
|
||||
}
|
@ -24,7 +24,6 @@ export const EditableTypebotName = ({ name, onNewName }: EditableProps) => {
|
||||
overflow="hidden"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
minW="100px"
|
||||
/>
|
||||
<EditableInput />
|
||||
</Editable>
|
||||
|
@ -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>
|
||||
|
37
apps/builder/components/shared/TypebotHeader/TypebotIcon.tsx
Normal file
37
apps/builder/components/shared/TypebotHeader/TypebotIcon.tsx
Normal 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} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -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"
|
||||
|
Reference in New Issue
Block a user