2
0

feat(bubbles): Add image bubble

This commit is contained in:
Baptiste Arnaud
2022-01-20 16:14:47 +01:00
parent c43fd1d386
commit 2d178978ef
33 changed files with 848 additions and 142 deletions

View File

@ -34,3 +34,6 @@ FACEBOOK_CLIENT_SECRET=
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# (Optional) Used for GIF search
NEXT_PUBLIC_GIPHY_API_KEY=

View File

@ -344,3 +344,25 @@ export const GoogleAnalyticsLogo = (props: IconProps) => (
</defs>
</Icon>
)
export const GiphyLogo = (props: IconProps) => (
<Icon viewBox="0 0 163.79999999999998 35" {...props}>
<g fill="none" fillRule="evenodd">
<path d="M4 4h20v27H4z" fill="#000" />
<g fillRule="nonzero">
<path d="M0 3h4v29H0z" fill="#04ff8e" />
<path d="M24 11h4v21h-4z" fill="#8e2eff" />
<path d="M0 31h28v4H0z" fill="#00c5ff" />
<path d="M0 0h16v4H0z" fill="#fff152" />
<path d="M24 8V4h-4V0h-4v12h12V8" fill="#ff5b5b" />
<path d="M24 16v-4h4" fill="#551c99" />
</g>
<path d="M16 0v4h-4" fill="#999131" />
<path
d="M59.1 12c-2-1.9-4.4-2.4-6.2-2.4-4.4 0-7.3 2.6-7.3 8 0 3.5 1.8 7.8 7.3 7.8 1.4 0 3.7-.3 5.2-1.4v-3.5h-6.9v-6h13.3v12.1c-1.7 3.5-6.4 5.3-11.7 5.3-10.7 0-14.8-7.2-14.8-14.3S42.7 3.2 52.9 3.2c3.8 0 7.1.8 10.7 4.4zm9.1 19.2V4h7.6v27.2zm20.1-7.4v7.3h-7.7V4h13.2c7.3 0 10.9 4.6 10.9 9.9 0 5.6-3.6 9.9-10.9 9.9zm0-6.5h5.5c2.1 0 3.2-1.6 3.2-3.3 0-1.8-1.1-3.4-3.2-3.4h-5.5zM125 31.2V20.9h-9.8v10.3h-7.7V4h7.7v10.3h9.8V4h7.6v27.2zm24.2-17.9l5.9-9.3h8.7v.3l-10.8 16v10.8h-7.7V20.3L135 4.3V4h8.7z"
fill="#000"
fillRule="nonzero"
/>
</g>
</Icon>
)

View File

