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

View File

@ -1,31 +1,59 @@
import { useEffect, useState } from 'react' import { ChangeEvent, useEffect, useState } from 'react'
import { Button, Flex, HStack, Stack, Text } from '@chakra-ui/react' import {
Button,
Flex,
HStack,
Stack,
Text,
Input as ClassicInput,
SimpleGrid,
GridItem,
} from '@chakra-ui/react'
import { SearchContextManager } from '@giphy/react-components' import { SearchContextManager } from '@giphy/react-components'
import { UploadButton } from '../buttons/UploadButton' import { UploadButton } from '../buttons/UploadButton'
import { GiphySearch } from './GiphySearch' import { GiphySearch } from './GiphySearch'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { Input } from '../Textbox' import { Input } from '../Textbox'
import { BaseEmoji, emojiIndex } from 'emoji-mart'
import { emojis } from './emojis'
type Props = { type Props = {
url?: string url?: string
onSubmit: (url: string) => void isEmojiEnabled?: boolean
isGiphyEnabled?: boolean isGiphyEnabled?: boolean
onSubmit: (url: string) => void
onClose?: () => void
} }
export const ImageUploadContent = ({ export const ImageUploadContent = ({
url, url,
onSubmit, onSubmit,
isEmojiEnabled = false,
isGiphyEnabled = true, isGiphyEnabled = true,
onClose,
}: Props) => { }: Props) => {
const [currentTab, setCurrentTab] = useState<'link' | 'upload' | 'giphy'>( const [currentTab, setCurrentTab] = useState<
'upload' 'link' | 'upload' | 'giphy' | 'emoji'
) >(isEmojiEnabled ? 'emoji' : 'upload')
const handleSubmit = (url: string) => {
onSubmit(url)
onClose && onClose()
}
const handleSubmit = (url: string) => onSubmit(url)
return ( return (
<Stack> <Stack>
<HStack> <HStack>
{isEmojiEnabled && (
<Button
variant={currentTab === 'emoji' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('emoji')}
size="sm"
>
Emoji
</Button>
)}
<Button <Button
variant={currentTab === 'upload' ? 'solid' : 'ghost'} variant={currentTab === 'upload' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('upload')} onClick={() => setCurrentTab('upload')}
@ -61,7 +89,7 @@ const BodyContent = ({
url, url,
onSubmit, onSubmit,
}: { }: {
tab: 'upload' | 'link' | 'giphy' tab: 'upload' | 'link' | 'giphy' | 'emoji'
url?: string url?: string
onSubmit: (url: string) => void onSubmit: (url: string) => void
}) => { }) => {
@ -72,6 +100,8 @@ const BodyContent = ({
return <EmbedLinkContent initialUrl={url} onNewUrl={onSubmit} /> return <EmbedLinkContent initialUrl={url} onNewUrl={onSubmit} />
case 'giphy': case 'giphy':
return <GiphyContent onNewUrl={onSubmit} /> 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) => { const GiphyContent = ({ onNewUrl }: ContentProps) => {
if (!process.env.NEXT_PUBLIC_GIPHY_API_KEY) if (!process.env.NEXT_PUBLIC_GIPHY_API_KEY)
return <Text>NEXT_PUBLIC_GIPHY_API_KEY is missing in environment</Text> 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" overflow="hidden"
display="flex" display="flex"
alignItems="center" alignItems="center"
minW="100px"
/> />
<EditableInput /> <EditableInput />
</Editable> </Editable>

View File

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

View File

@ -52,6 +52,7 @@ type UpdateTypebotPayload = Partial<{
publicId: string publicId: string
name: string name: string
publishedTypebotId: string publishedTypebotId: string
icon: string
}> }>
export type SetTypebot = ( export type SetTypebot = (
@ -167,10 +168,6 @@ export const TypebotContext = ({
new Date(typebot.updatedAt) > new Date(typebot.updatedAt) >
new Date(currentTypebotRef.current.updatedAt) new Date(currentTypebotRef.current.updatedAt)
) { ) {
console.log(
new Date(typebot.updatedAt),
new Date(currentTypebotRef.current.updatedAt)
)
setLocalTypebot({ ...typebot }) setLocalTypebot({ ...typebot })
} }

View File

@ -45,6 +45,7 @@
"db": "*", "db": "*",
"deep-object-diff": "^1.1.7", "deep-object-diff": "^1.1.7",
"dequal": "^2.0.2", "dequal": "^2.0.2",
"emoji-mart": "^3.0.1",
"focus-visible": "^5.2.0", "focus-visible": "^5.2.0",
"framer-motion": "^4", "framer-motion": "^4",
"google-auth-library": "^7.14.0", "google-auth-library": "^7.14.0",
@ -87,6 +88,7 @@
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.19.2", "@playwright/test": "^1.19.2",
"@types/canvas-confetti": "^1.4.2", "@types/canvas-confetti": "^1.4.2",
"@types/emoji-mart": "^3.0.9",
"@types/google-spreadsheet": "^3.1.5", "@types/google-spreadsheet": "^3.1.5",
"@types/jsonwebtoken": "8.5.8", "@types/jsonwebtoken": "8.5.8",
"@types/micro-cors": "^0.1.2", "@types/micro-cors": "^0.1.2",

View File

@ -46,7 +46,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
folderId, folderId,
}, },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
select: { name: true, publishedTypebotId: true, id: true }, select: { name: true, publishedTypebotId: true, id: true, icon: true },
}) })
return res.send({ typebots }) return res.send({ typebots })
} }

