├── .env.sample
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── app
├── (builder)
│ ├── chai
│ │ ├── api
│ │ │ ├── preview
│ │ │ │ └── route.ts
│ │ │ ├── revalidate
│ │ │ │ └── route.ts
│ │ │ └── route.ts
│ │ ├── assets
│ │ │ └── route.ts
│ │ ├── page.tsx
│ │ └── users
│ │ │ └── route.ts
│ ├── chaibuilder.tailwind.ts
│ ├── layout.tsx
│ └── styles.css
├── (public)
│ ├── [[...slug]]
│ │ └── page.tsx
│ ├── layout.tsx
│ └── public.css
├── favicon.ico
└── fonts
│ ├── GeistMonoVF.woff
│ └── GeistVF.woff
├── blocks
├── accordion
│ └── Accordion.tsx
├── blogs-grid
│ ├── BlogsGrid.tsx
│ └── data-provider.ts
├── dropdown
│ └── Dropdown.tsx
├── image
│ └── Image.tsx
├── index.server.ts
├── index.ts
├── link
│ └── Link.tsx
└── modal
│ └── Modal.tsx
├── chai
├── index.ts
└── theme-presets.ts
├── cms
└── index.ts
├── components.json
├── components
├── builder
│ ├── chaibuilder-pages.tsx
│ ├── loader.tsx
│ └── logo.tsx
├── preview-banner.tsx
└── ui
│ ├── accordion.tsx
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── checkbox.tsx
│ ├── collapsible.tsx
│ ├── command.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── popover.tsx
│ ├── progress.tsx
│ ├── scroll-area.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── skeleton.tsx
│ ├── slider.tsx
│ ├── textarea.tsx
│ ├── theme-provider.tsx
│ └── tooltip.tsx
├── data
├── global.ts
└── index.ts
├── declaration.d.ts
├── fonts
└── index.ts
├── lib
└── utils.ts
├── next.config.mjs
├── package.json
├── page-types
├── blog.ts
└── index.ts
├── pnpm-lock.yaml
├── postcss.config.mjs
├── scripts
├── check-env.cjs
└── tailwind.cjs
├── tailwind.config.ts
├── tsconfig.json
└── utils
└── styles-helper.ts
/.env.sample:
--------------------------------------------------------------------------------
1 | CHAIBUILDER_API_KEY=#specify the chaibuilder api key here
2 | CHAIBUILDER_WEBHOOK_SECRET=#specify your own webhook secret token here
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 | .idea
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 | public/chaistyles.css
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2024, Suraj Air
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name "Chai Builder" nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Chai builder + NextJS Starter
2 |
3 | This is a starter project for Chai builder + NextJS.
4 |
5 | ## Requirements
6 |
7 | - `CHAIBUILDER_API_KEY` - Get API KEY here [https://chaibuilder.com](https://chaibuilder.com/sites)
8 |
9 | ## Features
10 |
11 | - Website builder with drag and drop
12 | - One click publish
13 | - Revisions and restore
14 | - Page lock (prevent multiple users from editing the same page)
15 | - Multi-language support
16 | - SEO ( Basic, Open Graph, JSON-LD )
17 | - SSR and SSG support
18 | - Built in Vercel ISR support
19 | - Themeable with Tailwind CSS(Shadcn themes)
20 | - Global blocks for reusable content
21 | - Draft preview mode (preview changes before publishing)
22 | - Data binding with external data (e.g. from a CMS)
23 | - Custom page blocks (e.g. a team page with a list of team members)
24 | - Custom page types (e.g. a blog page template for all blogs)
25 | - AI content generation ( with multilingual support )
26 | - AI style editing
27 | - Dark mode support
28 | - Custom authentication ( Implement your own auth provider )
29 | - Custom DAM ( Implement your own or use A DAM solution )
30 |
31 | ## Installation:
32 |
33 | 1. Fork this repo
34 | 2. `pnpm install`
35 | 3. Create a new .env file and add env variables from .env.sample
36 | 4. `pnpm dev`
37 | 5. Goto `/chai` route to login and edit in builder
38 |
39 | ## Stack
40 |
41 | - NextJS15 + React 19
42 | - Tailwind CSS 3.4+
43 | - Shadcn UI
44 | - TypeScript
45 |
46 | ## Development
47 |
48 | We recomment using `pnpm` for development.
49 |
50 | ```bash
51 | pnpm install
52 | pnpm run dev
53 | ```
54 |
55 | Navigate to `/chai` route to view the builder and publish. Sign in and start publishing your website.
56 |
57 | ## Deployment
58 |
59 | We recomment using `Vercel` for deployment for better ISR support.
60 |
61 | ## License
62 |
63 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
64 |
--------------------------------------------------------------------------------
/app/(builder)/chai/api/preview/route.ts:
--------------------------------------------------------------------------------
1 | // route handler with secret and slug
2 | import { draftMode } from "next/headers";
3 | import { redirect } from "next/navigation";
4 |
5 | export async function GET(request: Request) {
6 | // Parse query string parameters
7 | const { searchParams } = new URL(request.url);
8 | const slug = searchParams.get("slug");
9 | const disable = searchParams.get("disable");
10 |
11 | // Check the secret and next parameters
12 | // This secret should only be known to this route handler and the CMS
13 | if (!slug) {
14 | return new Response("Invalid token", { status: 401 });
15 | }
16 |
17 | // Enable Draft Mode by setting the cookie
18 | if (disable === "true") {
19 | (await draftMode()).disable();
20 | } else {
21 | (await draftMode()).enable();
22 | }
23 |
24 | // Redirect to the path from the fetched post
25 | // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
26 | redirect(slug);
27 | }
28 |
--------------------------------------------------------------------------------
/app/(builder)/chai/api/revalidate/route.ts:
--------------------------------------------------------------------------------
1 | import { revalidatePath, revalidateTag } from 'next/cache'
2 | import { NextRequest, NextResponse } from 'next/server'
3 |
4 | const revalidateRoute = async (req: NextRequest) => {
5 | const { searchParams } = new URL(req.url)
6 | const secret = searchParams.get('secret')
7 |
8 | if (secret !== process.env.CHAI_SECRET_TOKEN) {
9 | return NextResponse.json({ error: 'Invalid secret' }, { status: 401 })
10 | }
11 |
12 | const tags = req.nextUrl.searchParams.get('tags') || ''
13 | const paths = req.nextUrl.searchParams.get('paths') || ''
14 |
15 | try {
16 | const tagsArray = Array.isArray(tags) ? tags : tags.split(',')
17 | await Promise.all(tagsArray.map((tag) => revalidateTag(tag)))
18 |
19 | const pathsArray = Array.isArray(paths) ? paths : paths.split(',')
20 | await Promise.all(pathsArray.map((path) => revalidatePath(path)))
21 |
22 | return NextResponse.json(
23 | { message: 'Tags and paths revalidated successfully' },
24 | { status: 200 }
25 | )
26 | } catch (error) {
27 | console.error(error)
28 | return NextResponse.json(
29 | { error: 'Failed to revalidate tags and paths' },
30 | { status: 500 }
31 | )
32 | }
33 | }
34 |
35 | export async function GET(req: NextRequest) {
36 | return revalidateRoute(req)
37 | }
38 |
--------------------------------------------------------------------------------
/app/(builder)/chai/api/route.ts:
--------------------------------------------------------------------------------
1 | import { chaiBuilderPages } from "@/chai";
2 | import "@/data";
3 | import "@/page-types";
4 | import { get, has } from "lodash";
5 | import { revalidateTag } from "next/cache";
6 | import { NextRequest, NextResponse } from "next/server";
7 |
8 | export async function POST(req: NextRequest) {
9 | const requestBody = await req.json();
10 |
11 | try {
12 | // Check for `authorization` header
13 | const authorization = req.headers.get("authorization");
14 | if (!authorization) {
15 | return NextResponse.json(
16 | { error: "Missing Authorization header" },
17 | { status: 401 }
18 | );
19 | }
20 |
21 | // Check and extract, valid token string `authorization`
22 | const authToken = authorization ? authorization.split(" ")[1] : "";
23 | const response = await chaiBuilderPages.handle(requestBody, authToken);
24 |
25 | const tags = get(response, "tags", []);
26 | for (const tag of tags) {
27 | revalidateTag(tag);
28 | }
29 |
30 | if (has(response, "error")) {
31 | return NextResponse.json(response, { status: response.status });
32 | }
33 | return NextResponse.json(response);
34 | } catch (error) {
35 | // * On error, throw if firebase auth error, else 500
36 | if (error instanceof Error) {
37 | return NextResponse.json(
38 | { error: "Invalid or expired token" },
39 | { status: 401 }
40 | );
41 | } else {
42 | return NextResponse.json(
43 | { error: "Something went wrong." },
44 | { status: 500 }
45 | );
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/(builder)/chai/assets/route.ts:
--------------------------------------------------------------------------------
1 | import { chaiBuilderPages } from "@/chai";
2 | import "@/data";
3 | import "@/page-types";
4 | import { has } from "lodash";
5 | import { NextRequest, NextResponse } from "next/server";
6 |
7 | export async function POST(req: NextRequest) {
8 | const requestBody = await req.json();
9 | try {
10 | // Check for `authorization` header
11 | const authorization = req.headers.get("authorization");
12 | if (!authorization) {
13 | return NextResponse.json(
14 | { error: "Missing Authorization header" },
15 | { status: 401 }
16 | );
17 | }
18 |
19 | const authToken = authorization ? authorization.split(" ")[1] : "";
20 | const response = await chaiBuilderPages.handle(requestBody, authToken);
21 | if (has(response, "error")) {
22 | return NextResponse.json(response, { status: response.status });
23 | }
24 | return NextResponse.json(response);
25 | } catch (error) {
26 | // * On error, throw if firebase auth error, else 500
27 | if (error instanceof Error) {
28 | return NextResponse.json(
29 | { error: "Invalid or expired token" },
30 | { status: 401 }
31 | );
32 | } else {
33 | return NextResponse.json(
34 | { error: "Something went wrong." },
35 | { status: 500 }
36 | );
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/(builder)/chai/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import FullScreenLoader from "@/components/builder/loader";
4 | import dynamic from "next/dynamic";
5 |
6 | const ChaiBuilderPages = dynamic(
7 | () => import("@/components/builder/chaibuilder-pages"),
8 | { ssr: false, loading: () => }
9 | );
10 |
11 | export default function Page() {
12 | return ;
13 | }
14 |
--------------------------------------------------------------------------------
/app/(builder)/chai/users/route.ts:
--------------------------------------------------------------------------------
1 | import { chaiBuilderPages } from "@/chai";
2 | import "@/data";
3 | import "@/page-types";
4 | import { has } from "lodash";
5 | import { NextRequest, NextResponse } from "next/server";
6 |
7 | export async function POST(req: NextRequest) {
8 | const requestBody = await req.json();
9 | try {
10 | const authorization = req.headers.get("authorization");
11 | const authToken = authorization ? authorization.split(" ")[1] : "";
12 | const response = await chaiBuilderPages.handle(requestBody, authToken);
13 | if (has(response, "error")) {
14 | return NextResponse.json(response, { status: response.status });
15 | }
16 | return NextResponse.json(response);
17 | } catch (error) {
18 | // * On error, throw if firebase auth error, else 500
19 | if (error instanceof Error) {
20 | return NextResponse.json(
21 | { error: "Invalid or expired token" },
22 | { status: 401 }
23 | );
24 | } else {
25 | return NextResponse.json(
26 | { error: "Something went wrong." },
27 | { status: 500 }
28 | );
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/(builder)/chaibuilder.tailwind.ts:
--------------------------------------------------------------------------------
1 | import { getChaiBuilderTailwindConfig } from "@chaibuilder/pages/tailwind";
2 | export default getChaiBuilderTailwindConfig([
3 | "./app/(builder)/**/*.{js,ts,jsx,tsx,mdx}",
4 | "./components/builder/**/*.{js,ts,jsx,tsx,mdx}",
5 | "./node_modules/@chaibuilder/pages/dist/**/*.{js,cjs}",
6 | "./node_modules/@chaibuilder/sdk/dist/**/*.{js,cjs}",
7 | ]);
8 |
--------------------------------------------------------------------------------
/app/(builder)/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/app/(builder)/styles.css";
2 | import "@chaibuilder/pages/styles";
3 |
4 | export const metadata = {
5 | title: "Chai Builder - Admin",
6 | description: "Start building your website with Chai Builder",
7 | };
8 |
9 | export default function RootLayout({
10 | children,
11 | }: {
12 | children: React.ReactNode;
13 | }) {
14 | return (
15 |
16 |
17 |
18 |
23 |
24 | {children}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/app/(builder)/styles.css:
--------------------------------------------------------------------------------
1 | @config "./chaibuilder.tailwind.ts";
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | /* #TODO: Remove this once we have a better way to handle pointer-events */
8 | body {
9 | pointer-events: all !important;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(public)/[[...slug]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { registerBlocks } from "@/blocks";
2 | import { registerServerBlocks } from "@/blocks/index.server";
3 | import {
4 | chaiBuilderPages,
5 | getChaiBuilderPage,
6 | getChaiPageData,
7 | getChaiPageSeoMetadata,
8 | getChaiPageStyles,
9 | getChaiSiteSettings,
10 | NextPageProps,
11 | } from "@/chai";
12 | import PreviewBanner from "@/components/preview-banner";
13 | import "@/page-types";
14 | import { ChaiBlock } from "@chaibuilder/pages/builder";
15 | import { RenderChaiBlocks } from "@chaibuilder/pages/render";
16 | import { ChaiPageProps } from "@chaibuilder/pages/runtime";
17 | import { loadWebBlocks } from "@chaibuilder/pages/web-blocks";
18 | import { get, noop } from "lodash";
19 | import isEmpty from "lodash/isEmpty";
20 | import { draftMode } from "next/headers";
21 | import { notFound } from "next/navigation";
22 |
23 | loadWebBlocks();
24 | registerBlocks();
25 | registerServerBlocks();
26 |
27 | export const dynamic = "force-static"; // Remove this if you want to use ssr mode
28 |
29 | export const generateMetadata = async (props: NextPageProps) => {
30 | const nextParams = await props.params;
31 | const slug = nextParams.slug ? `/${nextParams.slug.join("/")}` : "/";
32 |
33 | const siteSettings = await getChaiSiteSettings();
34 | chaiBuilderPages.setFallbackLang(get(siteSettings, "fallbackLang", ""));
35 | chaiBuilderPages.setLanguageFromSlug(nextParams.slug);
36 | const chaiPage = await getChaiBuilderPage(slug);
37 | const pageProps: ChaiPageProps = {
38 | slug,
39 | pageType: chaiPage.pageType,
40 | fallbackLang: chaiBuilderPages.getFallbackLang(),
41 | };
42 | return await getChaiPageSeoMetadata(pageProps);
43 | };
44 |
45 | export default async function Page({
46 | params,
47 | }: {
48 | params: Promise<{ slug: string[] }>;
49 | }) {
50 | const { isEnabled } = await draftMode();
51 | const nextParams = await params;
52 | const slug = nextParams.slug ? `/${nextParams.slug.join("/")}` : "/";
53 |
54 | const siteSettings = await getChaiSiteSettings();
55 | chaiBuilderPages.setFallbackLang(get(siteSettings, "fallbackLang", ""));
56 | chaiBuilderPages.setLanguageFromSlug(nextParams.slug);
57 |
58 | const chaiPage = await getChaiBuilderPage(slug);
59 |
60 | if ("error" in chaiPage && chaiPage.error === "PAGE_NOT_FOUND") {
61 | return notFound();
62 | }
63 |
64 | const pageProps: ChaiPageProps = {
65 | slug,
66 | pageType: chaiPage.pageType,
67 | fallbackLang: chaiBuilderPages.getFallbackLang(),
68 | };
69 |
70 | const pageStyles = await getChaiPageStyles(chaiPage.blocks as ChaiBlock[]);
71 | const fallbackLang = chaiBuilderPages.getFallbackLang();
72 |
73 | const pageData = await getChaiPageData(
74 | chaiPage.blocks as unknown as ChaiBlock[],
75 | chaiPage.pageType,
76 | pageProps
77 | );
78 |
79 | return (
80 | <>
81 |
85 | {isEnabled && }
86 |
94 | >
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/app/(public)/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/app/(public)/public.css";
2 | import { chaiBuilderPages, getChaiSiteSettings } from "@/chai";
3 | import { ThemeProvider } from "@/components/ui/theme-provider";
4 | import "@/data";
5 | import { registerFonts } from "@/fonts";
6 | import { getFontHref, getThemeCustomFontFace } from "@/utils/styles-helper";
7 | import { getChaiThemeCssVariables } from "@chaibuilder/pages/render";
8 | import { get } from "lodash";
9 | import { draftMode } from "next/headers";
10 |
11 | registerFonts();
12 |
13 | export default async function RootLayout({
14 | children,
15 | }: Readonly<{
16 | children: React.ReactNode;
17 | }>) {
18 | const { isEnabled } = await draftMode();
19 |
20 | //IMPORTANT: This is an important step to set the draft mode for the page.
21 | //Without this, the page will always be in live mode
22 | chaiBuilderPages.setDraftMode(isEnabled);
23 |
24 | const siteSettings = await getChaiSiteSettings();
25 | if ("error" in siteSettings) {
26 | console.error(siteSettings.error);
27 | }
28 |
29 | // Add empty theme object as fallback
30 | const theme = get(siteSettings, "theme", {});
31 | const themeCssVariables = getChaiThemeCssVariables(theme);
32 | const bodyFont = get(theme, "fontFamily.body", "Inter");
33 | const headingFont = get(theme, "fontFamily.heading", "Inter");
34 | const fontUrls = getFontHref([bodyFont, headingFont]);
35 | const customFontFace = getThemeCustomFontFace([bodyFont, headingFont]);
36 |
37 | return (
38 |
39 |
40 |
41 | {fontUrls.map((fontUrl: string) => (
42 |
49 | ))}
50 |
51 |
56 |
57 |
61 | {fontUrls.map((fontUrl: string) => (
62 |
63 | ))}
64 |
68 |
69 |
70 |
75 | {children}
76 |
77 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/app/(public)/public.css:
--------------------------------------------------------------------------------
1 | @config "../../tailwind.config.ts";
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | @layer utilities {
8 | .text-balance {
9 | text-wrap: balance;
10 | }
11 |
12 | .w-\[inherit\] {
13 | width: inherit;
14 | }
15 |
16 | .h-\[inherit\] {
17 | height: inherit;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chaibuilder/chaibuilder-nextjs/10d75aab2c8eadec1e7b3b60c7285852654c229e/app/favicon.ico
--------------------------------------------------------------------------------
/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chaibuilder/chaibuilder-nextjs/10d75aab2c8eadec1e7b3b60c7285852654c229e/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chaibuilder/chaibuilder-nextjs/10d75aab2c8eadec1e7b3b60c7285852654c229e/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/blocks/accordion/Accordion.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Accordion,
3 | AccordionContent,
4 | AccordionItem,
5 | AccordionTrigger,
6 | } from "@/components/ui/accordion";
7 | import {
8 | builderProp,
9 | ChaiBlock,
10 | ChaiBlockComponentProps,
11 | ChaiStyles,
12 | registerChaiBlock,
13 | registerChaiBlockSchema,
14 | StylesProp,
15 | } from "@chaibuilder/pages/runtime";
16 | import { CaretSortIcon } from "@radix-ui/react-icons";
17 |
18 | export type AccordionProps = {
19 | children: React.ReactNode;
20 | styles: ChaiStyles;
21 | type: "single" | "multiple";
22 | show: boolean;
23 | };
24 |
25 | type AccordionTriggerProps = {
26 | children: React.ReactNode;
27 | styles: ChaiStyles;
28 | content: string;
29 | };
30 |
31 | type AccordionContentProps = {
32 | children: React.ReactNode;
33 | styles: ChaiStyles;
34 | content: string;
35 | };
36 |
37 | const AccordionTriggerComponent = (
38 | props: ChaiBlockComponentProps
39 | ) => {
40 | const { blockProps, content, children, styles } = props;
41 | return (
42 |
43 |
44 | {children || content}
45 |
46 |
47 | );
48 | };
49 |
50 | registerChaiBlock(AccordionTriggerComponent, {
51 | type: "AccordionTrigger",
52 | label: "Accordion Trigger",
53 | group: "advanced",
54 | category: "core",
55 | hidden: true,
56 | canMove: () => false,
57 | canDelete: () => false,
58 | canAcceptBlock: () => true,
59 | canDuplicate: () => false,
60 | ...registerChaiBlockSchema({
61 | properties: {
62 | styles: StylesProp(""),
63 | content: {
64 | type: "string",
65 | title: "Content",
66 | default: "Accordion Button",
67 | },
68 | },
69 | }),
70 | i18nProps: ["content"],
71 | aiProps: ["content"],
72 | });
73 |
74 | const AccordionContentComponent = (
75 | props: ChaiBlockComponentProps
76 | ) => {
77 | const { blockProps, children, styles, content } = props;
78 |
79 | return (
80 |
81 | {children || content}
82 |
83 | );
84 | };
85 |
86 | registerChaiBlock(AccordionContentComponent, {
87 | type: "AccordionContent",
88 | label: "Accordion Content",
89 | group: "advanced",
90 | hidden: true,
91 | canMove: () => false,
92 | canDelete: () => false,
93 | canAcceptBlock: () => true,
94 | canDuplicate: () => false,
95 | ...registerChaiBlockSchema({
96 | properties: {
97 | styles: StylesProp(""),
98 | content: {
99 | type: "string",
100 | title: "Title",
101 | default: "This is accordion content",
102 | },
103 | },
104 | }),
105 | i18nProps: ["content"],
106 | aiProps: ["content"],
107 | });
108 |
109 | const Component = (props: ChaiBlockComponentProps) => {
110 | const { _id, show, styles, children, blockProps, inBuilder } = props;
111 | return (
112 |
116 |
120 |
121 | {children}
122 |
123 |
124 |
125 | );
126 | };
127 |
128 | const Config = {
129 | type: "Accordion",
130 | label: "Accordion",
131 | group: "advanced",
132 | category: "core",
133 | wrapper: true,
134 | icon: CaretSortIcon,
135 | blocks: () =>
136 | [
137 | {
138 | _type: "Accordion",
139 | _id: "accordion",
140 | styles: "#styles:,w-full border-b",
141 | },
142 | {
143 | _type: "AccordionTrigger",
144 | _id: "accordion-item",
145 | _parent: "accordion",
146 | content: "Accordion Button",
147 | styles: "#styles:,w-full overflow-x-hidden hover:underline py-4",
148 | },
149 | {
150 | _type: "AccordionContent",
151 | _id: "accordion-content",
152 | styles: "#styles:,w-full pb-4",
153 | _parent: "accordion",
154 | content: "This is accordion content",
155 | },
156 | ] as ChaiBlock[],
157 | ...registerChaiBlockSchema({
158 | properties: {
159 | show: builderProp({
160 | type: "boolean",
161 | title: "Expand Accordion",
162 | default: false,
163 | }),
164 | styles: StylesProp("relative w-max"),
165 | },
166 | }),
167 | };
168 |
169 | export { Component as Accordion, Config as AccordionConfig };
170 |
--------------------------------------------------------------------------------
/blocks/blogs-grid/BlogsGrid.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ChaiBlockComponentProps,
3 | ChaiStyles,
4 | registerChaiBlockSchema,
5 | StylesProp,
6 | } from "@chaibuilder/pages/runtime";
7 |
8 | type AsyncProp = T | undefined;
9 |
10 | export type BlogsListProps = {
11 | heading: string;
12 | blogCount: number;
13 | showBlogLink: boolean;
14 | hideReadMore: boolean;
15 | cardStyles: ChaiStyles;
16 | imageStyles: ChaiStyles;
17 | containerStyles: ChaiStyles;
18 | headingStyles: ChaiStyles;
19 | blogs: AsyncProp<
20 | {
21 | id: string;
22 | title: string;
23 | url: string;
24 | thumbnailUrl: string;
25 | }[]
26 | >;
27 | };
28 |
29 | const i18nTranslations: { [key: string]: { [key: string]: string } } = {
30 | fr: { heading: "This is a dynamic block in French" },
31 | en: { heading: "This is a dynamic block in English" },
32 | };
33 |
34 | export const BlogsList = (props: ChaiBlockComponentProps) => {
35 | return (
36 |
37 |
38 |
{props.heading}
39 | {i18nTranslations[props.lang]?.heading}
40 |
41 |
42 |
43 | {props.blogs?.map(
44 | (blog: {
45 | id: string;
46 | title: string;
47 | url: string;
48 | thumbnailUrl: string;
49 | }) => (
50 |
51 |
52 |

58 |
59 | Sponsored
60 |
61 |
62 |
63 |
64 |
{blog.title}
65 |
{blog.url}
66 | {!props.hideReadMore && (
67 |
68 | Read more
69 |
82 |
83 | )}
84 |
85 |
86 | )
87 | )}
88 |
89 | {props.showBlogLink && (
90 |
95 | )}
96 |
97 | );
98 | };
99 |
100 | export const BlogsListConfig = {
101 | type: "BlogsList",
102 | label: "Blogs List",
103 | group: "Custom",
104 | category: "core",
105 | dataProvider: () => {
106 | return {
107 | blogs: [
108 | {
109 | albumId: 1,
110 | id: 4,
111 | title: "culpa odio esse rerum omnis laboriosam voluptate repudiandae",
112 | url: "https://picsum.photos/200/301",
113 | thumbnailUrl: "https://picsum.photos/200/301",
114 | },
115 | {
116 | albumId: 1,
117 | id: 5,
118 | title: "natus nisi omnis corporis facere molestiae rerum in",
119 | url: "https://picsum.photos/200/302",
120 | thumbnailUrl: "https://picsum.photos/200/302",
121 | },
122 | {
123 | albumId: 1,
124 | id: 6,
125 | title: "accusamus ea aliquid et amet sequi nemo",
126 | url: "https://picsum.photos/200/303",
127 | thumbnailUrl: "https://picsum.photos/200/303",
128 | },
129 | ],
130 | };
131 | },
132 | ...registerChaiBlockSchema({
133 | properties: {
134 | containerStyles: StylesProp(
135 | "max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto"
136 | ),
137 | cardStyles: StylesProp("group flex flex-col focus:outline-none"),
138 | imageStyles: StylesProp(
139 | "size-full absolute top-0 start-0 object-cover group-hover:scale-105 group-focus:scale-105 transition-transform duration-500 ease-in-out rounded-xl"
140 | ),
141 | headingStyles: StylesProp(
142 | "text-2xl font-bold md:text-4xl md:leading-tight dark:text-white"
143 | ),
144 | heading: {
145 | type: "string",
146 | default: "This is a dynamic block",
147 | title: "Heading",
148 | },
149 | blogCount: {
150 | type: "number",
151 | default: 9,
152 | title: "Blog Count",
153 | },
154 | showBlogLink: {
155 | type: "boolean",
156 | default: true,
157 | title: "Show Blog Link",
158 | },
159 | hideReadMore: {
160 | type: "boolean",
161 | default: false,
162 | title: "Hide Read More",
163 | },
164 | },
165 | }),
166 | i18nProps: ["heading"],
167 | aiProps: ["heading"],
168 | };
169 |
--------------------------------------------------------------------------------
/blocks/blogs-grid/data-provider.ts:
--------------------------------------------------------------------------------
1 | import type { ChaiBlock } from "@chaibuilder/pages";
2 |
3 | export const blogsGridDataProvider = async ({
4 | block,
5 | }: {
6 | block: ChaiBlock;
7 | }) => {
8 | const { blogCount } = block;
9 | const response = await fetch("https://jsonplaceholder.typicode.com/photos");
10 | // pick on 10 posts
11 | const data = await response.json();
12 | const posts = data.slice(0, blogCount || 3);
13 | const blogs = posts.map(
14 | (
15 | item: {
16 | id: string;
17 | title: string;
18 | url: string;
19 | thumbnailUrl: string;
20 | },
21 | index: number
22 | ) => ({
23 | id: item.id,
24 | title: item.title,
25 | url: item.url,
26 | thumbnailUrl: "https://picsum.photos/200/30" + index,
27 | })
28 | );
29 | return { blogs, $metadata: { pageType: "BlogsList" } };
30 | };
31 |
--------------------------------------------------------------------------------
/blocks/dropdown/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DropdownMenu,
3 | DropdownMenuContent,
4 | DropdownMenuTrigger,
5 | } from "@/components/ui/dropdown-menu";
6 | import {
7 | builderProp,
8 | ChaiBlock,
9 | ChaiBlockComponentProps,
10 | ChaiRuntimeProp,
11 | ChaiStyles,
12 | closestBlockProp,
13 | registerChaiBlock,
14 | registerChaiBlockSchema,
15 | StylesProp,
16 | } from "@chaibuilder/pages/runtime";
17 | import { DropdownMenuIcon } from "@radix-ui/react-icons";
18 |
19 | export type DropdownLinksProps = {
20 | showDropdown: ChaiRuntimeProp;
21 | children: React.ReactNode;
22 | styles: ChaiStyles;
23 | };
24 |
25 | const DropdownButton = (
26 | props: ChaiBlockComponentProps<{
27 | content: string;
28 | icon: string;
29 | iconWidth: string;
30 | iconHeight: string;
31 | styles: ChaiStyles;
32 | show: ChaiRuntimeProp;
33 | }>
34 | ) => {
35 | const { blockProps, content, icon, iconWidth, iconHeight, styles } = props;
36 | return (
37 |
38 | {content}
39 |
44 |
45 | );
46 | };
47 |
48 | registerChaiBlock(DropdownButton, {
49 | type: "DropdownButton",
50 | label: "Dropdown Button",
51 | group: "advanced",
52 | category: "core",
53 | hidden: true,
54 | canMove: () => false,
55 | canDelete: () => false,
56 | ...registerChaiBlockSchema({
57 | properties: {
58 | show: closestBlockProp("Dropdown", "showDropdown"),
59 | content: { type: "string", title: "Title", default: "Menu Item" },
60 | icon: {
61 | type: "string",
62 | title: "Icon",
63 | default: "",
64 | ui: { "ui:widget": "icon" },
65 | },
66 | iconWidth: { type: "string", title: "Icon Width", default: "16px" },
67 | iconHeight: { type: "string", title: "Icon Height", default: "16px" },
68 | styles: StylesProp("flex items-center gap-2 px-4 py-1"),
69 | },
70 | }),
71 | i18nProps: ["content"],
72 | aiProps: ["content"],
73 | });
74 |
75 | const DropdownContent = (
76 | props: ChaiBlockComponentProps<{
77 | children: React.ReactNode;
78 | styles: ChaiStyles;
79 | show: ChaiRuntimeProp;
80 | }>
81 | ) => {
82 | const { blockProps, children, styles } = props;
83 |
84 | return (
85 |
86 | {children}
87 |
88 | );
89 | };
90 |
91 | registerChaiBlock(DropdownContent, {
92 | type: "DropdownContent",
93 | label: "Dropdown Content",
94 | group: "basic",
95 | hidden: true,
96 | canMove: () => false,
97 | canDelete: () => false,
98 | canAcceptBlock: () => true,
99 | ...registerChaiBlockSchema({
100 | properties: {
101 | show: closestBlockProp("Dropdown", "showDropdown"),
102 | styles: StylesProp("w-80 mt-0.5 bg-white rounded-lg shadow-lg z-50"),
103 | },
104 | }),
105 | });
106 |
107 | const Component = (props: ChaiBlockComponentProps) => {
108 | const { blockProps, showDropdown, children, styles, inBuilder } = props;
109 | return (
110 |
111 |
112 | {children}
113 |
114 |
115 | );
116 | };
117 |
118 | export type DropdownProps = {
119 | showDropdown: ChaiRuntimeProp;
120 | children: React.ReactNode;
121 | styles: ChaiStyles;
122 | };
123 |
124 | const Config = {
125 | type: "Dropdown",
126 | label: "Dropdown",
127 | group: "basic",
128 | icon: DropdownMenuIcon,
129 | blocks: () =>
130 | [
131 | { _type: "Dropdown", _id: "dropdown" },
132 | {
133 | _type: "DropdownButton",
134 | _id: "button",
135 | _parent: "dropdown",
136 | title: "Menu Item",
137 | icon: ``,
138 | styles: "#styles:,flex items-center gap-2 px-4 py-1",
139 | },
140 | {
141 | _type: "DropdownContent",
142 | _id: "content",
143 | _parent: "dropdown",
144 | styles: "#styles:,w-80 mt-0.5 bg-white rounded-lg shadow-lg z-50",
145 | },
146 | {
147 | _type: "Link",
148 | _id: "link",
149 | _parent: "content",
150 | content: "Link",
151 | styles: "#styles:,flex items-center gap-2 px-4 py-1",
152 | link: { href: "https://www.google.com", type: "url", target: "_self" },
153 | },
154 | ] as ChaiBlock[],
155 | category: "core",
156 | wrapper: true,
157 | ...registerChaiBlockSchema({
158 | properties: {
159 | showDropdown: builderProp({
160 | type: "boolean",
161 | title: "Show Dropdown",
162 | default: false,
163 | }),
164 | styles: StylesProp("relative w-max"),
165 | },
166 | }),
167 | };
168 |
169 | export { Component as Dropdown, Config as DropdownConfig };
170 |
--------------------------------------------------------------------------------
/blocks/image/Image.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ChaiBlockComponentProps,
3 | ChaiStyles,
4 | } from "@chaibuilder/pages/runtime";
5 | import Image from "next/image";
6 | import * as React from "react";
7 |
8 | export const ImageBlock = (
9 | props: ChaiBlockComponentProps<{
10 | height: string;
11 | width: string;
12 | alt: string;
13 | styles: ChaiStyles;
14 | lazyLoading: boolean;
15 | image: string;
16 | }>
17 | ) => {
18 | const { image, styles, alt, height, width, lazyLoading } = props;
19 |
20 | // If width or height are missing/invalid, use fill mode
21 | const shouldUseFill =
22 | !width || !height || isNaN(parseInt(width)) || isNaN(parseInt(height));
23 |
24 | const imageElement = React.createElement(Image, {
25 | ...styles,
26 | src: image,
27 | alt: alt || "",
28 | priority: !lazyLoading,
29 | fill: shouldUseFill,
30 | height: shouldUseFill ? undefined : parseInt(height),
31 | width: shouldUseFill ? undefined : parseInt(width),
32 | style: shouldUseFill ? { objectFit: "cover" } : undefined,
33 | });
34 |
35 | if (shouldUseFill) {
36 | return React.createElement(
37 | "div",
38 | { className: "relative flex w-full h-full" },
39 | imageElement
40 | );
41 | }
42 |
43 | return imageElement;
44 | };
45 |
46 | export const ImageConfig = {
47 | type: "Image",
48 | i18nProps: ["alt"],
49 | };
50 |
--------------------------------------------------------------------------------
/blocks/index.server.ts:
--------------------------------------------------------------------------------
1 | import {
2 | registerChaiServerBlock,
3 | setChaiServerBlockDataProvider,
4 | } from "@chaibuilder/pages/runtime";
5 |
6 | import { blogsGridDataProvider } from "./blogs-grid/data-provider";
7 | import { ImageBlock, ImageConfig } from "./image/Image";
8 | import { LinkBlock, LinkConfig } from "./link/Link";
9 |
10 | export const registerServerBlocks = () => {
11 | if (typeof window !== "undefined") {
12 | throw new Error("Index.server.ts is a server-only file");
13 | }
14 | registerChaiServerBlock(ImageBlock, ImageConfig);
15 | registerChaiServerBlock(LinkBlock, LinkConfig);
16 |
17 | //set Data Provider for RSC blocks
18 | setChaiServerBlockDataProvider("BlogsList", blogsGridDataProvider);
19 | };
20 |
--------------------------------------------------------------------------------
/blocks/index.ts:
--------------------------------------------------------------------------------
1 | import { registerChaiBlock } from "@chaibuilder/pages/runtime";
2 | import {
3 | Accordion,
4 | AccordionConfig,
5 | AccordionProps,
6 | } from "./accordion/Accordion";
7 | import {
8 | BlogsList,
9 | BlogsListConfig,
10 | BlogsListProps,
11 | } from "./blogs-grid/BlogsGrid";
12 | import { Dropdown, DropdownConfig, DropdownProps } from "./dropdown/Dropdown";
13 | import {
14 | Component as Modal,
15 | Config as ModalConfig,
16 | ModalProps,
17 | } from "./modal/Modal";
18 |
19 | export const registerBlocks = () => {
20 | registerChaiBlock(BlogsList, BlogsListConfig);
21 | registerChaiBlock(Modal, ModalConfig);
22 | registerChaiBlock(Dropdown, DropdownConfig);
23 | registerChaiBlock(Accordion, AccordionConfig);
24 | };
25 |
--------------------------------------------------------------------------------
/blocks/link/Link.tsx:
--------------------------------------------------------------------------------
1 | import { chaiBuilderPages } from "@/chai";
2 | import {
3 | ChaiBlockComponentProps,
4 | ChaiStyles,
5 | } from "@chaibuilder/pages/runtime";
6 | import Link from "next/link";
7 | import * as React from "react";
8 |
9 | type LinkProps = {
10 | styles: ChaiStyles;
11 | content: string;
12 | link: {
13 | type: "page" | "pageType" | "url" | "email" | "phone" | "element";
14 | target: "_self" | "_blank";
15 | href: string;
16 | };
17 | };
18 |
19 | export const LinkBlock = async (props: ChaiBlockComponentProps) => {
20 | const { link, styles, children, content } = props;
21 | const isPageTypeLink = link?.type === "pageType" && link?.href !== "";
22 | let href = link?.href;
23 | if (isPageTypeLink) {
24 | const parts = href.split(":"); // pageType href is of format "pageType:${pageTypeKey}:${id}"
25 | href = await chaiBuilderPages.resolveLink(parts[1], parts[2]);
26 | }
27 | if (children) {
28 | return (
29 |
34 | {children}
35 |
36 | );
37 | }
38 |
39 | return React.createElement(
40 | Link,
41 | {
42 | ...styles,
43 | href: href,
44 | target: link?.target || "_self",
45 | "aria-label": content,
46 | },
47 | content
48 | );
49 | };
50 |
51 | export const LinkConfig = {
52 | type: "Link",
53 | label: "Link",
54 | group: "basic",
55 | schema: {
56 | properties: {
57 | link: {
58 | type: "object",
59 | title: "Link",
60 | },
61 | },
62 | },
63 | i18nProps: ["content"],
64 | };
65 |
--------------------------------------------------------------------------------
/blocks/modal/Modal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogContent,
4 | DialogTitle,
5 | DialogTrigger,
6 | } from '@/components/ui/dialog'
7 |
8 | import {
9 | builderProp,
10 | ChaiBlock,
11 | ChaiBlockComponentProps,
12 | ChaiStyles,
13 | closestBlockProp,
14 | registerChaiBlock,
15 | registerChaiBlockSchema,
16 | StylesProp,
17 | } from '@chaibuilder/pages/runtime'
18 | import { Layers } from 'lucide-react'
19 |
20 | export type ModalProps = {
21 | children: React.ReactNode
22 | styles: ChaiStyles
23 | overlayStyles: ChaiStyles
24 | show: boolean
25 | }
26 |
27 | type ModalTriggerProps = {
28 | children: React.ReactNode
29 | styles: ChaiStyles
30 | content: string
31 | }
32 |
33 | type ModalContentProps = {
34 | children: React.ReactNode
35 | styles: ChaiStyles
36 | show: boolean
37 | }
38 |
39 | const ModalTriggerComponent = (
40 | props: ChaiBlockComponentProps
41 | ) => {
42 | const { blockProps, content, children, styles } = props
43 | return (
44 |
45 |
46 | {children || content}
47 |
48 |
49 | )
50 | }
51 |
52 | registerChaiBlock(ModalTriggerComponent, {
53 | type: 'ModalTrigger',
54 | label: 'Modal Trigger',
55 | group: 'advanced',
56 | category: 'core',
57 | hidden: true,
58 | canMove: () => false,
59 | canDelete: () => false,
60 | canAcceptBlock: () => true,
61 | canDuplicate: () => false,
62 | ...registerChaiBlockSchema({
63 | properties: {
64 | styles: StylesProp('w-max'),
65 | content: {
66 | type: 'string',
67 | title: 'Content',
68 | default: 'Edit Profile',
69 | },
70 | },
71 | }),
72 | i18nProps: ['content'],
73 | aiProps: ['content'],
74 | })
75 |
76 | const BuilderPortal = ({ children }: { children: any }) => {
77 | const container = document.getElementById('canvas-iframe')
78 | const width = container?.clientWidth
79 | const height = container?.clientHeight
80 | return (
81 |
85 |
86 | {children}
87 |
88 |
89 | )
90 | }
91 |
92 | const ModalContentComponent = (
93 | props: ChaiBlockComponentProps
94 | ) => {
95 | const { blockProps, children, styles, inBuilder, show } = props
96 |
97 | if (inBuilder) {
98 | // * In builder showing children with a custom portal
99 | if (!show) return null
100 | return (
101 |
102 |
103 | {children || 'Modal Placeholder'}
104 |
105 |
106 | )
107 | }
108 |
109 | return (
110 |
111 | Modal
112 |
113 | {children || 'Modal Placeholder'}
114 |
115 |
116 | )
117 | }
118 |
119 | registerChaiBlock(ModalContentComponent, {
120 | type: 'ModalContent',
121 | label: 'Modal Content',
122 | group: 'advanced',
123 | hidden: true,
124 | canMove: () => false,
125 | canDelete: () => false,
126 | canAcceptBlock: () => true,
127 | canDuplicate: () => false,
128 | ...registerChaiBlockSchema({
129 | properties: {
130 | styles: StylesProp('gap-4 border p-6 shadow-lg sm:rounded-lg'),
131 | show: closestBlockProp('Modal', 'show'),
132 | },
133 | }),
134 | })
135 |
136 | const Component = (props: ChaiBlockComponentProps) => {
137 | const { styles, children, blockProps, inBuilder } = props
138 |
139 | return (
140 |
145 | )
146 | }
147 |
148 | const Config = {
149 | type: 'Modal',
150 | label: 'Modal',
151 | group: 'advanced',
152 | category: 'core',
153 | wrapper: true,
154 | icon: Layers,
155 | blocks: () =>
156 | [
157 | { _type: 'Modal', _id: 'modal' },
158 | {
159 | _type: 'ModalTrigger',
160 | _id: 'modal-trigger',
161 | _parent: 'modal',
162 | content: 'Edit Profile',
163 | },
164 | {
165 | _type: 'ModalContent',
166 | _id: 'modal-content',
167 | _parent: 'modal',
168 | },
169 | ] as ChaiBlock[],
170 | ...registerChaiBlockSchema({
171 | properties: {
172 | styles: StylesProp(''),
173 | show: builderProp({
174 | type: 'boolean',
175 | title: 'Open Modal',
176 | default: false,
177 | }),
178 | },
179 | }),
180 | }
181 |
182 | export { Component, Config }
183 |
--------------------------------------------------------------------------------
/chai/index.ts:
--------------------------------------------------------------------------------
1 | //TODO: Create a separate @chaibuilder/nextjs package for this file
2 | import { filterDuplicateStyles } from "@/utils/styles-helper";
3 | import type { ChaiBlock } from "@chaibuilder/pages";
4 | import { getStylesForBlocks } from "@chaibuilder/pages/render";
5 | import {
6 | ChaiBuilderPages,
7 | ChaiBuilderPagesBackend,
8 | ChaiPageProps,
9 | } from "@chaibuilder/pages/server";
10 | import { each, isEmpty } from "lodash";
11 | import { unstable_cache as nextCache } from "next/cache";
12 | import { cache } from "react";
13 |
14 | const APP_API_KEY = process.env.CHAIBUILDER_API_KEY;
15 |
16 | export type NextPageProps = {
17 | params: Promise<{ slug: string[] }>;
18 | searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
19 | };
20 |
21 | const chaiBuilderPages = new ChaiBuilderPages(
22 | new ChaiBuilderPagesBackend(APP_API_KEY!)
23 | );
24 |
25 | export const getChaiBuilderPage = cache(async (slug: string) => {
26 | const response = await chaiBuilderPages.getPageBySlug(slug);
27 |
28 | if ("error" in response) {
29 | return response;
30 | }
31 |
32 | const tagPageId = response.id;
33 | return nextCache(
34 | async () => {
35 | const responseData = await chaiBuilderPages.getFullPage(response.id);
36 | return responseData;
37 | },
38 | ["page-" + response.lang, response.id],
39 | { tags: ["page-" + tagPageId] }
40 | )();
41 | });
42 |
43 | export const getChaiSiteSettings = cache(async () => {
44 | return nextCache(
45 | async () => await chaiBuilderPages.getSiteSettings(),
46 | ["site-settings"],
47 | { tags: ["site-settings"] }
48 | )();
49 | });
50 |
51 | export const getChaiPageData = cache(
52 | async (blocks: ChaiBlock[], pageType: string, props: ChaiPageProps) => {
53 | const pageData = await chaiBuilderPages.getPageData(
54 | blocks,
55 | pageType,
56 | props
57 | );
58 | return pageData;
59 | }
60 | );
61 |
62 | export const getChaiPageSeoMetadata = cache(async (props: ChaiPageProps) => {
63 | const slug = props.slug;
64 | const pageData = await getChaiBuilderPage(slug);
65 | let seoData = pageData?.seo ?? {};
66 | // check if the seo json has any dynamic values. stringify and check if it has any dynamic values.
67 | let seoJson = JSON.stringify(seoData);
68 | const hasDynamicValues = seoJson.match(/\{\{.*?\}\}/g);
69 | if (hasDynamicValues) {
70 | const pageSeoFields = await getChaiPageData([], pageData.pageType, props);
71 |
72 | if (!isEmpty(pageSeoFields)) {
73 | // Recursively get all possible paths from the pageSeoFields object
74 | const replaceNestedValues = (
75 | obj: Record,
76 | prefix = ""
77 | ): { [key: string]: string } => {
78 | let paths: { [key: string]: string } = {};
79 |
80 | for (const key in obj) {
81 | const value = obj[key];
82 | const newPrefix = prefix ? `${prefix}.${key}` : key;
83 |
84 | if (
85 | typeof value === "object" &&
86 | value !== null &&
87 | !Array.isArray(value)
88 | ) {
89 | paths = {
90 | ...paths,
91 | ...replaceNestedValues(
92 | value as Record,
93 | newPrefix
94 | ),
95 | };
96 | } else if (!Array.isArray(value)) {
97 | paths[newPrefix] = String(value);
98 | }
99 | }
100 |
101 | return paths;
102 | };
103 |
104 | const flattenedFields = replaceNestedValues(pageSeoFields);
105 |
106 | // Replace all dynamic values with their corresponding values
107 | each(flattenedFields, (value, path) => {
108 | seoJson = seoJson.replace(`{{${path}}}`, value);
109 | });
110 | }
111 |
112 | try {
113 | seoData = JSON.parse(seoJson);
114 | } catch (error) {
115 | console.error("Error parsing SEO JSON:", error);
116 | }
117 | return seoData;
118 | }
119 |
120 | return {
121 | title: seoData?.title,
122 | description: seoData?.description,
123 | openGraph: {
124 | title: seoData?.ogTitle,
125 | description: seoData?.ogDescription,
126 | images: seoData?.ogImage ? [seoData?.ogImage] : [],
127 | },
128 | };
129 | });
130 |
131 | export const getChaiPageStyles = async (blocks: ChaiBlock[]) => {
132 | const styles = await getStylesForBlocks(blocks);
133 | // minify styles and filter out duplicates
134 | const minifiedStyles = styles.replace(/\s+/g, " ").trim();
135 | const filteredStyles = await filterDuplicateStyles(minifiedStyles);
136 | return filteredStyles;
137 | };
138 |
139 | export { chaiBuilderPages };
140 |
--------------------------------------------------------------------------------
/chai/theme-presets.ts:
--------------------------------------------------------------------------------
1 | export const orangePreset = {
2 | fontFamily: {
3 | heading: "Poppins",
4 | body: "Poppins",
5 | },
6 | borderRadius: "12px",
7 | colors: {
8 | background: ["#FFFFFF", "#09090B"],
9 | foreground: ["#09090B", "#FFFFFF"],
10 | primary: ["#F85C2C", "#E34817"],
11 | "primary-foreground": ["#FFFFFF", "#FFFFFF"],
12 | secondary: ["#F4F4F5", "#27272A"],
13 | "secondary-foreground": ["#09090B", "#FFFFFF"],
14 | muted: ["#F4F4F5", "#27272A"],
15 | "muted-foreground": ["#71717A", "#A1A1AA"],
16 | accent: ["#F4F4F5", "#27272A"],
17 | "accent-foreground": ["#09090B", "#FFFFFF"],
18 | destructive: ["#EF4444", "#7F1D1D"],
19 | "destructive-foreground": ["#FFFFFF", "#FFFFFF"],
20 | border: ["#E4E4E7", "#27272A"],
21 | input: ["#E4E4E7", "#27272A"],
22 | ring: ["#F85C2C", "#E34817"],
23 | card: ["#FFFFFF", "#09090B"],
24 | "card-foreground": ["#09090B", "#FFFFFF"],
25 | popover: ["#FFFFFF", "#09090B"],
26 | "popover-foreground": ["#09090B", "#FFFFFF"],
27 | },
28 | };
29 |
30 | export const greenPreset = {
31 | fontFamily: {
32 | heading: "Playfair Display",
33 | body: "Playfair Display",
34 | },
35 | borderRadius: "0px",
36 | colors: {
37 | background: ["#FFFFFF", "#09090B"],
38 | foreground: ["#09090B", "#FFFFFF"],
39 | primary: ["#22C55E", "#16A34A"],
40 | "primary-foreground": ["#FFFFFF", "#FFFFFF"],
41 | secondary: ["#F4F4F5", "#27272A"],
42 | "secondary-foreground": ["#09090B", "#FFFFFF"],
43 | muted: ["#F4F4F5", "#27272A"],
44 | "muted-foreground": ["#71717A", "#A1A1AA"],
45 | accent: ["#F4F4F5", "#27272A"],
46 | "accent-foreground": ["#09090B", "#FFFFFF"],
47 | destructive: ["#EF4444", "#7F1D1D"],
48 | "destructive-foreground": ["#FFFFFF", "#FFFFFF"],
49 | border: ["#E4E4E7", "#27272A"],
50 | input: ["#E4E4E7", "#27272A"],
51 | ring: ["#22C55E", "#16A34A"],
52 | card: ["#FFFFFF", "#09090B"],
53 | "card-foreground": ["#09090B", "#FFFFFF"],
54 | popover: ["#FFFFFF", "#09090B"],
55 | "popover-foreground": ["#09090B", "#FFFFFF"],
56 | },
57 | };
58 |
59 | export const bluePreset = {
60 | fontFamily: {
61 | heading: "Poppins",
62 | body: "Roboto",
63 | },
64 | borderRadius: "30px",
65 | colors: {
66 | background: ["#FFFFFF", "#09090B"],
67 | foreground: ["#09090B", "#FFFFFF"],
68 | primary: ["#2563EB", "#3B82F6"],
69 | "primary-foreground": ["#FFFFFF", "#FFFFFF"],
70 | secondary: ["#F4F4F5", "#27272A"],
71 | "secondary-foreground": ["#09090B", "#FFFFFF"],
72 | muted: ["#F4F4F5", "#27272A"],
73 | "muted-foreground": ["#71717A", "#A1A1AA"],
74 | accent: ["#F4F4F5", "#27272A"],
75 | "accent-foreground": ["#09090B", "#FFFFFF"],
76 | destructive: ["#EF4444", "#7F1D1D"],
77 | "destructive-foreground": ["#FFFFFF", "#FFFFFF"],
78 | border: ["#E4E4E7", "#27272A"],
79 | input: ["#E4E4E7", "#27272A"],
80 | ring: ["#2563EB", "#3B82F6"],
81 | card: ["#FFFFFF", "#09090B"],
82 | "card-foreground": ["#09090B", "#FFFFFF"],
83 | popover: ["#FFFFFF", "#09090B"],
84 | "popover-foreground": ["#09090B", "#FFFFFF"],
85 | },
86 | };
87 |
--------------------------------------------------------------------------------
/cms/index.ts:
--------------------------------------------------------------------------------
1 | // Add your CMS code here
2 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/(builder)/styles.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/components/builder/chaibuilder-pages.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { registerBlocks } from "@/blocks";
3 | import { bluePreset, greenPreset, orangePreset } from "@/chai/theme-presets";
4 | import { registerFonts } from "@/fonts";
5 | import ChaiBuilderPages, {
6 | ChaiLibraryBlock,
7 | registerChaiLibrary,
8 | } from "@chaibuilder/pages";
9 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
10 | import { Logo } from "./logo";
11 |
12 | registerBlocks();
13 | registerFonts();
14 |
15 | registerChaiLibrary("meraki-ui", {
16 | name: "Meraki UI",
17 | description: "Meraki UI",
18 | getBlocksList: async () => {
19 | try {
20 | const response = await fetch(
21 | "https://chai-ui-blocks.vercel.app/blocks.json"
22 | );
23 | const blocks = await response.json();
24 | return blocks.map((b: any) => ({
25 | ...b,
26 | preview: "https://chai-ui-blocks.vercel.app" + b.preview,
27 | }));
28 | } catch {
29 | return [];
30 | }
31 | },
32 | getBlock: async ({
33 | block,
34 | }: {
35 | block: ChaiLibraryBlock<{ path?: string; uuid: string }>;
36 | }) => {
37 | const response = await fetch(
38 | "https://chai-ui-blocks.vercel.app" +
39 | (!block.path ? "/" + block.uuid + ".html" : "/blocks/" + block.path)
40 | );
41 | const html = await response.text();
42 | return html.replace(/---([\s\S]*?)---/g, "");
43 | },
44 | });
45 |
46 | const queryClient = new QueryClient({
47 | defaultOptions: {
48 | queries: {
49 | refetchOnWindowFocus: false,
50 | refetchOnReconnect: false,
51 | },
52 | },
53 | });
54 |
55 | export default function ChaiBuilderPagesWrapper() {
56 | return (
57 |
58 | `/chai/api/preview?slug=${slug}`}
65 | autoSaveSupport={false}
66 | logo={Logo}
67 | />
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/components/builder/loader.tsx:
--------------------------------------------------------------------------------
1 | import { Loader2 } from "lucide-react";
2 |
3 | export default function FullScreenLoader() {
4 | return (
5 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/components/builder/logo.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | export const Logo = ({
4 | width = 30,
5 | height = 30,
6 | }: {
7 | width?: number;
8 | height?: number;
9 | }) => (
10 |
27 | );
28 |
--------------------------------------------------------------------------------
/components/preview-banner.tsx:
--------------------------------------------------------------------------------
1 | export default function PreviewBanner({ slug }: { slug: string }) {
2 | return (
3 |
4 |
5 |
6 |
7 | You are viewing page in preview mode
8 |
9 |
10 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as AccordionPrimitive from '@radix-ui/react-accordion'
5 | import { ChevronDown } from 'lucide-react'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = 'AccordionItem'
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180',
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ))
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
59 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as AvatarPrimitive from '@radix-ui/react-avatar'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 |
4 | import { cn } from '@/lib/utils'
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground',
18 | },
19 | },
20 | defaultVariants: {
21 | variant: 'default',
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | },
22 | size: {
23 | default: 'h-10 px-4 py-2',
24 | sm: 'h-9 rounded-md px-3',
25 | lg: 'h-11 rounded-md px-8',
26 | icon: 'h-10 w-10',
27 | },
28 | },
29 | defaultVariants: {
30 | variant: 'default',
31 | size: 'default',
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : 'button'
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = 'Button'
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = 'Card'
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = 'CardHeader'
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = 'CardTitle'
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLDivElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = 'CardDescription'
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = 'CardContent'
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = 'CardFooter'
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
5 | import { Check } from 'lucide-react'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { type DialogProps } from '@radix-ui/react-dialog'
5 | import { Command as CommandPrimitive } from 'cmdk'
6 | import { Search } from 'lucide-react'
7 |
8 | import { cn } from '@/lib/utils'
9 | import { Dialog, DialogContent } from '@/components/ui/dialog'
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | const CommandDialog = ({ children, ...props }: DialogProps) => {
27 | return (
28 |
35 | )
36 | }
37 |
38 | const CommandInput = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
43 |
44 |
52 |
53 | ))
54 |
55 | CommandInput.displayName = CommandPrimitive.Input.displayName
56 |
57 | const CommandList = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
66 | ))
67 |
68 | CommandList.displayName = CommandPrimitive.List.displayName
69 |
70 | const CommandEmpty = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >((props, ref) => (
74 |
79 | ))
80 |
81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82 |
83 | const CommandGroup = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 |
97 | CommandGroup.displayName = CommandPrimitive.Group.displayName
98 |
99 | const CommandSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110 |
111 | const CommandItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
123 | ))
124 |
125 | CommandItem.displayName = CommandPrimitive.Item.displayName
126 |
127 | const CommandShortcut = ({
128 | className,
129 | ...props
130 | }: React.HTMLAttributes) => {
131 | return (
132 |
139 | )
140 | }
141 | CommandShortcut.displayName = 'CommandShortcut'
142 |
143 | export {
144 | Command,
145 | CommandDialog,
146 | CommandInput,
147 | CommandList,
148 | CommandEmpty,
149 | CommandGroup,
150 | CommandItem,
151 | CommandShortcut,
152 | CommandSeparator,
153 | }
154 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DialogPrimitive from '@radix-ui/react-dialog'
5 | import { X } from 'lucide-react'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = 'DialogHeader'
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = 'DialogFooter'
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
5 | import { Check, ChevronRight, Circle } from 'lucide-react'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = 'Input'
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as LabelPrimitive from '@radix-ui/react-label'
5 | import { cva, type VariantProps } from 'class-variance-authority'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const labelVariants = cva(
10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as PopoverPrimitive from '@radix-ui/react-popover'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as ProgressPrimitive from '@radix-ui/react-progress'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = 'vertical', ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SelectPrimitive from '@radix-ui/react-select'
5 | import { Check, ChevronDown, ChevronUp } from 'lucide-react'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1',
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = 'popper', ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = 'horizontal', decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SliderPrimitive from '@radix-ui/react-slider'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | interface SliderProps
9 | extends React.ComponentPropsWithoutRef {
10 | onValueCommit?: (value: number[]) => void
11 | formatValue?: (value: number) => string
12 | label?: string
13 | }
14 |
15 | const Slider = React.forwardRef<
16 | React.ElementRef,
17 | SliderProps
18 | >(({ className, onValueCommit, formatValue, label, ...props }, ref) => {
19 | const [editingIndex, setEditingIndex] = React.useState(null)
20 | const [editValue, setEditValue] = React.useState('')
21 |
22 | const handleValueClick = (
23 | e: React.MouseEvent,
24 | index: number,
25 | value: number
26 | ) => {
27 | e.stopPropagation()
28 | setEditingIndex(index)
29 | setEditValue(value.toString())
30 | }
31 |
32 | const handleInputChange = (e: React.ChangeEvent) => {
33 | setEditValue(e.target.value)
34 | }
35 |
36 | const handleValueCommit = () => {
37 | if (editingIndex === null) return
38 |
39 | const newValue = Number(editValue.replace(/[^0-9.-]/g, ''))
40 | if (isNaN(newValue)) {
41 | setEditingIndex(null)
42 | return
43 | }
44 |
45 | const clampedValue = Math.min(
46 | Math.max(newValue, props.min || 0),
47 | props.max || 100
48 | )
49 |
50 | const newValues = [...(props.value || [])]
51 | newValues[editingIndex] = clampedValue
52 |
53 | if (editingIndex === 0 && newValues[1] && clampedValue > newValues[1]) {
54 | newValues[1] = clampedValue
55 | } else if (
56 | editingIndex === 1 &&
57 | newValues[0] &&
58 | clampedValue < newValues[0]
59 | ) {
60 | newValues[0] = clampedValue
61 | }
62 |
63 | onValueCommit?.(newValues)
64 | setEditingIndex(null)
65 | }
66 |
67 | const handleKeyDown = (e: React.KeyboardEvent) => {
68 | if (e.key === 'Enter') {
69 | e.preventDefault()
70 | handleValueCommit()
71 | } else if (e.key === 'Escape') {
72 | setEditingIndex(null)
73 | }
74 | }
75 |
76 | return (
77 |
78 |
79 |
80 |
81 | {props.value?.map((value, index) => (
82 |
83 | {index > 0 && -}
84 | {editingIndex === index ? (
85 | e.stopPropagation()}
95 | />
96 | ) : (
97 | handleValueClick(e, index, value)}
99 | className='cursor-text hover:bg-primary/10 rounded px-1'
100 | >
101 | {formatValue ? formatValue(value) : value}
102 |
103 | )}
104 |
105 | ))}
106 |
107 |
108 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | )
124 | })
125 | Slider.displayName = SliderPrimitive.Root.displayName
126 |
127 | export { Slider }
128 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<'textarea'>
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = 'Textarea'
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/components/ui/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider as NextThemesProvider } from "next-themes";
4 | import * as React from "react";
5 |
6 | export function ThemeProvider({
7 | children,
8 | ...props
9 | }: React.ComponentProps) {
10 | return {children};
11 | }
12 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/data/global.ts:
--------------------------------------------------------------------------------
1 | import { registerChaiGlobalDataProvider } from "@chaibuilder/pages/server";
2 | import { cache } from "react";
3 |
4 | const globalDataProvider = cache(async ({ lang }: { lang: string }) => {
5 | console.log("lang", lang);
6 | return {
7 | name: "Chai Builder",
8 | address: "Pune, Maharashtra, India",
9 | email: "support@chaibuilder.com",
10 | social: {
11 | facebook: "https://www.facebook.com/chaibuilder",
12 | instagram: "https://www.instagram.com/chaibuilder",
13 | x: "https://x.com/chaibuilder",
14 | },
15 | };
16 | });
17 |
18 | registerChaiGlobalDataProvider(globalDataProvider);
19 |
--------------------------------------------------------------------------------
/data/index.ts:
--------------------------------------------------------------------------------
1 | import './global'
2 |
--------------------------------------------------------------------------------
/declaration.d.ts:
--------------------------------------------------------------------------------
1 | import "@chaibuilder/pages/runtime";
2 |
3 | declare module "@chaibuilder/pages/runtime" {
4 | interface ChaiPageProps {
5 | slug: string;
6 | pageType: string;
7 | fallbackLang: string;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/fonts/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChaiFontViaSrc,
3 | ChaiFontViaUrl,
4 | registerChaiFont,
5 | } from "@chaibuilder/pages/runtime";
6 |
7 | export const registerFonts = () => {
8 | // Google font
9 | registerChaiFont("Ubuntu", {
10 | url: "https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap",
11 | fallback: `sans-serif`,
12 | } as ChaiFontViaUrl);
13 |
14 | // Custom font files
15 | registerChaiFont("Geist", {
16 | fallback: `"Geist Fallback", Arial, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol`,
17 | src: [
18 | {
19 | url: "https://vercel.com/vc-ap-vercel-docs/_next/static/media/93f479601ee12b01.p.woff2",
20 | format: "woff2",
21 | },
22 | {
23 | url: "https://vercel.com/vc-ap-vercel-docs/_next/static/media/569ce4b8f30dc480-s.p.woff2",
24 | format: "woff2",
25 | },
26 | ],
27 | } as ChaiFontViaSrc);
28 | };
29 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | //NOTE: Update this list as needed
5 | remotePatterns: [
6 | { hostname: "ucarecdn.com" },
7 | { hostname: "placehold.co" },
8 | { hostname: "img.shields.io" },
9 | { hostname: "cdn.rareblocks.xyz" },
10 | { hostname: "picsum.photos" },
11 | { hostname: "fakeimg.pl" },
12 | { hostname: "fldwljgzcktqnysdkxnn.supabase.co" },
13 | ],
14 | },
15 | };
16 |
17 | export default nextConfig;
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chaibuilder-nextjs",
3 | "version": "0.1.4",
4 | "private": true,
5 | "scripts": {
6 | "dev": "pnpm install && node scripts/check-env.cjs && concurrently \"node scripts/tailwind.cjs --dev\" \"next dev --turbopack\"",
7 | "prebuild": "node scripts/tailwind.cjs",
8 | "build": "next build",
9 | "dev-no-check": "pnpm install && next dev",
10 | "build-no-check": "pnpm install && pnpm run prebuild && next build",
11 | "start": "next start",
12 | "lint": "next lint & tsc --noEmit",
13 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
14 | "check-env": "node scripts/check-env.cjs"
15 | },
16 | "dependencies": {
17 | "@chaibuilder/pages": "~0.5.7",
18 | "@chaibuilder/sdk": "~2.2.31",
19 | "@radix-ui/react-accordion": "^1.2.2",
20 | "@radix-ui/react-collapsible": "^1.1.3",
21 | "@radix-ui/react-dialog": "^1.1.6",
22 | "@radix-ui/react-dropdown-menu": "^2.1.5",
23 | "@radix-ui/react-label": "^2.1.2",
24 | "@radix-ui/react-popover": "^1.1.5",
25 | "@radix-ui/react-progress": "^1.1.1",
26 | "@radix-ui/react-scroll-area": "^1.2.3",
27 | "@radix-ui/react-select": "^2.1.6",
28 | "@radix-ui/react-separator": "^1.1.2",
29 | "@radix-ui/react-slider": "^1.2.3",
30 | "@radix-ui/react-tooltip": "^1.1.7",
31 | "@tailwindcss/container-queries": "^0.1.1",
32 | "@tanstack/react-query": "^5.62.8",
33 | "axios": "^1.7.8",
34 | "canvas": "^3.1.0",
35 | "cmdk": "1.0.0",
36 | "lodash": "^4.17.21",
37 | "lucide-react": "0.471.1",
38 | "next": "15.3.2",
39 | "next-themes": "^0.4.6",
40 | "react": "19.1.0",
41 | "react-dom": "19.1.0",
42 | "sonner": "^2.0.1"
43 | },
44 | "devDependencies": {
45 | "@mhsdesign/jit-browser-tailwindcss": "^0.4.1",
46 | "@radix-ui/react-avatar": "^1.1.2",
47 | "@radix-ui/react-checkbox": "^1.1.4",
48 | "@radix-ui/react-icons": "^1.3.2",
49 | "@radix-ui/react-progress": "^1.1.0",
50 | "@radix-ui/react-slot": "^1.1.1",
51 | "@tailwindcss/aspect-ratio": "^0.4.2",
52 | "@tailwindcss/forms": "^0.5.9",
53 | "@tailwindcss/typography": "^0.5.15",
54 | "@types/lodash": "^4.17.14",
55 | "@types/node": "^20.17.12",
56 | "@types/react": "19.0.8",
57 | "@types/react-dom": "19.0.3",
58 | "@typescript-eslint/parser": "^5.62.0",
59 | "autoprefixer": "^10.4.20",
60 | "class-variance-authority": "^0.7.1",
61 | "clsx": "^2.1.1",
62 | "concurrently": "^9.1.2",
63 | "eslint": "^8.57.1",
64 | "eslint-config-next": "15.1.7",
65 | "eslint-config-prettier": "^9.1.0",
66 | "eslint-plugin-prettier": "^5.2.1",
67 | "fs-extra": "^11.3.0",
68 | "postcss": "^8.4.49",
69 | "prettier": "^3.4.2",
70 | "prettier-eslint": "^16.3.0",
71 | "prettier-eslint-cli": "^8.0.1",
72 | "tailwind-merge": "^2.5.5",
73 | "tailwindcss": "^3.4.13",
74 | "tailwindcss-animate": "^1.0.7",
75 | "typescript": "^5.7.2"
76 | },
77 | "pnpm": {
78 | "overrides": {
79 | "@types/react": "19.0.8",
80 | "@types/react-dom": "19.0.3"
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/page-types/blog.ts:
--------------------------------------------------------------------------------
1 | import { registerChaiPageType } from "@chaibuilder/pages/server";
2 |
3 | registerChaiPageType("blog", {
4 | name: "Blog",
5 | helpText: "A blog post page.",
6 | icon: '',
7 | dynamicSegments: "/[a-z0-9]+(?:-[a-z0-9]+)*$", // regex for slug. starts with / and should contain only lowercase letters, numbers and hyphens
8 | dynamicSlug: "{{slug}}",
9 | dataProvider: async () => {
10 | return {
11 | blog: {
12 | title: "Blog",
13 | description: "A blog post page.",
14 | posts: [
15 | {
16 | title: "Post 1",
17 | },
18 | ],
19 | },
20 | };
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/page-types/index.ts:
--------------------------------------------------------------------------------
1 | import "./blog";
2 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | autoprefixer: {},
6 | },
7 | };
8 |
9 | export default config;
10 |
--------------------------------------------------------------------------------
/scripts/check-env.cjs:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 |
4 | // Read .env.sample and .env files
5 | const sampleEnvPath = path.resolve(process.cwd(), ".env.sample");
6 | const envPath = path.resolve(process.cwd(), ".env");
7 |
8 | const delimiter = "\n" + "-".repeat(48) + "\n";
9 |
10 | try {
11 | const sampleEnvContent = fs.readFileSync(sampleEnvPath, "utf8");
12 | const envContent = fs.readFileSync(envPath, "utf8");
13 |
14 | // Extract variables and their comments from .env.sample
15 | const sampleVarsWithComments = sampleEnvContent
16 | .split("\n")
17 | .filter((line) => line.trim() && !line.startsWith("#"))
18 | .map((line) => {
19 | const [varPart, ...commentParts] = line.split("#");
20 | const [key] = varPart.split("=");
21 | return {
22 | key: key.trim(),
23 | comment: commentParts.join("#").trim(), // Join in case comment contains #
24 | };
25 | });
26 |
27 | // Extract variables from .env
28 | const envVars = envContent
29 | .split("\n")
30 | .filter((line) => line.trim() && !line.startsWith("#"))
31 | .reduce((acc, line) => {
32 | const [key, ...valueParts] = line.split("=");
33 | // Join value parts in case the value contains = characters
34 | const value = valueParts.join("=");
35 | // Remove any comments from the value
36 | const valueWithoutComments = value.split("#")[0].trim();
37 | acc[key.trim()] = valueWithoutComments;
38 | return acc;
39 | }, {});
40 |
41 | // Check for missing or empty variables
42 | const missingVars = [];
43 | const emptyVars = [];
44 | const extraVars = [];
45 |
46 | // Check for missing and empty vars from .env.sample
47 | sampleVarsWithComments.forEach(({ key, comment }) => {
48 | if (!(key in envVars)) {
49 | missingVars.push({ key, comment });
50 | } else if (!envVars[key] || envVars[key].length === 0) {
51 | emptyVars.push({ key, comment });
52 | }
53 | });
54 |
55 | // Check for extra vars in .env that are not in .env.sample
56 | const sampleVarKeys = sampleVarsWithComments.map(({ key }) => key);
57 | Object.keys(envVars).forEach((key) => {
58 | if (!sampleVarKeys.includes(key)) {
59 | extraVars.push(key);
60 | }
61 | });
62 |
63 | const hasErrors = missingVars.length > 0 || emptyVars.length > 0;
64 |
65 | if (hasErrors) {
66 | console.error(
67 | "\x1b[31m%s\x1b[0m",
68 | "❌ Environment variables check failed!"
69 | );
70 | } else {
71 | console.log(
72 | "\x1b[32m%s\x1b[0m",
73 | "✅ All defined environment variables are properly set!"
74 | );
75 | }
76 |
77 | if (missingVars.length > 0) {
78 | console.error("\nMissing variables in .env file:");
79 | missingVars.forEach(({ key, comment }) =>
80 | console.error(` - ${key}${comment ? `\n\t${comment}` : ""}`)
81 | );
82 | }
83 |
84 | if (emptyVars.length > 0) {
85 | console.error("\nEmpty variables in .env file:");
86 | emptyVars.forEach(({ key, comment }) =>
87 | console.error(` - ${key}${comment ? `\n\t${comment}` : ""}`)
88 | );
89 | }
90 |
91 | if (hasErrors) {
92 | console.error(
93 | "\nPlease check your .env.sample file and update your .env file accordingly."
94 | );
95 | process.exit(1);
96 | }
97 | } catch (error) {
98 | if (error.code === "ENOENT") {
99 | console.error("\x1b[31m%s\x1b[0m", "❌ Error: Missing required files!");
100 | if (!fs.existsSync(sampleEnvPath)) {
101 | console.error(" - .env.sample file not found");
102 | }
103 | if (!fs.existsSync(envPath)) {
104 | console.error(" - .env file not found");
105 | }
106 | } else {
107 | console.error("An error occurred:", error);
108 | }
109 | process.exit(1);
110 | }
111 |
--------------------------------------------------------------------------------
/scripts/tailwind.cjs:
--------------------------------------------------------------------------------
1 | const { exec } = require("child_process");
2 |
3 | const tailwindConfigPath = "./tailwind.config.ts"; // Adjust if your config is in a different location
4 | const inputCssPath = "./app/(public)/public.css"; // Tailwind input CSS file
5 | const publicCssPath = "./public/chaistyles.css"; // Final output location
6 |
7 | const generateTailwindCss = (devMode = false) => {
8 | console.log("Generating Tailwind CSS...");
9 |
10 | const watchFlag = devMode ? "--watch" : "";
11 | const minifyFlag = devMode ? "" : "--minify";
12 |
13 | const command = `npx tailwindcss -c ${tailwindConfigPath} -i "${inputCssPath}" -o "${publicCssPath}" ${watchFlag} ${minifyFlag}`;
14 |
15 | console.log(`Running: ${command}`);
16 |
17 | const process = exec(command, (err, stdout, stderr) => {
18 | if (err) {
19 | console.error("Error generating Tailwind CSS:", stderr);
20 | return;
21 | }
22 |
23 | if (!devMode) {
24 | console.log(stdout);
25 | console.log("Copying CSS to public folder...");
26 | console.log("CSS generation and copy complete!");
27 | }
28 | });
29 |
30 | if (devMode) {
31 | process.stdout.on("data", (data) => {
32 | console.log(data.toString());
33 | });
34 |
35 | process.stderr.on("data", (data) => {
36 | console.error(data.toString());
37 | });
38 |
39 | console.log("Watching for CSS changes...");
40 | }
41 | };
42 |
43 | // Check if running in dev mode
44 | const args = process.argv.slice(2);
45 | const devMode = args.includes("--dev") || args.includes("-d");
46 |
47 | generateTailwindCss(devMode);
48 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { getChaiBuilderTheme } from "@chaibuilder/pages/tailwind";
2 | import aspectRatio from "@tailwindcss/aspect-ratio";
3 | import containerQueries from "@tailwindcss/container-queries";
4 | import forms from "@tailwindcss/forms";
5 | import typography from "@tailwindcss/typography";
6 | import type { Config } from "tailwindcss";
7 | import animate from "tailwindcss-animate";
8 | import plugin from "tailwindcss/plugin";
9 | import { CustomThemeConfig } from "tailwindcss/types/config";
10 |
11 | const config: Config = {
12 | darkMode: "class",
13 | content: [
14 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
15 | "./blocks/**/*.{js,ts,jsx,tsx,mdx}",
16 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
17 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
18 | "./node_modules/@chaibuilder/sdk/dist/web-blocks/*.{js,cjs}",
19 | ],
20 | safelist: ["w-[inherit]", "h-[inherit]"],
21 | theme: {
22 | extend: {
23 | ...(getChaiBuilderTheme() as Partial),
24 | keyframes: {
25 | "accordion-down": {
26 | from: {
27 | height: "0",
28 | },
29 | to: {
30 | height: "var(--radix-accordion-content-height)",
31 | },
32 | },
33 | "accordion-up": {
34 | from: {
35 | height: "var(--radix-accordion-content-height)",
36 | },
37 | to: {
38 | height: "0",
39 | },
40 | },
41 | },
42 | animation: {
43 | "accordion-down": "accordion-down 0.2s ease-out",
44 | "accordion-up": "accordion-up 0.2s ease-out",
45 | },
46 | },
47 | },
48 | plugins: [
49 | typography,
50 | aspectRatio,
51 | forms,
52 | plugin(function ({ addBase, theme }) {
53 | addBase({
54 | "h1,h2,h3,h4,h5,h6": {
55 | fontFamily: theme("fontFamily.heading"),
56 | },
57 | body: {
58 | fontFamily: theme("fontFamily.body"),
59 | color: theme("colors.foreground"),
60 | backgroundColor: theme("colors.background"),
61 | },
62 | });
63 | }),
64 | animate,
65 | containerQueries,
66 | ],
67 | };
68 | export default config;
69 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "paths": {
25 | "@/*": [
26 | "./*"
27 | ]
28 | },
29 | "forceConsistentCasingInFileNames": true,
30 | "target": "ES2017"
31 | },
32 | "include": [
33 | "next-env.d.ts",
34 | "**/*.ts",
35 | "**/*.tsx",
36 | ".next/types/**/*.ts"
37 | ],
38 | "exclude": [
39 | "node_modules"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/utils/styles-helper.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChaiFont,
3 | ChaiFontViaSrc,
4 | getRegisteredFont,
5 | } from "@chaibuilder/pages/runtime";
6 | import fs from "fs/promises";
7 | import { compact, filter, has, map, uniqBy } from "lodash";
8 | import path from "path";
9 | import postcss from "postcss";
10 |
11 | async function findTailwindCssFile(): Promise {
12 | // Use absolute path for production to work on Vercel
13 | return path.resolve("./public/chaistyles.css");
14 | }
15 |
16 | export async function filterDuplicateStyles(
17 | newStyles: string
18 | ): Promise {
19 | try {
20 | const tailwindCssPath = await findTailwindCssFile();
21 | const tailwindCss = await fs.readFile(tailwindCssPath, "utf-8");
22 |
23 | const tailwindRoot = postcss.parse(tailwindCss);
24 | const newStylesRoot = postcss.parse(newStyles);
25 |
26 | const tailwindSelectors = new Set();
27 | tailwindRoot.walkRules((rule) => {
28 | tailwindSelectors.add(rule.selector);
29 | });
30 |
31 | newStylesRoot.walkRules((rule) => {
32 | // Check if the rule has a parent and if it's a media query (breakpoint)
33 | const hasBreakpoint =
34 | rule.parent?.type === "atrule" &&
35 | "name" in rule.parent &&
36 | rule.parent.name === "media";
37 |
38 | // Only remove the rule if it's in tailwindSelectors and doesn't have a breakpoint
39 | if (tailwindSelectors.has(rule.selector) && !hasBreakpoint) {
40 | rule.remove();
41 | }
42 | });
43 |
44 | return newStylesRoot.toString();
45 | } catch (error) {
46 | console.error("Error filtering styles:", error);
47 | return newStyles;
48 | }
49 | }
50 |
51 | export const getChaiCommonStyles = async () => {
52 | const tailwindCssPath = await findTailwindCssFile();
53 | const tailwindCss = await fs.readFile(tailwindCssPath, "utf-8");
54 | return tailwindCss;
55 | };
56 |
57 | /** TODO: Move to @chaibuilder/pages/render */
58 | export const getThemeCustomFontFace = (fonts: string[]) => {
59 | const fontdefintions = filter(
60 | compact(map(fonts, getRegisteredFont)),
61 | (font: ChaiFont) => has(font, "src")
62 | );
63 | return getThemeCustomFontFaceStyle(fontdefintions as ChaiFontViaSrc[]);
64 | };
65 |
66 | export const getThemeCustomFontFaceStyle = (fonts: ChaiFontViaSrc[]) => {
67 | if (!fonts || fonts.length === 0) return "";
68 |
69 | return uniqBy(fonts, "family")
70 | .map((font: ChaiFontViaSrc) =>
71 | font.src
72 | .map(
73 | (source) => `@font-face {
74 | font-family: "${font.family}";
75 | src: url("${source.url}") format("${source.format}");
76 | font-display: swap;
77 | ${source.fontWeight ? `font-weight: ${source.fontWeight};` : ""}
78 | ${source.fontStyle ? `font-style: ${source.fontStyle};` : ""}
79 | ${source.fontStretch ? `font-stretch: ${source.fontStretch};` : ""}
80 | }`
81 | )
82 | .join("\n")
83 | )
84 | .join("\n");
85 | };
86 |
87 | export const getFontHref = (fonts: string[]): string[] => {
88 | const fontdefintions = filter(
89 | uniqBy(compact(map(fonts, getRegisteredFont)), "family"),
90 | (font: ChaiFont) => has(font, "url")
91 | );
92 | if (fontdefintions.length === 0) return [];
93 | return map(fontdefintions, "url") as string[];
94 | };
95 |
--------------------------------------------------------------------------------