📄 Add Commercial License for ee folder (#1532)
This commit is contained in:
56
ee/apps/landing-page/app/blog/Posts.tsx
Normal file
56
ee/apps/landing-page/app/blog/Posts.tsx
Normal 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>
|
||||
)
|
178
ee/apps/landing-page/app/blog/[slug]/Post.tsx
Normal file
178
ee/apps/landing-page/app/blog/[slug]/Post.tsx
Normal 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)`
|
||||
}
|
||||
}
|
37
ee/apps/landing-page/app/blog/[slug]/Table.tsx
Normal file
37
ee/apps/landing-page/app/blog/[slug]/Table.tsx
Normal 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>
|
||||
)
|
36
ee/apps/landing-page/app/blog/[slug]/Tweet.tsx
Normal file
36
ee/apps/landing-page/app/blog/[slug]/Tweet.tsx
Normal 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>
|
||||
)
|
||||
}
|
67
ee/apps/landing-page/app/blog/[slug]/page.tsx
Normal file
67
ee/apps/landing-page/app/blog/[slug]/page.tsx
Normal 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} />
|
||||
}
|
63
ee/apps/landing-page/app/blog/[slug]/tweet.css
Normal file
63
ee/apps/landing-page/app/blog/[slug]/tweet.css
Normal 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;
|
||||
}
|
14
ee/apps/landing-page/app/blog/page.tsx
Normal file
14
ee/apps/landing-page/app/blog/page.tsx
Normal 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} />
|
||||
}
|
60
ee/apps/landing-page/app/db/blog.ts
Normal file
60
ee/apps/landing-page/app/db/blog.ts
Normal 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'))
|
||||
}
|
40
ee/apps/landing-page/app/layout.tsx
Normal file
40
ee/apps/landing-page/app/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
59
ee/apps/landing-page/app/og/route.tsx
Normal file
59
ee/apps/landing-page/app/og/route.tsx
Normal 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',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
13
ee/apps/landing-page/app/providers.tsx
Normal file
13
ee/apps/landing-page/app/providers.tsx
Normal 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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user