View File

@ -18,7 +18,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const sharedTypebots = await prisma.collaboratorsOnTypebots.findMany({ const sharedTypebots = await prisma.collaboratorsOnTypebots.findMany({
where: { userId: user.id }, where: { userId: user.id },
include: { include: {
typebot: { select: { name: true, publishedTypebotId: true, id: true } }, typebot: {
select: {
name: true,
publishedTypebotId: true,
id: true,
icon: true,
},
},
}, },
}) })
return res.send({ return res.send({

View File

@ -173,6 +173,7 @@ const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
publishedTypebotId: null, publishedTypebotId: null,
customDomain: null, customDomain: null,
icon: null,
variables: [{ id: 'var1', name: 'var1' }], variables: [{ id: 'var1', name: 'var1' }],
...partialTypebot, ...partialTypebot,
edges: [ edges: [

View File

@ -125,4 +125,28 @@ test.describe.parallel('Editor', () => {
await page.click('button[aria-label="Redo"]') await page.click('button[aria-label="Redo"]')
await expect(page.locator('text="Block #1"')).toBeHidden() await expect(page.locator('text="Block #1"')).toBeHidden()
}) })
test('Rename and icon change should work', async ({ page }) => {
const typebotId = cuid()
await createTypebots([
{
id: typebotId,
name: 'My awesome typebot',
...parseDefaultBlockWithStep({
type: InputStepType.TEXT,
options: defaultTextInputOptions,
}),
},
])
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text="My awesome typebot"')
await page.fill('input[value="My awesome typebot"]', 'My superb typebot')
await page.click('[data-testid="editable-icon"]')
await page.fill('input[placeholder="Search..."]', 'love')
await page.click('text="😍"')
await page.goto(`/typebots`)
await expect(page.locator('text="😍"')).toBeVisible()
await expect(page.locator('text="My superb typebot"')).toBeVisible()
})
}) })

View File

@ -37,6 +37,7 @@ export const parsePublicTypebotToTypebot = (
publishedTypebotId: typebot.id, publishedTypebotId: typebot.id,
folderId: existingTypebot.folderId, folderId: existingTypebot.folderId,
ownerId: existingTypebot.ownerId, ownerId: existingTypebot.ownerId,
icon: existingTypebot.icon,
}) })
export const createPublishedTypebot = async (typebot: PublicTypebot) => export const createPublishedTypebot = async (typebot: PublicTypebot) =>

View File

