♻️ (viewer) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 10:28:03 +01:00
committed by Baptiste Arnaud
parent 643571fe7d
commit a9d04798bc
80 changed files with 523 additions and 491 deletions

View File

@@ -0,0 +1,33 @@
import React from 'react'
import { getViewerUrl, isEmpty } from 'utils'
export const ErrorPage = ({ error }: { error: Error }) => {
return (
<div
style={{
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
}}
>
{isEmpty(getViewerUrl()) ? (
<>
<h1 style={{ fontWeight: 'bold', fontSize: '30px' }}>
NEXT_PUBLIC_VIEWER_URL is missing
</h1>
<h2>
Make sure to configure the viewer properly (
<a href="https://docs.typebot.io/self-hosting/configuration#viewer">
https://docs.typebot.io/self-hosting/configuration#viewer
</a>
)
</h2>
</>
) : (
<p style={{ fontSize: '24px' }}>{error.message}</p>
)}
</div>
)
}

View File

@@ -0,0 +1,14 @@
export const NotFoundPage = () => (
<div
style={{
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
}}
>
<h1 style={{ fontWeight: 'bold', fontSize: '30px' }}>404</h1>
<h2>The bot you&apos;re looking for doesn&apos;t exist</h2>
</div>
)

View File

@@ -0,0 +1,65 @@
import { Metadata } from 'models'
import Head from 'next/head'
import React from 'react'
type SEOProps = {
url: string
typebotName: string
metadata: Metadata
}
export const SEO = ({
url,
typebotName,
metadata: { title, description, favIconUrl, imageUrl },
}: SEOProps) => (
<Head>
<title>{title ?? typebotName}</title>
<meta name="robots" content="noindex" />
<link
rel="icon"
type="image/png"
href={favIconUrl ?? 'https://bot.typebot.io/favicon.png'}
/>
<meta name="title" content={title ?? typebotName} />
<meta
name="description"
content={
description ??
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.'
}
/>
<meta property="og:type" content="website" />
<meta property="og:url" content={url ?? 'https://bot.typebot.io'} />
<meta property="og:title" content={title ?? typebotName} />
<meta property="og:site_name" content={title ?? typebotName} />
<meta
property="og:description"
content={
description ??
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.'
}
/>
<meta
property="og:image"
itemProp="image"
content={imageUrl ?? 'https://bot.typebot.io/site-preview.png'}
/>
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={url ?? 'https://bot.typebot.io'} />
<meta property="twitter:title" content={title ?? typebotName} />
<meta
property="twitter:description"
content={
description ??
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.'
}
/>
<meta
property="twitter:image"
content={imageUrl ?? 'https://bot.typebot.io/site-preview.png'}
/>
</Head>
)

View File

@@ -0,0 +1,159 @@
import { TypebotViewer } from 'bot-engine'
import { Answer, PublicTypebot, Typebot, VariableWithValue } from 'models'
import { useRouter } from 'next/router'
import React, { useEffect, useState } from 'react'
import { isDefined, isNotDefined } from 'utils'
import { SEO } from './Seo'
import { ErrorPage } from './ErrorPage'
import { createResultQuery, updateResultQuery } from '@/features/results'
import { upsertAnswerQuery } from '@/features/answers'
export type TypebotPageProps = {
publishedTypebot: Omit<PublicTypebot, 'createdAt' | 'updatedAt'> & {
typebot: Pick<Typebot, 'name' | 'isClosed' | 'isArchived'>
}
url: string
isIE: boolean
customHeadCode: string | null
}
const sessionStorageKey = 'resultId'
export const TypebotPage = ({
publishedTypebot,
isIE,
url,
customHeadCode,
}: TypebotPageProps) => {
const { asPath, push } = useRouter()
const [showTypebot, setShowTypebot] = useState(false)
const [predefinedVariables, setPredefinedVariables] = useState<{
[key: string]: string
}>()
const [error, setError] = useState<Error | undefined>(
isIE ? new Error('Internet explorer is not supported') : undefined
)
const [resultId, setResultId] = useState<string | undefined>()
const [variableUpdateQueue, setVariableUpdateQueue] = useState<
VariableWithValue[][]
>([])
const [chatStarted, setChatStarted] = useState(false)
useEffect(() => {
setShowTypebot(true)
const urlParams = new URLSearchParams(location.search)
clearQueryParams()
const predefinedVariables: { [key: string]: string } = {}
urlParams.forEach((value, key) => {
predefinedVariables[key] = value
})
setPredefinedVariables(predefinedVariables)
initializeResult().then()
if (isDefined(customHeadCode))
document.head.innerHTML = document.head.innerHTML + customHeadCode
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const clearQueryParams = () => {
const hasQueryParams = asPath.includes('?')
if (
hasQueryParams &&
publishedTypebot.settings.general.isHideQueryParamsEnabled !== false
)
push(asPath.split('?')[0], undefined, { shallow: true })
}
const initializeResult = async () => {
const resultIdFromSession = getExistingResultFromSession()
if (resultIdFromSession) setResultId(resultIdFromSession)
else {
const { error, data } = await createResultQuery(
publishedTypebot.typebotId
)
if (error) return setError(error)
if (data?.hasReachedLimit)
return setError(new Error('This bot is now closed.'))
if (data?.result) {
setResultId(data.result.id)
if (
publishedTypebot.settings.general.isNewResultOnRefreshEnabled !== true
)
setResultInSession(data.result.id)
}
}
}
useEffect(() => {
if (!resultId || variableUpdateQueue.length === 0) return
Promise.all(variableUpdateQueue.map(sendNewVariables(resultId))).then()
setVariableUpdateQueue([])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resultId])
const handleNewVariables = async (variables: VariableWithValue[]) => {
if (variables.length === 0) return
if (!resultId)
return setVariableUpdateQueue([...variableUpdateQueue, variables])
await sendNewVariables(resultId)(variables)
}
const sendNewVariables =
(resultId: string) => async (variables: VariableWithValue[]) => {
const { error } = await updateResultQuery(resultId, { variables })
if (error) setError(error)
}
const handleNewAnswer = async (
answer: Answer & { uploadedFiles: boolean }
) => {
if (!resultId) return setError(new Error('Error: result was not created'))
const { error } = await upsertAnswerQuery({ ...answer, resultId })
if (error) setError(error)
if (chatStarted) return
updateResultQuery(resultId, {
hasStarted: true,
}).then(({ error }) => (error ? setError(error) : setChatStarted(true)))
}
const handleCompleted = async () => {
if (!resultId) return setError(new Error('Error: result was not created'))
const { error } = await updateResultQuery(resultId, { isCompleted: true })
if (error) setError(error)
}
if (error) {
return <ErrorPage error={error} />
}
return (
<div style={{ height: '100vh' }}>
<SEO
url={url}
typebotName={publishedTypebot.typebot.name}
metadata={publishedTypebot.settings.metadata}
/>
{showTypebot && (
<TypebotViewer
typebot={publishedTypebot}
resultId={resultId}
predefinedVariables={predefinedVariables}
onNewAnswer={handleNewAnswer}
onCompleted={handleCompleted}
onVariablesUpdated={handleNewVariables}
isLoading={isNotDefined(resultId)}
/>
)}
</div>
)
}
const getExistingResultFromSession = () => {
try {
return sessionStorage.getItem(sessionStorageKey)
} catch {}
}
const setResultInSession = (resultId: string) => {
try {
return sessionStorage.setItem(sessionStorageKey, resultId)
} catch {}
}