♻️ (viewer) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
643571fe7d
commit
a9d04798bc
33
apps/viewer/src/components/ErrorPage.tsx
Normal file
33
apps/viewer/src/components/ErrorPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
14
apps/viewer/src/components/NotFoundPage.tsx
Normal file
14
apps/viewer/src/components/NotFoundPage.tsx
Normal 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're looking for doesn't exist</h2>
|
||||
</div>
|
||||
)
|
||||
65
apps/viewer/src/components/Seo.tsx
Normal file
65
apps/viewer/src/components/Seo.tsx
Normal 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>
|
||||
)
|
||||
159
apps/viewer/src/components/TypebotPage.tsx
Normal file
159
apps/viewer/src/components/TypebotPage.tsx
Normal 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 {}
|
||||
}
|
||||
Reference in New Issue
Block a user