@ -56,7 +56,7 @@ import { Plan, User } from 'db'
export type TypebotInDashboard = Pick< export type TypebotInDashboard = Pick<
Typebot, Typebot,
'id' | 'name' | 'publishedTypebotId' 'id' | 'name' | 'publishedTypebotId' | 'icon'
> >
export const useTypebots = ({ export const useTypebots = ({
folderId, folderId,
@ -351,6 +351,7 @@ export const parseNewTypebot = ({
| 'publishedTypebotId' | 'publishedTypebotId'
| 'publicId' | 'publicId'
| 'customDomain' | 'customDomain'
| 'icon'
> => { > => {
const startBlockId = cuid() const startBlockId = cuid()
const startStepId = cuid() const startStepId = cuid()

View File

@ -30,7 +30,12 @@ export const useSharedTypebots = ({
onError: (error: Error) => void onError: (error: Error) => void
}) => { }) => {
const { data, error, mutate } = useSWR< const { data, error, mutate } = useSWR<
{ sharedTypebots: Pick<Typebot, 'name' | 'id' | 'publishedTypebotId'>[] }, {
sharedTypebots: Pick<
Typebot,
'name' | 'id' | 'publishedTypebotId' | 'icon'
>[]
},
Error Error
>(userId ? `/api/users/${userId}/sharedTypebots` : null, fetcher) >(userId ? `/api/users/${userId}/sharedTypebots` : null, fetcher)
if (error) onError(error) if (error) onError(error)

View File

@ -76,6 +76,7 @@ const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
folderId: null, folderId: null,
name: 'My typebot', name: 'My typebot',
ownerId: 'proUser', ownerId: 'proUser',
icon: null,
theme: defaultTheme, theme: defaultTheme,
settings: defaultSettings, settings: defaultSettings,
publicId: partialTypebot.id + '-public', publicId: partialTypebot.id + '-public',

View File

@ -14,7 +14,7 @@
"@prisma/client": "^3.10.0" "@prisma/client": "^3.10.0"
}, },
"scripts": { "scripts": {
"dx": "dotenv -e ../../apps/builder/.env.local prisma db push && yarn start:sutdio ", "dx": "dotenv -e ../../apps/builder/.env.local prisma db push && yarn generate:schema && yarn start:sutdio ",
"build": "yarn generate:schema", "build": "yarn generate:schema",
"start:sutdio": "dotenv -e ../../apps/builder/.env.local -v BROWSER=none prisma studio", "start:sutdio": "dotenv -e ../../apps/builder/.env.local -v BROWSER=none prisma studio",
"generate:schema": "dotenv -e ../../apps/builder/.env.local prisma generate", "generate:schema": "dotenv -e ../../apps/builder/.env.local prisma generate",

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Typebot" ADD COLUMN "icon" TEXT;

View File

@ -114,6 +114,7 @@ model Typebot {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
icon String?
name String name String
ownerId String ownerId String
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)

View File

@ -4104,6 +4104,13 @@
dependencies: dependencies:
cssnano "*" cssnano "*"
"@types/emoji-mart@^3.0.9":
version "3.0.9"
resolved "https://registry.yarnpkg.com/@types/emoji-mart/-/emoji-mart-3.0.9.tgz#2f7ef5d9ec194f28029c46c81a5fc1e5b0efa73c"
integrity sha512-qdBo/2Y8MXaJ/2spKjDZocuq79GpnOhkwMHnK2GnVFa8WYFgfA+ei6sil3aeWQPCreOKIx9ogPpR5+7MaOqYAA==
dependencies:
"@types/react" "*"
"@types/eslint-scope@^3.7.3": "@types/eslint-scope@^3.7.3":
version "3.7.3" version "3.7.3"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224"
@ -7315,6 +7322,14 @@ emittery@^0.8.1:
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==
emoji-mart@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-3.0.1.tgz#9ce86706e02aea0506345f98464814a662ca54c6"
integrity sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==
dependencies:
"@babel/runtime" "^7.0.0"
prop-types "^15.6.0"
emoji-regex@^8.0.0: emoji-regex@^8.0.0:
version "8.0.0" version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"