2
0

📄 Add Commercial License for ee folder (#1532)

This commit is contained in:
Baptiste Arnaud
2024-05-23 10:42:23 +02:00
committed by GitHub
parent 5680829906
commit 0eacbebbbe
246 changed files with 1472 additions and 1588 deletions

View File

@ -0,0 +1,56 @@
'use client'
import { Heading, Stack, Text } from '@chakra-ui/react'
import { Link } from '@chakra-ui/next-js'
type Props = {
allBlogs: {
metadata: {
title: string
publishedAt: string
}
slug: string
}[]
}
export const Posts = ({ allBlogs }: Props) => (
<Stack
spacing={10}
mx="auto"
maxW="3xl"
my="20"
fontSize="17px"
textAlign="justify"
>
<Heading>Latest blog posts:</Heading>
<Stack>
{allBlogs
.filter((post) => post.metadata.publishedAt)
.sort((a, b) => {
if (
new Date(a.metadata.publishedAt) > new Date(b.metadata.publishedAt)
) {
return -1
}
return 1
})
.map((post) => (
<Link key={post.slug} href={`/blog/${post.slug}`}>
<Stack
w="full"
rounded="md"
borderColor="gray.600"
borderWidth={1}
p="4"
>
<Heading as="h2" fontSize="2xl">
{post.metadata.title}
</Heading>
<Text color="gray.500">
{new Date(post.metadata.publishedAt).toDateString()}
</Text>
</Stack>
</Link>
))}
</Stack>
</Stack>
)

View File

@ -0,0 +1,178 @@
/* eslint-disable jsx-a11y/alt-text */
'use client'
import { Link } from '@chakra-ui/next-js'
import {
Alert,
AlertIcon,
AlertTitle,
Heading,
Stack,
Text,
} from '@chakra-ui/react'
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'
import { highlight } from 'sugar-high'
import { Tweet } from './Tweet'
import { Standard } from '@typebot.io/nextjs'
import { EndCta } from '@/components/Homepage/EndCta'
import { Table } from './Table'
import Image from 'next/image'
type Props = {
metadata: {
title: string
publishedAt: string
}
mdxSource: MDXRemoteSerializeResult
}
export const Post = ({ metadata, mdxSource }: Props) => (
<Stack spacing={10} my="20" w="full">
<Stack mx="auto" w="full" maxW={['full', '46rem']} px={[3, 3, 0]}>
<Heading>{metadata.title}</Heading>
<Text>{formatDate(metadata.publishedAt)}</Text>
</Stack>
<Stack
mx="auto"
spacing={0}
as="article"
px={3}
w="full"
className="prose prose-quoteless prose-neutral prose-invert max-w-none"
>
<MDXRemote
{...mdxSource}
components={{
h1: (props) => <Heading as="h1" {...props} />,
h2: (props) => <Heading as="h2" fontSize="3xl" {...props} />,
h3: (props) => <Heading as="h3" fontSize="2xl" {...props} />,
h4: (props) => <Heading as="h4" fontSize="xl" {...props} />,
h5: (props) => <Heading as="h5" fontSize="lg" {...props} />,
h6: (props) => <Heading as="h6" fontSize="md" {...props} />,
code: ({ children, ...props }) => {
const codeHTML = highlight(children?.toString() ?? '')
return (
<code dangerouslySetInnerHTML={{ __html: codeHTML }} {...props} />
)
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
link: (props: any) => <Link {...props} />,
Image: (props) => (
<Image {...props} style={{ borderRadius: '.5rem' }} />
),
Callout: ({ children, title, ...props }) => (
<Alert rounded="md" {...props}>
<AlertIcon />
{title ? <AlertTitle>{title}</AlertTitle> : null}
{children}
</Alert>
),
Tweet,
Typebot: (props) => (
<Standard
{...props}
typebot={props.typebot}
style={{
borderRadius: '0.375rem',
borderWidth: '1px',
height: '533px',
}}
/>
),
Youtube: ({ id }: { id: string }) => (
<div className="w-full">
<div
style={{
position: 'relative',
paddingBottom: '64.63195691202873%',
height: 0,
width: '100%',
}}
>
<iframe
src={`https://www.youtube.com/embed/${id}`}
allowFullScreen
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
></iframe>
</div>
</div>
),
Loom: ({ id }: { id: string }) => (
<div className="w-full">
<div
style={{
position: 'relative',
paddingBottom: '64.63195691202873%',
height: 0,
width: '100%',
}}
>
<iframe
src={`https://www.loom.com/embed/${id}`}
allowFullScreen
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
></iframe>
</div>
</div>
),
Cta: (props) => (
<EndCta
{...props}
style={{ maxWidth: 'none' }}
w="full"
h="auto"
py="0"
className="w-full"
bgGradient={undefined}
polygonsBaseTop="0px"
/>
),
Table,
}}
/>
</Stack>
</Stack>
)
function formatDate(date: string) {
const currentDate = new Date().getTime()
if (!date.includes('T')) {
date = `${date}T00:00:00`
}
const targetDate = new Date(date).getTime()
const timeDifference = Math.abs(currentDate - targetDate)
const daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24))
const fullDate = new Date(date).toLocaleString('en-us', {
month: 'long',
day: 'numeric',
year: 'numeric',
})
if (daysAgo < 1) {
return 'Today'
} else if (daysAgo < 7) {
return `${fullDate} (${daysAgo}d ago)`
} else if (daysAgo < 30) {
const weeksAgo = Math.floor(daysAgo / 7)
return `${fullDate} (${weeksAgo}w ago)`
} else if (daysAgo < 365) {
const monthsAgo = Math.floor(daysAgo / 30)
return `${fullDate} (${monthsAgo}mo ago)`
} else {
const yearsAgo = Math.floor(daysAgo / 365)
return `${fullDate} (${yearsAgo}y ago)`
}
}

