💄 (buttons) Improve multiple choice form UI
This commit is contained in:
@ -30,7 +30,7 @@ export const AudioBubble = (props: Props) => {
|
||||
|
||||
return (
|
||||
<div class="flex flex-col animate-fade-in">
|
||||
<div class="flex mb-2 w-full items-center">
|
||||
<div class="flex w-full items-center">
|
||||
<div class={'flex relative z-10 items-start typebot-host-bubble'}>
|
||||
<div
|
||||
class="flex items-center absolute px-4 py-2 bubble-typing z-10 "
|
||||
|
@ -29,7 +29,7 @@ export const EmbedBubble = (props: Props) => {
|
||||
|
||||
return (
|
||||
<div class="flex flex-col w-full animate-fade-in">
|
||||
<div class="flex mb-2 w-full items-center">
|
||||
<div class="flex w-full items-center">
|
||||
<div
|
||||
class={'flex relative z-10 items-start typebot-host-bubble w-full'}
|
||||
>
|
||||
|
@ -55,7 +55,7 @@ export const ImageBubble = (props: Props) => {
|
||||
|
||||
return (
|
||||
<div class="flex flex-col animate-fade-in">
|
||||
<div class="flex mb-2 w-full items-center">
|
||||
<div class="flex w-full items-center">
|
||||
<div class={'flex relative z-10 items-start typebot-host-bubble'}>
|
||||
<div
|
||||
class="flex items-center absolute px-4 py-2 bubble-typing z-10 "
|
||||
|
@ -51,7 +51,7 @@ export const TextBubble = (props: Props) => {
|
||||
|
||||
return (
|
||||
<div class="flex flex-col animate-fade-in">
|
||||
<div class="flex mb-2 w-full items-center">
|
||||
<div class="flex w-full items-center">
|
||||
<div class="flex relative items-start typebot-host-bubble">
|
||||
<div
|
||||
class="flex items-center absolute px-4 py-2 bubble-typing "
|
||||
|
@ -33,7 +33,7 @@ export const VideoBubble = (props: Props) => {
|
||||
|
||||
return (
|
||||
<div class="flex flex-col animate-fade-in">
|
||||
<div class="flex mb-2 w-full items-center">
|
||||
<div class="flex w-full items-center">
|
||||
<div class={'flex relative z-10 items-start typebot-host-bubble'}>
|
||||
<div
|
||||
class="flex items-center absolute px-4 py-2 bubble-typing z-10 "
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { Button } from '@/components/Button'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||
import { For } from 'solid-js'
|
||||
|
||||
type Props = {
|
||||
inputIndex: number
|
||||
items: ChoiceInputBlock['items']
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const Buttons = (props: Props) => {
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const handleClick = (itemIndex: number) => () =>
|
||||
props.onSubmit({ value: props.items[itemIndex].content ?? '' })
|
||||
|
||||
return (
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<For each={props.items}>
|
||||
{(item, index) => (
|
||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||
<Button
|
||||
on:click={handleClick(index())}
|
||||
data-itemid={item.id}
|
||||
class="w-full"
|
||||
>
|
||||
{item.content}
|
||||
</Button>
|
||||
{props.inputIndex === 0 && props.items.length === 1 && (
|
||||
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { CheckIcon } from '@/components/icons/CheckIcon'
|
||||
import { Show } from 'solid-js'
|
||||
|
||||
type Props = {
|
||||
isChecked: boolean
|
||||
}
|
||||
|
||||
export const Checkbox = (props: Props) => {
|
||||
return (
|
||||
<div
|
||||
class={'w-4 h-4 typebot-checkbox' + (props.isChecked ? ' checked' : '')}
|
||||
>
|
||||
<Show when={props.isChecked}>
|
||||
<CheckIcon />
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
import { SendButton } from '@/components/SendButton'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||
import { createSignal, For } from 'solid-js'
|
||||
|
||||
type Props = {
|
||||
inputIndex: number
|
||||
block: ChoiceInputBlock
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const ChoiceForm = (props: Props) => {
|
||||
const [selectedIndices, setSelectedIndices] = createSignal<number[]>([])
|
||||
|
||||
const handleClick = (itemIndex: number) => {
|
||||
if (props.block.options?.isMultipleChoice)
|
||||
toggleSelectedItemIndex(itemIndex)
|
||||
else props.onSubmit({ value: props.block.items[itemIndex].content ?? '' })
|
||||
}
|
||||
|
||||
const toggleSelectedItemIndex = (itemIndex: number) => {
|
||||
const existingIndex = selectedIndices().indexOf(itemIndex)
|
||||
if (existingIndex !== -1) {
|
||||
setSelectedIndices((selectedIndices) =>
|
||||
selectedIndices.filter((index) => index !== itemIndex)
|
||||
)
|
||||
} else {
|
||||
setSelectedIndices((selectedIndices) => [...selectedIndices, itemIndex])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () =>
|
||||
props.onSubmit({
|
||||
value: selectedIndices()
|
||||
.map((itemIndex) => props.block.items[itemIndex].content)
|
||||
.join(', '),
|
||||
})
|
||||
|
||||
return (
|
||||
<form class="flex flex-col items-end" onSubmit={handleSubmit}>
|
||||
<div class="flex flex-wrap justify-end">
|
||||
<For each={props.block.items}>
|
||||
{(item, index) => (
|
||||
<span class="relative inline-flex ml-2 mb-2">
|
||||
<button
|
||||
role={
|
||||
props.block.options?.isMultipleChoice ? 'checkbox' : 'button'
|
||||
}
|
||||
type="button"
|
||||
on:click={() => handleClick(index())}
|
||||
class={
|
||||
'py-2 px-4 text-left font-semibold transition-all filter hover:brightness-90 active:brightness-75 duration-100 focus:outline-none typebot-button ' +
|
||||
(selectedIndices().some(
|
||||
(selectedIndex) => selectedIndex === index()
|
||||
) || !props.block.options?.isMultipleChoice
|
||||
? ''
|
||||
: 'selectable')
|
||||
}
|
||||
data-itemid={item.id}
|
||||
>
|
||||
{item.content}
|
||||
</button>
|
||||
{props.inputIndex === 0 && props.block.items.length === 1 && (
|
||||
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="flex">
|
||||
{selectedIndices().length > 0 && (
|
||||
<SendButton disableIcon>
|
||||
{props.block.options?.buttonLabel ?? 'Send'}
|
||||
</SendButton>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import { SendButton } from '@/components/SendButton'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||
import { createSignal, For } from 'solid-js'
|
||||
import { Checkbox } from './Checkbox'
|
||||
|
||||
type Props = {
|
||||
inputIndex: number
|
||||
items: ChoiceInputBlock['items']
|
||||
options: ChoiceInputBlock['options']
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const MultipleChoicesForm = (props: Props) => {
|
||||
const [selectedIndices, setSelectedIndices] = createSignal<number[]>([])
|
||||
|
||||
const handleClick = (itemIndex: number) => {
|
||||
toggleSelectedItemIndex(itemIndex)
|
||||
}
|
||||
|
||||
const toggleSelectedItemIndex = (itemIndex: number) => {
|
||||
const existingIndex = selectedIndices().indexOf(itemIndex)
|
||||
if (existingIndex !== -1) {
|
||||
setSelectedIndices((selectedIndices) =>
|
||||
selectedIndices.filter((index) => index !== itemIndex)
|
||||
)
|
||||
} else {
|
||||
setSelectedIndices((selectedIndices) => [...selectedIndices, itemIndex])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () =>
|
||||
props.onSubmit({
|
||||
value: selectedIndices()
|
||||
.map((itemIndex) => props.items[itemIndex].content)
|
||||
.join(', '),
|
||||
})
|
||||
|
||||
return (
|
||||
<form class="flex flex-col items-end gap-2" onSubmit={handleSubmit}>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<For each={props.items}>
|
||||
{(item, index) => (
|
||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||
<div
|
||||
role="checkbox"
|
||||
aria-checked={selectedIndices().some(
|
||||
(selectedIndex) => selectedIndex === index()
|
||||
)}
|
||||
on:click={() => handleClick(index())}
|
||||
class={
|
||||
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable' +
|
||||
(selectedIndices().some(
|
||||
(selectedIndex) => selectedIndex === index()
|
||||
)
|
||||
? ' selected'
|
||||
: '')
|
||||
}
|
||||
data-itemid={item.id}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
isChecked={selectedIndices().some(
|
||||
(selectedIndex) => selectedIndex === index()
|
||||
)}
|
||||
/>
|
||||
<span>{item.content}</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="flex">
|
||||
{selectedIndices().length > 0 && (
|
||||
<SendButton disableIcon>
|
||||
{props.options?.buttonLabel ?? 'Send'}
|
||||
</SendButton>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { ChoiceForm } from './components/ChoiceForm'
|
@ -36,11 +36,11 @@ export const DateForm = (props: Props) => {
|
||||
<div
|
||||
class={
|
||||
'flex items-center p-4 ' +
|
||||
(props.options?.isRange ? 'pb-0' : '')
|
||||
(props.options?.isRange ? 'pb-0 gap-2' : '')
|
||||
}
|
||||
>
|
||||
{props.options?.isRange && (
|
||||
<p class="font-semibold mr-2">
|
||||
<p class="font-semibold">
|
||||
{props.options.labels?.from ?? 'From:'}
|
||||
</p>
|
||||
)}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { SendButton, Spinner } from '@/components/SendButton'
|
||||
import { SendButton } from '@/components/SendButton'
|
||||
import { BotContext, InputSubmitContent } from '@/types'
|
||||
import { guessApiHost } from '@/utils/guessApiHost'
|
||||
import { FileInputBlock } from '@typebot.io/schemas'
|
||||
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file'
|
||||
import { createSignal, Match, Show, Switch } from 'solid-js'
|
||||
import { uploadFiles } from '@typebot.io/lib'
|
||||
import { Button } from '@/components/Button'
|
||||
import { Spinner } from '@/components/Spinner'
|
||||
|
||||
type Props = {
|
||||
context: BotContext
|
||||
@ -111,12 +113,17 @@ export const FileUploadForm = (props: Props) => {
|
||||
|
||||
const clearFiles = () => setSelectedFiles([])
|
||||
|
||||
const skip = () =>
|
||||
props.onSkip(
|
||||
props.block.options.labels.skip ?? defaultFileInputOptions.labels.skip
|
||||
)
|
||||
|
||||
return (
|
||||
<form class="flex flex-col w-full" onSubmit={handleSubmit}>
|
||||
<form class="flex flex-col w-full gap-2" onSubmit={handleSubmit}>
|
||||
<label
|
||||
for="dropzone-file"
|
||||
class={
|
||||
'typebot-upload-input py-6 flex flex-col justify-center items-center w-full bg-gray-50 border-2 border-gray-300 border-dashed cursor-pointer hover:bg-gray-100 px-8 mb-2 ' +
|
||||
'typebot-upload-input py-6 flex flex-col justify-center items-center w-full bg-gray-50 border-2 border-gray-300 border-dashed cursor-pointer hover:bg-gray-100 px-8 ' +
|
||||
(isDraggingOver() ? 'dragging-over' : '')
|
||||
}
|
||||
onDragOver={handleDragOver}
|
||||
@ -179,20 +186,10 @@ export const FileUploadForm = (props: Props) => {
|
||||
}
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
class={
|
||||
'py-2 px-4 justify-center font-semibold text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button '
|
||||
}
|
||||
on:click={() =>
|
||||
props.onSkip(
|
||||
props.block.options.labels.skip ??
|
||||
defaultFileInputOptions.labels.skip
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button on:click={skip}>
|
||||
{props.block.options.labels.skip ??
|
||||
defaultFileInputOptions.labels.skip}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show
|
||||
@ -203,17 +200,12 @@ export const FileUploadForm = (props: Props) => {
|
||||
}
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<div class="flex">
|
||||
<div class="flex gap-2">
|
||||
<Show when={selectedFiles().length}>
|
||||
<button
|
||||
class={
|
||||
'secondary-button py-2 px-4 justify-center font-semibold text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 mr-2'
|
||||
}
|
||||
on:click={clearFiles}
|
||||
>
|
||||
<Button variant="secondary" on:click={clearFiles}>
|
||||
{props.block.options.labels.clear ??
|
||||
defaultFileInputOptions.labels.clear}
|
||||
</button>
|
||||
</Button>
|
||||
</Show>
|
||||
<SendButton type="submit" disableIcon>
|
||||
{props.block.options.labels.button ===
|
||||
|
@ -3,6 +3,7 @@ import { InputSubmitContent } from '@/types'
|
||||
import type { RatingInputBlock, RatingInputOptions } from '@typebot.io/schemas'
|
||||
import { createSignal, For, Match, Switch } from 'solid-js'
|
||||
import { isDefined, isEmpty, isNotDefined } from '@typebot.io/lib'
|
||||
import { Button } from '@/components/Button'
|
||||
|
||||
type Props = {
|
||||
block: RatingInputBlock
|
||||
@ -29,13 +30,13 @@ export const RatingForm = (props: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<form class="flex flex-col" onSubmit={handleSubmit}>
|
||||
<form class="flex flex-col gap-2" onSubmit={handleSubmit}>
|
||||
{props.block.options.labels.left && (
|
||||
<span class="text-sm w-full mb-2 rating-label">
|
||||
<span class="text-sm w-full rating-label">
|
||||
{props.block.options.labels.left}
|
||||
</span>
|
||||
)}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
<For
|
||||
each={Array.from(
|
||||
Array(
|
||||
@ -57,12 +58,12 @@ export const RatingForm = (props: Props) => {
|
||||
</For>
|
||||
</div>
|
||||
{props.block.options.labels.right && (
|
||||
<span class="text-sm w-full text-right mb-2 pr-2 rating-label">
|
||||
<span class="text-sm w-full text-right pr-2 rating-label">
|
||||
{props.block.options.labels.right}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div class="flex justify-end mr-2">
|
||||
<div class="flex justify-end">
|
||||
{isDefined(rating()) && (
|
||||
<SendButton disableIcon>
|
||||
{props.block.options?.labels?.button ?? 'Send'}
|
||||
@ -80,29 +81,29 @@ type RatingButtonProps = {
|
||||
} & RatingInputOptions
|
||||
|
||||
const RatingButton = (props: RatingButtonProps) => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
props.onClick(props.idx)
|
||||
}
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={props.buttonType === 'Numbers'}>
|
||||
<button
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
props.onClick(props.idx)
|
||||
}}
|
||||
<Button
|
||||
on:click={handleClick}
|
||||
class={
|
||||
'py-2 px-4 mr-2 mb-2 text-left font-semibold transition-all filter hover:brightness-90 active:brightness-75 duration-100 focus:outline-none typebot-button ' +
|
||||
(props.isOneClickSubmitEnabled ||
|
||||
props.isOneClickSubmitEnabled ||
|
||||
(isDefined(props.rating) && props.idx <= props.rating)
|
||||
? ''
|
||||
: 'selectable')
|
||||
: 'selectable'
|
||||
}
|
||||
>
|
||||
{props.idx}
|
||||
</button>
|
||||
</Button>
|
||||
</Match>
|
||||
<Match when={props.buttonType !== 'Numbers'}>
|
||||
<div
|
||||
class={
|
||||
'flex justify-center items-center rating-icon-container cursor-pointer mr-2 mb-2 ' +
|
||||
'flex justify-center items-center rating-icon-container cursor-pointer ' +
|
||||
(isDefined(props.rating) && props.idx <= props.rating
|
||||
? 'selected'
|
||||
: '')
|
||||
|
Reference in New Issue
Block a user