♻️ Re-organize workspace folders
This commit is contained in:
160
packages/embeds/js/src/features/bubble/components/Bubble.tsx
Normal file
160
packages/embeds/js/src/features/bubble/components/Bubble.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
createSignal,
|
||||
onMount,
|
||||
Show,
|
||||
splitProps,
|
||||
onCleanup,
|
||||
createEffect,
|
||||
} from 'solid-js'
|
||||
import styles from '../../../assets/index.css'
|
||||
import { CommandData } from '../../commands'
|
||||
import { BubbleButton } from './BubbleButton'
|
||||
import { PreviewMessage, PreviewMessageProps } from './PreviewMessage'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { BubbleParams } from '../types'
|
||||
import { Bot, BotProps } from '../../../components/Bot'
|
||||
|
||||
export type BubbleProps = BotProps &
|
||||
BubbleParams & {
|
||||
onOpen?: () => void
|
||||
onClose?: () => void
|
||||
onPreviewMessageClick?: () => void
|
||||
}
|
||||
|
||||
export const Bubble = (props: BubbleProps) => {
|
||||
const [bubbleProps, botProps] = splitProps(props, [
|
||||
'onOpen',
|
||||
'onClose',
|
||||
'previewMessage',
|
||||
'onPreviewMessageClick',
|
||||
'theme',
|
||||
])
|
||||
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
botProps.prefilledVariables
|
||||
)
|
||||
const [isPreviewMessageDisplayed, setIsPreviewMessageDisplayed] =
|
||||
createSignal(false)
|
||||
const [previewMessage, setPreviewMessage] = createSignal<
|
||||
Pick<PreviewMessageProps, 'avatarUrl' | 'message'>
|
||||
>({
|
||||
message: bubbleProps.previewMessage?.message ?? '',
|
||||
avatarUrl: bubbleProps.previewMessage?.avatarUrl,
|
||||
})
|
||||
|
||||
const [isBotOpened, setIsBotOpened] = createSignal(false)
|
||||
const [isBotStarted, setIsBotStarted] = createSignal(false)
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('message', processIncomingEvent)
|
||||
const autoShowDelay = bubbleProps.previewMessage?.autoShowDelay
|
||||
if (isDefined(autoShowDelay)) {
|
||||
setTimeout(() => {
|
||||
showMessage()
|
||||
}, autoShowDelay)
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener('message', processIncomingEvent)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.prefilledVariables) return
|
||||
setPrefilledVariables((existingPrefilledVariables) => ({
|
||||
...existingPrefilledVariables,
|
||||
...props.prefilledVariables,
|
||||
}))
|
||||
})
|
||||
|
||||
const processIncomingEvent = (event: MessageEvent<CommandData>) => {
|
||||
const { data } = event
|
||||
if (!data.isFromTypebot) return
|
||||
if (data.command === 'open') openBot()
|
||||
if (data.command === 'close') closeBot()
|
||||
if (data.command === 'toggle') toggleBot()
|
||||
if (data.command === 'showPreviewMessage') showMessage(data.message)
|
||||
if (data.command === 'hidePreviewMessage') hideMessage()
|
||||
if (data.command === 'setPrefilledVariables')
|
||||
setPrefilledVariables((existingPrefilledVariables) => ({
|
||||
...existingPrefilledVariables,
|
||||
...data.variables,
|
||||
}))
|
||||
}
|
||||
|
||||
const openBot = () => {
|
||||
if (!isBotStarted()) setIsBotStarted(true)
|
||||
hideMessage()
|
||||
setIsBotOpened(true)
|
||||
if (isBotOpened()) bubbleProps.onOpen?.()
|
||||
}
|
||||
|
||||
const closeBot = () => {
|
||||
setIsBotOpened(false)
|
||||
if (isBotOpened()) bubbleProps.onClose?.()
|
||||
}
|
||||
|
||||
const toggleBot = () => {
|
||||
isBotOpened() ? closeBot() : openBot()
|
||||
}
|
||||
|
||||
const handlePreviewMessageClick = () => {
|
||||
bubbleProps.onPreviewMessageClick?.()
|
||||
openBot()
|
||||
}
|
||||
|
||||
const showMessage = (
|
||||
previewMessage?: Pick<PreviewMessageProps, 'avatarUrl' | 'message'>
|
||||
) => {
|
||||
if (previewMessage) setPreviewMessage(previewMessage)
|
||||
if (isBotOpened()) return
|
||||
setIsPreviewMessageDisplayed(true)
|
||||
}
|
||||
|
||||
const hideMessage = () => {
|
||||
setIsPreviewMessageDisplayed(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{styles}</style>
|
||||
<Show when={isPreviewMessageDisplayed()}>
|
||||
<PreviewMessage
|
||||
{...previewMessage()}
|
||||
previewMessageTheme={bubbleProps.theme?.previewMessage}
|
||||
onClick={handlePreviewMessageClick}
|
||||
onCloseClick={hideMessage}
|
||||
/>
|
||||
</Show>
|
||||
<BubbleButton
|
||||
{...bubbleProps.theme?.button}
|
||||
toggleBot={toggleBot}
|
||||
isBotOpened={isBotOpened()}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
height: 'calc(100% - 80px)',
|
||||
transition:
|
||||
'transform 200ms cubic-bezier(0, 1.2, 1, 1), opacity 150ms ease-out',
|
||||
'transform-origin': 'bottom right',
|
||||
transform: isBotOpened() ? 'scale3d(1, 1, 1)' : 'scale3d(0, 0, 1)',
|
||||
'box-shadow': 'rgb(0 0 0 / 16%) 0px 5px 40px',
|
||||
'background-color': bubbleProps.theme?.chatWindow?.backgroundColor,
|
||||
'z-index': 42424242,
|
||||
}}
|
||||
class={
|
||||
'fixed bottom-20 sm:right-4 rounded-lg w-full sm:w-[400px] max-h-[704px] ' +
|
||||
(isBotOpened() ? 'opacity-1' : 'opacity-0 pointer-events-none')
|
||||
}
|
||||
>
|
||||
<Show when={isBotStarted()}>
|
||||
<Bot
|
||||
{...botProps}
|
||||
prefilledVariables={prefilledVariables()}
|
||||
class="rounded-lg"
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Show } from 'solid-js'
|
||||
import { isNotDefined } from '@typebot.io/lib'
|
||||
import { ButtonTheme } from '../types'
|
||||
|
||||
type Props = ButtonTheme & {
|
||||
isBotOpened: boolean
|
||||
toggleBot: () => void
|
||||
}
|
||||
|
||||
const defaultButtonColor = '#0042DA'
|
||||
const defaultIconColor = 'white'
|
||||
|
||||
export const BubbleButton = (props: Props) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => props.toggleBot()}
|
||||
class={
|
||||
'fixed bottom-4 right-4 shadow-md w-12 h-12 rounded-full hover:scale-110 active:scale-95 transition-transform duration-200 flex justify-center items-center animate-fade-in'
|
||||
}
|
||||
style={{
|
||||
'background-color': props.backgroundColor ?? defaultButtonColor,
|
||||
'z-index': 42424242,
|
||||
}}
|
||||
>
|
||||
<Show when={isNotDefined(props.customIconSrc)} keyed>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
style={{
|
||||
stroke: props.iconColor ?? defaultIconColor,
|
||||
}}
|
||||
class={
|
||||
`w-7 stroke-2 fill-transparent absolute duration-200 transition ` +
|
||||
(props.isBotOpened ? 'scale-0 opacity-0' : 'scale-100 opacity-100')
|
||||
}
|
||||
>
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||||
</svg>
|
||||
</Show>
|
||||
<Show when={props.customIconSrc}>
|
||||
<img
|
||||
src={props.customIconSrc}
|
||||
class="w-7 h-7 rounded-full object-cover"
|
||||
alt="Bubble button icon"
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
style={{ fill: props.iconColor ?? 'white' }}
|
||||
class={
|
||||
`w-7 absolute duration-200 transition ` +
|
||||
(props.isBotOpened
|
||||
? 'scale-100 rotate-0 opacity-100'
|
||||
: 'scale-0 -rotate-180 opacity-0')
|
||||
}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M18.601 8.39897C18.269 8.06702 17.7309 8.06702 17.3989 8.39897L12 13.7979L6.60099 8.39897C6.26904 8.06702 5.73086 8.06702 5.39891 8.39897C5.06696 8.73091 5.06696 9.2691 5.39891 9.60105L11.3989 15.601C11.7309 15.933 12.269 15.933 12.601 15.601L18.601 9.60105C18.9329 9.2691 18.9329 8.73091 18.601 8.39897Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import { PreviewMessageParams, PreviewMessageTheme } from '../types'
|
||||
|
||||
export type PreviewMessageProps = Pick<
|
||||
PreviewMessageParams,
|
||||
'avatarUrl' | 'message'
|
||||
> & {
|
||||
previewMessageTheme?: PreviewMessageTheme
|
||||
onClick: () => void
|
||||
onCloseClick: () => void
|
||||
}
|
||||
|
||||
const defaultBackgroundColor = '#F7F8FF'
|
||||
const defaultTextColor = '#303235'
|
||||
|
||||
export const PreviewMessage = (props: PreviewMessageProps) => {
|
||||
const [isPreviewMessageHovered, setIsPreviewMessageHovered] =
|
||||
createSignal(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => props.onClick()}
|
||||
class="fixed bottom-20 right-4 max-w-[256px] rounded-md duration-200 flex items-center gap-4 shadow-md animate-fade-in cursor-pointer hover:shadow-lg p-4"
|
||||
style={{
|
||||
'background-color':
|
||||
props.previewMessageTheme?.backgroundColor ?? defaultBackgroundColor,
|
||||
color: props.previewMessageTheme?.textColor ?? defaultTextColor,
|
||||
'z-index': 42424242,
|
||||
}}
|
||||
onMouseEnter={() => setIsPreviewMessageHovered(true)}
|
||||
onMouseLeave={() => setIsPreviewMessageHovered(false)}
|
||||
>
|
||||
<CloseButton
|
||||
isHovered={isPreviewMessageHovered()}
|
||||
previewMessageTheme={props.previewMessageTheme}
|
||||
onClick={props.onCloseClick}
|
||||
/>
|
||||
<Show when={props.avatarUrl} keyed>
|
||||
{(avatarUrl) => (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
class="rounded-full w-8 h-8 object-cover"
|
||||
alt="Bot avatar"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
<p>{props.message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseButton = (props: {
|
||||
isHovered: boolean
|
||||
previewMessageTheme?: PreviewMessageTheme
|
||||
onClick: () => void
|
||||
}) => (
|
||||
<button
|
||||
class={
|
||||
`absolute -top-2 -right-2 rounded-full w-6 h-6 p-1 hover:brightness-95 active:brightness-90 transition-all border ` +
|
||||
(props.isHovered ? 'opacity-100' : 'opacity-0')
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
return props.onClick()
|
||||
}}
|
||||
style={{
|
||||
'background-color':
|
||||
props.previewMessageTheme?.closeButtonBackgroundColor ??
|
||||
defaultBackgroundColor,
|
||||
color:
|
||||
props.previewMessageTheme?.closeButtonIconColor ?? defaultTextColor,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Bubble'
|
||||
Reference in New Issue
Block a user