View File

@ -0,0 +1,37 @@
import {
Table as ChakraTable,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
} from '@chakra-ui/react'
type Props = {
headers: string[]
rows: string[][]
}
export const Table = ({ headers, rows }: Props) => (
<TableContainer maxW="60rem">
<ChakraTable>
<Thead>
<Tr>
{headers.map((header, index) => (
<Th key={index}>{header}</Th>
))}
</Tr>
</Thead>
<Tbody>
{rows.map((row, index) => (
<Tr key={index}>
{row.map((cell, cellIndex) => (
<Td key={cellIndex}>{cell}</Td>
))}
</Tr>
))}
</Tbody>
</ChakraTable>
</TableContainer>
)

View File

@ -0,0 +1,36 @@
import { getTweet } from 'react-tweet/api'
import { EmbeddedTweet, TweetNotFound, type TweetProps } from 'react-tweet'
import './tweet.css'
const TweetContent = async ({ id, components, onError }: TweetProps) => {
let error
const tweet = id
? await getTweet(id).catch((err) => {
if (onError) {
error = onError(err)
} else {
console.error(err)
error = err
}
})
: undefined
if (!tweet) {
const NotFound = components?.TweetNotFound || TweetNotFound
return <NotFound error={error} />
}
return <EmbeddedTweet tweet={tweet} components={components} />
}
export const ReactTweet = (props: TweetProps) => <TweetContent {...props} />
export async function Tweet({ id }: { id: string }) {
return (
<div className="tweet my-6">
<div className={`flex justify-center`}>
<ReactTweet id={id} />
</div>
</div>
)
}

View File

