Skip to content

Commit

Permalink
🦴 Add viewer backbone
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Dec 24, 2021
1 parent 9a78a34 commit d369b4d
Show file tree
Hide file tree
Showing 24 changed files with 576 additions and 182 deletions.
2 changes: 1 addition & 1 deletion apps/builder/components/board/preview/PreviewDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useEditor } from 'contexts/EditorContext'
import { useGraph } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext'
import React, { useMemo, useState } from 'react'
import { parseTypebotToPublicTypebot } from 'services/typebots'
import { parseTypebotToPublicTypebot } from 'services/publicTypebot'

export const PreviewDrawer = () => {
const { typebot } = useTypebot()
Expand Down
13 changes: 11 additions & 2 deletions apps/builder/components/shared/buttons/PublishButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { Button } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext'

export const PublishButton = () => {
const { isPublishing, isPublished, publishTypebot } = useTypebot()

return (
<Button ml={2} colorScheme={'blue'}>
Publish
<Button
ml={2}
colorScheme="blue"
isLoading={isPublishing}
isDisabled={isPublished}
onClick={publishTypebot}
>
{isPublished ? 'Published' : 'Publish'}
</Button>
)
}
2 changes: 1 addition & 1 deletion apps/builder/components/theme/ThemeContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react'
import { TypebotViewer } from 'bot-engine'
import { useTypebot } from 'contexts/TypebotContext'
import React, { useMemo } from 'react'
import { parseTypebotToPublicTypebot } from 'services/typebots'
import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
import { SideMenu } from './SideMenu'

export const ThemeContent = () => {
Expand Down
77 changes: 68 additions & 9 deletions apps/builder/contexts/TypebotContext.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
import { useToast } from '@chakra-ui/react'
import {
Block,
PublicTypebot,
Settings,
Step,
StepType,
Target,
Theme,
Typebot,
} from 'bot-engine'
import { deepEqual } from 'fast-equals'
import { useRouter } from 'next/router'
import {
createContext,
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import {
createPublishedTypebot,
parseTypebotToPublicTypebot,
updatePublishedTypebot,
} from 'services/publicTypebot'
import {
checkIfPublished,
checkIfTypebotsAreEqual,
parseDefaultPublicId,
parseNewBlock,
parseNewStep,
updateTypebot,
} from 'services/typebots'
import {
fetcher,
insertItemInList,
isDefined,
omit,
preventUserFromRefreshing,
} from 'services/utils'
import useSWR from 'swr'
import { NewBlockPayload, Coordinates } from './GraphContext'

const typebotContext = createContext<{
typebot?: Typebot
publishedTypebot?: PublicTypebot
isPublished: boolean
isPublishing: boolean
hasUnsavedChanges: boolean
isSavingLoading: boolean
save: () => void
Expand All @@ -57,6 +72,7 @@ const typebotContext = createContext<{
updateTheme: (theme: Theme) => void
updateSettings: (settings: Settings) => void
updatePublicId: (publicId: string) => void
publishTypebot: () => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
Expand All @@ -74,7 +90,7 @@ export const TypebotContext = ({
status: 'error',
})
const [undoStack, setUndoStack] = useState<Typebot[]>([])
const { typebot, isLoading, mutate } = useFetchedTypebot({
const { typebot, publishedTypebot, isLoading, mutate } = useFetchedTypebot({
typebotId,
onError: (error) =>
toast({
Expand All @@ -83,18 +99,33 @@ export const TypebotContext = ({
}),
})
const [localTypebot, setLocalTypebot] = useState<Typebot>()
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
const [localPublishedTypebot, setLocalPublishedTypebot] =
useState<PublicTypebot>()
const [isSavingLoading, setIsSavingLoading] = useState(false)
const [isPublishing, setIsPublishing] = useState(false)

const hasUnsavedChanges = useMemo(
() =>
isDefined(typebot) &&
isDefined(localTypebot) &&
!deepEqual(localTypebot, typebot),
[typebot, localTypebot]
)
const isPublished = useMemo(
() =>
isDefined(typebot) &&
isDefined(publishedTypebot) &&
checkIfPublished(typebot, publishedTypebot),
[typebot, publishedTypebot]
)

useEffect(() => {
if (!localTypebot || !typebot) return
if (!checkIfTypebotsAreEqual(localTypebot, typebot)) {
setHasUnsavedChanges(true)
pushNewTypebotInUndoStack(localTypebot)
window.removeEventListener('beforeunload', preventUserFromRefreshing)
window.addEventListener('beforeunload', preventUserFromRefreshing)
} else {
setHasUnsavedChanges(false)
window.removeEventListener('beforeunload', preventUserFromRefreshing)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -108,6 +139,7 @@ export const TypebotContext = ({
return
}
setLocalTypebot({ ...typebot })
if (publishedTypebot) setLocalPublishedTypebot({ ...publishedTypebot })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading])

Expand All @@ -128,7 +160,6 @@ export const TypebotContext = ({
setIsSavingLoading(false)
if (error) return toast({ title: error.name, description: error.message })
mutate({ typebot: localTypebot })
setHasUnsavedChanges(false)
window.removeEventListener('beforeunload', preventUserFromRefreshing)
}

Expand Down Expand Up @@ -290,10 +321,34 @@ export const TypebotContext = ({
setLocalTypebot({ ...localTypebot, publicId })
}

const publishTypebot = async () => {
if (!localTypebot) return
if (!localPublishedTypebot)
updatePublicId(parseDefaultPublicId(localTypebot.name, localTypebot.id))
if (hasUnsavedChanges) await saveTypebot()
setIsPublishing(true)
if (localPublishedTypebot) {
const { error } = await updatePublishedTypebot(
localPublishedTypebot.id,
omit(parseTypebotToPublicTypebot(localTypebot), 'id')
)
setIsPublishing(false)
if (error) return toast({ title: error.name, description: error.message })
} else {
const { error } = await createPublishedTypebot(
omit(parseTypebotToPublicTypebot(localTypebot), 'id')
)
setIsPublishing(false)
if (error) return toast({ title: error.name, description: error.message })
}
mutate({ typebot: localTypebot })
}

return (
<typebotContext.Provider
value={{
typebot: localTypebot,
publishedTypebot: localPublishedTypebot,
updateStep,
addNewBlock,
addStepToBlock,
Expand All @@ -308,6 +363,9 @@ export const TypebotContext = ({
updateTheme,
updateSettings,
updatePublicId,
publishTypebot,
isPublishing,
isPublished,
}}
>
{children}
Expand All @@ -324,13 +382,14 @@ export const useFetchedTypebot = ({
typebotId?: string
onError: (error: Error) => void
}) => {
const { data, error, mutate } = useSWR<{ typebot: Typebot }, Error>(
typebotId ? `/api/typebots/${typebotId}` : null,
fetcher
)
const { data, error, mutate } = useSWR<
{ typebot: Typebot; publishedTypebot?: PublicTypebot },
Error
>(typebotId ? `/api/typebots/${typebotId}` : null, fetcher)
if (error) onError(error)
return {
typebot: data?.typebot,
publishedTypebot: data?.publishedTypebot,
isLoading: !error && !data,
mutate,
}
Expand Down
30 changes: 30 additions & 0 deletions apps/builder/pages/api/publicTypebots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getSession } from 'next-auth/react'
import { methodNotAllowed } from 'services/api/utils'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({ req })

if (!session?.user)
return res.status(401).json({ message: 'Not authenticated' })

try {
if (req.method === 'POST') {
const data = JSON.parse(req.body)
const typebot = await prisma.publicTypebot.create({
data: { ...data },
})
return res.send(typebot)
}
return methodNotAllowed(res)
} catch (err) {
console.error(err)
if (err instanceof Error) {
return res.status(500).send({ title: err.name, message: err.message })
}
return res.status(500).send({ message: 'An error occured', error: err })
}
}

export default handler
24 changes: 24 additions & 0 deletions apps/builder/pages/api/publicTypebots/[id].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getSession } from 'next-auth/react'
import { methodNotAllowed } from 'services/api/utils'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({ req })

if (!session?.user)
return res.status(401).json({ message: 'Not authenticated' })

const id = req.query.id.toString()
if (req.method === 'PUT') {
const data = JSON.parse(req.body)
const typebots = await prisma.publicTypebot.update({
where: { id },
data,
})
return res.send({ typebots })
}
return methodNotAllowed(res)
}

export default handler
7 changes: 6 additions & 1 deletion apps/builder/pages/api/typebots/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
const typebot = await prisma.typebot.findUnique({
where: { id },
include: {
publishedTypebot: true,
},
})
return res.send({ typebot })
if (!typebot) return res.send({ typebot: null })
const { publishedTypebot, ...restOfTypebot } = typebot
return res.send({ typebot: restOfTypebot, publishedTypebot })
}
if (req.method === 'DELETE') {
const typebots = await prisma.typebot.delete({
Expand Down
35 changes: 35 additions & 0 deletions apps/builder/services/publicTypebot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PublicTypebot, Typebot } from 'bot-engine'
import { sendRequest } from './utils'
import shortId from 'short-uuid'

export const parseTypebotToPublicTypebot = (
typebot: Typebot
): PublicTypebot => ({
id: shortId.generate(),
blocks: typebot.blocks,
name: typebot.name,
startBlock: typebot.startBlock,
typebotId: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
publicId: typebot.publicId,
})

export const createPublishedTypebot = async (
typebot: Omit<PublicTypebot, 'id'>
) =>
sendRequest({
url: `/api/publicTypebots`,
method: 'POST',
body: typebot,
})

export const updatePublishedTypebot = async (
id: string,
typebot: Omit<PublicTypebot, 'id'>
) =>
sendRequest({
url: `/api/publicTypebots/${id}`,
method: 'PUT',
body: typebot,
})
24 changes: 11 additions & 13 deletions apps/builder/services/typebots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
StepType,
Block,
TextStep,
PublicTypebot,
TextInputStep,
PublicTypebot,
} from 'bot-engine'
import shortId from 'short-uuid'
import { Typebot } from 'bot-engine'
Expand Down Expand Up @@ -154,18 +154,16 @@ export const checkIfTypebotsAreEqual = (
}
)

export const parseTypebotToPublicTypebot = (
typebot: Typebot
): PublicTypebot => ({
id: shortId.generate(),
blocks: typebot.blocks,
name: typebot.name,
startBlock: typebot.startBlock,
typebotId: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
publicId: typebot.publicId,
})
export const checkIfPublished = (
typebot: Typebot,
publicTypebot: PublicTypebot
) =>
deepEqual(typebot.blocks, publicTypebot.blocks) &&
deepEqual(typebot.startBlock, publicTypebot.startBlock) &&
typebot.name === publicTypebot.name &&
typebot.publicId === publicTypebot.publicId &&
deepEqual(typebot.settings, publicTypebot.settings) &&
deepEqual(typebot.theme, publicTypebot.theme)

export const parseDefaultPublicId = (name: string, id: string) =>
toKebabCase(`${name}-${id?.slice(0, 5)}`)
20 changes: 20 additions & 0 deletions apps/builder/services/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,23 @@ export const toKebabCase = (value: string) => {
if (!matched) return ''
return matched.map((x) => x.toLowerCase()).join('-')
}

interface Omit {
// eslint-disable-next-line @typescript-eslint/ban-types
<T extends object, K extends [...(keyof T)[]]>(obj: T, ...keys: K): {
[K2 in Exclude<keyof T, K[number]>]: T[K2]
}
}

export const omit: Omit = (obj, ...keys) => {
const ret = {} as {
[K in keyof typeof obj]: typeof obj[K]
}
let key: keyof typeof obj
for (key in obj) {
if (!keys.includes(key)) {
ret[key] = obj[key]
}
}
return ret
}
3 changes: 3 additions & 0 deletions apps/viewer/assets/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
margin: 0;
}
Loading

1 comment on commit d369b4d

@vercel
Copy link

@vercel vercel bot commented on d369b4d Dec 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viewer-v2 – ./apps/viewer

viewer-v2-git-main-typebot-io.vercel.app
viewer-v2-typebot-io.vercel.app
typebot-io.vercel.app

Please sign in to comment.