@ -15,7 +15,6 @@ import { UploadIcon } from 'assets/icons'
import { UploadButton } from 'components/shared/buttons/UploadButton'
import { useUser } from 'contexts/UserContext'
import React, { ChangeEvent, useState } from 'react'
import { uploadFile } from 'services/utils'
export const PersonalInfoForm = () => {
const {
@ -27,14 +26,10 @@ export const PersonalInfoForm = () => {
isOAuthProvider,
} = useUser()
const [reloadParam, setReloadParam] = useState('')
const [isUploading, setIsUploading] = useState(false)
const handleFileChange = async (file: File) => {
setIsUploading(true)
const { url } = await uploadFile(file, `${user?.id}/avatar`)
const handleFileUploaded = async (url: string) => {
setReloadParam(Date.now().toString())
updateUser({ image: url })
setIsUploading(false)
}
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
@ -60,9 +55,9 @@ export const PersonalInfoForm = () => {
<Stack>
<UploadButton
size="sm"
filePath={`users/${user?.id}/avatar`}
leftIcon={<UploadIcon />}
isLoading={isUploading}
onUploadChange={handleFileChange}
onFileUploaded={handleFileUploaded}
>
Change photo
</UploadButton>

View File

@ -9,6 +9,7 @@ import {
FilterIcon,
FlagIcon,
GlobeIcon,
ImageIcon,
NumberIcon,
PhoneIcon,
TextIcon,
@ -29,6 +30,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
switch (type) {
case BubbleStepType.TEXT:
return <ChatIcon {...props} />
case BubbleStepType.IMAGE:
return <ImageIcon {...props} />
case InputStepType.TEXT:
return <TextIcon {...props} />
case InputStepType.NUMBER:

View File

@ -16,6 +16,8 @@ export const StepTypeLabel = ({ type }: Props) => {
case InputStepType.TEXT: {
return <Text>Text</Text>
}
case BubbleStepType.IMAGE:
return <Text>Image</Text>
case InputStepType.NUMBER: {
return <Text>Number</Text>
}

View File

@ -43,7 +43,6 @@ export const StepTypesList = () => {
const x = e.clientX - rect.left
const y = e.clientY - rect.top
setRelativeCoordinates({ x, y })
console.log({ x: rect.left, y: rect.top })
setDraggedStepType(type)
}

View File

@ -0,0 +1,54 @@
import {
Portal,
PopoverContent,
PopoverArrow,
PopoverBody,
} from '@chakra-ui/react'
import { ImagePopoverContent } from 'components/shared/ImageUploadContent'
import { useTypebot } from 'contexts/TypebotContext'
import {
BubbleStep,
BubbleStepType,
ImageBubbleContent,
ImageBubbleStep,
TextBubbleStep,
} from 'models'
import { useRef } from 'react'
type Props = {
step: Exclude<BubbleStep, TextBubbleStep>
}
export const ContentPopover = ({ step }: Props) => {
const ref = useRef<HTMLDivElement | null>(null)
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
return (
<Portal>
<PopoverContent onMouseDown={handleMouseDown} w="500px">
<PopoverArrow />
<PopoverBody ref={ref} shadow="lg">
<StepContent step={step} />
</PopoverBody>
</PopoverContent>
</Portal>
)
}
export const StepContent = ({ step }: Props) => {
const { updateStep } = useTypebot()
const handleContentChange = (content: ImageBubbleContent) =>
updateStep(step.id, { content } as Partial<ImageBubbleStep>)
const handleNewImageSubmit = (url: string) => handleContentChange({ url })
switch (step.type) {
case BubbleStepType.IMAGE: {
return (
<ImagePopoverContent
url={step.content?.url}
onSubmit={handleNewImageSubmit}
/>
)
}
}
}

View File

@ -0,0 +1 @@
export { ContentPopover } from './ContentPopover'

View File

@ -15,6 +15,7 @@ import {
LogicStepType,
Step,
StepOptions,
TextBubbleStep,
} from 'models'
import { useRef } from 'react'
import {
@ -33,7 +34,7 @@ import { RedirectSettings } from './bodies/RedirectSettings'
import { SetVariableSettingsBody } from './bodies/SetVariableSettingsBody'
type Props = {
step: Step
step: Exclude<Step, TextBubbleStep>
onExpandClick: () => void
}

View File

@ -7,15 +7,10 @@ import {
useEventListener,
} from '@chakra-ui/react'
import React, { useEffect, useState } from 'react'
import { DraggableStep, Step } from 'models'
import { BubbleStep, DraggableStep, Step, TextBubbleStep } from 'models'
import { useGraph } from 'contexts/GraphContext'
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
import {
isInputStep,
isLogicStep,
isTextBubbleStep,
isIntegrationStep,
} from 'utils'
import { isBubbleStep, isTextBubbleStep } from 'utils'
import { Coordinates } from '@dnd-kit/core/dist/types'
import { TextEditor } from './TextEditor/TextEditor'
import { StepNodeContent } from './StepNodeContent'
@ -29,6 +24,7 @@ import { TargetEndpoint } from './TargetEndpoint'
import { useRouter } from 'next/router'
import { SettingsModal } from './SettingsPopoverContent/SettingsModal'
import { StepSettings } from './SettingsPopoverContent/SettingsPopoverContent'
import { ContentPopover } from './ContentPopover'
export const StepNode = ({
step,
@ -185,15 +181,16 @@ export const StepNode = ({
}}
pos="absolute"
right="15px"
top="18px"
bottom="18px"
/>
)}
</HStack>
</Flex>
</PopoverTrigger>
{hasPopover(step) && (
{hasSettingsPopover(step) && (
<SettingsPopoverContent step={step} onExpandClick={onModalOpen} />
)}
{hasContentPopover(step) && <ContentPopover step={step} />}
<SettingsModal isOpen={isModalOpen} onClose={onModalClose}>
<StepSettings step={step} />
</SettingsModal>
@ -203,5 +200,10 @@ export const StepNode = ({
)
}
const hasPopover = (step: Step) =>
isInputStep(step) || isLogicStep(step) || isIntegrationStep(step)
const hasSettingsPopover = (step: Step): step is Exclude<Step, BubbleStep> =>
!isBubbleStep(step)
const hasContentPopover = (
step: Step
): step is Exclude<BubbleStep, TextBubbleStep> =>
isBubbleStep(step) && !isTextBubbleStep(step)

View File

@ -1,4 +1,4 @@
import { Flex, HStack, Stack, Tag, Text } from '@chakra-ui/react'
import { Box, Flex, HStack, Image, Stack, Tag, Text } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext'
import {
Step,
@ -34,6 +34,20 @@ export const StepNodeContent = ({ step }: Props) => {
/>
)
}
case BubbleStepType.IMAGE: {
return !step.content?.url ? (
<Text color={'gray.500'}>Click to edit...</Text>
) : (
<Box w="full">
<Image
src={step.content?.url}
alt="Step image"
rounded="md"
objectFit="cover"
/>
</Box>
)
}
case InputStepType.TEXT: {
return (
<Text color={'gray.500'}>

View File

@ -12,7 +12,7 @@ import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { BaseSelection, createEditor, Transforms } from 'slate'
import { ToolBar } from './ToolBar'
import { parseHtmlStringToPlainText } from 'services/utils'
import { TextStep, Variable } from 'models'
import { TextBubbleStep, Variable } from 'models'
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
import { ReactEditor } from 'slate-react'
@ -87,7 +87,7 @@ export const TextEditor = ({
richText: value,
plainText: parseHtmlStringToPlainText(html),
},
} as TextStep)
} as TextBubbleStep)
}
const handleMouseDown = (e: React.MouseEvent) => {

View File

@ -0,0 +1,61 @@
import { Flex, Input, Stack } from '@chakra-ui/react'
import { GiphyFetch } from '@giphy/js-fetch-api'
import { Grid, SearchContext } from '@giphy/react-components'
import { GiphyLogo } from 'assets/logos'
import React, { useContext, useState, useEffect } from 'react'
import { useDebounce } from 'use-debounce'
type GiphySearchProps = {
onSubmit: (url: string) => void
}
const giphyFetch = new GiphyFetch(
process.env.NEXT_PUBLIC_GIPHY_API_KEY as string
)
export const GiphySearch = ({ onSubmit }: GiphySearchProps) => {
const { fetchGifs, searchKey, setSearch } = useContext(SearchContext)
const fetchGifsTrending = (offset: number) =>
giphyFetch.trending({ offset, limit: 10 })
const [inputValue, setInputValue] = useState('')
const [debouncedInputValue] = useDebounce(inputValue, 300)
useEffect(() => {
if (debouncedInputValue === '') return
setSearch(debouncedInputValue)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedInputValue])
const updateUrl = (url: string) => {
onSubmit(url)
}
return (
<Stack>
<Flex align="center">
<Input
flex="1"
autoFocus
placeholder="Search..."
onChange={(e) => setInputValue(e.target.value)}
value={inputValue}
/>
<GiphyLogo w="100px" />
</Flex>
<Flex overflowY="scroll" maxH="400px">
<Grid
onGifClick={(gif, e) => {
e.preventDefault()
updateUrl(gif.images.downsized.url)
}}
key={searchKey}
fetchGifs={searchKey === '' ? fetchGifsTrending : fetchGifs}
width={475}
columns={3}
className="my-4"
/>
</Flex>
</Stack>
)
}

View File

@ -0,0 +1,119 @@
import { ChangeEvent, FormEvent, useState } from 'react'
import { Button, HStack, Input, Stack } from '@chakra-ui/react'
import { SearchContextManager } from '@giphy/react-components'
import { UploadButton } from '../buttons/UploadButton'
import { GiphySearch } from './GiphySearch'
import { useTypebot } from 'contexts/TypebotContext'
type Props = {
url?: string
onSubmit: (url: string) => void
}
export const ImageUploadContent = ({ url, onSubmit }: Props) => {
const [currentTab, setCurrentTab] = useState<'link' | 'upload' | 'giphy'>(
'upload'
)
return (
<Stack>
<HStack>
<Button
variant={currentTab === 'upload' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('upload')}
size="sm"
>
Upload
</Button>
<Button
variant={currentTab === 'link' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('link')}
size="sm"
>
Embed link
</Button>
{process.env.NEXT_PUBLIC_GIPHY_API_KEY && (
<Button
variant={currentTab === 'giphy' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('giphy')}
size="sm"
>
Giphy
</Button>
)}
</HStack>
<BodyContent tab={currentTab} onSubmit={onSubmit} url={url} />
</Stack>
)
}
const BodyContent = ({
tab,
url,
onSubmit,
}: {
tab: 'upload' | 'link' | 'giphy'
url?: string
onSubmit: (url: string) => void
}) => {
switch (tab) {
case 'upload':
return <UploadFileContent onNewUrl={onSubmit} />
case 'link':
return <EmbedLinkContent initialUrl={url} onNewUrl={onSubmit} />
case 'giphy':
return <GiphyContent onNewUrl={onSubmit} />
}
}
type ContentProps = { initialUrl?: string; onNewUrl: (url: string) => void }
const UploadFileContent = ({ onNewUrl }: ContentProps) => {
const { typebot } = useTypebot()
return (
<Stack>
<UploadButton
filePath={`typebots/${typebot?.id}`}
onFileUploaded={onNewUrl}
includeFileName
colorScheme="blue"
>
Choose an image
</UploadButton>
</Stack>
)
}
const EmbedLinkContent = ({ initialUrl, onNewUrl }: ContentProps) => {
const [imageUrl, setImageUrl] = useState(initialUrl ?? '')
const handleImageUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
setImageUrl(e.target.value)
const handleUrlSubmit = (e: FormEvent) => {
e.preventDefault()
onNewUrl(imageUrl)
}
return (
<Stack as="form" onSubmit={handleUrlSubmit}>
<Input
placeholder={'Paste the image link...'}
onChange={handleImageUrlChange}
value={imageUrl}
/>
<Button type="submit" disabled={imageUrl === ''} colorScheme="blue">
Embed image
</Button>
</Stack>
)
}
const GiphyContent = ({ onNewUrl }: ContentProps) => (
<SearchContextManager
apiKey={process.env.NEXT_PUBLIC_GIPHY_API_KEY as string}
>
<GiphySearch onSubmit={onNewUrl} />
</SearchContextManager>
)

View File

@ -0,0 +1 @@
export { ImageUploadContent as ImagePopoverContent } from './ImageUploadContent'

View File

@ -1,19 +1,37 @@
import { Button, ButtonProps, chakra } from '@chakra-ui/react'
import React, { ChangeEvent } from 'react'
import React, { ChangeEvent, useState } from 'react'
import { compressFile, uploadFile } from 'services/utils'
type UploadButtonProps = { onUploadChange: (file: File) => void } & ButtonProps
type UploadButtonProps = {
filePath: string
includeFileName?: boolean
onFileUploaded: (url: string) => void
} & ButtonProps
export const UploadButton = ({
onUploadChange,
filePath,
includeFileName,
onFileUploaded,
...props
}: UploadButtonProps) => {
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const [isUploading, setIsUploading] = useState(false)
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target?.files) return
onUploadChange(e.target.files[0])
setIsUploading(true)
const file = e.target.files[0]
const { url } = await uploadFile(
await compressFile(file),
filePath + (includeFileName ? `/${file.name}` : '')
)
if (url) onFileUploaded(url)
setIsUploading(false)
}
return (
<>
<chakra.input
data-testid="file-upload-input"
type="file"
id="file-input"
display="none"
@ -25,6 +43,7 @@ export const UploadButton = ({
size="sm"
htmlFor="file-input"
cursor="pointer"
isLoading={isUploading}
{...props}
>
{props.children}

View File

@ -1,6 +1,6 @@
import { userIds } from 'cypress/plugins/data'
describe('Dashboard page', () => {
describe('Account page', () => {
before(() => {
cy.intercept({
url: 'https://s3.eu-west-3.amazonaws.com/typebot',
@ -35,7 +35,7 @@ describe('Dashboard page', () => {
.should('have.attr', 'src')
.should(
'include',
`https://s3.eu-west-3.amazonaws.com/typebot/${userIds[0]}/avatar`
`https://s3.eu-west-3.amazonaws.com/typebot/users/${userIds[0]}/avatar`
)
cy.findByRole('button', { name: 'Save' }).should('exist').click()
cy.wait('@getUpdatedSession')
@ -45,7 +45,7 @@ describe('Dashboard page', () => {
.should('have.attr', 'src')
.should(
'include',
`https://s3.eu-west-3.amazonaws.com/typebot/${userIds[0]}/avatar`
`https://s3.eu-west-3.amazonaws.com/typebot/users/${userIds[0]}/avatar`
)
cy.findByRole('button', { name: 'Save' }).should('not.exist')
})

View File

@ -0,0 +1,86 @@
import { createTypebotWithStep } from 'cypress/plugins/data'
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
import { getIframeBody } from 'cypress/support'
import { BubbleStepType, Step } from 'models'
const unsplashImageSrc =
'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
describe('Image bubbles', () => {
before(() => {
cy.intercept({
url: 'https://s3.eu-west-3.amazonaws.com/typebot',
method: 'POST',
}).as('postImage')
})
afterEach(() => {
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
describe('Content settings', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({
type: BubbleStepType.IMAGE,
} as Omit<Step, 'id' | 'blockId'>)
cy.signOut()
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByText('Click to edit...').click()
})
it('upload image file correctly', () => {
cy.findByTestId('file-upload-input').attachFile('avatar.jpg')
cy.wait('@postImage')
cy.findByRole('img')
.should('have.attr', 'src')
.should(
'include',
`https://s3.eu-west-3.amazonaws.com/typebot/typebots/typebot3/avatar.jpg`
)
})
it('should import image links correctly', () => {
cy.findByRole('button', { name: 'Embed link' }).click()
cy.findByPlaceholderText('Paste the image link...')
.clear()
.type(unsplashImageSrc)
cy.findByRole('button', { name: 'Embed image' }).click()
cy.findByRole('img')
.should('have.attr', 'src')
.should('include', unsplashImageSrc)
})
it.only('should import giphy gifs correctly', () => {
cy.findByRole('button', { name: 'Giphy' }).click()
cy.findAllByRole('img').eq(3).click()
cy.findAllByRole('img')
.first()
.should('have.attr', 'src')
.should('contain', `giphy.com/media`)
})
})
describe('Preview', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({
type: BubbleStepType.IMAGE,
content: {
url: unsplashImageSrc,
},
} as Omit<Step, 'id' | 'blockId'>)
cy.signOut()
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
})
it('should display correctly', () => {
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByRole('img')
.should('have.attr', 'src')
.should('eq', unsplashImageSrc)
})
})
})

View File

@ -1,43 +1,15 @@
import { userIds } from 'cypress/plugins/data'
import {
parseTestTypebot,
preventUserFromRefreshing,
} from 'cypress/plugins/utils'
import { BubbleStepType } from 'models'
import { createTypebotWithStep } from 'cypress/plugins/data'
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
import { getIframeBody } from 'cypress/support'
import { BubbleStepType, Step } from 'models'
describe('Text bubbles', () => {
beforeEach(() => {
cy.task('seed')
cy.task(
'createTypebot',
parseTestTypebot({
id: 'typebot3',
name: 'Typebot #3',
ownerId: userIds[1],
steps: {
byId: {
step1: {
id: 'step1',
blockId: 'block1',
type: BubbleStepType.TEXT,
content: { html: '', plainText: '', richText: [] },
},
},
allIds: ['step1'],
},
blocks: {
byId: {
block1: {
id: 'block1',
graphCoordinates: { x: 400, y: 200 },
title: 'Block #1',
stepIds: ['step1'],
},
},
allIds: ['block1'],
},
})
)
createTypebotWithStep({
type: BubbleStepType.TEXT,
content: { html: '', plainText: '', richText: [] },
} as Omit<Step, 'id' | 'blockId'>)
cy.signOut()
})
@ -78,11 +50,3 @@ describe('Text bubbles', () => {
.should('contain.text', 'Underlined text')
})
})
const getIframeBody = () => {
return cy
.get('#typebot-iframe')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
}

View File

@ -1,7 +1,7 @@
import path from 'path'
import { parse } from 'papaparse'
describe('ResultsPage', () => {
describe('Results page', () => {
beforeEach(() => {
cy.intercept({ url: '/api/typebots/typebot2/results*', method: 'GET' }).as(
'getResults'
@ -35,7 +35,7 @@ describe('ResultsPage', () => {
cy.findByText('content50').should('not.exist')
cy.findByText('content199').should('exist')
cy.findByTestId('table-wrapper').scrollTo('bottom')
cy.findByText('content149').should('exist')
cy.findByText('content149', { timeout: 10000 }).should('exist')
cy.findByTestId('table-wrapper').scrollTo('bottom')
cy.findByText('content99').should('exist')
cy.findByTestId('table-wrapper').scrollTo('bottom')

View File

@ -16,6 +16,9 @@
"@dnd-kit/sortable": "^5.1.0",
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@giphy/js-fetch-api": "^4.1.2",
"@giphy/js-types": "^4.1.0",
"@giphy/react-components": "^5.4.0",
"@googleapis/drive": "^2.1.0",
"@next-auth/prisma-adapter": "next",
"@udecode/plate-basic-marks": "^9.0.0",
@ -26,6 +29,7 @@
"@udecode/plate-ui-toolbar": "^9.0.0",
"aws-sdk": "^2.1051.0",
"bot-engine": "*",
"browser-image-compression": "^1.0.17",
"db": "*",
"fast-equals": "^2.0.4",
"focus-visible": "^5.2.0",

View File

@ -8,39 +8,35 @@ const handler = async (
req: NextApiRequest,
res: NextApiResponse
): Promise<void> => {
try {
res.setHeader('Access-Control-Allow-Origin', '*')
if (req.method === 'GET') {
const session = await getSession({ req })
if (!session) {
res.status(401)
return
}
aws.config.update({
accessKeyId: process.env.S3_UPLOAD_KEY,
secretAccessKey: process.env.S3_UPLOAD_SECRET,
region: process.env.S3_UPLOAD_REGION,
signatureVersion: 'v4',
})
const s3 = new aws.S3()
const post = s3.createPresignedPost({
Bucket: process.env.S3_UPLOAD_BUCKET,
Fields: {
ACL: 'public-read',
key: req.query.key,
'Content-Type': req.query.fileType,
},
Expires: 120, // seconds
Conditions: [['content-length-range', 0, maxUploadFileSize]],
})
return res.status(200).json(post)
res.setHeader('Access-Control-Allow-Origin', '*')
if (req.method === 'GET') {
const session = await getSession({ req })
if (!session) {
res.status(401)
return
}
return methodNotAllowed(res)
} catch (err) {
console.log(err)
aws.config.update({
accessKeyId: process.env.S3_UPLOAD_KEY,
secretAccessKey: process.env.S3_UPLOAD_SECRET,
region: process.env.S3_UPLOAD_REGION,
signatureVersion: 'v4',
})
const s3 = new aws.S3()
const post = s3.createPresignedPost({
Bucket: process.env.S3_UPLOAD_BUCKET,
Fields: {
ACL: 'public-read',
key: req.query.key,
'Content-Type': req.query.fileType,
},
Expires: 120, // seconds
Conditions: [['content-length-range', 0, maxUploadFileSize]],
})
return res.status(200).json(post)
}
return methodNotAllowed(res)
}
export default handler

View File

@ -1,6 +1,6 @@
import {
Block,
TextStep,
TextBubbleStep,
PublicTypebot,
BackgroundType,
Settings,
@ -119,7 +119,7 @@ export const parseNewStep = (
const id = `s${shortId.generate()}`
switch (type) {
case BubbleStepType.TEXT: {
const textStep: Pick<TextStep, 'type' | 'content'> = {
const textStep: Pick<TextBubbleStep, 'type' | 'content'> = {
type,
content: { html: '', richText: [], plainText: '' },
}

View File

@ -1,3 +1,4 @@
import imageCompression from 'browser-image-compression'
import { Parser } from 'htmlparser2'
import { Step } from 'models'
@ -78,6 +79,16 @@ export const uploadFile = async (file: File, key: string) => {
}
}
export const compressFile = async (file: File) => {
const options = {
maxSizeMB: 0.5,
maxWidthOrHeight: 1600,
}
return ['image/jpg', 'image/jpeg', 'image/png'].includes(file.type)
? imageCompression(file, options)
: file
}
export const removeUndefinedFields = <T>(obj: T): T =>
Object.keys(obj).reduce(
(acc, key) =>

View File

@ -13,7 +13,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
column: req.query['referenceCell[column]'],
value: req.query['referenceCell[value]'],
} as Cell
console.log(referenceCell)
const extractingColumns = req.query.columns as string[]
const doc = new GoogleSpreadsheet(spreadsheetId)
doc.useOAuth2Client(await getAuthenticatedGoogleClient(credentialsId))

View File

@ -4,14 +4,14 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { ChatStep } from './ChatStep'
import { AvatarSideContainer } from './AvatarSideContainer'
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
import { Edge, Step, Target } from 'models'
import { Step } from 'models'
import { useTypebot } from '../../contexts/TypebotContext'
import {
isBubbleStep,
isChoiceInput,
isInputStep,
isIntegrationStep,
isLogicStep,
isTextBubbleStep,
} from 'utils'
import { executeLogic } from 'services/logic'
import { getSingleChoiceTargetId } from 'services/inputs'
@ -104,7 +104,7 @@ export const ChatBlock = ({
<div className="flex flex-col w-full">
<TransitionGroup>
{displayedSteps
.filter((step) => isInputStep(step) || isTextBubbleStep(step))
.filter((step) => isInputStep(step) || isBubbleStep(step))
.map((step) => (
<CSSTransition
key={step.id}

View File

@ -3,11 +3,11 @@ import { useAnswers } from '../../../contexts/AnswersContext'
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
import { InputStep, InputStepType, Step } from 'models'
import { GuestBubble } from './bubbles/GuestBubble'
import { HostMessageBubble } from './bubbles/HostMessageBubble'
import { TextForm } from './inputs/TextForm'
import { isInputStep, isTextBubbleStep } from 'utils'
import { isBubbleStep, isInputStep } from 'utils'
import { DateForm } from './inputs/DateForm'
import { ChoiceForm } from './inputs/ChoiceForm'
import { HostBubble } from './bubbles/HostBubble'
export const ChatStep = ({
step,
@ -23,8 +23,8 @@ export const ChatStep = ({
onTransitionEnd(content)
}
if (isTextBubbleStep(step))
return <HostMessageBubble step={step} onTransitionEnd={onTransitionEnd} />
if (isBubbleStep(step))
return <HostBubble step={step} onTransitionEnd={onTransitionEnd} />
if (isInputStep(step))
return <InputChatStep step={step} onSubmit={handleInputSubmit} />
return <span>No step</span>

View File

@ -0,0 +1,18 @@
import { BubbleStep, BubbleStepType } from 'models'
import React from 'react'
import { ImageBubble } from './ImageBubble'
import { TextBubble } from './TextBubble'
type Props = {
step: BubbleStep
onTransitionEnd: () => void
}
export const HostBubble = ({ step, onTransitionEnd }: Props) => {
switch (step.type) {
case BubbleStepType.TEXT:
return <TextBubble step={step} onTransitionEnd={onTransitionEnd} />
case BubbleStepType.IMAGE:
return <ImageBubble step={step} onTransitionEnd={onTransitionEnd} />
}
}

View File

@ -0,0 +1,91 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useHostAvatars } from 'contexts/HostAvatarsContext'
import { useTypebot } from 'contexts/TypebotContext'
import { ImageBubbleStep } from 'models'
import { TypingContent } from './TypingContent'
import { parseVariables } from 'services/variable'
type Props = {
step: ImageBubbleStep
onTransitionEnd: () => void
}
export const showAnimationDuration = 400
export const mediaLoadingFallbackTimeout = 5000
export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
const { typebot } = useTypebot()
const { updateLastAvatarOffset } = useHostAvatars()
const messageContainer = useRef<HTMLDivElement | null>(null)
const image = useRef<HTMLImageElement | null>(null)
const [isTyping, setIsTyping] = useState(true)
const url = useMemo(
() =>
parseVariables({ text: step.content?.url, variables: typebot.variables }),
[typebot.variables]
)
useEffect(() => {
showContentAfterMediaLoad()
}, [])
const showContentAfterMediaLoad = () => {
if (!image.current) return
const timeout = setTimeout(() => {
setIsTyping(false)
onTypingEnd()
}, mediaLoadingFallbackTimeout)
image.current.onload = () => {
clearTimeout(timeout)
setIsTyping(false)
onTypingEnd()
}
}
const onTypingEnd = () => {
setIsTyping(false)
setTimeout(() => {
sendAvatarOffset()
onTransitionEnd()
}, showAnimationDuration)
}
const sendAvatarOffset = () => {
if (!messageContainer.current) return
const containerDimensions = messageContainer.current.getBoundingClientRect()
updateLastAvatarOffset(containerDimensions.top + containerDimensions.height)
}
return (
<div className="flex flex-col" ref={messageContainer}>
<div className="flex mb-2 w-full lg:w-11/12 items-center">
<div className={'flex relative z-10 items-start typebot-host-bubble'}>
<div
className="flex items-center absolute px-4 py-2 rounded-lg bubble-typing z-10 "
style={{
width: isTyping ? '4rem' : '100%',
height: isTyping ? '2rem' : '100%',
}}
>
{isTyping ? <TypingContent /> : <></>}
</div>
<img
ref={image}
src={url}
className={
'p-4 content-opacity z-10 w-auto ' +
(isTyping ? 'opacity-0' : 'opacity-100')
}
style={{
maxHeight: '32rem',
height: isTyping ? '2rem' : 'auto',
maxWidth: '100%',
}}
/>
</div>
</div>
</div>
)
}

View File

@ -1,24 +1,19 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useHostAvatars } from 'contexts/HostAvatarsContext'
import { useTypebot } from 'contexts/TypebotContext'
import { BubbleStepType, TextStep } from 'models'
import { BubbleStepType, TextBubbleStep } from 'models'
import { computeTypingTimeout } from 'services/chat'
import { TypingContent } from './TypingContent'
import { parseVariables } from 'services/variable'
type HostMessageBubbleProps = {
step: TextStep
type Props = {
step: TextBubbleStep
onTransitionEnd: () => void
}
export const showAnimationDuration = 400
export const mediaLoadingFallbackTimeout = 5000
export const HostMessageBubble = ({
step,
onTransitionEnd,
}: HostMessageBubbleProps) => {
export const TextBubble = ({ step, onTransitionEnd }: Props) => {
const { typebot } = useTypebot()
const { typingEmulation } = typebot.settings
const { updateLastAvatarOffset } = useHostAvatars()

View File

@ -1,12 +1,28 @@
import { StepBase } from '.'
export type BubbleStep = TextStep
export type BubbleStep = TextBubbleStep | ImageBubbleStep
export enum BubbleStepType {
TEXT = 'text',
IMAGE = 'image',
}
export type TextStep = StepBase & {
export type TextBubbleStep = StepBase & {
type: BubbleStepType.TEXT
content: { html: string; richText: unknown[]; plainText: string }
content: TextBubbleContent
}
export type ImageBubbleStep = StepBase & {
type: BubbleStepType.IMAGE
content?: ImageBubbleContent
}
export type TextBubbleContent = {
html: string
richText: unknown[]
plainText: string
}
export type ImageBubbleContent = {
url?: string
}

View File

@ -12,7 +12,7 @@ import {
Step,
Table,
TextInputStep,
TextStep,
TextBubbleStep,
} from 'models'
export const sendRequest = async <ResponseData>({
@ -57,7 +57,7 @@ export const isBubbleStep = (step: Step): step is BubbleStep =>
export const isLogicStep = (step: Step): step is LogicStep =>
(Object.values(LogicStepType) as string[]).includes(step.type)
export const isTextBubbleStep = (step: Step): step is TextStep =>
export const isTextBubbleStep = (step: Step): step is TextBubbleStep =>
step.type === BubbleStepType.TEXT
export const isTextInputStep = (step: Step): step is TextInputStep =>

248
yarn.lock
View File

@ -824,6 +824,16 @@
source-map "^0.5.7"
stylis "4.0.13"
"@emotion/cache@^10.0.27":
version "10.0.29"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==
dependencies:
"@emotion/sheet" "0.9.4"
"@emotion/stylis" "0.8.5"
"@emotion/utils" "0.11.3"
"@emotion/weak-memoize" "0.2.5"
"@emotion/cache@^11.7.1":
version "11.7.1"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539"
@ -835,12 +845,33 @@
"@emotion/weak-memoize" "^0.2.5"
stylis "4.0.13"
"@emotion/hash@^0.8.0":
"@emotion/core@10.1.1":
version "10.1.1"
resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.1.1.tgz#c956c1365f2f2481960064bcb8c4732e5fb612c3"
integrity sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==
dependencies:
"@babel/runtime" "^7.5.5"
"@emotion/cache" "^10.0.27"
"@emotion/css" "^10.0.27"
"@emotion/serialize" "^0.11.15"
"@emotion/sheet" "0.9.4"
"@emotion/utils" "0.11.3"
"@emotion/css@^10.0.27":
version "10.0.27"
resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c"
integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==
dependencies:
"@emotion/serialize" "^0.11.15"
"@emotion/utils" "0.11.3"
babel-plugin-emotion "^10.0.27"
"@emotion/hash@0.8.0", "@emotion/hash@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
"@emotion/is-prop-valid@^0.8.2", "@emotion/is-prop-valid@^0.8.8":
"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.2", "@emotion/is-prop-valid@^0.8.8":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
@ -877,6 +908,17 @@
"@emotion/weak-memoize" "^0.2.5"
hoist-non-react-statics "^3.3.1"
"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16":
version "0.11.16"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad"
integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==
dependencies:
"@emotion/hash" "0.8.0"
"@emotion/memoize" "0.7.4"
"@emotion/unitless" "0.7.5"
"@emotion/utils" "0.11.3"
csstype "^2.5.7"
"@emotion/serialize@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965"
@ -888,11 +930,34 @@
"@emotion/utils" "^1.0.0"
csstype "^3.0.2"
"@emotion/sheet@0.9.4":
version "0.9.4"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5"
integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==
"@emotion/sheet@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2"
integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==
"@emotion/styled-base@^10.0.27":
version "10.3.0"
resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.3.0.tgz#9aa2c946100f78b47316e4bc6048321afa6d4e36"
integrity sha512-PBRqsVKR7QRNkmfH78hTSSwHWcwDpecH9W6heujWAcyp2wdz/64PP73s7fWS1dIPm8/Exc8JAzYS8dEWXjv60w==
dependencies:
"@babel/runtime" "^7.5.5"
"@emotion/is-prop-valid" "0.8.8"
"@emotion/serialize" "^0.11.15"
"@emotion/utils" "0.11.3"
"@emotion/styled@10.0.27":
version "10.0.27"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.0.27.tgz#12cb67e91f7ad7431e1875b1d83a94b814133eaf"
integrity sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q==
dependencies:
"@emotion/styled-base" "^10.0.27"
babel-plugin-emotion "^10.0.27"
"@emotion/styled@^11.6.0":
version "11.6.0"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.6.0.tgz#9230d1a7bcb2ebf83c6a579f4c80e0664132d81d"
@ -904,22 +969,27 @@
"@emotion/serialize" "^1.0.2"
"@emotion/utils" "^1.0.0"
"@emotion/stylis@^0.8.4":
"@emotion/stylis@0.8.5", "@emotion/stylis@^0.8.4":
version "0.8.5"
resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04"
integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
"@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5":
"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
"@emotion/utils@0.11.3":
version "0.11.3"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924"
integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
"@emotion/utils@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af"
integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==
"@emotion/weak-memoize@^0.2.5":
"@emotion/weak-memoize@0.2.5", "@emotion/weak-memoize@^0.2.5":
version "0.2.5"
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
@ -939,6 +1009,63 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@giphy/js-analytics@^4.0.7":
version "4.0.7"
resolved "https://registry.yarnpkg.com/@giphy/js-analytics/-/js-analytics-4.0.7.tgz#98f5179e933719da00c9f01ae70d9205449026c3"
integrity sha512-s4+GUXWwyxJVm6i7GHiQvQlMaXkHGCkh4uqjpisX5IiHxTNheSDMHHX0SyRLpTL5rdnvBkiBxlH8iOv9w3pNwg==
dependencies:
"@giphy/js-types" "^4.1.0"
"@giphy/js-util" "^4.0.1"
append-query "^2.1.0"
throttle-debounce "^3.0.1"
"@giphy/js-brand@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@giphy/js-brand/-/js-brand-2.0.4.tgz#3a1e31aa7861088963ff634418078f6ec70c5f02"
integrity sha512-q2iRyRWmKpCLAt1G7LzcHjw8s/cvSSoA1SfoQL47Tx0/yGwo8xCiFcFPFZJZsCcVMLCwA7/UmxTkdRydQVhCNw==
dependencies:
emotion "10.0.27"
"@giphy/js-fetch-api@^4.1.2":
version "4.1.2"
resolved "https://registry.yarnpkg.com/@giphy/js-fetch-api/-/js-fetch-api-4.1.2.tgz#0c4a975567efa6591fd47d04d5d7f7012a2c527f"
integrity sha512-wDfDQu8HiVkLb+YXcZf8QFbznmMHWbg86ZBydYmnp2mfuHyaHKsz9n9PnxdH3RorMS9YM/Ca/zqAM5y89Qj+Hw==
dependencies:
"@giphy/js-types" "^4.1.0"
"@giphy/js-util" "^4.0.1"
qs "^6.9.4"
"@giphy/js-types@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@giphy/js-types/-/js-types-4.1.0.tgz#987b184bf848c81092ba61ab10895096eefa78de"
integrity sha512-+qSN4Mx4TmrjLQm4SC0I/ZBkb5eWM94sljXwfjIlqn0GMSR3geqEqwmE9Uf/ldgzFh+XMMCasQjIdUl2nWc++Q==
"@giphy/js-util@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@giphy/js-util/-/js-util-4.0.1.tgz#dc1987465cf69267a1aa92ae027c7de3d3dab3b2"
integrity sha512-46wXgt5Y+MxZjuzjE6JlvMLE+6Vlag+PYxbyTxpsunhmOKNoYK99d51E09iynmXTFuZWYgWJR9LcfnzqsWHy+Q==
dependencies:
"@giphy/js-types" "^4.1.0"
dompurify "^2.2.2"
uuid "^8.3.0"
"@giphy/react-components@^5.4.0":
version "5.4.0"
resolved "https://registry.yarnpkg.com/@giphy/react-components/-/react-components-5.4.0.tgz#9f4135934554696eb7736fadaee0187876be68a5"
integrity sha512-n8BhFzVZoETPsdYVDBuqXc1yCzsX0lypEkNQAIuouY4VEoGfb4yvRT507O79FPLwYLAXxGWiGE9S2h/0TLJvLw==
dependencies:
"@emotion/core" "10.1.1"
"@emotion/styled" "10.0.27"
"@giphy/js-analytics" "^4.0.7"
"@giphy/js-brand" "^2.0.4"
"@giphy/js-fetch-api" "^4.1.2"
"@giphy/js-types" "^4.1.0"
"@giphy/js-util" "^4.0.1"
emotion-theming "10.0.27"
intersection-observer "^0.11.0"
react-use "17.2.4"
throttle-debounce "^3.0.1"
"@googleapis/drive@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@googleapis/drive/-/drive-2.1.0.tgz#2c62ce6184784f433725810edc5dc1798f9da4a3"
@ -1935,6 +2062,13 @@ anymatch@~3.1.1, anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
append-query@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/append-query/-/append-query-2.1.1.tgz#0682e8c3ad6f2fa01e78153c4c73a6283d2a88f6"
integrity sha512-adm0E8o1o7ay+HbkWvGIpNNeciLB/rxJ0heThHuzSSVq5zcdQ5/ZubFnUoY0imFmk6gZVghSpwoubLVtwi9EHQ==
dependencies:
extend "^3.0.2"
arch@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
@ -2142,7 +2276,23 @@ axobject-query@^2.2.0:
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==
babel-plugin-macros@^2.6.1:
babel-plugin-emotion@^10.0.27:
version "10.2.2"
resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz#a1fe3503cff80abfd0bdda14abd2e8e57a79d17d"
integrity sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@emotion/hash" "0.8.0"
"@emotion/memoize" "0.7.4"
"@emotion/serialize" "^0.11.16"
babel-plugin-macros "^2.0.0"
babel-plugin-syntax-jsx "^6.18.0"
convert-source-map "^1.5.0"
escape-string-regexp "^1.0.5"
find-root "^1.1.0"
source-map "^0.5.7"
babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.6.1:
version "2.8.0"
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==
@ -2243,6 +2393,14 @@ brorand@^1.0.1, brorand@^1.1.0:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
browser-image-compression@^1.0.17:
version "1.0.17"
resolved "https://registry.yarnpkg.com/browser-image-compression/-/browser-image-compression-1.0.17.tgz#5b97bb65297dadfee123be6c40ecb355c4758cf2"
integrity sha512-TMDh3gyNlVA5Vvn0D0AdWr33s2ftIeokHK406z8cYlLJ4ANAq1x6eMaSzqedDoZwUGyTVB+0rhNVaSFgM3YAZg==
dependencies:
core-js "^3.16.1"
uzip "0.20201231.0"
browserify-aes@^1.0.0, browserify-aes@^1.0.4:
version "1.2.0"
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
@ -2698,6 +2856,11 @@ core-js-pure@^3.19.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.20.2.tgz#5d263565f0e34ceeeccdc4422fae3e84ca6b8c0f"
integrity sha512-CmWHvSKn2vNL6p6StNp1EmMIfVY/pqn3JLAjfZQ8WZGPOlGoO92EkX9/Mk81i6GxvoPXjUqEQnpM3rJ5QxxIOg==
core-js@^3.16.1:
version "3.20.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.20.3.tgz#c710d0a676e684522f3db4ee84e5e18a9d11d69a"
integrity sha512-vVl8j8ph6tRS3B8qir40H7yw7voy17xL0piAjlbBUsH7WIfzoedL/ZOr1OV9FyZQLWXsayOJyV4tnRyXR85/ag==
core-util-is@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -2743,6 +2906,16 @@ create-ecdh@^4.0.0:
bn.js "^4.1.0"
elliptic "^6.5.3"
create-emotion@^10.0.27:
version "10.0.27"
resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-10.0.27.tgz#cb4fa2db750f6ca6f9a001a33fbf1f6c46789503"
integrity sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==
dependencies:
"@emotion/cache" "^10.0.27"
"@emotion/serialize" "^0.11.15"
"@emotion/sheet" "0.9.4"
"@emotion/utils" "0.11.3"
create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
@ -2948,6 +3121,11 @@ csso@^4.2.0:
dependencies:
css-tree "^1.1.2"
csstype@^2.5.7:
version "2.6.19"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.19.tgz#feeb5aae89020bb389e1f63669a5ed490e391caa"
integrity sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==
csstype@^3.0.2, csstype@^3.0.6, csstype@^3.0.9:
version "3.0.10"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
@ -3209,6 +3387,11 @@ domhandler@^4.2.0, domhandler@^4.2.2, domhandler@^4.3.0:
dependencies:
domelementtype "^2.2.0"
dompurify@^2.2.2:
version "2.3.4"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.4.tgz#1cf5cf0105ccb4debdf6db162525bd41e6ddacc6"
integrity sha512-6BVcgOAVFXjI0JTjEvZy901Rghm+7fDQOrNIcxB4+gdhj6Kwp6T9VBhBY/AbagKHJocRkDYGd6wvI+p4/10xtQ==
domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
@ -3286,6 +3469,23 @@ emojis-list@^2.0.0:
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
emotion-theming@10.0.27:
version "10.0.27"
resolved "https://registry.yarnpkg.com/emotion-theming/-/emotion-theming-10.0.27.tgz#1887baaec15199862c89b1b984b79806f2b9ab10"
integrity sha512-MlF1yu/gYh8u+sLUqA0YuA9JX0P4Hb69WlKc/9OLo+WCXuX6sy/KoIa+qJimgmr2dWqnypYKYPX37esjDBbhdw==
dependencies:
"@babel/runtime" "^7.5.5"
"@emotion/weak-memoize" "0.2.5"
hoist-non-react-statics "^3.3.0"
emotion@10.0.27:
version "10.0.27"
resolved "https://registry.yarnpkg.com/emotion/-/emotion-10.0.27.tgz#f9ca5df98630980a23c819a56262560562e5d75e"
integrity sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==
dependencies:
babel-plugin-emotion "^10.0.27"
create-emotion "^10.0.27"
encoding@0.1.13:
version "0.1.13"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
@ -4265,7 +4465,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.1:
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -4492,6 +4692,11 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
intersection-observer@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.11.0.tgz#f4ea067070326f68393ee161cc0a2ca4c0040c6f"
integrity sha512-KZArj2QVnmdud9zTpKf279m2bbGfG+4/kn16UU0NL3pTVl52ZHiJ9IRNSsnn6jaHrL9EGLFM5eWjTx2fz/+zoQ==
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@ -6280,7 +6485,7 @@ qs@^6.10.2, qs@^6.6.0:
dependencies:
side-channel "^1.0.4"
qs@^6.10.3, qs@^6.7.0:
qs@^6.10.3, qs@^6.7.0, qs@^6.9.4:
version "6.10.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==
@ -6487,6 +6692,26 @@ react-universal-interface@^0.6.2:
resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"
integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==
react-use@17.2.4:
version "17.2.4"
resolved "https://registry.yarnpkg.com/react-use/-/react-use-17.2.4.tgz#1f89be3db0a8237c79253db0a15e12bbe3cfeff1"
integrity sha512-vQGpsAM0F5UIlshw5UI8ULGPS4yn5rm7/qvn3T1Gnkrz7YRMEEMh+ynKcmRloOyiIeLvKWiQjMiwRGtdbgs5qQ==
dependencies:
"@types/js-cookie" "^2.2.6"
"@xobotyi/scrollbar-width" "^1.9.5"
copy-to-clipboard "^3.3.1"
fast-deep-equal "^3.1.3"
fast-shallow-equal "^1.0.0"
js-cookie "^2.2.1"
nano-css "^5.3.1"
react-universal-interface "^0.6.2"
resize-observer-polyfill "^1.5.1"
screenfull "^5.1.0"
set-harmonic-interval "^1.0.1"
throttle-debounce "^3.0.1"
ts-easing "^0.2.0"
tslib "^2.1.0"
react-use@^17.1.1:
version "17.3.2"
resolved "https://registry.yarnpkg.com/react-use/-/react-use-17.3.2.tgz#448abf515f47c41c32455024db28167cb6e53be8"
@ -7807,11 +8032,16 @@ uuid@3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
uuid@^8.0.0, uuid@^8.3.2:
uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uzip@0.20201231.0:
version "0.20201231.0"
resolved "https://registry.yarnpkg.com/uzip/-/uzip-0.20201231.0.tgz#9e64b065b9a8ebf26eb7583fe8e77e1d9a15ed14"
integrity sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"