@ -0,0 +1,67 @@
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getBlogPosts } from '@/app/db/blog'
import { Post } from './Post'
import { serialize } from 'next-mdx-remote/serialize'
import '@/assets/prose.css'
import { env } from '@typebot.io/env'
export async function generateMetadata({
params,
}: {
params: { slug: string }
}): Promise<Metadata | undefined> {
const post = getBlogPosts().find(
(post) => post.slug === params.slug && post.metadata.publishedAt
)
if (!post) {
return
}
const {
title,
publishedAt: publishedTime,
summary: description,
image,
} = post.metadata
const ogImage = image
? `${env.LANDING_PAGE_URL}${image}`
: `${env.LANDING_PAGE_URL}/og?title=${title}`
return {
title,
description,
openGraph: {
title,
description,
type: 'article',
publishedTime,
url: `${env.LANDING_PAGE_URL}/blog/${post.slug}`,
images: [
{
url: ogImage,
},
],
},
twitter: {
card: 'summary_large_image',
title,
description,
images: [ogImage],
},
}
}
export default async function Blog({ params }: { params: { slug: string } }) {
const post = getBlogPosts().find(
(post) => post.slug === params.slug && post.metadata.publishedAt
)
if (!post) {
notFound()
}
const mdxSource = await serialize(post.content)
return <Post metadata={post.metadata} mdxSource={mdxSource} />
}

View File

@ -0,0 +1,63 @@
/* Light theme (default) */
.tweet .react-tweet-theme {
/* margin is handled by our wrappers */
--tweet-container-margin: 0;
--tweet-font-family: inherit;
--tweet-font-color: inherit;
--tweet-bg-color: #222;
--tweet-bg-color-hover: var(--tweet-bg-color);
--tweet-quoted-bg-color-hover: rgba(255, 255, 255, 0.03);
--tweet-border: 1px solid #333;
--tweet-color-blue-secondary: theme('colors.white');
--tweet-color-blue-secondary-hover: #333;
--tweet-font-color-secondary: theme('colors.gray.400');
/* Common properties for both themes */
--tweet-quoted-bg-color-hover: rgba(0, 0, 0, 0.03);
--tweet-border: 1px solid rgb(64, 64, 64);
--tweet-skeleton-gradient: linear-gradient(
270deg,
#fafafa,
#eaeaea,
#eaeaea,
#fafafa
);
--tweet-color-red-primary: rgb(249, 24, 128);
--tweet-color-red-primary-hover: rgba(249, 24, 128, 0.1);
--tweet-color-green-primary: rgb(0, 186, 124);
--tweet-color-green-primary-hover: rgba(0, 186, 124, 0.1);
--tweet-twitter-icon-color: var(--tweet-font-color);
--tweet-verified-old-color: rgb(130, 154, 171);
--tweet-verified-blue-color: var(--tweet-color-blue-primary);
--tweet-actions-font-weight: 500;
--tweet-replies-font-weight: 500;
}
/* Common styles for both themes */
.tweet .react-tweet-theme p {
font-size: inherit;
line-height: 1.3rem;
}
.tweet .react-tweet-theme p a {
@apply border-b transition-[border-color] border-gray-500 text-white hover:border-white;
}
/* Remove link underline on hover for both themes */
.tweet .react-tweet-theme p a:hover {
text-decoration: none;
}
.tweet a div {
@apply font-medium tracking-tight;
}
.tweet div[class*='mediaWrapper'] {
max-height: 250px;
}
.tweet .react-tweet-theme img {
margin: 0;
}

View File

@ -0,0 +1,14 @@
import { getBlogPosts } from '@/app/db/blog'
import { Posts } from './Posts'
export const metadata = {
title: 'Typebot Blog',
description:
'The official Typebot blog where we share our thoughts and tips on everything related to chatbots, conversational marketing, customer support and more.',
}
export default function Home() {
const allBlogs = getBlogPosts()
return <Posts allBlogs={allBlogs} />
}

View File

