Skip to content

Commit

Permalink
feat(admin): added net promoter score in botpress (#11202)
Browse files Browse the repository at this point in the history
* Basic NPS functionality without text tracking

feat(nps): added comment functionnality for the netpromotingscore

feat(nps): use realtime

feat(nps): use time to display the nps form

* use ms library for millisecond time

* added traduction to missing traduction

* Added Toaster and close banner when comments are added

* Created a Folder for the Net Promoter application

* Modified Prompt net promoter score

* Updates the initial delay

The three times are now confirmed with SK.

* Modified traduction in english

* fix(translation): modified one translation

Co-authored-by: Patrick <87815239+ptrckbp@users.noreply.github.com>
Co-authored-by: Samuel Massé <samuelmasse4@gmail.com>
  • Loading branch information
3 people committed Jan 24, 2022
1 parent bba2dbc commit fbbf064
Show file tree
Hide file tree
Showing 16 changed files with 468 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { lang } from 'botpress/shared'
import React, { ReactNode } from 'react'
import NPSScale from './components/NPSScale'
import styles from './style.scss'

interface Props {
animated?: boolean
dismissed?: boolean
score?: number | null
question?: string
scaleWorstLabel?: string
scaleBestLabel?: string
onSubmit?: (score: number) => void
onDismissed?: () => void
children?: ReactNode
}
export function NPS({
animated = true,
question = lang.tr('admin.netPromotingScore.question'),
dismissed,
score = null,
scaleWorstLabel,
scaleBestLabel,
onSubmit,
onDismissed,
children = <p>{lang.tr('admin.netPromotingScore.feedback')}</p>
}: Props) {
const handleDismiss = () => {
onDismissed && onDismissed()
}
const handleSubmit = (score: number) => {
onSubmit && onSubmit(score)
}

return dismissed ? null : (
<div className={`${styles.root} ${animated ? styles.animated : ''}`}>
<button className={styles.close} onClick={handleDismiss}>
</button>

{score ? (
<div className={styles.inner}>{children}</div>
) : (
<div className={styles.inner}>
<p className={styles.message}>{question}</p>
<NPSScale worstLabel={scaleWorstLabel} bestLabel={scaleBestLabel} score={score} onSubmit={handleSubmit} />
</div>
)}
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,14 @@ const trackEvent = (eventName: string, payload?: any, options?: any, callback?:
if (analytics) {
return analytics.track(eventName, payload, options, callback)
}
return Promise.reject(new Error('No analytics found'))
}

const trackPage = (data?: PageData, options?: any, callback?: (...params: any[]) => any) => {
if (analytics) {
return analytics.page(data, options, callback)
}
return Promise.reject(new Error('No analytics found'))
}

export { trackEvent, trackPage }
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Button, Dialog, FormGroup, TextArea, Classes, Intent } from '@blueprintjs/core'
import { lang, toast } from 'botpress/shared'
import React, { FC, useEffect, useState } from 'react'
import { trackEvent } from '../../SegmentHandler'

interface Props {
modalValue: boolean
onChange: any
}

const CreateNPSModalComment: FC<Props> = props => {
const [isNPSModalOpen, setIsNPSModalOpen] = useState(props.modalValue)
const [comment, setComment] = useState('')
const [isButtonDisabled, setIsButtonDisabled] = useState(true)

useEffect(() => {
if (comment !== '' && comment.length > 5) {
return setIsButtonDisabled(false)
}
setIsButtonDisabled(true)
}, [comment])

const createComment = e => {
try {
void trackEvent('nps_comment', { npsComment: comment })
toggleDialog(e)
toast.success(lang.tr('admin.netPromotingScore.addComment'))
} catch {
toast.failure(lang.tr('admin.netPromoting.error'))
}
}

const toggleDialog = e => {
setIsNPSModalOpen(!isNPSModalOpen)
e.target.value = isNPSModalOpen
props.onChange(e)
}

return (
<Dialog
title={lang.tr('admin.netPromotingScore.title')}
icon="add"
isOpen={isNPSModalOpen}
onClose={toggleDialog}
transitionDuration={0}
canOutsideClickClose={false}
>
<form>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={lang.tr('admin.netPromotingScore.modalTitle')}
labelFor="comment"
labelInfo="*"
helperText={lang.tr('admin.netPromotingScore.helper')}
>
<TextArea
id="input-comment"
fill={true}
growVertically={true}
intent={Intent.PRIMARY}
large={true}
value={comment}
onChange={event => setComment(event.target.value)}
/>
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
text={lang.tr('admin.netPromotingScore.button')}
type="submit"
onClick={createComment}
intent={Intent.PRIMARY}
disabled={isButtonDisabled}
/>
</div>
</div>
</form>
</Dialog>
)
}

export default CreateNPSModalComment
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Button } from '@blueprintjs/core'
import { lang } from 'botpress/shared'
import React, { FC, useState } from 'react'
import NPSModal from './NPSModal'

import style from './style.scss'

interface Props {
onDismissed?: () => void
}
const NPSAdditionComment: FC<Props> = props => {
const [showForm, setShowForm] = useState(false)
const openModal = e => {
e.preventDefault()
setShowForm(!showForm)
}
const handleDismiss = () => {
props.onDismissed && props.onDismissed()
}
const handleChange = e => {
setShowForm(!e.target.value)
handleDismiss()
}

return (
<div className={style.flexBox}>
<p>{lang.tr('admin.netPromotingScore.feedback')}</p>
<Button intent="primary" onClick={openModal}>
{lang.tr('admin.netPromotingScore.moreContent')}
</Button>
{showForm && <NPSModal modalValue={showForm} onChange={handleChange} />}
</div>
)
}

export default NPSAdditionComment
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.flexBox {
display: flex;
}

.flexBox > * {
flex: 1 1 auto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'flexBox': string;
}
export const cssExports: CssExports;
export default cssExports;
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { lang } from 'botpress/shared'
import React from 'react'
import styles from './style.scss'

const MIN = 0
const MAX = 10

interface Props {
score: number | null
worstLabel?: string
bestLabel?: string
onSubmit?: (value: number) => void
}
export default function NPSScale({
score,
worstLabel = lang.tr('admin.netPromotingScore.worstLabel'),
bestLabel = lang.tr('admin.netPromotingScore.bestLabel'),
onSubmit
}: Props) {
const [npsScore, setNpsScore] = React.useState<number | null>(score)
const handleMouseEnter = (value: number) => {
setNpsScore(value)
}
const handleMouseLeave = () => {
setNpsScore(null)
}
const handleClick = (value: number) => {
onSubmit && onSubmit(value)
}
return (
<div className={styles.root}>
<div>
{range(MIN, MAX).map(i => (
<div
key={i}
className={`${styles.value} ${npsScore !== null && npsScore >= i ? styles.selected : ''}`}
onMouseEnter={() => handleMouseEnter(i)}
onMouseLeave={handleMouseLeave}
onClick={() => handleClick(i)}
>
<div>{i}</div>
</div>
))}
</div>
<div className={styles.legend}>
<div className={`${styles.label} ${styles.left}`}>{worstLabel}</div>
<div className={`${styles.label} ${styles.right}`}>{bestLabel}</div>
</div>
</div>
)
}

function range(start: number, end: number) {
return Array.from({ length: end - start + 1 }).map((_, idx) => start + idx)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
.root {
width: auto;
max-width: 418px;
margin: 0px auto;
}
.value {
padding: 0px 3px;
display: inline-block;
}

.value div {
background: #f2f5fd;
width: 32px;
height: 32px;
line-height: 32px;
border-radius: 32px;
cursor: pointer;
transition: 0.15s ease all;
color: #999;
}
.selected div {
background: #3884ff;
color: #fff;
}
.selected div {
background: #3884ff;
color: #fff;
}
.value:hover div {
transform: scale(1.25);
}

.legend {
display: flex;
margin-top: 12px;
}

.label {
flex: 1;
color: #999;
font-size: 12px;
}
.left {
text-align: left;
}
.right {
text-align: right;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'label': string;
'left': string;
'legend': string;
'right': string;
'root': string;
'selected': string;
'value': string;
}
export const cssExports: CssExports;
export default cssExports;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { NPS } from './NPS'
export default NPS
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
.root {
position: fixed;
bottom: 0px;
left: 0px;
right: 0px;
border-top: 1px solid #e5e5e5;
background: #fff;
box-shadow: 0px -10px 10px rgba(200, 200, 200, 0.08);
display: flex;
align-items: flex-start;
flex-direction: row-reverse;
}
.animated {
animation-duration: 2s;
animation-name: NPSInput-slidein;
}
.close {
position: absolute;
top: 10px;
right: 10px;
background: transparent;
outline: none;
display: inline-block;
zoom: 1;
line-height: normal;
white-space: nowrap;
vertical-align: baseline;
text-align: center;
cursor: pointer;
user-select: none;
font-family: inherit;
font-size: 100%;
padding: 0.5em 1em;
text-decoration: none;
border: 0;
opacity: 0.4;
font-size: 16px;
}

.close:hover {
opacity: 1;
}

.inner {
width: 100%;
max-width: 1000px;
margin: 20px auto;
text-align: center;
}

.message {
margin: 0px;
margin-bottom: 15px;
font-size: 16px;
}

@keyframes NPSInput-slidein {
from {
bottom: -100%;
}

to {
bottom: 0px;
}
}

0 comments on commit fbbf064

Please sign in to comment.