feat(engine): Add Rating input

This commit is contained in:
Baptiste Arnaud
2022-06-07 19:09:08 +02:00
parent 301097623b
commit b1aecf843b
19 changed files with 455 additions and 28 deletions

View File

@@ -198,3 +198,23 @@ textarea {
.ping span {
background-color: var(--typebot-button-bg-color);
}
.rating-icon-container svg {
width: 42px;
height: 42px;
stroke: var(--typebot-button-bg-color);
fill: var(--typebot-host-bubble-bg-color);
transition: fill 100ms ease-out;
}
.rating-icon-container.selected svg {
fill: var(--typebot-button-bg-color);
}
.rating-icon-container:hover svg {
filter: brightness(0.9);
}
.rating-icon-container:active svg {
filter: brightness(0.75);
}

View File

@@ -10,6 +10,7 @@ import { useTypebot } from 'contexts/TypebotContext'
import { parseVariables } from '../../../services/variable'
import { isInputValid } from 'services/inputs'
import { PaymentForm } from './inputs/PaymentForm'
import { RatingForm } from './inputs/RatingForm'
export const InputChatStep = ({
step,
@@ -115,5 +116,7 @@ const Input = ({
onSuccess={() => onSubmit(step.options.labels.success ?? 'Success')}
/>
)
case InputStepType.RATING:
return <RatingForm step={step} onSubmit={onSubmit} />
}
}

View File

@@ -69,7 +69,7 @@ export const ChoiceForm = ({ step, onSubmit }: ChoiceFormProps) => {
</div>
<div className="flex">
{selectedIndices.length > 0 && (
<SendButton label={step.options?.buttonLabel ?? 'Send'} />
<SendButton label={step.options?.buttonLabel ?? 'Send'} disableIcon />
)}
</div>
</form>

View File

@@ -0,0 +1,108 @@
import { RatingInputOptions, RatingInputStep } from 'models'
import React, { FormEvent, useRef, useState } from 'react'
import { isDefined, isEmpty, isNotDefined } from 'utils'
import { SendButton } from './SendButton'
type Props = {
step: RatingInputStep
onSubmit: (value: string) => void
}
export const RatingForm = ({ step, onSubmit }: Props) => {
const [rating, setRating] = useState<number>()
const scaleElement = useRef<HTMLDivElement | null>(null)
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
if (isNotDefined(rating)) return
onSubmit(rating.toString())
}
const handleClick = (rating: number) => setRating(rating)
return (
<form className="flex flex-col" onSubmit={handleSubmit}>
<div className="flex flex-col" ref={scaleElement}>
<div className="flex">
{Array.from(Array(step.options.length)).map((_, idx) => (
<RatingButton
{...step.options}
key={idx}
rating={rating}
idx={idx + 1}
onClick={handleClick}
/>
))}
</div>
<div className="flex justify-between mr-2 mb-2">
{<span className="text-sm w-full ">{step.options.labels.left}</span>}
{!isEmpty(step.options.labels.middle) && (
<span className="text-sm w-full text-center">
{step.options.labels.middle}
</span>
)}
{
<span className="text-sm w-full text-right">
{step.options.labels.right}
</span>
}
</div>
</div>
<div className="flex justify-end mr-2">
{isDefined(rating) && (
<SendButton
label={step.options?.labels.button ?? 'Send'}
disableIcon
/>
)}
</div>
</form>
)
}
const RatingButton = ({
rating,
idx,
buttonType,
customIcon,
onClick,
}: Pick<RatingInputOptions, 'buttonType' | 'customIcon'> & {
rating: number | undefined
idx: number
onClick: (idx: number) => void
}) => {
if (buttonType === 'Numbers')
return (
<button
onClick={(e) => {
e.preventDefault()
onClick(idx)
}}
className={
'py-2 px-4 mr-2 mb-2 text-left font-semibold rounded-md transition-all filter hover:brightness-90 active:brightness-75 duration-100 focus:outline-none typebot-button ' +
(isDefined(rating) && idx <= rating ? '' : 'selectable')
}
>
{idx}
</button>
)
return (
<div
className={
'flex justify-center items-center rating-icon-container cursor-pointer mr-2 mb-2 ' +
(isDefined(rating) && idx <= rating ? 'selected' : '')
}
onClick={() => onClick(idx)}
dangerouslySetInnerHTML={{
__html:
customIcon.isEnabled && !isEmpty(customIcon.svg)
? customIcon.svg
: defaultIcon,
}}
/>
)
}
const defaultIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>`

View File

@@ -7,3 +7,4 @@ export * from './date'
export * from './choice'
export * from './payment'
export * from './phone'
export * from './rating'

View File

@@ -9,6 +9,7 @@ import {
phoneNumberInputOptionsSchema,
phoneNumberInputStepSchema,
} from './phone'
import { ratingInputOptionsSchema, ratingInputStepSchema } from './rating'
import { textInputOptionsSchema, textInputSchema } from './text'
import { urlInputOptionsSchema, urlInputSchema } from './url'
@@ -22,6 +23,7 @@ export const inputStepOptionsSchema = textInputOptionsSchema
.or(phoneNumberInputOptionsSchema)
.or(dateInputOptionsSchema)
.or(paymentInputOptionsSchema)
.or(ratingInputOptionsSchema)
export const inputStepSchema = textInputSchema
.or(numberInputSchema)
@@ -31,6 +33,7 @@ export const inputStepSchema = textInputSchema
.or(phoneNumberInputStepSchema)
.or(choiceInputSchema)
.or(paymentInputSchema)
.or(ratingInputStepSchema)
export type InputStep = z.infer<typeof inputStepSchema>
export type InputStepOptions = z.infer<typeof inputStepOptionsSchema>

View File

@@ -0,0 +1,41 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputStepType,
optionBaseSchema,
stepBaseSchema,
} from '../shared'
export const defaultRatingInputOptions: RatingInputOptions = {
buttonType: 'Numbers',
length: 10,
labels: { button: defaultButtonLabel },
customIcon: { isEnabled: false },
}
export const ratingInputOptionsSchema = optionBaseSchema.and(
z.object({
buttonType: z.literal('Icons').or(z.literal('Numbers')),
length: z.number(),
labels: z.object({
left: z.string().optional(),
right: z.string().optional(),
middle: z.string().optional(),
button: z.string(),
}),
customIcon: z.object({
isEnabled: z.boolean(),
svg: z.string().optional(),
}),
})
)
export const ratingInputStepSchema = stepBaseSchema.and(
z.object({
type: z.literal(InputStepType.RATING),
options: ratingInputOptionsSchema,
})
)
export type RatingInputStep = z.infer<typeof ratingInputStepSchema>
export type RatingInputOptions = z.infer<typeof ratingInputOptionsSchema>

View File

@@ -39,6 +39,7 @@ export enum InputStepType {
PHONE = 'phone number input',
CHOICE = 'choice input',
PAYMENT = 'payment input',
RATING = 'rating input',
}
export enum LogicStepType {