├── .env.example
├── .gitignore
├── .prettierignore
├── README.md
├── components.json
├── eslint.config.mjs
├── next.config.ts
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── src
├── app
│ ├── apple
│ │ ├── icon.svg
│ │ └── page.tsx
│ ├── docs
│ │ └── component
│ │ │ ├── button
│ │ │ └── page.tsx
│ │ │ ├── confirm-account
│ │ │ └── page.tsx
│ │ │ ├── grid
│ │ │ └── page.tsx
│ │ │ └── header
│ │ │ └── page.tsx
│ ├── dub
│ │ ├── icon.svg
│ │ └── page.tsx
│ ├── framer
│ │ ├── icon.svg
│ │ └── page.tsx
│ ├── globals.css
│ ├── icon.svg
│ ├── layout.tsx
│ ├── myna-ui
│ │ ├── icon.svg
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── notion
│ │ ├── icon.svg
│ │ └── page.tsx
│ ├── opengraph-image.jpg
│ ├── page.tsx
│ ├── peerlist
│ │ ├── icon.svg
│ │ └── page.tsx
│ ├── softgen
│ │ ├── icon.svg
│ │ └── page.tsx
│ ├── substack
│ │ ├── icon.svg
│ │ └── page.tsx
│ ├── supabase
│ │ ├── icon.svg
│ │ └── page.tsx
│ └── twitter-image.jpg
├── components
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── checkbox-tree.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── command.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── hover-card.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── loading.tsx
│ │ ├── multiselect.tsx
│ │ ├── pagination.tsx
│ │ ├── popover.tsx
│ │ ├── radio-group.tsx
│ │ ├── read-component-source.ts
│ │ ├── scroll-area.tsx
│ │ ├── select-native.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── skeleton.tsx
│ │ ├── slider.tsx
│ │ ├── sonner.tsx
│ │ ├── switch.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── toggle-group.tsx
│ │ ├── toggle.tsx
│ │ └── tooltip.tsx
├── email
│ ├── apple
│ │ └── welcome.tsx
│ ├── components
│ │ ├── button
│ │ │ ├── button-with-border.tsx
│ │ │ ├── button-with-icon.tsx
│ │ │ ├── single-width-full.tsx
│ │ │ ├── single.tsx
│ │ │ └── two-button.tsx
│ │ ├── confirm-account
│ │ │ └── one.tsx
│ │ ├── grid
│ │ │ └── content-grid.tsx
│ │ └── header
│ │ │ ├── one.tsx
│ │ │ ├── spline.tsx
│ │ │ └── two.tsx
│ ├── dub
│ │ ├── product-update-email.tsx
│ │ └── welcome-email.tsx
│ ├── framer
│ │ ├── 1+4--welcome-to-framer.tsx
│ │ ├── 3+4--enhance-your-framer-experience.tsx
│ │ └── invitation-email.tsx
│ ├── myna-ui
│ │ ├── 1-welcome-email.tsx
│ │ ├── 2-verify-email.tsx
│ │ ├── 3-account-verified.tsx
│ │ ├── 4-login-alert.tsx
│ │ ├── 5-account-locked.tsx
│ │ ├── 6-password-updated.tsx
│ │ └── 7-product-updates.tsx
│ ├── notion
│ │ └── notion-newsletter.tsx
│ ├── peerlist
│ │ ├── inbox-message.tsx
│ │ ├── job-application.tsx
│ │ └── welcome.tsx
│ ├── softgen
│ │ ├── 1-welcome-email.tsx
│ │ ├── 2-payment-email.tsx
│ │ ├── 3-payment-package-email.tsx
│ │ ├── 4-purchase-token-email.tsx
│ │ └── 5-team-invitation-email.tsx
│ ├── substack
│ │ ├── newsletter-recommendation.tsx
│ │ └── unread-post.tsx
│ └── supabase
│ │ ├── supabase-verification-email.tsx
│ │ └── welcome.tsx
└── features
│ ├── analytics
│ └── posthog-page-view.tsx
│ ├── email
│ └── send
│ │ ├── email-popover.tsx
│ │ ├── email-trigger.ts
│ │ └── index.tsx
│ ├── global
│ ├── cli-commands.tsx
│ ├── code-block-wrapper.tsx
│ ├── code-block.tsx
│ ├── code-format-selector.tsx
│ ├── component-source.tsx
│ ├── copy-button.tsx
│ ├── cta.tsx
│ ├── footer.tsx
│ ├── github-button.tsx
│ ├── header.tsx
│ ├── illustration.tsx
│ ├── image.tsx
│ ├── page-header.tsx
│ ├── read-brand-source.ts
│ ├── tag-scroll.tsx
│ ├── template-layout.tsx
│ ├── theme-provider.tsx
│ ├── theme-toggle.tsx
│ └── toast.tsx
│ ├── hooks
│ ├── use-character-limit.ts
│ ├── use-colors.ts
│ ├── use-config.ts
│ ├── use-copy-to-clipboard.ts
│ ├── use-copy.ts
│ ├── use-image-upload.ts
│ ├── use-mounted.ts
│ ├── use-pagination.ts
│ ├── use-slider-with-input.ts
│ └── use-toast.ts
│ ├── lib
│ ├── color.ts
│ ├── component.ts
│ ├── email-to-html.tsx
│ ├── regisry-color.ts
│ ├── template-list.tsx
│ └── utils.ts
│ └── providers
│ ├── ph-provider.tsx
│ └── query-provider.tsx
├── tailwind.config.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | PLUNK_PUBLIC_KEY=""
2 | NEXT_PUBLIC_PLUNK_SECRET_KEY=""
3 |
4 | NEXT_PUBLIC_POSTHOG_KEY=""
5 | NEXT_PUBLIC_POSTHOG_HOST=""
--------------------------------------------------------------------------------
/.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.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .next
3 | pnpm-lock.yaml
4 | public/r
5 |
6 | # Build output
7 | .next
8 | build
9 | dist
10 |
11 | # Cache
12 | .cache
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # reactui email
2 |
3 | **A [react.email](https://react.email) based template library with full Tailwind CSS support**
4 |
5 | ## Overview
6 |
7 | ReactUI Email is a simple collection of email templates built on top of [react.email](https://react.email) and fully styled with [tailwindcss](https://tailwindcss.com). The goal is to provide an easy-to-use and customizable solution for creating email templates in React. The project are still a work in progress.
8 |
9 | ## Features
10 |
11 | - **Tailwind CSS**: Fully integrated for easy customization.
12 | - **React-based**: Built on [react.email](https://react.email) for easy integration.
13 | - **Responsive**: Optimized for mobile and desktop email clients.
14 |
15 | ## Roadmap
16 |
17 | - Add more templates.
18 | - Improve template responsiveness.
19 | - Add documentation for easy integration with other tools.
20 |
21 | ## Contributing
22 |
23 | Feel free to contribute to this project by submitting pull requests or opening issues. Since this is a template in progress, your feedback and suggestions are appreciated.
24 |
--------------------------------------------------------------------------------
/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": "src/app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "lib": "@/lib",
17 | "ui": "@/components/ui",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { FlatCompat } from "@eslint/eslintrc";
2 | import { dirname } from "path";
3 | import { fileURLToPath } from "url";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [...compat.extends("next/core-web-vitals", "next/typescript")];
13 |
14 | export default eslintConfig;
15 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | images: {
5 | remotePatterns: [
6 | {
7 | protocol: "https",
8 | hostname: "cdn.brandfetch.io",
9 | port: "",
10 | pathname: "/**",
11 | },
12 | {
13 | protocol: "https",
14 | hostname: "res.cloudinary.com",
15 | port: "",
16 | pathname: "/**",
17 | },
18 | ],
19 | },
20 | };
21 |
22 | export default nextConfig;
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactui-email",
3 | "version": "0.1.0",
4 | "private": true,
5 | "prettier": {
6 | "semi": true,
7 | "singleQuote": false,
8 | "printWidth": 100,
9 | "plugins": [
10 | "prettier-plugin-organize-imports",
11 | "prettier-plugin-tailwindcss"
12 | ],
13 | "overrides": [
14 | {
15 | "files": [
16 | "tsconfig.json"
17 | ],
18 | "options": {
19 | "parser": "jsonc"
20 | }
21 | }
22 | ]
23 | },
24 | "scripts": {
25 | "dev": "next dev --turbopack",
26 | "build": "next build",
27 | "start": "next start",
28 | "lint": "next lint",
29 | "format": "prettier --write . && pnpm run lint"
30 | },
31 | "dependencies": {
32 | "@hookform/resolvers": "^3.10.0",
33 | "@mynaui/icons-react": "^0.3.3",
34 | "@plunk/node": "^3.0.3",
35 | "@radix-ui/react-accordion": "^1.2.2",
36 | "@radix-ui/react-alert-dialog": "^1.1.4",
37 | "@radix-ui/react-avatar": "^1.1.2",
38 | "@radix-ui/react-checkbox": "^1.1.3",
39 | "@radix-ui/react-collapsible": "^1.1.2",
40 | "@radix-ui/react-dialog": "^1.1.4",
41 | "@radix-ui/react-dropdown-menu": "^2.1.4",
42 | "@radix-ui/react-hover-card": "^1.1.4",
43 | "@radix-ui/react-icons": "^1.3.2",
44 | "@radix-ui/react-label": "^2.1.1",
45 | "@radix-ui/react-popover": "^1.1.4",
46 | "@radix-ui/react-radio-group": "^1.2.2",
47 | "@radix-ui/react-scroll-area": "^1.2.2",
48 | "@radix-ui/react-select": "^2.1.4",
49 | "@radix-ui/react-separator": "^1.1.1",
50 | "@radix-ui/react-slider": "^1.2.2",
51 | "@radix-ui/react-slot": "^1.1.1",
52 | "@radix-ui/react-switch": "^1.1.2",
53 | "@radix-ui/react-tabs": "^1.1.2",
54 | "@radix-ui/react-toast": "^1.2.4",
55 | "@radix-ui/react-toggle": "^1.1.1",
56 | "@radix-ui/react-toggle-group": "^1.1.1",
57 | "@radix-ui/react-tooltip": "^1.1.6",
58 | "@react-email/components": "^0.0.32",
59 | "@react-email/render": "1.0.4",
60 | "@remixicon/react": "^4.6.0",
61 | "@tanstack/react-query": "^5.64.2",
62 | "class-variance-authority": "^0.7.1",
63 | "clsx": "^2.1.1",
64 | "cmdk": "^1.0.4",
65 | "lucide-react": "^0.471.0",
66 | "next": "15.2.1",
67 | "next-themes": "^0.4.4",
68 | "posthog-js": "^1.215.4",
69 | "react": "^19.0.0",
70 | "react-dom": "^19.0.0",
71 | "react-hook-form": "^7.54.2",
72 | "react-icons": "^5.4.0",
73 | "react-resizable-panels": "^2.1.7",
74 | "shiki": "^1.26.1",
75 | "sonner": "^1.7.1",
76 | "tailwind-merge": "^2.6.0",
77 | "tailwindcss-animate": "^1.0.7",
78 | "vaul": "^1.1.2",
79 | "zod": "^3.24.1",
80 | "zustand": "^5.0.3"
81 | },
82 | "devDependencies": {
83 | "@eslint/eslintrc": "^3",
84 | "@types/node": "^20",
85 | "@types/react": "^19",
86 | "@types/react-dom": "^19",
87 | "eslint": "^9",
88 | "eslint-config-next": "15.1.4",
89 | "postcss": "^8",
90 | "prettier": "^3.4.2",
91 | "prettier-plugin-organize-imports": "^4.1.0",
92 | "prettier-plugin-tailwindcss": "^0.6.9",
93 | "tailwindcss": "^3.4.1",
94 | "typescript": "^5"
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/src/app/apple/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/app/apple/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { t } from "@/features/lib/utils";
6 | import { Metadata } from "next";
7 | import Link from "next/link";
8 | import path from "path";
9 |
10 | export const revalidate = 3600;
11 |
12 | export const metadata: Metadata = {
13 | title: "Apple Email Templates | ReactUI Email",
14 | description:
15 | "Collection of Apple-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
16 | keywords: [
17 | "ReactUI Email",
18 | "Apple Email Templates",
19 | "TailwindCSS Email",
20 | "Next.js Email Templates",
21 | "Responsive Email Design",
22 | "Email Template Code",
23 | "React Email Components",
24 | "Apple Email Design",
25 | "Email Development",
26 | "Email Marketing Templates",
27 | ],
28 | openGraph: {
29 | title: "Apple Email Templates | ReactUI Email",
30 | description:
31 | "Collection of Apple-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
32 | url: "https://reactui.email/apple",
33 | images: [
34 | {
35 | url: "https://reactui.email/opengraph-image.jpg",
36 | alt: "Apple Email Templates Preview",
37 | width: 1200,
38 | height: 630,
39 | },
40 | ],
41 | },
42 | twitter: {
43 | card: "summary_large_image",
44 | title: "Apple Email Templates | ReactUI Email",
45 | description:
46 | "Collection of Apple-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
47 | images: ["https://reactui.email/opengraph-image.jpg"],
48 | },
49 | alternates: {
50 | canonical: "https://reactui.email/apple",
51 | },
52 | robots: {
53 | index: true,
54 | follow: true,
55 | },
56 | };
57 |
58 | const Page = async () => {
59 | const brand = "apple";
60 |
61 | const emailSource = await readBrandSources(brand);
62 |
63 | const emailPreviews = await Promise.all(
64 | emailSource.map(async (source) => {
65 | const emailModule = await import(
66 | `@/email/${brand}/${path.basename(source.filePath, ".tsx")}`
67 | );
68 | const EmailComponent = emailModule.default;
69 |
70 | const emailHtml = await render( );
71 |
72 | const fileName = path
73 | .basename(source.filePath, ".tsx")
74 | .split("-")
75 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
76 | .join(" ");
77 |
78 | return {
79 | fileName,
80 | content: source.content,
81 | emailHtml,
82 | };
83 | }),
84 | );
85 |
86 | return (
87 |
88 |
89 |
90 |
91 |
92 |
93 | Apple
94 | {" "}
95 |
96 | is a global technology company that designs, develops, and sells consumer electronics,
97 | software, and services.
98 |
99 |
100 | {emailPreviews.map((preview) => (
101 |
109 | ))}
110 |
111 |
112 |
113 | );
114 | };
115 |
116 | export default Page;
117 |
--------------------------------------------------------------------------------
/src/app/docs/component/button/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { Metadata } from "next";
6 | import Link from "next/link";
7 | import path from "path";
8 |
9 | export const revalidate = 3600;
10 |
11 | export const metadata: Metadata = {
12 | title: "Button | ReactUI Email",
13 | description:
14 | "Preview and code for the React Email Button component, built with Next.js and TailwindCSS for ReactUI Email.",
15 | keywords: [
16 | "ReactUI Email",
17 | "React Email Button",
18 | "TailwindCSS Email Components",
19 | "Next.js Email Templates",
20 | "Responsive Email Button",
21 | "Email Template Code",
22 | "React Email Components",
23 | ],
24 | openGraph: {
25 | title: "Button | ReactUI Email",
26 | description:
27 | "Explore the React Email Button component, featuring a clean, responsive design and code preview, built with ReactUI Email.",
28 | url: "https://reactui.email/button",
29 | images: [
30 | {
31 | url: "https://reactui.email/opengraph-button.jpg",
32 | alt: "React Email Button Component Preview",
33 | width: 1200,
34 | height: 630,
35 | },
36 | ],
37 | },
38 | twitter: {
39 | card: "summary_large_image",
40 | title: "Button | ReactUI Email",
41 | description:
42 | "Check out the React Email Button component with preview and code, built with Next.js and TailwindCSS for ReactUI Email.",
43 | images: ["https://reactui.email/opengraph-image.jpg"],
44 | },
45 | alternates: {
46 | canonical: "https://reactui.email/button",
47 | },
48 | robots: {
49 | index: true,
50 | follow: true,
51 | },
52 | };
53 |
54 | const Button = async () => {
55 | const component = "button";
56 |
57 | const componentSource = await readBrandSources(component, ["src", "email", "components"]);
58 |
59 | const componentPreview = await Promise.all(
60 | componentSource.map(async (source) => {
61 | const emailModule = await import(
62 | `@/email/components/${component}/${path.basename(source.filePath, ".tsx")}`
63 | );
64 | const EmailComponent = emailModule.default;
65 |
66 | const emailHtml = await render( );
67 |
68 | const fileName = path
69 | .basename(source.filePath, ".tsx")
70 | .split("-")
71 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
72 | .join(" ");
73 |
74 | return {
75 | fileName,
76 | content: source.content,
77 | emailHtml,
78 | };
79 | }),
80 | );
81 |
82 | return (
83 |
84 |
85 |
86 |
87 | A simple button component with{" "}
88 |
89 | React Email
90 | {" "}
91 | &&{" "}
92 |
93 | Tailwind CSS
94 |
95 |
96 |
97 | {componentPreview.map((preview) => (
98 |
106 | ))}
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | export default Button;
114 |
--------------------------------------------------------------------------------
/src/app/docs/component/confirm-account/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { Metadata } from "next";
6 | import Link from "next/link";
7 | import path from "path";
8 |
9 | export const revalidate = 3600;
10 |
11 | export const metadata: Metadata = {
12 | title: "Confirm Account | ReactUI Email",
13 | description:
14 | "Preview and code for the React Email Confirm Account component, built with Next.js and TailwindCSS for ReactUI Email.",
15 | keywords: [
16 | "ReactUI Email",
17 | "React Email Confirm Account",
18 | "TailwindCSS Email Components",
19 | "Next.js Email Templates",
20 | "Responsive Email Confirm Account",
21 | "Email Template Code",
22 | "React Email Components",
23 | ],
24 | openGraph: {
25 | title: "Confirm Account | ReactUI Email",
26 | description:
27 | "Explore the React Email Card component, featuring a clean, responsive design and code preview, built with ReactUI Email.",
28 | url: "https://reactui.email/confirm-account",
29 | images: [
30 | {
31 | url: "https://reactui.email/opengraph-confirm-account.jpg",
32 | alt: "React Email Confirm Account Component Preview",
33 | width: 1200,
34 | height: 630,
35 | },
36 | ],
37 | },
38 | twitter: {
39 | card: "summary_large_image",
40 | title: "Confirm Account | ReactUI Email",
41 | description:
42 | "Check out the React Email Confirm Account component with preview and code, built with Next.js and TailwindCSS for ReactUI Email.",
43 | images: ["https://reactui.email/opengraph-image.jpg"],
44 | },
45 | alternates: {
46 | canonical: "https://reactui.email/confirm-account",
47 | },
48 | robots: {
49 | index: true,
50 | follow: true,
51 | },
52 | };
53 |
54 | const ConfirmAccount = async () => {
55 | const component = "confirm-account";
56 |
57 | const componentSource = await readBrandSources(component, ["src", "email", "components"]);
58 |
59 | const componentPreview = await Promise.all(
60 | componentSource.map(async (source) => {
61 | const emailModule = await import(
62 | `@/email/components/${component}/${path.basename(source.filePath, ".tsx")}`
63 | );
64 | const EmailComponent = emailModule.default;
65 |
66 | const emailHtml = await render( );
67 |
68 | const fileName = path
69 | .basename(source.filePath, ".tsx")
70 | .split("-")
71 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
72 | .join(" ");
73 |
74 | return {
75 | fileName,
76 | content: source.content,
77 | emailHtml,
78 | };
79 | }),
80 | );
81 |
82 | return (
83 |
84 |
85 |
86 |
87 | A simple confirm account component with{" "}
88 |
89 | React Email
90 | {" "}
91 | &&{" "}
92 |
93 | Tailwind CSS
94 |
95 |
96 |
97 | {componentPreview.map((preview) => (
98 |
106 | ))}
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | export default ConfirmAccount;
114 |
--------------------------------------------------------------------------------
/src/app/docs/component/grid/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { Metadata } from "next";
6 | import Link from "next/link";
7 | import path from "path";
8 |
9 | export const revalidate = 3600;
10 |
11 | export const metadata: Metadata = {
12 | title: "Grid | ReactUI Email",
13 | description:
14 | "Preview and code for the React Email Grid component, built with Next.js and TailwindCSS for ReactUI Email.",
15 | keywords: [
16 | "ReactUI Email",
17 | "React Email Grid",
18 | "TailwindCSS Email Components",
19 | "Next.js Email Templates",
20 | "Responsive Email Grid",
21 | "Email Template Code",
22 | "React Email Components",
23 | ],
24 | openGraph: {
25 | title: "Grid | ReactUI Email",
26 | description:
27 | "Explore the React Email Grid component, featuring a clean, responsive design and code preview, built with ReactUI Email.",
28 | url: "https://reactui.email/grid",
29 | images: [
30 | {
31 | url: "https://reactui.email/opengraph-image.jpg",
32 | alt: "React Email Grid Component Preview",
33 | width: 1200,
34 | height: 630,
35 | },
36 | ],
37 | },
38 | twitter: {
39 | card: "summary_large_image",
40 | title: "Grid | ReactUI Email",
41 | description:
42 | "Check out the React Email Grid component with preview and code, built with Next.js and TailwindCSS for ReactUI Email.",
43 | images: ["https://reactui.email/opengraph-image.jpg"],
44 | },
45 | alternates: {
46 | canonical: "https://reactui.email/grid",
47 | },
48 | robots: {
49 | index: true,
50 | follow: true,
51 | },
52 | };
53 |
54 | const Header = async () => {
55 | const component = "grid";
56 |
57 | const componentSource = await readBrandSources(component, ["src", "email", "components"]);
58 |
59 | const componentPreview = await Promise.all(
60 | componentSource.map(async (source) => {
61 | const emailModule = await import(
62 | `@/email/components/${component}/${path.basename(source.filePath, ".tsx")}`
63 | );
64 | const EmailComponent = emailModule.default;
65 |
66 | const emailHtml = await render( );
67 |
68 | const fileName = path
69 | .basename(source.filePath, ".tsx")
70 | .split("-")
71 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
72 | .join(" ");
73 |
74 | return {
75 | fileName,
76 | content: source.content,
77 | emailHtml,
78 | };
79 | }),
80 | );
81 |
82 | return (
83 |
84 |
85 |
86 |
87 | A simple header component with{" "}
88 |
89 | React Email
90 | {" "}
91 | &&{" "}
92 |
93 | Tailwind CSS
94 |
95 |
96 |
97 | {componentPreview.map((preview) => (
98 |
107 | ))}
108 |
109 |
110 |
111 | );
112 | };
113 |
114 | export default Header;
115 |
--------------------------------------------------------------------------------
/src/app/docs/component/header/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { Metadata } from "next";
6 | import Link from "next/link";
7 | import path from "path";
8 |
9 | export const revalidate = 3600;
10 |
11 | export const metadata: Metadata = {
12 | title: "Header | ReactUI Email",
13 | description:
14 | "Preview and code for the React Email Header component, built with Next.js and TailwindCSS for ReactUI Email.",
15 | keywords: [
16 | "ReactUI Email",
17 | "React Email Header",
18 | "TailwindCSS Email Components",
19 | "Next.js Email Templates",
20 | "Responsive Email Header",
21 | "Email Template Code",
22 | "React Email Components",
23 | ],
24 | openGraph: {
25 | title: "Header | ReactUI Email",
26 | description:
27 | "Explore the React Email Header component, featuring a clean, responsive design and code preview, built with ReactUI Email.",
28 | url: "https://reactui.email/header",
29 | images: [
30 | {
31 | url: "https://reactui.email/opengraph-header.jpg",
32 | alt: "React Email Header Component Preview",
33 | width: 1200,
34 | height: 630,
35 | },
36 | ],
37 | },
38 | twitter: {
39 | card: "summary_large_image",
40 | title: "Header | ReactUI Email",
41 | description:
42 | "Check out the React Email Header component with preview and code, built with Next.js and TailwindCSS for ReactUI Email.",
43 | images: ["https://reactui.email/opengraph-image.jpg"],
44 | },
45 | alternates: {
46 | canonical: "https://reactui.email/header",
47 | },
48 | robots: {
49 | index: true,
50 | follow: true,
51 | },
52 | };
53 |
54 | const Header = async () => {
55 | const component = "header";
56 |
57 | const componentSource = await readBrandSources(component, ["src", "email", "components"]);
58 |
59 | const componentPreview = await Promise.all(
60 | componentSource.map(async (source) => {
61 | const emailModule = await import(
62 | `@/email/components/${component}/${path.basename(source.filePath, ".tsx")}`
63 | );
64 | const EmailComponent = emailModule.default;
65 |
66 | const emailHtml = await render( );
67 |
68 | const fileName = path
69 | .basename(source.filePath, ".tsx")
70 | .split("-")
71 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
72 | .join(" ");
73 |
74 | return {
75 | fileName,
76 | content: source.content,
77 | emailHtml,
78 | };
79 | }),
80 | );
81 |
82 | return (
83 |
84 |
85 |
86 |
87 | A simple header component with{" "}
88 |
89 | React Email
90 | {" "}
91 | &&{" "}
92 |
93 | Tailwind CSS
94 |
95 |
96 |
97 | {componentPreview.map((preview) => (
98 |
106 | ))}
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | export default Header;
114 |
--------------------------------------------------------------------------------
/src/app/dub/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/app/dub/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { t } from "@/features/lib/utils";
6 | import { Metadata } from "next";
7 | import Link from "next/link";
8 | import path from "path";
9 |
10 | export const revalidate = 3600;
11 |
12 | export const metadata: Metadata = {
13 | title: "Dub | ReactUI Email",
14 | description:
15 | "Preview and code for the Dub Welcome Email template, built with Next.js and TailwindCSS for ReactUI Email.",
16 | keywords: [
17 | "ReactUI Email",
18 | "Dub Email Template",
19 | "TailwindCSS Email",
20 | "Next.js Email Templates",
21 | "Responsive Email Design",
22 | "Email Template Code",
23 | "React Email Components",
24 | "Dub Welcome Email",
25 | "Dub React Components",
26 | ],
27 | openGraph: {
28 | title: "Dub | ReactUI Email",
29 | description:
30 | "Explore the Dub Welcome Email template, featuring a clean, responsive design and code preview, built with ReactUI Email.",
31 | url: "https://reactui.email",
32 | images: [
33 | {
34 | url: "https://reactui.email/opengraph-image.jpg",
35 | alt: "Dub Welcome Email Template Preview",
36 | width: 1200,
37 | height: 630,
38 | },
39 | ],
40 | },
41 | twitter: {
42 | card: "summary_large_image",
43 | title: "Dub | ReactUI Email",
44 | description:
45 | "Check out the Dub Welcome Email template with preview and code, built with Next.js and TailwindCSS for ReactUI Email.",
46 | images: ["https://reactui.email/opengraph-image.jpg"],
47 | },
48 | alternates: {
49 | canonical: "https://reactui.email/dub",
50 | },
51 | robots: {
52 | index: true,
53 | follow: true,
54 | },
55 | };
56 |
57 | const Page = async () => {
58 | const brand = "dub";
59 |
60 | const emailSource = await readBrandSources(brand);
61 |
62 | const emailPreviews = await Promise.all(
63 | emailSource.map(async (source) => {
64 | const emailModule = await import(
65 | `@/email/${brand}/${path.basename(source.filePath, ".tsx")}`
66 | );
67 | const EmailComponent = emailModule.default;
68 |
69 | const emailHtml = await render( );
70 |
71 | const fileNameParts = path
72 | .basename(source.filePath, ".tsx")
73 | .split("--")
74 | .reverse()
75 | .map((part, index) => {
76 | if (index === 1) {
77 | return part.replace("+", " of ");
78 | }
79 |
80 | if (index === 0) {
81 | return part
82 | .split("-")
83 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
84 | .join(" ");
85 | }
86 |
87 | return part;
88 | });
89 |
90 | const shouldAddParentheses = /\d+\+\d+--/.test(path.basename(source.filePath));
91 |
92 | const fileName = fileNameParts.join(" - (") + (shouldAddParentheses ? ")" : "");
93 |
94 | return {
95 | fileName,
96 | content: source.content,
97 | emailHtml,
98 | };
99 | }),
100 | );
101 |
102 | return (
103 |
104 |
105 |
106 |
107 |
108 |
109 | Dub.co
110 | {" "}
111 |
112 | is an open-source link management tool for modern marketing teams to create, share, and
113 | track short links.
114 |
115 |
116 | {emailPreviews.map((preview) => (
117 |
125 | ))}
126 |
127 |
128 |
129 | );
130 | };
131 |
132 | export default Page;
133 |
--------------------------------------------------------------------------------
/src/app/framer/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/app/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster as Sonner } from "@/components/ui/sonner";
2 | import { Toaster } from "@/components/ui/toaster";
3 | import Header from "@/features/global/header";
4 | import Illustration from "@/features/global/illustration";
5 | import { ThemeProvider } from "@/features/global/theme-provider";
6 | import { cn } from "@/features/lib/utils";
7 | import { PostHogProvider } from "@/features/providers/ph-provider";
8 | import type { Metadata, Viewport } from "next";
9 | import { Inter as FontSans } from "next/font/google";
10 | import "./globals.css";
11 |
12 | const fontSans = FontSans({
13 | subsets: ["latin"],
14 | variable: "--font-sans",
15 | });
16 |
17 | export const viewport: Viewport = {
18 | initialScale: 1,
19 | width: "device-width",
20 | maximumScale: 1,
21 | viewportFit: "cover",
22 | };
23 |
24 | export const metadata: Metadata = {
25 | metadataBase: new URL("https://reactui.email"),
26 | title: "ReactUI Email - Email Templates for React",
27 | description:
28 | "A library of customizable React email components built with Tailwind CSS. Ready-to-use email templates, designed to be mobile-friendly and easily adaptable for your projects.",
29 | keywords: [
30 | "React",
31 | "email templates",
32 | "email components",
33 | "Tailwind CSS",
34 | "open-source",
35 | "React UI components",
36 | "React UI email components",
37 | "React UI email templates",
38 | ],
39 | authors: [{ name: "Jay Suthar", url: "https://peerlist.io/sutharjay" }],
40 | };
41 |
42 | export default function RootLayout({
43 | children,
44 | }: Readonly<{
45 | children: React.ReactNode;
46 | }>) {
47 | return (
48 |
49 |
50 |
51 |
57 |
58 |
59 |
60 | {children}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/myna-ui/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/app/myna-ui/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import React, { useEffect } from "react";
5 |
6 | type Props = {
7 | children: React.ReactNode;
8 | };
9 |
10 | const MynaUILayout = (props: Props) => {
11 | const { setTheme } = useTheme();
12 |
13 | useEffect(() => {
14 | setTheme("light");
15 | }, [setTheme]);
16 |
17 | return (
18 | <>
19 | {props.children}
20 | >
21 | );
22 | };
23 |
24 | export default MynaUILayout;
25 |
--------------------------------------------------------------------------------
/src/app/myna-ui/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { Metadata } from "next";
6 | import Link from "next/link";
7 | import path from "path";
8 |
9 | export const revalidate = 3600;
10 |
11 | export const metadata: Metadata = {
12 | title: "Myna UI | ReactUI Email",
13 | description:
14 | "Preview and code for the Myna UI Email templates, built with Next.js and TailwindCSS for ReactUI Email.",
15 | keywords: [
16 | "ReactUI Email",
17 | "Myna UI Email Template",
18 | "TailwindCSS Email",
19 | "Next.js Email Templates",
20 | "Responsive Email Design",
21 | "Email Template Code",
22 | "React Email Components",
23 | "Myna UI Welcome Email",
24 | "Myna UI React Components",
25 | ],
26 | openGraph: {
27 | title: "Myna UI | ReactUI Email",
28 | description:
29 | "Explore the Myna UI Email templates, featuring a clean, responsive design and code preview, built with ReactUI Email.",
30 | url: "https://reactui.email",
31 | images: [
32 | {
33 | url: "https://reactui.email/opengraph-image.jpg",
34 | alt: "Myna UI Email Template Preview",
35 | width: 1200,
36 | height: 630,
37 | },
38 | ],
39 | },
40 | twitter: {
41 | card: "summary_large_image",
42 | title: "Myna UI | ReactUI Email",
43 | description:
44 | "Check out the Myna UI Email templates with preview and code, built with Next.js and TailwindCSS for ReactUI Email.",
45 | images: ["https://reactui.email/opengraph-image.jpg"],
46 | },
47 | alternates: {
48 | canonical: "https://reactui.email/myna-ui",
49 | },
50 | robots: {
51 | index: true,
52 | follow: true,
53 | },
54 | };
55 |
56 | const Page = async () => {
57 | const brand = "myna-ui";
58 |
59 | const emailSource = await readBrandSources(brand);
60 |
61 | const emailPreviews = await Promise.all(
62 | emailSource.map(async (source) => {
63 | const emailModule = await import(
64 | `@/email/${brand}/${path.basename(source.filePath, ".tsx")}`
65 | );
66 | const EmailComponent = emailModule.default;
67 |
68 | const emailHtml = await render( );
69 |
70 | const fileName = path
71 | .basename(source.filePath, ".tsx")
72 | .replace(/^\d+-/, "")
73 | .split("-")
74 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
75 | .join(" ");
76 |
77 | return {
78 | fileName,
79 | content: source.content,
80 | emailHtml,
81 | };
82 | }),
83 | );
84 |
85 | return (
86 |
87 |
88 |
89 |
90 |
91 |
92 | Myna UI
93 | {" "}
94 |
95 | is a modern, accessible, and customizable UI library built with TailwindCSS.
96 |
97 |
98 | {emailPreviews.map((preview) => (
99 |
107 | ))}
108 |
109 |
110 |
111 | );
112 | };
113 |
114 | export default Page;
115 |
--------------------------------------------------------------------------------
/src/app/notion/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/app/notion/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { t } from "@/features/lib/utils";
6 | import { Metadata } from "next";
7 | import Link from "next/link";
8 | import path from "path";
9 |
10 | export const revalidate = 3600;
11 |
12 | export const metadata: Metadata = {
13 | title: "Notion Email Templates | ReactUI Email",
14 | description:
15 | "Collection of Notion-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
16 | keywords: [
17 | "ReactUI Email",
18 | "Notion Email Templates",
19 | "TailwindCSS Email",
20 | "Next.js Email Templates",
21 | "Responsive Email Design",
22 | "Email Template Code",
23 | "React Email Components",
24 | "Notion Email Design",
25 | "Email Development",
26 | "Email Marketing Templates",
27 | ],
28 | openGraph: {
29 | title: "Notion Email Templates | ReactUI Email",
30 | description:
31 | "Collection of Notion-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
32 | url: "https://reactui.email/notion",
33 | images: [
34 | {
35 | url: "https://reactui.email/opengraph-image.jpg",
36 | alt: "Notion Email Templates Preview",
37 | width: 1200,
38 | height: 630,
39 | },
40 | ],
41 | },
42 | twitter: {
43 | card: "summary_large_image",
44 | title: "Notion Email Templates | ReactUI Email",
45 | description:
46 | "Collection of Notion-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
47 | images: ["https://reactui.email/opengraph-image.jpg"],
48 | },
49 | alternates: {
50 | canonical: "https://reactui.email/notion",
51 | },
52 | robots: {
53 | index: true,
54 | follow: true,
55 | },
56 | };
57 |
58 | const Page = async () => {
59 | const brand = "notion";
60 |
61 | const emailSource = await readBrandSources(brand);
62 |
63 | const emailPreviews = await Promise.all(
64 | emailSource.map(async (source) => {
65 | const emailModule = await import(
66 | `@/email/${brand}/${path.basename(source.filePath, ".tsx")}`
67 | );
68 | const EmailComponent = emailModule.default;
69 |
70 | const emailHtml = await render( );
71 |
72 | const fileName = path
73 | .basename(source.filePath, ".tsx")
74 | .split("-")
75 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
76 | .join(" ");
77 |
78 | return {
79 | fileName,
80 | content: source.content,
81 | emailHtml,
82 | };
83 | }),
84 | );
85 |
86 | return (
87 |
88 |
89 |
90 |
91 |
92 |
93 | Notion
94 |
95 | {" "}
96 | is an all-in-one workspace for notes, project management, documents, and collaboration.
97 |
98 |
99 | {emailPreviews.map((preview) => (
100 |
108 | ))}
109 |
110 |
111 |
112 | );
113 | };
114 |
115 | export default Page;
116 |
--------------------------------------------------------------------------------
/src/app/opengraph-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sutharjay1/reactui-email/5ed0acd7f68b910c848656b8c9fdb6570c436325/src/app/opengraph-image.jpg
--------------------------------------------------------------------------------
/src/app/peerlist/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/app/peerlist/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { t } from "@/features/lib/utils";
6 | import { Metadata } from "next";
7 | import Link from "next/link";
8 | import path from "path";
9 |
10 | export const revalidate = 3600;
11 |
12 | export const metadata: Metadata = {
13 | title: "Peerlist Email Templates | ReactUI Email",
14 | description:
15 | "Collection of Peerlist-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
16 | keywords: [
17 | "ReactUI Email",
18 | "Peerlist Email Templates",
19 | "TailwindCSS Email",
20 | "Next.js Email Templates",
21 | "Responsive Email Design",
22 | "Email Template Code",
23 | "React Email Components",
24 | "Peerlist Email Design",
25 | "Email Development",
26 | "Email Marketing Templates",
27 | ],
28 | openGraph: {
29 | title: "Peerlist Email Templates | ReactUI Email",
30 | description:
31 | "Collection of Peerlist-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
32 | url: "https://reactui.email/peerlist",
33 | images: [
34 | {
35 | url: "https://reactui.email/opengraph-image.jpg",
36 | alt: "Peerlist Email Templates Preview",
37 | width: 1200,
38 | height: 630,
39 | },
40 | ],
41 | },
42 | twitter: {
43 | card: "summary_large_image",
44 | title: "Peerlist Email Templates | ReactUI Email",
45 | description:
46 | "Collection of Peerlist-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
47 | images: ["https://reactui.email/opengraph-image.jpg"],
48 | },
49 | alternates: {
50 | canonical: "https://reactui.email/peerlist",
51 | },
52 | robots: {
53 | index: true,
54 | follow: true,
55 | },
56 | };
57 |
58 | const Page = async () => {
59 | const brand = "peerlist";
60 |
61 | const emailSource = await readBrandSources(brand);
62 |
63 | const emailPreviews = await Promise.all(
64 | emailSource.map(async (source) => {
65 | const emailModule = await import(
66 | `@/email/${brand}/${path.basename(source.filePath, ".tsx")}`
67 | );
68 | const EmailComponent = emailModule.default;
69 |
70 | const emailHtml = await render( );
71 |
72 | const fileName = path
73 | .basename(source.filePath, ".tsx")
74 | .split("-")
75 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
76 | .join(" ");
77 |
78 | return {
79 | fileName,
80 | content: source.content,
81 | emailHtml,
82 | };
83 | }),
84 | );
85 |
86 | return (
87 |
88 |
89 |
90 |
91 |
92 |
93 | Peerlist
94 | {" "}
95 |
96 | is a professional network for tech people to showcase their work, connect with peers,
97 | and find their next opportunity.
98 |
99 |
100 | {emailPreviews.map((preview) => (
101 |
109 | ))}
110 |
111 |
112 |
113 | );
114 | };
115 |
116 | export default Page;
117 |
--------------------------------------------------------------------------------
/src/app/softgen/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
9 |
12 |
15 |
16 |
--------------------------------------------------------------------------------
/src/app/softgen/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { t } from "@/features/lib/utils";
6 | import { Metadata } from "next";
7 | import Link from "next/link";
8 | import path from "path";
9 |
10 | export const revalidate = 3600;
11 |
12 | export const metadata: Metadata = {
13 | title: "SoftGen | ReactUI Email",
14 | description:
15 | "Preview and code for the SoftGen Email templates, built with Next.js and TailwindCSS for ReactUI Email.",
16 | keywords: [
17 | "ReactUI Email",
18 | "SoftGen Email Template",
19 | "TailwindCSS Email",
20 | "Next.js Email Templates",
21 | "Responsive Email Design",
22 | "Email Template Code",
23 | "React Email Components",
24 | "SoftGen Welcome Email",
25 | "SoftGen React Components",
26 | ],
27 | openGraph: {
28 | title: "SoftGen | ReactUI Email",
29 | description:
30 | "Explore the SoftGen Email templates, featuring a clean, responsive design and code preview, built with ReactUI Email.",
31 | url: "https://reactui.email",
32 | images: [
33 | {
34 | url: "https://reactui.email/opengraph-image.jpg",
35 | alt: "SoftGen Email Template Preview",
36 | width: 1200,
37 | height: 630,
38 | },
39 | ],
40 | },
41 | twitter: {
42 | card: "summary_large_image",
43 | title: "SoftGen | ReactUI Email",
44 | description:
45 | "Check out the SoftGen Email templates with preview and code, built with Next.js and TailwindCSS for ReactUI Email.",
46 | images: ["https://reactui.email/opengraph-image.jpg"],
47 | },
48 | alternates: {
49 | canonical: "https://reactui.email/softgen",
50 | },
51 | robots: {
52 | index: true,
53 | follow: true,
54 | },
55 | };
56 |
57 | const Page = async () => {
58 | const brand = "softgen";
59 |
60 | const emailSource = await readBrandSources(brand);
61 |
62 | const emailPreviews = await Promise.all(
63 | emailSource.map(async (source) => {
64 | const emailModule = await import(
65 | `@/email/${brand}/${path.basename(source.filePath, ".tsx")}`
66 | );
67 | const EmailComponent = emailModule.default;
68 |
69 | const emailHtml = await render( );
70 |
71 | const fileName = path
72 | .basename(source.filePath, ".tsx")
73 | .replace(/^\d+-/, "")
74 | .split("-")
75 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
76 | .join(" ");
77 |
78 | return {
79 | fileName,
80 | content: source.content,
81 | emailHtml,
82 | };
83 | }),
84 | );
85 |
86 | return (
87 |
88 |
89 |
90 |
91 |
92 |
93 | Softgen
94 | {" "}
95 |
96 | is an AI-powered platform that builds full-stack web apps from your instructions. No
97 | coding needed.
98 |
99 |
100 | {emailPreviews.map((preview) => (
101 |
109 | ))}
110 |
111 |
112 |
113 | );
114 | };
115 |
116 | export default Page;
117 |
--------------------------------------------------------------------------------
/src/app/substack/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/app/substack/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { t } from "@/features/lib/utils";
6 | import { Metadata } from "next";
7 | import Link from "next/link";
8 | import path from "path";
9 |
10 | export const revalidate = 3600;
11 |
12 | export const metadata: Metadata = {
13 | title: "Substack Email Templates | ReactUI Email",
14 | description:
15 | "Collection of Substack-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
16 | keywords: [
17 | "ReactUI Email",
18 | "Substack Email Templates",
19 | "TailwindCSS Email",
20 | "Next.js Email Templates",
21 | "Responsive Email Design",
22 | "Email Template Code",
23 | "React Email Components",
24 | "Substack Email Design",
25 | "Email Development",
26 | "Email Marketing Templates",
27 | ],
28 | openGraph: {
29 | title: "Substack Email Templates | ReactUI Email",
30 | description:
31 | "Collection of Substack-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
32 | url: "https://reactui.email/substack",
33 | images: [
34 | {
35 | url: "https://reactui.email/opengraph-image.jpg",
36 | alt: "Substack Email Templates Preview",
37 | width: 1200,
38 | height: 630,
39 | },
40 | ],
41 | },
42 | twitter: {
43 | card: "summary_large_image",
44 | title: "Substack Email Templates | ReactUI Email",
45 | description:
46 | "Collection of Substack-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
47 | images: ["https://reactui.email/opengraph-image.jpg"],
48 | },
49 | alternates: {
50 | canonical: "https://reactui.email/substack",
51 | },
52 | robots: {
53 | index: true,
54 | follow: true,
55 | },
56 | };
57 |
58 | const Page = async () => {
59 | const brand = "substack";
60 |
61 | const emailSource = await readBrandSources(brand);
62 |
63 | const emailPreviews = await Promise.all(
64 | emailSource.map(async (source) => {
65 | const emailModule = await import(
66 | `@/email/${brand}/${path.basename(source.filePath, ".tsx")}`
67 | );
68 | const EmailComponent = emailModule.default;
69 |
70 | const emailHtml = await render( );
71 |
72 | const fileName = path
73 | .basename(source.filePath, ".tsx")
74 | .split("-")
75 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
76 | .join(" ");
77 |
78 | return {
79 | fileName,
80 | content: source.content,
81 | emailHtml,
82 | };
83 | }),
84 | );
85 |
86 | return (
87 |
88 |
89 |
90 |
91 |
92 |
93 | Substack
94 | {" "}
95 |
96 | is a platform for writers and creators to publish newsletters, podcasts and build media
97 | businesses.
98 |
99 |
100 | {emailPreviews.map((preview) => (
101 |
109 | ))}
110 |
111 |
112 |
113 | );
114 | };
115 |
116 | export default Page;
117 |
--------------------------------------------------------------------------------
/src/app/supabase/icon.svg:
--------------------------------------------------------------------------------
1 |
3 |
6 |
9 |
12 |
13 |
15 |
16 |
17 |
18 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/app/supabase/page.tsx:
--------------------------------------------------------------------------------
1 | import PageHeader from "@/features/global/page-header";
2 | import { readBrandSources } from "@/features/global/read-brand-source";
3 | import { TemplateLayout } from "@/features/global/template-layout";
4 | import { render } from "@/features/lib/email-to-html";
5 | import { t } from "@/features/lib/utils";
6 | import { Metadata } from "next";
7 | import Link from "next/link";
8 | import path from "path";
9 |
10 | export const revalidate = 3600;
11 |
12 | export const metadata: Metadata = {
13 | title: "Supabase Email Templates | ReactUI Email",
14 | description:
15 | "Collection of Supabase-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
16 | keywords: [
17 | "ReactUI Email",
18 | "Supabase Email Templates",
19 | "TailwindCSS Email",
20 | "Next.js Email Templates",
21 | "Responsive Email Design",
22 | "Email Template Code",
23 | "React Email Components",
24 | "Supabase Email Design",
25 | "Email Development",
26 | "Email Marketing Templates",
27 | ],
28 | openGraph: {
29 | title: "Supabase Email Templates | ReactUI Email",
30 | description:
31 | "Collection of Supabase-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
32 | url: "https://reactui.email/supabase",
33 | images: [
34 | {
35 | url: "https://reactui.email/opengraph-image.jpg",
36 | alt: "Supabase Email Templates Preview",
37 | width: 1200,
38 | height: 630,
39 | },
40 | ],
41 | },
42 | twitter: {
43 | card: "summary_large_image",
44 | title: "Supabase Email Templates | ReactUI Email",
45 | description:
46 | "Collection of Supabase-style email templates built with React, Next.js and TailwindCSS. View previews and get the code for responsive, customizable email designs.",
47 | images: ["https://reactui.email/opengraph-image.jpg"],
48 | },
49 | alternates: {
50 | canonical: "https://reactui.email/supabase",
51 | },
52 | robots: {
53 | index: true,
54 | follow: true,
55 | },
56 | };
57 |
58 | const Page = async () => {
59 | const brand = "supabase";
60 |
61 | const emailSource = await readBrandSources(brand);
62 |
63 | const emailPreviews = await Promise.all(
64 | emailSource.map(async (source) => {
65 | const emailModule = await import(
66 | `@/email/${brand}/${path.basename(source.filePath, ".tsx")}`
67 | );
68 | const EmailComponent = emailModule.default;
69 |
70 | const emailHtml = await render( );
71 |
72 | const fileName = path
73 | .basename(source.filePath, ".tsx")
74 | .split("-")
75 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
76 | .join(" ");
77 |
78 | return {
79 | fileName,
80 | content: source.content,
81 | emailHtml,
82 | };
83 | }),
84 | );
85 |
86 | return (
87 |
88 |
89 |
90 |
91 |
92 |
93 | Supabase
94 | {" "}
95 |
96 | is an open source Firebase alternative providing all the backend features you need to
97 | build a product.
98 |
99 |
100 | {emailPreviews.map((preview) => (
101 |
109 | ))}
110 |
111 |
112 |
113 | );
114 | };
115 |
116 | export default Page;
117 |
--------------------------------------------------------------------------------
/src/app/twitter-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sutharjay1/reactui-email/5ed0acd7f68b910c848656b8c9fdb6570c436325/src/app/twitter-image.jpg
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
4 | import { ChevronDown } from "lucide-react";
5 | import * as React from "react";
6 |
7 | import { cn } from "@/features/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 |
43 |
44 |
45 | ));
46 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
47 |
48 | const AccordionContent = React.forwardRef<
49 | React.ElementRef,
50 | React.ComponentPropsWithoutRef
51 | >(({ className, children, ...props }, ref) => (
52 |
57 | {children}
58 |
59 | ));
60 |
61 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
62 |
63 | export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
64 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/features/lib/utils";
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
17 | ));
18 | Avatar.displayName = AvatarPrimitive.Root.displayName;
19 |
20 | const AvatarImage = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
31 |
32 | const AvatarFallback = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
46 |
47 | export { Avatar, AvatarFallback, AvatarImage };
48 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import { cva, type VariantProps } from "class-variance-authority";
2 | import * as React from "react";
3 |
4 | import { cn } from "@/features/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center justify-center rounded-full border px-1.5 text-xs font-medium leading-normal transition-colors outline-offset-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70",
8 | {
9 | variants: {
10 | variant: {
11 | default: "border-transparent bg-primary text-primary-foreground",
12 | secondary: "border-transparent bg-secondary text-secondary-foreground",
13 | destructive: "border-transparent bg-destructive text-destructive-foreground",
14 | outline: "text-foreground",
15 | },
16 | },
17 | defaultVariants: {
18 | variant: "default",
19 | },
20 | },
21 | );
22 |
23 | export interface BadgeProps
24 | extends React.HTMLAttributes,
25 | VariantProps {}
26 |
27 | function Badge({ className, variant, ...props }: BadgeProps) {
28 | return
;
29 | }
30 |
31 | export { Badge, badgeVariants };
32 |
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot";
2 | import { ChevronRight, MoreHorizontal } from "lucide-react";
3 | import * as React from "react";
4 |
5 | import { cn } from "@/features/lib/utils";
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode;
11 | }
12 | >(({ ...props }, ref) => );
13 | Breadcrumb.displayName = "Breadcrumb";
14 |
15 | const BreadcrumbList = React.forwardRef>(
16 | ({ className, ...props }, ref) => (
17 |
25 | ),
26 | );
27 | BreadcrumbList.displayName = "BreadcrumbList";
28 |
29 | const BreadcrumbItem = React.forwardRef>(
30 | ({ className, ...props }, ref) => (
31 |
32 | ),
33 | );
34 | BreadcrumbItem.displayName = "BreadcrumbItem";
35 |
36 | const BreadcrumbLink = React.forwardRef<
37 | HTMLAnchorElement,
38 | React.ComponentPropsWithoutRef<"a"> & {
39 | asChild?: boolean;
40 | }
41 | >(({ asChild, className, ...props }, ref) => {
42 | const Comp = asChild ? Slot : "a";
43 |
44 | return (
45 |
50 | );
51 | });
52 | BreadcrumbLink.displayName = "BreadcrumbLink";
53 |
54 | const BreadcrumbPage = React.forwardRef>(
55 | ({ className, ...props }, ref) => (
56 |
64 | ),
65 | );
66 | BreadcrumbPage.displayName = "BreadcrumbPage";
67 |
68 | const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
69 |
70 | {children ?? }
71 |
72 | );
73 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
74 |
75 | const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
76 |
82 |
83 |
84 | );
85 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
86 |
87 | export {
88 | Breadcrumb,
89 | BreadcrumbEllipsis,
90 | BreadcrumbItem,
91 | BreadcrumbLink,
92 | BreadcrumbList,
93 | BreadcrumbPage,
94 | BreadcrumbSeparator,
95 | };
96 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 | import * as React from "react";
4 |
5 | import { cn } from "@/features/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium transition-colors outline-offset-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70 disabled:pointer-events-none disabled:opacity-75 [&_svg]:pointer-events-none [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground shadow-sm shadow-black/5 hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground shadow-sm shadow-black/5 hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background shadow-sm shadow-black/5 hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground shadow-sm shadow-black/5 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-9 px-4 py-2",
24 | sm: "h-8 rounded-lg px-3 text-xs",
25 | lg: "h-10 rounded-lg px-8",
26 | icon: "h-9 w-9",
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 |
47 | );
48 | },
49 | );
50 | Button.displayName = "Button";
51 |
52 | export { Button, buttonVariants };
53 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cn } from "../../features/lib/utils";
3 |
4 | const Card = React.forwardRef>(
5 | ({ className, ...props }, ref) => (
6 |
11 | ),
12 | );
13 | Card.displayName = "Card";
14 |
15 | const CardHeader = React.forwardRef>(
16 | ({ className, ...props }, ref) => (
17 |
18 | ),
19 | );
20 | CardHeader.displayName = "CardHeader";
21 |
22 | const CardTitle = React.forwardRef>(
23 | ({ className, ...props }, ref) => (
24 |
29 | ),
30 | );
31 | CardTitle.displayName = "CardTitle";
32 |
33 | const CardDescription = React.forwardRef>(
34 | ({ className, ...props }, ref) => (
35 |
36 | ),
37 | );
38 | CardDescription.displayName = "CardDescription";
39 |
40 | const CardContent = React.forwardRef>(
41 | ({ className, ...props }, ref) => (
42 |
43 | ),
44 | );
45 | CardContent.displayName = "CardContent";
46 |
47 | const CardFooter = React.forwardRef>(
48 | ({ className, ...props }, ref) => (
49 |
50 | ),
51 | );
52 | CardFooter.displayName = "CardFooter";
53 |
54 | export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
55 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox-tree.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * IMPORTANT: This component was built for demo purposes only and has not been tested in production.
3 | * It serves as a proof of concept for a checkbox tree implementation.
4 | * If you‘re interested in collaborating to create a more robust, production-ready
5 | * headless component, your contributions are welcome!
6 | */
7 |
8 | "use client";
9 |
10 | import React, { useCallback, useMemo, useState } from "react";
11 |
12 | interface TreeNode {
13 | id: string;
14 | label: string;
15 | defaultChecked?: boolean;
16 | children?: TreeNode[];
17 | }
18 |
19 | function useCheckboxTree(initialTree: TreeNode) {
20 | const initialCheckedNodes = useMemo(() => {
21 | const checkedSet = new Set();
22 | const initializeCheckedNodes = (node: TreeNode) => {
23 | if (node.defaultChecked) {
24 | checkedSet.add(node.id);
25 | }
26 | node.children?.forEach(initializeCheckedNodes);
27 | };
28 | initializeCheckedNodes(initialTree);
29 | return checkedSet;
30 | }, [initialTree]);
31 |
32 | const [checkedNodes, setCheckedNodes] = useState>(initialCheckedNodes);
33 |
34 | const isChecked = useCallback(
35 | (node: TreeNode): boolean | "indeterminate" => {
36 | if (!node.children) {
37 | return checkedNodes.has(node.id);
38 | }
39 |
40 | const childrenChecked = node.children.map((child) => isChecked(child));
41 | if (childrenChecked.every((status) => status === true)) {
42 | return true;
43 | }
44 | if (childrenChecked.some((status) => status === true || status === "indeterminate")) {
45 | return "indeterminate";
46 | }
47 | return false;
48 | },
49 | [checkedNodes],
50 | );
51 |
52 | const handleCheck = useCallback(
53 | (node: TreeNode) => {
54 | const newCheckedNodes = new Set(checkedNodes);
55 |
56 | const toggleNode = (n: TreeNode, check: boolean) => {
57 | if (check) {
58 | newCheckedNodes.add(n.id);
59 | } else {
60 | newCheckedNodes.delete(n.id);
61 | }
62 | n.children?.forEach((child) => toggleNode(child, check));
63 | };
64 |
65 | const currentStatus = isChecked(node);
66 | const newCheck = currentStatus !== true;
67 |
68 | toggleNode(node, newCheck);
69 | setCheckedNodes(newCheckedNodes);
70 | },
71 | [checkedNodes, isChecked],
72 | );
73 |
74 | return { isChecked, handleCheck };
75 | }
76 |
77 | interface CheckboxTreeProps {
78 | tree: TreeNode;
79 | renderNode: (props: {
80 | node: TreeNode;
81 | isChecked: boolean | "indeterminate";
82 | onCheckedChange: () => void;
83 | children: React.ReactNode;
84 | }) => React.ReactNode;
85 | }
86 |
87 | export function CheckboxTree({ tree, renderNode }: CheckboxTreeProps) {
88 | const { isChecked, handleCheck } = useCheckboxTree(tree);
89 |
90 | const renderTreeNode = (node: TreeNode): React.ReactNode => {
91 | const children = node.children?.map(renderTreeNode);
92 |
93 | return renderNode({
94 | node,
95 | isChecked: isChecked(node),
96 | onCheckedChange: () => handleCheck(node),
97 | children,
98 | });
99 | };
100 |
101 | return renderTreeNode(tree);
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/features/lib/utils";
7 |
8 | const Checkbox = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 | {props.checked === "indeterminate" ? (
22 |
29 |
34 |
35 | ) : (
36 |
43 |
48 |
49 | )}
50 |
51 |
52 | ));
53 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
54 |
55 | export { Checkbox };
56 |
--------------------------------------------------------------------------------
/src/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, CollapsibleContent, CollapsibleTrigger };
12 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as DialogPrimitive from "@radix-ui/react-dialog";
4 | import { X } from "lucide-react";
5 | import * as React from "react";
6 |
7 | import { cn } from "@/features/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 |
53 | Close
54 |
55 |
56 |
57 | ));
58 | DialogContent.displayName = DialogPrimitive.Content.displayName;
59 |
60 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
61 |
62 | );
63 | DialogHeader.displayName = "DialogHeader";
64 |
65 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
66 |
70 | );
71 | DialogFooter.displayName = "DialogFooter";
72 |
73 | const DialogTitle = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
82 | ));
83 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
84 |
85 | const DialogDescription = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
94 | ));
95 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
96 |
97 | export {
98 | Dialog,
99 | DialogClose,
100 | DialogContent,
101 | DialogDescription,
102 | DialogFooter,
103 | DialogHeader,
104 | DialogOverlay,
105 | DialogPortal,
106 | DialogTitle,
107 | DialogTrigger,
108 | };
109 |
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Drawer as DrawerPrimitive } from "vaul";
5 | import { cn } from "../../features/lib/utils";
6 |
7 | const Drawer = ({
8 | shouldScaleBackground = true,
9 | ...props
10 | }: React.ComponentProps) => (
11 |
12 | );
13 | Drawer.displayName = "Drawer";
14 |
15 | const DrawerTrigger = DrawerPrimitive.Trigger;
16 |
17 | const DrawerPortal = DrawerPrimitive.Portal;
18 |
19 | const DrawerClose = DrawerPrimitive.Close;
20 |
21 | const DrawerOverlay = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
30 | ));
31 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
32 |
33 | const DrawerContent = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, children, ...props }, ref) => (
37 |
38 |
39 |
47 |
48 | {children}
49 |
50 |
51 | ));
52 | DrawerContent.displayName = "DrawerContent";
53 |
54 | const DrawerHeader = ({ className, ...props }: React.HTMLAttributes) => (
55 |
56 | );
57 | DrawerHeader.displayName = "DrawerHeader";
58 |
59 | const DrawerFooter = ({ className, ...props }: React.HTMLAttributes) => (
60 |
61 | );
62 | DrawerFooter.displayName = "DrawerFooter";
63 |
64 | const DrawerTitle = React.forwardRef<
65 | React.ElementRef,
66 | React.ComponentPropsWithoutRef
67 | >(({ className, ...props }, ref) => (
68 |
73 | ));
74 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
75 |
76 | const DrawerDescription = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ));
86 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
87 |
88 | export {
89 | Drawer,
90 | DrawerClose,
91 | DrawerContent,
92 | DrawerDescription,
93 | DrawerFooter,
94 | DrawerHeader,
95 | DrawerOverlay,
96 | DrawerPortal,
97 | DrawerTitle,
98 | DrawerTrigger,
99 | };
100 |
--------------------------------------------------------------------------------
/src/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/features/lib/utils";
7 |
8 | const HoverCard = HoverCardPrimitive.Root;
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger;
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef & {
15 | showArrow?: boolean;
16 | }
17 | >(({ className, align = "center", sideOffset = 4, showArrow = false, ...props }, ref) => (
18 |
28 | {props.children}
29 | {showArrow && (
30 |
31 | )}
32 |
33 | ));
34 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
35 |
36 | export { HoverCard, HoverCardContent, HoverCardTrigger };
37 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/features/lib/utils";
2 | import * as React from "react";
3 |
4 | const Input = React.forwardRef>(
5 | ({ className, type, ...props }, ref) => {
6 | return (
7 |
20 | );
21 | },
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import { cn } from "@/features/lib/utils";
6 |
7 | const Label = React.forwardRef>(
8 | ({ className, ...props }, ref) => (
9 |
17 | ),
18 | );
19 | Label.displayName = "Label";
20 |
21 | export { Label };
22 |
--------------------------------------------------------------------------------
/src/components/ui/loading.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/features/lib/utils";
2 |
3 | type Props = {
4 | className?: string;
5 | };
6 |
7 | const Loading = ({ className }: Props) => {
8 | return (
9 |
15 |
23 |
28 |
29 | );
30 | };
31 |
32 | export default Loading;
33 |
--------------------------------------------------------------------------------
/src/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
2 | import * as React from "react";
3 |
4 | import { ButtonProps, buttonVariants } from "@/components/ui/button";
5 | import { cn } from "@/features/lib/utils";
6 |
7 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
8 |
14 | );
15 | Pagination.displayName = "Pagination";
16 |
17 | const PaginationContent = React.forwardRef>(
18 | ({ className, ...props }, ref) => (
19 |
20 | ),
21 | );
22 | PaginationContent.displayName = "PaginationContent";
23 |
24 | const PaginationItem = React.forwardRef>(
25 | ({ className, ...props }, ref) => ,
26 | );
27 | PaginationItem.displayName = "PaginationItem";
28 |
29 | type PaginationLinkProps = {
30 | isActive?: boolean;
31 | isDisabled?: boolean;
32 | } & Pick &
33 | React.ComponentProps<"a">;
34 |
35 | const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
36 |
47 | );
48 | PaginationLink.displayName = "PaginationLink";
49 |
50 | const PaginationPrevious = ({
51 | className,
52 | ...props
53 | }: React.ComponentProps) => (
54 |
60 |
61 | Previous
62 |
63 | );
64 | PaginationPrevious.displayName = "PaginationPrevious";
65 |
66 | const PaginationNext = ({ className, ...props }: React.ComponentProps) => (
67 |
73 | Next
74 |
75 |
76 | );
77 | PaginationNext.displayName = "PaginationNext";
78 |
79 | const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
80 |
85 |
86 | More pages
87 |
88 | );
89 | PaginationEllipsis.displayName = "PaginationEllipsis";
90 |
91 | export {
92 | Pagination,
93 | PaginationContent,
94 | PaginationEllipsis,
95 | PaginationItem,
96 | PaginationLink,
97 | PaginationNext,
98 | PaginationPrevious,
99 | };
100 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as PopoverPrimitive from "@radix-ui/react-popover";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/features/lib/utils";
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverAnchor = PopoverPrimitive.Anchor;
13 |
14 | const PopoverContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef & {
17 | showArrow?: boolean;
18 | }
19 | >(({ className, align = "center", sideOffset = 4, showArrow = false, ...props }, ref) => (
20 |
21 |
31 | {props.children}
32 | {showArrow && (
33 |
34 | )}
35 |
36 |
37 | ));
38 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
39 |
40 | export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };
41 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/features/lib/utils";
7 |
8 | const RadioGroup = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => {
12 | return ;
13 | });
14 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
15 |
16 | const RadioGroupItem = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, ...props }, ref) => {
20 | return (
21 |
29 |
30 |
37 |
38 |
39 |
40 |
41 | );
42 | });
43 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
44 |
45 | export { RadioGroup, RadioGroupItem };
46 |
--------------------------------------------------------------------------------
/src/components/ui/read-component-source.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs";
2 | import path from "path";
3 |
4 | export async function readComponentSource(componentName: string) {
5 | const filePath = path.join(
6 | process.cwd(),
7 | "registry",
8 | "default",
9 | "components",
10 | `${componentName}.tsx`,
11 | );
12 | try {
13 | const source = await fs.readFile(filePath, "utf8");
14 | return source;
15 | } catch (error) {
16 | console.error(`Error reading file ${filePath}:`, error);
17 | return null;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/features/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 |
41 |
42 |
43 | ));
44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
45 |
46 | export { ScrollArea, ScrollBar };
47 |
--------------------------------------------------------------------------------
/src/components/ui/select-native.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/features/lib/utils";
2 | import { ChevronDown } from "lucide-react";
3 | import * as React from "react";
4 |
5 | export interface SelectPropsNative extends React.SelectHTMLAttributes {
6 | children: React.ReactNode;
7 | }
8 |
9 | const SelectNative = React.forwardRef(
10 | ({ className, children, ...props }, ref) => {
11 | return (
12 |
13 | *]:px-3 [&>*]:py-1 [&_option:checked]:bg-accent"
18 | : "h-9 pe-8 ps-3",
19 | className,
20 | )}
21 | ref={ref}
22 | {...props}
23 | >
24 | {children}
25 |
26 | {!props.multiple && (
27 |
28 |
29 |
30 | )}
31 |
32 | );
33 | },
34 | );
35 | SelectNative.displayName = "SelectNative";
36 |
37 | export { SelectNative };
38 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
4 | import * as React from "react";
5 | import { cn } from "../../features/lib/utils";
6 |
7 | const Separator = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
11 |
22 | ));
23 | Separator.displayName = SeparatorPrimitive.Root.displayName;
24 |
25 | export { Separator };
26 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "../../features/lib/utils";
2 |
3 | function Skeleton({ className, ...props }: React.HTMLAttributes) {
4 | return
;
5 | }
6 |
7 | export { Skeleton };
8 |
--------------------------------------------------------------------------------
/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as SliderPrimitive from "@radix-ui/react-slider";
4 | import * as React from "react";
5 |
6 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
7 | import { cn } from "@/features/lib/utils";
8 |
9 | const Slider = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef & {
12 | showTooltip?: boolean;
13 | tooltipContent?: (value: number) => React.ReactNode;
14 | }
15 | >(({ className, showTooltip = false, tooltipContent, ...props }, ref) => {
16 | const [showTooltipState, setShowTooltipState] = React.useState(false);
17 | const [internalValue, setInternalValue] = React.useState(
18 | (props.defaultValue as number[]) ?? (props.value as number[]) ?? [0],
19 | );
20 |
21 | React.useEffect(() => {
22 | if (props.value !== undefined) {
23 | setInternalValue(props.value as number[]);
24 | }
25 | }, [props.value]);
26 |
27 | const handleValueChange = (newValue: number[]) => {
28 | setInternalValue(newValue);
29 | props.onValueChange?.(newValue);
30 | };
31 |
32 | const handlePointerDown = () => {
33 | if (showTooltip) {
34 | setShowTooltipState(true);
35 | }
36 | };
37 |
38 | const handlePointerUp = React.useCallback(() => {
39 | if (showTooltip) {
40 | setShowTooltipState(false);
41 | }
42 | }, [showTooltip]);
43 |
44 | React.useEffect(() => {
45 | if (showTooltip) {
46 | document.addEventListener("pointerup", handlePointerUp);
47 | return () => {
48 | document.removeEventListener("pointerup", handlePointerUp);
49 | };
50 | }
51 | }, [showTooltip, handlePointerUp]);
52 |
53 | const renderThumb = (value: number) => {
54 | const thumb = (
55 |
59 | );
60 |
61 | if (!showTooltip) return thumb;
62 |
63 | return (
64 |
65 |
66 | {thumb}
67 |
72 | {tooltipContent ? tooltipContent(value) : value}
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | return (
80 |
89 |
90 |
91 |
92 | {internalValue?.map((value, index) => (
93 | {renderThumb(value)}
94 | ))}
95 |
96 | );
97 | });
98 | Slider.displayName = SliderPrimitive.Root.displayName;
99 |
100 | export { Slider };
101 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { Toaster as Sonner } from "sonner";
5 |
6 | type ToasterProps = React.ComponentProps;
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme();
10 |
11 | return (
12 |
29 | );
30 | };
31 |
32 | export { Toaster };
33 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as SwitchPrimitives from "@radix-ui/react-switch";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/features/lib/utils";
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as TabsPrimitive from "@radix-ui/react-tabs";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/features/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsContent, TabsList, TabsTrigger };
56 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/features/lib/utils";
4 |
5 | const Textarea = React.forwardRef>(
6 | ({ className, ...props }, ref) => {
7 | return (
8 |
16 | );
17 | },
18 | );
19 | Textarea.displayName = "Textarea";
20 |
21 | export { Textarea };
22 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast";
11 | import { useToast } from "@/features/hooks/use-toast";
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 |
23 |
24 | {title && {title} }
25 | {description && {description} }
26 |
27 |
{action}
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | })}
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
4 | import { type VariantProps } from "class-variance-authority";
5 | import * as React from "react";
6 |
7 | import { toggleVariants } from "@/components/ui/toggle";
8 | import { cn } from "@/features/lib/utils";
9 |
10 | const ToggleGroupContext = React.createContext>({
11 | size: "default",
12 | variant: "default",
13 | });
14 |
15 | const ToggleGroup = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef &
18 | VariantProps
19 | >(({ className, variant, size, children, ...props }, ref) => (
20 |
25 | {children}
26 |
27 | ));
28 |
29 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
30 |
31 | const ToggleGroupItem = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, children, variant, size, ...props }, ref) => {
36 | const context = React.useContext(ToggleGroupContext);
37 |
38 | return (
39 |
50 | {children}
51 |
52 | );
53 | });
54 |
55 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
56 |
57 | export { ToggleGroup, ToggleGroupItem };
58 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as TogglePrimitive from "@radix-ui/react-toggle";
4 | import { cva, type VariantProps } from "class-variance-authority";
5 | import * as React from "react";
6 |
7 | import { cn } from "@/features/lib/utils";
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center rounded-lg text-sm font-medium transition-colors hover:bg-muted outline-offset-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-9 px-3",
20 | sm: "h-8 px-2",
21 | lg: "h-10 px-3",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | },
29 | );
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef & VariantProps
34 | >(({ className, variant, size, ...props }, ref) => (
35 |
40 | ));
41 |
42 | Toggle.displayName = TogglePrimitive.Root.displayName;
43 |
44 | export { Toggle, toggleVariants };
45 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/features/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 | showArrow?: boolean;
18 | }
19 | >(({ className, sideOffset = 4, showArrow = false, ...props }, ref) => (
20 |
21 |
30 | {props.children}
31 | {showArrow && (
32 |
33 | )}
34 |
35 |
36 | ));
37 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
38 |
39 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
40 |
--------------------------------------------------------------------------------
/src/email/components/button/button-with-border.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Html,
7 | Preview,
8 | Section,
9 | Tailwind,
10 | } from "@react-email/components";
11 |
12 | export default function ButtonWithBorder() {
13 | return (
14 |
15 |
16 | Get started with our Button Component.
17 |
74 |
75 |
76 |
77 |
89 |
90 |
91 |
95 | Start Building Your App →
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/src/email/components/button/button-with-icon.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Column,
5 | Container,
6 | Head,
7 | Html,
8 | Preview,
9 | Row,
10 | Tailwind,
11 | } from "@react-email/components";
12 |
13 | export default function ButtonOne() {
14 | return (
15 |
16 |
17 | Get started with our Button Component.
18 |
76 |
77 |
78 |
79 |
80 |
84 |
85 | Button
86 |
87 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | );
111 | }
112 |
--------------------------------------------------------------------------------
/src/email/components/button/single-width-full.tsx:
--------------------------------------------------------------------------------
1 | import { Body, Button, Container, Head, Html, Preview, Tailwind } from "@react-email/components";
2 |
3 | export default function ButtonOne() {
4 | return (
5 |
6 |
7 | Get started with our Button Component.
8 |
66 |
67 |
68 |
72 | Get Started
73 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/email/components/button/single.tsx:
--------------------------------------------------------------------------------
1 | import { Body, Button, Container, Head, Html, Preview, Tailwind } from "@react-email/components";
2 |
3 | export default function ButtonOne() {
4 | return (
5 |
6 |
7 | Get started with our Button Component.
8 |
66 |
67 |
68 |
72 | Get Started
73 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/email/components/button/two-button.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Column,
5 | Container,
6 | Head,
7 | Html,
8 | Preview,
9 | Row,
10 | Tailwind,
11 | } from "@react-email/components";
12 |
13 | export default function ButtonOne() {
14 | return (
15 |
16 |
17 | Get started with our Button Component.
18 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
86 | Get Started
87 |
88 |
89 |
90 |
94 | Sign Up
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/src/email/components/header/one.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Column,
4 | Container,
5 | Head,
6 | Html,
7 | Img,
8 | Preview,
9 | Row,
10 | Section,
11 | Tailwind,
12 | Text,
13 | } from "@react-email/components";
14 |
15 | export default function HeaderOne() {
16 | return (
17 |
18 |
19 | Header Preview
20 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
91 |
92 |
93 |
94 | {new Date().toLocaleDateString()}
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | );
107 | }
108 |
--------------------------------------------------------------------------------
/src/email/components/header/spline.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Container,
4 | Head,
5 | Hr,
6 | Html,
7 | Img,
8 | Preview,
9 | Section,
10 | Tailwind,
11 | } from "@react-email/components";
12 |
13 | export default function HeaderOne() {
14 | return (
15 |
16 |
17 | Get started with our Fundamentals course and discover the power of Framer.
18 |
75 |
76 |
77 |
78 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/src/email/peerlist/inbox-message.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Hr,
7 | Html,
8 | Img,
9 | Link,
10 | Preview,
11 | Section,
12 | Tailwind,
13 | Text,
14 | } from "@react-email/components";
15 |
16 | export default function PeerlistMessageEmail() {
17 | return (
18 |
19 |
20 | Harshit sent you a message on Peerlist
21 |
78 |
79 |
80 |
81 |
88 |
89 |
90 |
91 | Akash Bhadange
92 | sent you a message on Peerlist.
93 |
94 |
95 |
96 |
97 | Thanks for featuring Peerlist emails! 💚
98 |
99 |
100 |
101 |
105 | Go to Inbox →
106 |
107 |
108 |
109 |
110 |
111 |
112 | You can unsubscribe and manage email notifications from{" "}
113 |
114 | your profile
115 |
116 | .
117 |
118 |
119 |
120 |
121 |
122 |
123 | );
124 | }
125 |
--------------------------------------------------------------------------------
/src/features/analytics/posthog-page-view.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname, useSearchParams } from "next/navigation";
4 | import { usePostHog } from "posthog-js/react";
5 | import { Suspense, useEffect } from "react";
6 |
7 | function PostHogPageView(): null {
8 | const pathname = usePathname();
9 | const searchParams = useSearchParams();
10 | const posthog = usePostHog();
11 |
12 | useEffect(() => {
13 | if (pathname && posthog) {
14 | let url = window.origin + pathname;
15 | if (searchParams.toString()) {
16 | url = url + `?${searchParams.toString()}`;
17 | }
18 |
19 | posthog.capture("$pageview", { $current_url: url });
20 | }
21 | }, [pathname, searchParams, posthog]);
22 |
23 | return null;
24 | }
25 |
26 | export default function SuspendedPostHogPageView() {
27 | return (
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/features/email/send/email-trigger.ts:
--------------------------------------------------------------------------------
1 | import Plunk from "@plunk/node";
2 | import { render } from "@react-email/render";
3 | import { JSXElementConstructor, ReactElement } from "react";
4 |
5 | const plunk = new Plunk(process.env.NEXT_PUBLIC_PLUNK_SECRET_KEY!);
6 |
7 | export const emailTrigger = async ({
8 | template,
9 | subject,
10 | to,
11 | }: {
12 | template: ReactElement>;
13 | subject: string;
14 | to: string;
15 | }) => {
16 | try {
17 | const body = await render(template);
18 | const response = await plunk.emails.send({
19 | to: to,
20 | subject,
21 | body,
22 | });
23 |
24 | return response.success;
25 | } catch (error) {
26 | console.error(error);
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/features/email/send/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryProvider } from "@/features/providers/query-provider";
4 | import { EmailSendPopover } from "./email-popover";
5 |
6 | const SendEmail = ({ html, label, brand }: { html: string; label: string; brand: string }) => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default SendEmail;
15 |
--------------------------------------------------------------------------------
/src/features/global/cli-commands.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
4 | import CopyButton from "@/features/global/copy-button";
5 | import { useConfig } from "@/features/hooks/use-config";
6 |
7 | const CliCommands = () => {
8 | const { config, setConfig } = useConfig();
9 | const packageManager = config.packageManager || "pnpm";
10 |
11 | const commands = {
12 | pnpm: `pnpm add @react-email/components -E`,
13 | npm: `npm install @react-email/components -E`,
14 | yarn: `yarn add @react-email/components -E`,
15 | bun: `bun add @react-email/components -E`,
16 | };
17 |
18 | return (
19 |
20 |
{
23 | setConfig({
24 | ...config,
25 | packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
26 | });
27 | }}
28 | className="rounded-lg bg-zinc-950 dark:bg-zinc-900"
29 | >
30 |
31 |
35 | pnpm
36 |
37 |
41 | npm
42 |
43 |
47 | yarn
48 |
49 |
53 | bun
54 |
55 |
56 | {Object.entries(commands).map(([pkg, command]) => (
57 |
58 | {command}
59 |
60 | ))}
61 |
62 |
66 |
67 | );
68 | };
69 |
70 | export default CliCommands;
71 |
--------------------------------------------------------------------------------
/src/features/global/code-block-wrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
5 | import { cn } from "@/features/lib/utils";
6 | import * as React from "react";
7 |
8 | interface CodeBlockProps extends React.HTMLAttributes {
9 | expandButtonTitle?: string;
10 | }
11 |
12 | export function CodeBlockWrapper({
13 | expandButtonTitle = "View Code",
14 | className,
15 | children,
16 | ...props
17 | }: CodeBlockProps) {
18 | const [isOpened, setIsOpened] = React.useState(false);
19 |
20 | return (
21 |
22 |
23 |
24 |
30 | {children}
31 |
32 |
33 |
39 |
40 |
41 | {isOpened ? "Collapse" : expandButtonTitle}
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/features/global/code-block.tsx:
--------------------------------------------------------------------------------
1 | import type { BundledLanguage } from "shiki";
2 | import { codeToHtml } from "shiki";
3 |
4 | type Props = {
5 | children: string;
6 | lang: BundledLanguage;
7 | };
8 |
9 | export async function CodeBlock(props: Props) {
10 | const code = await codeToHtml(props.children, {
11 | lang: props.lang,
12 | theme: "github-dark",
13 | });
14 |
15 | return (
16 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/features/global/code-format-selector.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
4 | import { Skeleton } from "@/components/ui/skeleton";
5 | import { useColors } from "@/features/hooks/use-colors";
6 | import { Color, getColorFormat } from "@/features/lib/color";
7 | import { cn } from "@/features/lib/utils";
8 | import * as React from "react";
9 |
10 | export function ColorFormatSelector({
11 | color,
12 | className,
13 | ...props
14 | }: Omit, "color"> & {
15 | color: Color;
16 | }) {
17 | const { format, setFormat, isLoading } = useColors();
18 | const formats = React.useMemo(() => getColorFormat(color), [color]);
19 |
20 | if (isLoading) {
21 | return ;
22 | }
23 |
24 | return (
25 |
26 |
30 | Format:
31 | {format}
32 |
33 |
34 | {Object.entries(formats).map(([format, value]) => (
35 |
40 | {format}
41 | {value}
42 |
43 | ))}
44 |
45 |
46 | );
47 | }
48 |
49 | export function ColorFormatSelectorSkeleton({
50 | className,
51 | ...props
52 | }: React.ComponentProps) {
53 | return ;
54 | }
55 |
--------------------------------------------------------------------------------
/src/features/global/component-source.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/features/lib/utils";
4 | import * as React from "react";
5 | import { CodeBlockWrapper } from "./code-block-wrapper";
6 |
7 | interface ComponentSourceProps extends React.HTMLAttributes {
8 | src: string;
9 | }
10 |
11 | export function ComponentSource({ children, className }: ComponentSourceProps) {
12 | return (
13 |
17 | {children}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/features/global/copy-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
5 | import { useCopy } from "@/features/hooks/use-copy";
6 | import { cn } from "@/features/lib/utils";
7 |
8 | const CopyButton = ({
9 | componentSource,
10 | className,
11 | }: {
12 | componentSource: string;
13 | className?: string;
14 | }) => {
15 | const { copied, copy } = useCopy();
16 |
17 | return (
18 |
19 |
20 |
21 |
22 | copy(componentSource)}
27 | aria-label={copied ? "Copied" : "Copy component source"}
28 | disabled={copied}
29 | >
30 |
49 |
66 |
67 |
68 |
69 | Copy
70 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default CopyButton;
78 |
--------------------------------------------------------------------------------
/src/features/global/cta.tsx:
--------------------------------------------------------------------------------
1 | export default function Cta() {
2 | return (
3 | <>
4 |
17 | >
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/features/global/footer.tsx:
--------------------------------------------------------------------------------
1 | import { Separator } from "@/components/ui/separator";
2 | import { BrandGithubSolid, BrandX } from "@mynaui/icons-react";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 |
6 | const socialLinks = [
7 | {
8 | name: "GitHub",
9 | url: "https://github.com/sutharjay1/reactui-email",
10 | icon: ,
11 | },
12 | {
13 | name: "Peerlist",
14 | url: "https://peerlist.io/sutharjay",
15 | icon: (
16 |
22 | ),
23 | },
24 | {
25 | name: "Twitter",
26 | url: "https://x.com/sutharjay0",
27 | icon: ,
28 | },
29 | ];
30 |
31 | export default function Footer() {
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 | © {new Date().getFullYear()} ReactUI Email. All rights reserved.
40 |
41 |
42 | {socialLinks.map((link) => (
43 |
51 |
56 | {link.icon}
57 |
58 |
59 | ))}
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/features/global/github-button.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import Link from "next/link";
3 |
4 | export default function GithubButton() {
5 | return (
6 |
7 |
12 |
20 |
21 |
22 | GitHub
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/features/global/illustration.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { useEffect, useState } from "react";
5 |
6 | export default function Illustration() {
7 | const { resolvedTheme } = useTheme();
8 | const [mounted, setMounted] = useState(false);
9 |
10 | useEffect(() => {
11 | setMounted(true);
12 | }, []);
13 |
14 | const stopColor = mounted ? (resolvedTheme === "dark" ? "#71717A" : "#52525B") : "#52525B";
15 |
16 | return (
17 | <>
18 |
27 |
28 |
34 |
35 |
36 |
42 |
43 |
44 |
52 |
53 |
54 |
55 |
63 |
64 |
65 |
66 |
75 |
76 |
77 |
78 |
79 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | >
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/src/features/global/image.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import Image from "next/image";
5 |
6 | type Props = {
7 | logo: {
8 | light: string;
9 | dark: string;
10 | };
11 | alt: string;
12 | };
13 |
14 | export const ThemeImage = ({ logo, alt }: Props) => {
15 | const { theme } = useTheme();
16 |
17 | const currentTheme = theme || "light";
18 |
19 | return (
20 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/features/global/page-header.tsx:
--------------------------------------------------------------------------------
1 | import { t } from "@/features/lib/utils";
2 | import CliCommands from "./cli-commands";
3 |
4 | interface PageHeaderProps {
5 | title: string;
6 | children: React.ReactNode;
7 | }
8 |
9 | export default function PageHeader({ title, children }: PageHeaderProps) {
10 | return (
11 | <>
12 |
13 |
14 | {t(title)}
15 |
16 |
{children}
17 |
18 |
19 |
20 |
21 | Install
22 |
23 |
24 | Install component from your command line.
25 |
26 |
27 |
28 |
29 | >
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/features/global/read-brand-source.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs/promises";
2 | import path from "path";
3 |
4 | export async function readBrandSources(brand: string, location: string[] = ["src", "email"]) {
5 | const dirPath = path.join(process.cwd(), location.map((dir) => dir).join("/"), brand);
6 |
7 | async function getTSXFiles(dir: string): Promise {
8 | const entries = await fs.readdir(dir, { withFileTypes: true });
9 | const tsxFiles: string[] = [];
10 |
11 | for (const entry of entries) {
12 | const entryPath = path.join(dir, entry.name);
13 | if (entry.isDirectory()) {
14 | const nestedFiles = await getTSXFiles(entryPath);
15 | tsxFiles.push(...nestedFiles);
16 | } else if (entry.isFile() && entry.name.endsWith(".tsx")) {
17 | tsxFiles.push(entryPath);
18 | }
19 | }
20 |
21 | return tsxFiles;
22 | }
23 |
24 | try {
25 | const tsxFilePaths = await getTSXFiles(dirPath);
26 | const fileContents = await Promise.all(
27 | tsxFilePaths.map(async (filePath) => {
28 | try {
29 | const content = await fs.readFile(filePath, "utf8");
30 | return { filePath, content };
31 | } catch (error) {
32 | console.error(`Error reading file ${filePath}:`, error);
33 | return { filePath, content: null };
34 | }
35 | }),
36 | );
37 | return fileContents;
38 | } catch (error) {
39 | console.error(`Error reading files in directory ${dirPath}:`, error);
40 | return [];
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/features/global/tag-scroll.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectItem,
7 | SelectTrigger,
8 | SelectValue,
9 | } from "@/components/ui/select";
10 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
11 | import { cn } from "@/features/lib/utils";
12 |
13 | type Props = {
14 | emailPreviews: {
15 | fileName: string;
16 | content: string | null;
17 | emailHtml: string;
18 | }[];
19 | };
20 |
21 | const TagScroll = ({ emailPreviews }: Props) => {
22 | const handleValueChange = (value: string) => {
23 | document.getElementById(value)?.scrollIntoView({
24 | behavior: "smooth",
25 | block: "start",
26 | });
27 | };
28 |
29 | return (
30 |
31 |
37 | {emailPreviews.map((preview) => (
38 |
39 |
40 | {
42 | document.getElementById(preview.fileName)?.scrollIntoView({
43 | behavior: "smooth",
44 | block: "start",
45 | });
46 | }}
47 | className={cn(
48 | "h-3 w-8 rounded-full transition-all hover:w-12",
49 | "cursor-pointer hover:opacity-80",
50 | "bg-secondary/90 hover:bg-secondary",
51 | )}
52 | aria-label={`Navigate to ${preview.fileName}`}
53 | />
54 |
55 |
56 | {preview.fileName.replace(/ - .*/, "")}
57 |
58 |
59 | ))}
60 |
61 |
62 |
67 |
68 |
69 |
70 |
71 |
72 | {emailPreviews.map((preview) => (
73 |
74 | {preview.fileName.replace(/ - .*/, "")}
75 |
76 | ))}
77 |
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default TagScroll;
85 |
--------------------------------------------------------------------------------
/src/features/global/template-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge";
2 | import { Card, CardContent } from "@/components/ui/card";
3 | import { Separator } from "@/components/ui/separator";
4 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
5 | import { cn } from "@/features/lib/utils";
6 | import SendEmail from "../email/send";
7 | import { CodeBlock } from "./code-block";
8 | import { CodeBlockWrapper } from "./code-block-wrapper";
9 | import CopyButton from "./copy-button";
10 |
11 | type Props = React.HTMLAttributes & {
12 | brand?: string;
13 | component?: string;
14 | label: string;
15 | emailSource: string;
16 | emailHtml: string;
17 | badge?: string;
18 | };
19 |
20 | export const TemplateLayout = ({
21 | label,
22 | emailHtml,
23 | emailSource,
24 | brand,
25 | component,
26 | badge,
27 | ...props
28 | }: Props) => {
29 | return (
30 |
31 |
32 |
33 | {label}
34 |
35 | {badge && (
36 |
37 | {badge}
38 |
39 | )}
40 |
41 |
42 |
43 |
44 |
45 |
49 | Preview
50 |
51 |
55 | Code
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
80 | {emailSource as string}
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | );
91 | };
92 |
--------------------------------------------------------------------------------
/src/features/global/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from "next-themes";
4 |
5 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
6 | return {children} ;
7 | }
8 |
--------------------------------------------------------------------------------
/src/features/global/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { MoonStarSolid, Sun } from "@mynaui/icons-react";
4 | import { useTheme } from "next-themes";
5 | import { useId } from "react";
6 |
7 | export default function ThemeToggle() {
8 | const id = useId();
9 | const { theme, setTheme } = useTheme();
10 |
11 | return (
12 |
13 | setTheme(theme === "dark" ? "light" : "dark")}
20 | aria-label="Toggle dark mode"
21 | />
22 |
27 |
28 |
29 |
30 |
31 | Switch to light / dark version
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/features/hooks/use-character-limit.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ChangeEvent, useState } from "react";
4 |
5 | type UseCharacterLimitProps = {
6 | maxLength: number;
7 | initialValue?: string;
8 | };
9 |
10 | export function useCharacterLimit({ maxLength, initialValue = "" }: UseCharacterLimitProps) {
11 | const [value, setValue] = useState(initialValue);
12 | const [characterCount, setCharacterCount] = useState(initialValue.length);
13 |
14 | const handleChange = (e: ChangeEvent) => {
15 | const newValue = e.target.value;
16 | if (newValue.length <= maxLength) {
17 | setValue(newValue);
18 | setCharacterCount(newValue.length);
19 | }
20 | };
21 |
22 | return {
23 | value,
24 | characterCount,
25 | handleChange,
26 | maxLength,
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/src/features/hooks/use-colors.ts:
--------------------------------------------------------------------------------
1 | import { ColorFormat } from "@/features/lib/color";
2 | import { create } from "zustand";
3 | import { persist } from "zustand/middleware";
4 | import { useMounted } from "./use-mounted";
5 |
6 | type ColorsStore = {
7 | format: ColorFormat;
8 | setFormat: (format: ColorFormat) => void;
9 | };
10 |
11 | export const useColorsStore = create()(
12 | persist(
13 | (set) => ({
14 | format: "hsl",
15 | setFormat: (format: ColorFormat) => set({ format }),
16 | }),
17 | {
18 | name: "colors",
19 | },
20 | ),
21 | );
22 |
23 | export function useColors() {
24 | const { format, setFormat } = useColorsStore();
25 | const mounted = useMounted();
26 |
27 | return {
28 | isLoading: !mounted,
29 | format,
30 | setFormat,
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/src/features/hooks/use-config.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { persist } from "zustand/middleware";
3 |
4 | type Config = {
5 | packageManager: "npm" | "yarn" | "pnpm" | "bun";
6 | };
7 |
8 | interface ConfigStore {
9 | config: Config;
10 | setConfig: (config: Config) => void;
11 | }
12 |
13 | const useConfigStore = create()(
14 | persist(
15 | (set) => ({
16 | config: {
17 | packageManager: "pnpm",
18 | },
19 | setConfig: (config) => set({ config }),
20 | }),
21 | {
22 | name: "config",
23 | },
24 | ),
25 | );
26 |
27 | export function useConfig() {
28 | const { config, setConfig } = useConfigStore();
29 | return { config, setConfig };
30 | }
31 |
--------------------------------------------------------------------------------
/src/features/hooks/use-copy-to-clipboard.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | export function useCopyToClipboard({
6 | timeout = 2000,
7 | onCopy,
8 | }: {
9 | timeout?: number;
10 | onCopy?: () => void;
11 | } = {}) {
12 | const [isCopied, setIsCopied] = React.useState(false);
13 |
14 | const copyToClipboard = (value: string) => {
15 | if (typeof window === "undefined" || !navigator.clipboard.writeText) {
16 | return;
17 | }
18 |
19 | if (!value) return;
20 |
21 | navigator.clipboard.writeText(value).then(() => {
22 | setIsCopied(true);
23 |
24 | if (onCopy) {
25 | onCopy();
26 | }
27 |
28 | setTimeout(() => {
29 | setIsCopied(false);
30 | }, timeout);
31 | }, console.error);
32 | };
33 |
34 | return { isCopied, copyToClipboard };
35 | }
36 |
--------------------------------------------------------------------------------
/src/features/hooks/use-copy.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export function useCopy(duration = 1500) {
4 | const [copied, setCopied] = useState(false);
5 |
6 | const copy = async (text: string) => {
7 | try {
8 | await navigator.clipboard.writeText(text);
9 | setCopied(true);
10 | setTimeout(() => setCopied(false), duration);
11 | return true;
12 | } catch (err) {
13 | console.error("Failed to copy text: ", err);
14 | return false;
15 | }
16 | };
17 |
18 | return {
19 | copied,
20 | copy,
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/src/features/hooks/use-image-upload.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef, useState } from "react";
2 |
3 | interface UseImageUploadProps {
4 | onUpload?: (url: string) => void;
5 | }
6 |
7 | export function useImageUpload({ onUpload }: UseImageUploadProps = {}) {
8 | const previewRef = useRef(null);
9 | const fileInputRef = useRef(null);
10 | const [previewUrl, setPreviewUrl] = useState(null);
11 | const [fileName, setFileName] = useState(null);
12 |
13 | const handleThumbnailClick = useCallback(() => {
14 | fileInputRef.current?.click();
15 | }, []);
16 |
17 | const handleFileChange = useCallback(
18 | (event: React.ChangeEvent) => {
19 | const file = event.target.files?.[0];
20 | if (file) {
21 | setFileName(file.name);
22 | const url = URL.createObjectURL(file);
23 | setPreviewUrl(url);
24 | previewRef.current = url;
25 | onUpload?.(url);
26 | }
27 | },
28 | [onUpload],
29 | );
30 |
31 | const handleRemove = useCallback(() => {
32 | if (previewUrl) {
33 | URL.revokeObjectURL(previewUrl);
34 | }
35 | setPreviewUrl(null);
36 | setFileName(null);
37 | previewRef.current = null;
38 | if (fileInputRef.current) {
39 | fileInputRef.current.value = "";
40 | }
41 | }, [previewUrl]);
42 |
43 | useEffect(() => {
44 | return () => {
45 | if (previewRef.current) {
46 | URL.revokeObjectURL(previewRef.current);
47 | }
48 | };
49 | }, []);
50 |
51 | return {
52 | previewUrl,
53 | fileName,
54 | fileInputRef,
55 | handleThumbnailClick,
56 | handleFileChange,
57 | handleRemove,
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/src/features/hooks/use-mounted.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export function useMounted() {
4 | const [mounted, setMounted] = React.useState(false);
5 |
6 | React.useEffect(() => {
7 | setMounted(true);
8 | }, []);
9 |
10 | return mounted;
11 | }
12 |
--------------------------------------------------------------------------------
/src/features/hooks/use-pagination.ts:
--------------------------------------------------------------------------------
1 | type UsePaginationProps = {
2 | currentPage: number;
3 | totalPages: number;
4 | paginationItemsToDisplay: number;
5 | };
6 |
7 | type UsePaginationReturn = {
8 | pages: number[];
9 | showLeftEllipsis: boolean;
10 | showRightEllipsis: boolean;
11 | };
12 |
13 | export function usePagination({
14 | currentPage,
15 | totalPages,
16 | paginationItemsToDisplay,
17 | }: UsePaginationProps): UsePaginationReturn {
18 | const showLeftEllipsis = currentPage - 1 > paginationItemsToDisplay / 2;
19 | const showRightEllipsis = totalPages - currentPage + 1 > paginationItemsToDisplay / 2;
20 |
21 | function calculatePaginationRange(): number[] {
22 | if (totalPages <= paginationItemsToDisplay) {
23 | return Array.from({ length: totalPages }, (_, i) => i + 1);
24 | }
25 |
26 | const halfDisplay = Math.floor(paginationItemsToDisplay / 2);
27 | const initialRange = {
28 | start: currentPage - halfDisplay,
29 | end: currentPage + halfDisplay,
30 | };
31 |
32 | const adjustedRange = {
33 | start: Math.max(1, initialRange.start),
34 | end: Math.min(totalPages, initialRange.end),
35 | };
36 |
37 | if (adjustedRange.start === 1) {
38 | adjustedRange.end = paginationItemsToDisplay;
39 | }
40 | if (adjustedRange.end === totalPages) {
41 | adjustedRange.start = totalPages - paginationItemsToDisplay + 1;
42 | }
43 |
44 | if (showLeftEllipsis) adjustedRange.start++;
45 | if (showRightEllipsis) adjustedRange.end--;
46 |
47 | return Array.from(
48 | { length: adjustedRange.end - adjustedRange.start + 1 },
49 | (_, i) => adjustedRange.start + i,
50 | );
51 | }
52 |
53 | const pages = calculatePaginationRange();
54 |
55 | return {
56 | pages,
57 | showLeftEllipsis,
58 | showRightEllipsis,
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/src/features/hooks/use-slider-with-input.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 |
3 | type UseSliderWithInputProps = {
4 | minValue?: number;
5 | maxValue?: number;
6 | initialValue?: number[];
7 | defaultValue?: number[];
8 | };
9 |
10 | export function useSliderWithInput({
11 | minValue = 0,
12 | maxValue = 100,
13 | initialValue = [minValue],
14 | defaultValue = [minValue],
15 | }: UseSliderWithInputProps) {
16 | const [sliderValue, setSliderValue] = useState(initialValue);
17 | const [inputValues, setInputValues] = useState(initialValue.map((v) => v.toString()));
18 |
19 | const validateAndUpdateValue = useCallback(
20 | (rawValue: string, index: number) => {
21 | if (rawValue === "" || rawValue === "-") {
22 | const newInputValues = [...inputValues];
23 | newInputValues[index] = "0";
24 | setInputValues(newInputValues);
25 |
26 | const newSliderValues = [...sliderValue];
27 | newSliderValues[index] = 0;
28 | setSliderValue(newSliderValues);
29 | return;
30 | }
31 |
32 | const numValue = parseFloat(rawValue);
33 |
34 | if (isNaN(numValue)) {
35 | const newInputValues = [...inputValues];
36 | newInputValues[index] = sliderValue[index].toString();
37 | setInputValues(newInputValues);
38 | return;
39 | }
40 |
41 | let clampedValue = Math.min(maxValue, Math.max(minValue, numValue));
42 |
43 | if (sliderValue.length > 1) {
44 | if (index === 0) {
45 | clampedValue = Math.min(clampedValue, sliderValue[1]);
46 | } else {
47 | clampedValue = Math.max(clampedValue, sliderValue[0]);
48 | }
49 | }
50 |
51 | const newSliderValues = [...sliderValue];
52 | newSliderValues[index] = clampedValue;
53 | setSliderValue(newSliderValues);
54 |
55 | const newInputValues = [...inputValues];
56 | newInputValues[index] = clampedValue.toString();
57 | setInputValues(newInputValues);
58 | },
59 | [sliderValue, inputValues, minValue, maxValue],
60 | );
61 |
62 | const handleInputChange = useCallback(
63 | (e: React.ChangeEvent, index: number) => {
64 | const newValue = e.target.value;
65 | if (newValue === "" || /^-?\d*\.?\d*$/.test(newValue)) {
66 | const newInputValues = [...inputValues];
67 | newInputValues[index] = newValue;
68 | setInputValues(newInputValues);
69 | }
70 | },
71 | [inputValues],
72 | );
73 |
74 | const handleSliderChange = useCallback((newValue: number[]) => {
75 | setSliderValue(newValue);
76 | setInputValues(newValue.map((v) => v.toString()));
77 | }, []);
78 |
79 | const resetToDefault = useCallback(() => {
80 | setSliderValue(defaultValue);
81 | setInputValues(defaultValue.map((v) => v.toString()));
82 | }, [defaultValue]);
83 |
84 | return {
85 | sliderValue,
86 | inputValues,
87 | validateAndUpdateValue,
88 | handleInputChange,
89 | handleSliderChange,
90 | resetToDefault,
91 | };
92 | }
93 |
--------------------------------------------------------------------------------
/src/features/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react";
5 |
6 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
7 |
8 | const TOAST_LIMIT = 1;
9 | const TOAST_REMOVE_DELAY = 1000000;
10 |
11 | type ToasterToast = ToastProps & {
12 | id: string;
13 | title?: React.ReactNode;
14 | description?: React.ReactNode;
15 | action?: ToastActionElement;
16 | };
17 |
18 | let count = 0;
19 |
20 | function genId() {
21 | count = (count + 1) % Number.MAX_SAFE_INTEGER;
22 | return count.toString();
23 | }
24 |
25 | type Action =
26 | | {
27 | type: "ADD_TOAST";
28 | toast: ToasterToast;
29 | }
30 | | {
31 | type: "UPDATE_TOAST";
32 | toast: Partial;
33 | }
34 | | {
35 | type: "DISMISS_TOAST";
36 | toastId?: ToasterToast["id"];
37 | }
38 | | {
39 | type: "REMOVE_TOAST";
40 | toastId?: ToasterToast["id"];
41 | };
42 |
43 | interface State {
44 | toasts: ToasterToast[];
45 | }
46 |
47 | const toastTimeouts = new Map>();
48 |
49 | const addToRemoveQueue = (toastId: string) => {
50 | if (toastTimeouts.has(toastId)) {
51 | return;
52 | }
53 |
54 | const timeout = setTimeout(() => {
55 | toastTimeouts.delete(toastId);
56 | dispatch({
57 | type: "REMOVE_TOAST",
58 | toastId: toastId,
59 | });
60 | }, TOAST_REMOVE_DELAY);
61 |
62 | toastTimeouts.set(toastId, timeout);
63 | };
64 |
65 | export const reducer = (state: State, action: Action): State => {
66 | switch (action.type) {
67 | case "ADD_TOAST":
68 | return {
69 | ...state,
70 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
71 | };
72 |
73 | case "UPDATE_TOAST":
74 | return {
75 | ...state,
76 | toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
77 | };
78 |
79 | case "DISMISS_TOAST": {
80 | const { toastId } = action;
81 |
82 | // ! Side effects ! - This could be extracted into a dismissToast() action,
83 | // but I'll keep it here for simplicity
84 | if (toastId) {
85 | addToRemoveQueue(toastId);
86 | } else {
87 | state.toasts.forEach((toast) => {
88 | addToRemoveQueue(toast.id);
89 | });
90 | }
91 |
92 | return {
93 | ...state,
94 | toasts: state.toasts.map((t) =>
95 | t.id === toastId || toastId === undefined
96 | ? {
97 | ...t,
98 | open: false,
99 | }
100 | : t,
101 | ),
102 | };
103 | }
104 | case "REMOVE_TOAST":
105 | if (action.toastId === undefined) {
106 | return {
107 | ...state,
108 | toasts: [],
109 | };
110 | }
111 | return {
112 | ...state,
113 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
114 | };
115 | }
116 | };
117 |
118 | const listeners: Array<(state: State) => void> = [];
119 |
120 | let memoryState: State = { toasts: [] };
121 |
122 | function dispatch(action: Action) {
123 | memoryState = reducer(memoryState, action);
124 | listeners.forEach((listener) => {
125 | listener(memoryState);
126 | });
127 | }
128 |
129 | type Toast = Omit;
130 |
131 | function toast({ ...props }: Toast) {
132 | const id = genId();
133 |
134 | const update = (props: ToasterToast) =>
135 | dispatch({
136 | type: "UPDATE_TOAST",
137 | toast: { ...props, id },
138 | });
139 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
140 |
141 | dispatch({
142 | type: "ADD_TOAST",
143 | toast: {
144 | ...props,
145 | id,
146 | open: true,
147 | onOpenChange: (open: boolean) => {
148 | if (!open) dismiss();
149 | },
150 | },
151 | });
152 |
153 | return {
154 | id: id,
155 | dismiss,
156 | update,
157 | };
158 | }
159 |
160 | function useToast() {
161 | const [state, setState] = React.useState(memoryState);
162 |
163 | React.useEffect(() => {
164 | listeners.push(setState);
165 | return () => {
166 | const index = listeners.indexOf(setState);
167 | if (index > -1) {
168 | listeners.splice(index, 1);
169 | }
170 | };
171 | }, [state]);
172 |
173 | return {
174 | ...state,
175 | toast,
176 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
177 | };
178 | }
179 |
180 | export { toast, useToast };
181 |
--------------------------------------------------------------------------------
/src/features/lib/color.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { colors } from "./regisry-color";
3 |
4 | const colorSchema = z.object({
5 | name: z.string(),
6 | id: z.string(),
7 | scale: z.number(),
8 | className: z.string(),
9 | hex: z.string(),
10 | rgb: z.string(),
11 | hsl: z.string(),
12 | foreground: z.string(),
13 | });
14 |
15 | const colorPaletteSchema = z.object({
16 | name: z.string(),
17 | colors: z.array(colorSchema),
18 | });
19 |
20 | export type ColorPalette = z.infer;
21 |
22 | export function getColorFormat(color: Color) {
23 | return {
24 | className: `bg-${color.name}-100`,
25 | hex: color.hex,
26 | rgb: color.rgb,
27 | hsl: color.hsl,
28 | };
29 | }
30 |
31 | export type ColorFormat = keyof ReturnType;
32 |
33 | export function getColors() {
34 | const tailwindColors = colorPaletteSchema.array().parse(
35 | Object.entries(colors)
36 | .map(([name, color]) => {
37 | if (!Array.isArray(color)) {
38 | return null;
39 | }
40 |
41 | return {
42 | name,
43 | colors: color.map((color) => {
44 | const rgb = color.rgb.replace(/^rgb\((\d+),(\d+),(\d+)\)$/, "$1 $2 $3");
45 |
46 | return {
47 | ...color,
48 | name,
49 | id: `${name}-${color.scale}`,
50 | className: `${name}-${color.scale}`,
51 | rgb,
52 | hsl: color.hsl.replace(/^hsl\(([\d.]+),([\d.]+%),([\d.]+%)\)$/, "$1 $2 $3"),
53 | foreground: getForegroundFromBackground(rgb),
54 | };
55 | }),
56 | };
57 | })
58 | .filter(Boolean),
59 | );
60 |
61 | return tailwindColors;
62 | }
63 |
64 | export type Color = ReturnType[number]["colors"][number];
65 |
66 | function getForegroundFromBackground(rgb: string) {
67 | const [r, g, b] = rgb.split(" ").map(Number);
68 |
69 | function toLinear(number: number): number {
70 | const base = number / 255;
71 | return base <= 0.04045 ? base / 12.92 : Math.pow((base + 0.055) / 1.055, 2.4);
72 | }
73 |
74 | const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
75 |
76 | return luminance > 0.179 ? "#000" : "#fff";
77 | }
78 |
--------------------------------------------------------------------------------
/src/features/lib/component.ts:
--------------------------------------------------------------------------------
1 | export const components = [
2 | {
3 | label: "Confirm Account",
4 | live: true,
5 | },
6 | {
7 | label: "Button",
8 | live: true,
9 | },
10 | {
11 | label: "Grid",
12 | live: true,
13 | },
14 | {
15 | label: "Header",
16 | live: true,
17 | },
18 | ];
19 |
--------------------------------------------------------------------------------
/src/features/lib/email-to-html.tsx:
--------------------------------------------------------------------------------
1 | import { render as emailRender } from "@react-email/render";
2 | import { JSXElementConstructor, ReactElement } from "react";
3 |
4 | export const render = async (
5 | template: ReactElement>,
6 | ) => {
7 | const html = await emailRender(template, {
8 | pretty: true,
9 | });
10 |
11 | return html;
12 | };
13 |
--------------------------------------------------------------------------------
/src/features/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 |
8 | export function t(str: string) {
9 | return str
10 | .replace(/&/g, "&")
11 | .replace(//g, ">")
13 | .replace(/"/g, """)
14 | .replace(/'/g, "'");
15 | }
16 |
--------------------------------------------------------------------------------
/src/features/providers/ph-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import posthog from "posthog-js";
4 | import { PostHogProvider as PHProvider } from "posthog-js/react";
5 | import { useEffect } from "react";
6 | import SuspendedPostHogPageView from "../analytics/posthog-page-view";
7 |
8 | export function PostHogProvider({ children }: { children: React.ReactNode }) {
9 | useEffect(() => {
10 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
11 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST!,
12 | capture_pageview: false,
13 | capture_pageleave: true,
14 | });
15 | }, []);
16 |
17 | return (
18 |
19 |
20 | {children}
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/features/providers/query-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4 | import { useState, type ReactNode } from "react";
5 |
6 | export function QueryProvider({ children }: { children: ReactNode }) {
7 | const [queryClient] = useState(
8 | () =>
9 | new QueryClient({
10 | defaultOptions: {
11 | queries: {
12 | staleTime: 60 * 1000,
13 | refetchInterval: 60 * 1000,
14 | retry: 1,
15 | },
16 | },
17 | }),
18 | );
19 |
20 | return {children} ;
21 | }
22 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | darkMode: ["class"],
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 | "./src/features/**/*.{js,ts,jsx,tsx,mdx}",
10 | ],
11 | theme: {
12 | extend: {
13 | fontFamily: {
14 | sans: ["var(--font-sans)"],
15 | },
16 | colors: {
17 | background: "hsl(var(--background))",
18 | foreground: "hsl(var(--foreground))",
19 | card: {
20 | DEFAULT: "hsl(var(--card))",
21 | foreground: "hsl(var(--card-foreground))",
22 | },
23 | popover: {
24 | DEFAULT: "hsl(var(--popover))",
25 | foreground: "hsl(var(--popover-foreground))",
26 | },
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | muted: {
36 | DEFAULT: "hsl(var(--muted))",
37 | foreground: "hsl(var(--muted-foreground))",
38 | },
39 | accent: {
40 | DEFAULT: "hsl(var(--accent))",
41 | foreground: "hsl(var(--accent-foreground))",
42 | },
43 | destructive: {
44 | DEFAULT: "hsl(var(--destructive))",
45 | foreground: "hsl(var(--destructive-foreground))",
46 | },
47 | border: "hsl(var(--border))",
48 | input: "hsl(var(--input))",
49 | ring: "hsl(var(--ring))",
50 | chart: {
51 | "1": "hsl(var(--chart-1))",
52 | "2": "hsl(var(--chart-2))",
53 | "3": "hsl(var(--chart-3))",
54 | "4": "hsl(var(--chart-4))",
55 | "5": "hsl(var(--chart-5))",
56 | },
57 | },
58 | borderRadius: {
59 | lg: "var(--radius)",
60 | md: "calc(var(--radius) - 2px)",
61 | sm: "calc(var(--radius) - 4px)",
62 | },
63 | },
64 | },
65 | plugins: [require("tailwindcss-animate")],
66 | } satisfies Config;
67 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next",
19 | },
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"],
23 | "@features/*": ["./src/features/*"],
24 | "@email/*": ["./src/email/*"],
25 | "@components/*": ["./src/email/components/*"],
26 | },
27 | },
28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
29 | "exclude": ["node_modules"],
30 | }
31 |
--------------------------------------------------------------------------------