🦴 Add share page backbone
This commit is contained in:
@ -185,3 +185,9 @@ export const PencilIcon = (props: IconProps) => (
|
|||||||
<circle cx="11" cy="11" r="2"></circle>
|
<circle cx="11" cy="11" r="2"></circle>
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const EditIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
import { FolderPlusIcon } from 'assets/icons'
|
import { FolderPlusIcon } from 'assets/icons'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { createFolder, useFolders } from 'services/folders'
|
import { createFolder, useFolders } from 'services/folders'
|
||||||
import { updateTypebot, useTypebots } from 'services/typebots'
|
import { patchTypebot, useTypebots } from 'services/typebots'
|
||||||
import { BackButton } from './FolderContent/BackButton'
|
import { BackButton } from './FolderContent/BackButton'
|
||||||
import { CreateBotButton } from './FolderContent/CreateBotButton'
|
import { CreateBotButton } from './FolderContent/CreateBotButton'
|
||||||
import { ButtonSkeleton, FolderButton } from './FolderContent/FolderButton'
|
import { ButtonSkeleton, FolderButton } from './FolderContent/FolderButton'
|
||||||
@ -83,7 +83,7 @@ export const FolderContent = ({ folder }: Props) => {
|
|||||||
|
|
||||||
const moveTypebotToFolder = async (typebotId: string, folderId: string) => {
|
const moveTypebotToFolder = async (typebotId: string, folderId: string) => {
|
||||||
if (!typebots) return
|
if (!typebots) return
|
||||||
const { error } = await updateTypebot(typebotId, {
|
const { error } = await patchTypebot(typebotId, {
|
||||||
folderId: folderId === 'root' ? null : folderId,
|
folderId: folderId === 'root' ? null : folderId,
|
||||||
})
|
})
|
||||||
if (error) toast({ description: error.message })
|
if (error) toast({ description: error.message })
|
||||||
|
70
apps/builder/components/share/EditableUrl.tsx
Normal file
70
apps/builder/components/share/EditableUrl.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
HStack,
|
||||||
|
Tooltip,
|
||||||
|
EditablePreview,
|
||||||
|
EditableInput,
|
||||||
|
Text,
|
||||||
|
Editable,
|
||||||
|
Button,
|
||||||
|
ButtonProps,
|
||||||
|
useEditableControls,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { EditIcon } from 'assets/icons'
|
||||||
|
import { CopyButton } from 'components/shared/buttons/CopyButton'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type EditableUrlProps = {
|
||||||
|
publicId?: string
|
||||||
|
onPublicIdChange: (publicId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditableUrl = ({
|
||||||
|
publicId,
|
||||||
|
onPublicIdChange,
|
||||||
|
}: EditableUrlProps) => {
|
||||||
|
return (
|
||||||
|
<Editable
|
||||||
|
as={HStack}
|
||||||
|
spacing={3}
|
||||||
|
defaultValue={publicId}
|
||||||
|
onSubmit={onPublicIdChange}
|
||||||
|
>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Text>https://</Text>
|
||||||
|
<Tooltip label="Edit">
|
||||||
|
<EditablePreview
|
||||||
|
mx={1}
|
||||||
|
bgColor="blue.500"
|
||||||
|
color="white"
|
||||||
|
px={3}
|
||||||
|
rounded="md"
|
||||||
|
cursor="pointer"
|
||||||
|
display="flex"
|
||||||
|
fontWeight="semibold"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<EditableInput px={2} />
|
||||||
|
|
||||||
|
<Text>.typebot.io/</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack>
|
||||||
|
<EditButton size="xs" />
|
||||||
|
<CopyButton size="xs" textToCopy={`https://${publicId}.typebot.io/`} />
|
||||||
|
</HStack>
|
||||||
|
</Editable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditButton = (props: ButtonProps) => {
|
||||||
|
const { isEditing, getEditButtonProps } = useEditableControls()
|
||||||
|
|
||||||
|
return isEditing ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<Button leftIcon={<EditIcon />} {...props} {...getEditButtonProps()}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
35
apps/builder/components/share/ShareContent.tsx
Normal file
35
apps/builder/components/share/ShareContent.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Flex, Heading, Stack } from '@chakra-ui/react'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
|
import React from 'react'
|
||||||
|
import { parseDefaultPublicId } from 'services/typebots'
|
||||||
|
import { EditableUrl } from './EditableUrl'
|
||||||
|
|
||||||
|
export const ShareContent = () => {
|
||||||
|
const { typebot, updatePublicId } = useTypebot()
|
||||||
|
|
||||||
|
const handlePublicIdChange = (publicId: string) => {
|
||||||
|
if (publicId === typebot?.publicId) return
|
||||||
|
updatePublicId(publicId)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Flex h="full" w="full" justifyContent="center" align="flex-start">
|
||||||
|
<Stack maxW="1000px" w="full" pt="10" spacing={6}>
|
||||||
|
<Heading fontSize="2xl" as="h1">
|
||||||
|
Your typebot link
|
||||||
|
</Heading>
|
||||||
|
{typebot && (
|
||||||
|
<EditableUrl
|
||||||
|
publicId={
|
||||||
|
typebot?.publicId ??
|
||||||
|
parseDefaultPublicId(typebot.name, typebot.id)
|
||||||
|
}
|
||||||
|
onPublicIdChange={handlePublicIdChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Heading fontSize="2xl" as="h1">
|
||||||
|
Embed your typebot
|
||||||
|
</Heading>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
25
apps/builder/components/shared/buttons/CopyButton.tsx
Normal file
25
apps/builder/components/shared/buttons/CopyButton.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { ButtonProps, Button, useClipboard } from '@chakra-ui/react'
|
||||||
|
|
||||||
|
interface CopyButtonProps extends ButtonProps {
|
||||||
|
textToCopy: string
|
||||||
|
onCopied?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CopyButton = (props: CopyButtonProps) => {
|
||||||
|
const { textToCopy, onCopied, ...buttonProps } = props
|
||||||
|
const { hasCopied, onCopy } = useClipboard(textToCopy)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
isDisabled={hasCopied}
|
||||||
|
onClick={() => {
|
||||||
|
onCopy()
|
||||||
|
if (onCopied) onCopied()
|
||||||
|
}}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
{!hasCopied ? 'Copy' : 'Copied'}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
@ -56,6 +56,7 @@ const typebotContext = createContext<{
|
|||||||
undo: () => void
|
undo: () => void
|
||||||
updateTheme: (theme: Theme) => void
|
updateTheme: (theme: Theme) => void
|
||||||
updateSettings: (settings: Settings) => void
|
updateSettings: (settings: Settings) => void
|
||||||
|
updatePublicId: (publicId: string) => void
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
@ -284,6 +285,11 @@ export const TypebotContext = ({
|
|||||||
setLocalTypebot({ ...localTypebot, settings })
|
setLocalTypebot({ ...localTypebot, settings })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatePublicId = (publicId: string) => {
|
||||||
|
if (!localTypebot) return
|
||||||
|
setLocalTypebot({ ...localTypebot, publicId })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<typebotContext.Provider
|
<typebotContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@ -301,6 +307,7 @@ export const TypebotContext = ({
|
|||||||
undo,
|
undo,
|
||||||
updateTheme,
|
updateTheme,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
|
updatePublicId,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -22,6 +22,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
})
|
})
|
||||||
return res.send({ typebots })
|
return res.send({ typebots })
|
||||||
}
|
}
|
||||||
|
if (req.method === 'PUT') {
|
||||||
|
const data = JSON.parse(req.body)
|
||||||
|
const typebots = await prisma.typebot.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
return res.send({ typebots })
|
||||||
|
}
|
||||||
if (req.method === 'PATCH') {
|
if (req.method === 'PATCH') {
|
||||||
const data = JSON.parse(req.body)
|
const data = JSON.parse(req.body)
|
||||||
const typebots = await prisma.typebot.update({
|
const typebots = await prisma.typebot.update({
|
||||||
|
23
apps/builder/pages/typebots/[id]/share.tsx
Normal file
23
apps/builder/pages/typebots/[id]/share.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Flex } from '@chakra-ui/layout'
|
||||||
|
import withAuth from 'components/HOC/withUser'
|
||||||
|
import { Seo } from 'components/Seo'
|
||||||
|
import { ShareContent } from 'components/share/ShareContent'
|
||||||
|
import { TypebotHeader } from 'components/shared/TypebotHeader'
|
||||||
|
import { TypebotContext } from 'contexts/TypebotContext'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const SharePage = () => {
|
||||||
|
const { query } = useRouter()
|
||||||
|
return (
|
||||||
|
<TypebotContext typebotId={query.id?.toString()}>
|
||||||
|
<Seo title="Share" />
|
||||||
|
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||||
|
<TypebotHeader />
|
||||||
|
<ShareContent />
|
||||||
|
</Flex>
|
||||||
|
</TypebotContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuth(SharePage)
|
@ -9,7 +9,7 @@ import {
|
|||||||
import shortId from 'short-uuid'
|
import shortId from 'short-uuid'
|
||||||
import { Typebot } from 'bot-engine'
|
import { Typebot } from 'bot-engine'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import { fetcher, sendRequest } from './utils'
|
import { fetcher, sendRequest, toKebabCase } from './utils'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
|
|
||||||
export const useTypebots = ({
|
export const useTypebots = ({
|
||||||
@ -71,7 +71,14 @@ export const deleteTypebot = async (id: string) =>
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateTypebot = async (id: string, typebot: Partial<Typebot>) =>
|
export const updateTypebot = async (id: string, typebot: Typebot) =>
|
||||||
|
sendRequest({
|
||||||
|
url: `/api/typebots/${id}`,
|
||||||
|
method: 'PUT',
|
||||||
|
body: typebot,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const patchTypebot = async (id: string, typebot: Partial<Typebot>) =>
|
||||||
sendRequest({
|
sendRequest({
|
||||||
url: `/api/typebots/${id}`,
|
url: `/api/typebots/${id}`,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@ -157,4 +164,8 @@ export const parseTypebotToPublicTypebot = (
|
|||||||
typebotId: typebot.id,
|
typebotId: typebot.id,
|
||||||
theme: typebot.theme,
|
theme: typebot.theme,
|
||||||
settings: typebot.settings,
|
settings: typebot.settings,
|
||||||
|
publicId: typebot.publicId,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const parseDefaultPublicId = (name: string, id: string) =>
|
||||||
|
toKebabCase(`${name}-${id?.slice(0, 5)}`)
|
||||||
|
@ -63,3 +63,11 @@ export const parseHtmlStringToPlainText = (html: string): string => {
|
|||||||
parser.end()
|
parser.end()
|
||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const toKebabCase = (value: string) => {
|
||||||
|
const matched = value.match(
|
||||||
|
/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g
|
||||||
|
)
|
||||||
|
if (!matched) return ''
|
||||||
|
return matched.map((x) => x.toLowerCase()).join('-')
|
||||||
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[publicId]` on the table `PublicTypebot` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- A unique constraint covering the columns `[publicId]` on the table `Typebot` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PublicTypebot" ADD COLUMN "publicId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Typebot" ADD COLUMN "publicId" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "PublicTypebot_publicId_key" ON "PublicTypebot"("publicId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Typebot_publicId_key" ON "Typebot"("publicId");
|
@ -91,6 +91,7 @@ model Typebot {
|
|||||||
startBlock Json
|
startBlock Json
|
||||||
theme Json
|
theme Json
|
||||||
settings Json
|
settings Json
|
||||||
|
publicId String? @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
model PublicTypebot {
|
model PublicTypebot {
|
||||||
@ -102,6 +103,7 @@ model PublicTypebot {
|
|||||||
startBlock Json
|
startBlock Json
|
||||||
theme Json
|
theme Json
|
||||||
settings Json
|
settings Json
|
||||||
|
publicId String? @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
model Result {
|
model Result {
|
||||||
|
Reference in New Issue
Block a user