@ -0,0 +1,60 @@
import fs from 'fs'
import path from 'path'
type Metadata = {
title: string
publishedAt: string
summary: string
image?: string
}
function parseFrontmatter(fileContent: string) {
const frontmatterRegex = /---\s*([\s\S]*?)\s*---/
const match = frontmatterRegex.exec(fileContent)
const frontMatterBlock = match![1]
const content = fileContent.replace(frontmatterRegex, '').trim()
const frontMatterLines = frontMatterBlock.trim().split('\n')
const metadata: Partial<Metadata> = {}
frontMatterLines.forEach((line) => {
const [key, ...valueArr] = line.split(': ')
let value = valueArr.join(': ').trim()
value = value.replace(/^['"](.*)['"]$/, '$1') // Remove quotes
metadata[key.trim() as keyof Metadata] = value
})
return { metadata: metadata as Metadata, content }
}
function getMDXFiles(dir: fs.PathLike) {
return fs.readdirSync(dir).filter((file) => path.extname(file) === '.mdx')
}
function readMDXFile(filePath: fs.PathOrFileDescriptor) {
const rawContent = fs.readFileSync(filePath, 'utf-8')
return parseFrontmatter(rawContent)
}
function extractTweetIds(content: string) {
const tweetMatches = content.match(/<StaticTweet\sid="[0-9]+"\s\/>/g)
return tweetMatches?.map((tweet) => tweet.match(/[0-9]+/g)?.[0]) || []
}
function getMDXData(dir: string) {
const mdxFiles = getMDXFiles(dir)
return mdxFiles.map((file) => {
const { metadata, content } = readMDXFile(path.join(dir, file))
const slug = path.basename(file, path.extname(file))
const tweetIds = extractTweetIds(content)
return {
metadata,
slug,
tweetIds,
content,
}
})
}
export function getBlogPosts() {
return getMDXData(path.join(process.cwd(), 'content'))
}

View File

@ -0,0 +1,40 @@
/* eslint-disable @next/next/no-sync-scripts */
/* eslint-disable @next/next/no-page-custom-font */
import type { Metadata } from 'next'
import { Header } from 'components/common/Header/Header'
import { Footer } from 'components/common/Footer'
import { Providers } from './providers'
import 'assets/style.css'
export const metadata: Metadata = {
title: 'Typebot - Open-source conversational apps builder',
description:
'Powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<head>
<link rel="icon" type="image/png" href="/favicon.png" />
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&family=Indie+Flower:wght@400&display=swap"
rel="stylesheet"
/>
<script src="/__ENV.js" />
</head>
<body style={{ backgroundColor: '#171923' }}>
<Providers>
<Header />
{children}
<Footer />
</Providers>
</body>
</html>
)
}

View File

@ -0,0 +1,59 @@
import { ImageResponse } from 'next/og'
import { NextRequest } from 'next/server'
import { env } from '@typebot.io/env'
export const runtime = 'edge'
export async function GET(req: NextRequest) {
const { searchParams } = req.nextUrl
const postTitle = searchParams.get('title')
const font = fetch(
new URL('../../assets/Outfit-Medium.ttf', import.meta.url)
).then((res) => res.arrayBuffer())
const fontData = await font
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'center',
backgroundImage: `url(${env.LANDING_PAGE_URL}/images/og-bg.png)`,
}}
>
<div
style={{
marginLeft: 190,
marginRight: 190,
display: 'flex',
fontSize: 130,
fontFamily: 'Outfit',
letterSpacing: '-0.05em',
fontStyle: 'normal',
color: 'white',
lineHeight: '120px',
whiteSpace: 'pre-wrap',
}}
>
{postTitle}
</div>
</div>
),
{
width: 1280,
height: 720,
fonts: [
{
name: 'Outfit',
data: fontData,
style: 'normal',
},
],
}
)
}

View File

@ -0,0 +1,13 @@
'use client'
import { theme } from '@/lib/chakraTheme'
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ChakraProvider theme={theme}>
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
{children}
</ChakraProvider>
)
}