├── .eslintrc.json ├── lib ├── constants.ts ├── utils.ts └── prismadb.ts ├── app ├── favicon.ico ├── (auth) │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ └── sign-up │ │ └── [[...sign-up]] │ │ └── page.tsx ├── (public) │ └── lm │ │ └── [username] │ │ └── [leadMagnetSlug] │ │ ├── loading.tsx │ │ ├── components │ │ └── LeadMagnetView.tsx │ │ └── page.tsx ├── api │ ├── uploadthing │ │ ├── route.ts │ │ └── core.ts │ ├── profile │ │ ├── profile.schema.ts │ │ └── route.ts │ ├── openai │ │ └── route.ts │ ├── lead │ │ └── route.ts │ ├── lead-magnet │ │ ├── schema.ts │ │ ├── unpublish │ │ │ └── route.ts │ │ ├── publish │ │ │ └── route.ts │ │ └── route.ts │ ├── stripe │ │ └── route.ts │ ├── webhooks │ │ └── stripe │ │ │ └── route.ts │ └── account │ │ └── route.ts ├── (dashboard) │ ├── layout.tsx │ ├── lead-magnet-editor │ │ └── [[...leadMagnetId]] │ │ │ ├── lead-magnet-constants.ts │ │ │ ├── components │ │ │ ├── LeadMagnetContentPreview.tsx │ │ │ ├── LeadMagnetProfilePreview.tsx │ │ │ ├── LeadMagnetEditorContainer.tsx │ │ │ ├── LeadMagnetSettings.tsx │ │ │ ├── LeadMagnetEditor.tsx │ │ │ ├── LeadMagnetEmailEditor.tsx │ │ │ ├── LeadMagnetEditorSidebar.tsx │ │ │ ├── LeadMagnetPromptEditor.tsx │ │ │ ├── LeadMagnetProfileEditor.1.tsx │ │ │ ├── LeadMagnetProfileEditor.tsx │ │ │ ├── LeadMagnetContentEditor.tsx │ │ │ └── LeadMagnetEditorNavbar.tsx │ │ │ └── page.tsx │ ├── leads │ │ └── [leadMagnetId] │ │ │ ├── components │ │ │ ├── LeadsContainer.tsx │ │ │ └── LeadsTable.tsx │ │ │ └── page.tsx │ ├── account │ │ ├── page.tsx │ │ └── components │ │ │ └── AccountContainer.tsx │ └── lead-magnets │ │ ├── page.tsx │ │ └── components │ │ ├── LeadMagnetTable.tsx │ │ └── LeadMagnetsContainer.tsx ├── (landing) │ ├── components │ │ ├── LandingPageFooter.tsx │ │ └── LandingPageNavbar.tsx │ ├── layout.tsx │ └── page.tsx ├── layout.tsx └── globals.css ├── postcss.config.js ├── public ├── images │ ├── landing-page-step-1.png │ ├── landing-page-step-2.png │ └── landing-page-step-3.png ├── vercel.svg └── next.svg ├── next.config.js ├── utils ├── uploadthing.ts └── stripe.ts ├── components.json ├── .gitignore ├── components ├── LoadingScreen.tsx ├── LeadMagnetNotFound.tsx ├── ui │ ├── input.tsx │ ├── button.tsx │ ├── card.tsx │ └── table.tsx ├── DashboardNavbar.tsx ├── LeadMagnetEmailCaptureModal.tsx ├── LeadMagnetEmailCapturePreview.tsx └── LeadMagnetAIChatContainer.tsx ├── tailwind.config.ts ├── middleware.ts ├── tsconfig.json ├── README.md ├── package.json ├── prisma ├── seed.ts └── schema.prisma ├── tailwind.config.js └── context ├── ProfileEditorContext.tsx └── LeadMagnetEditorContex.tsx /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAXIMUM_FREE_LEAD_MAGNETS = 2; 2 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhancockio/lead-convert-yt/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/images/landing-page-step-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhancockio/lead-convert-yt/HEAD/public/images/landing-page-step-1.png -------------------------------------------------------------------------------- /public/images/landing-page-step-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhancockio/lead-convert-yt/HEAD/public/images/landing-page-step-2.png -------------------------------------------------------------------------------- /public/images/landing-page-step-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhancockio/lead-convert-yt/HEAD/public/images/landing-page-step-3.png -------------------------------------------------------------------------------- /app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["utfs.io"], 5 | }, 6 | }; 7 | 8 | module.exports = nextConfig; 9 | -------------------------------------------------------------------------------- /utils/uploadthing.ts: -------------------------------------------------------------------------------- 1 | import { generateComponents } from "@uploadthing/react"; 2 | 3 | import type { uploadThingFileRouter } from "@/app/api/uploadthing/core"; 4 | 5 | export const { UploadButton } = generateComponents(); 6 | -------------------------------------------------------------------------------- /app/(public)/lm/[username]/[leadMagnetSlug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import LoadingScreen from "@/components/LoadingScreen"; 2 | import React from "react"; 3 | 4 | function LeadMagnetLoading() { 5 | return ; 6 | } 7 | 8 | export default LeadMagnetLoading; 9 | -------------------------------------------------------------------------------- /app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | import { createNextRouteHandler } from "uploadthing/next"; 2 | 3 | import { uploadThingFileRouter } from "./core"; 4 | 5 | // Export routes for Next App Router 6 | export const { GET, POST } = createNextRouteHandler({ 7 | router: uploadThingFileRouter, 8 | }); 9 | -------------------------------------------------------------------------------- /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.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import DashboardNavbar from "@/components/DashboardNavbar"; 2 | import { Toaster } from "react-hot-toast"; 3 | 4 | export default function DashboardLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | return ( 10 |
11 | 12 | {children} 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | import slugify from "slugify"; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | 9 | export function slugifyLeadMagnet(title: string) { 10 | return slugify(title, { 11 | replacement: "-", 12 | strict: false, 13 | lower: true, 14 | trim: false, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /lib/prismadb.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const globalForPrisma = globalThis as unknown as { 4 | prisma: PrismaClient | undefined; 5 | }; 6 | 7 | export const prismadb = 8 | globalForPrisma.prisma ?? 9 | new PrismaClient({ 10 | log: 11 | process.env.NODE_ENV === "development" 12 | ? ["query", "error", "warn"] 13 | : ["error"], 14 | }); 15 | 16 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prismadb; 17 | -------------------------------------------------------------------------------- /utils/stripe.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from "@prisma/client"; 2 | import dayjs from "dayjs"; 3 | import Stripe from "stripe"; 4 | 5 | export const stripe = new Stripe(process.env.STRIPE_API_KEY ?? "", { 6 | apiVersion: "2023-10-16", 7 | typescript: true, 8 | }); 9 | 10 | export const getPayingStatus = (subscription: Subscription | null): boolean => { 11 | return ( 12 | !!subscription && 13 | !!subscription.stripeCurrentPeriodEnd && 14 | dayjs(subscription.stripeCurrentPeriodEnd).isAfter(dayjs()) 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /app/api/profile/profile.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const profileCreateRequest = z.object({ 4 | title: z.string({ required_error: "Title is required" }), 5 | description: z.string({ required_error: "Description is required" }), 6 | profileImageUrl: z.string({ required_error: "Profile image is required" }), 7 | }); 8 | 9 | export const profileUpdateRequest = profileCreateRequest.extend({ 10 | id: z.string({ required_error: "ID is required" }), 11 | userId: z.string({ required_error: "User ID is required" }), 12 | }); 13 | -------------------------------------------------------------------------------- /app/(landing)/components/LandingPageFooter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function LandingPageFooter() { 4 | return ( 5 |
6 | Contact: 7 | 8 | 12 | brandon@brandonhancock.io 13 | 14 | 15 |
16 | ); 17 | } 18 | 19 | export default LandingPageFooter; 20 | -------------------------------------------------------------------------------- /app/(landing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import LandingPageNavbar from "./components/LandingPageNavbar"; 3 | import LandingPageFooter from "./components/LandingPageFooter"; 4 | 5 | function LandingLayout({ 6 | children, // will be a page or nested layout 7 | }: { 8 | children: React.ReactNode; 9 | }) { 10 | return ( 11 |
12 | 13 | 14 |
{children}
15 | 16 | 17 |
18 | ); 19 | } 20 | 21 | export default LandingLayout; 22 | -------------------------------------------------------------------------------- /components/LoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RingLoader from "react-spinners/RingLoader"; 3 | 4 | function LoadingScreen() { 5 | return ( 6 |
7 |
8 |
9 | LeadConvert 10 |
11 | 12 |
13 |
14 | ); 15 | } 16 | 17 | export default LoadingScreen; 18 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/LeadMagnetNotFound.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | import { Button } from "./ui/button"; 4 | 5 | interface LeadMagnetNotFoundProps { 6 | returnLink: string; 7 | } 8 | 9 | function LeadMagnetNotFound({ returnLink }: LeadMagnetNotFoundProps) { 10 | return ( 11 |
12 |

Lead Magnet Not Found

13 | 14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | 21 | export default LeadMagnetNotFound; 22 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import { withUt } from "uploadthing/tw"; 3 | 4 | const config: Config = { 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | backgroundImage: { 13 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 14 | "gradient-conic": 15 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 16 | }, 17 | }, 18 | }, 19 | plugins: [], 20 | }; 21 | export default withUt(config); 22 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Create Next App", 10 | description: "Generated by create next app", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@clerk/nextjs"; 2 | 3 | // This example protects all routes including api/trpc routes 4 | // Please edit this to allow other routes to be public as needed. 5 | // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your middleware 6 | export default authMiddleware({ 7 | publicRoutes: [ 8 | "/", 9 | "/api/account", 10 | "/api/lead-magnet", 11 | "/api/webhooks/stripe", 12 | "/api/lead-magnet/publish", 13 | "/api/lead-magnet/unpublish", 14 | ], 15 | }); 16 | 17 | export const config = { 18 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 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 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnet-editor/[[...leadMagnetId]]/lead-magnet-constants.ts: -------------------------------------------------------------------------------- 1 | import { LeadMagnet } from "@prisma/client"; 2 | 3 | export const DEFAULT_LEAD_MAGNET: LeadMagnet = { 4 | name: "New Lead Magnet", 5 | status: "draft", 6 | draftBody: "", 7 | draftTitle: "", 8 | draftSubtitle: "", 9 | draftPrompt: "", 10 | draftFirstQuestion: "", 11 | draftEmailCapture: "", 12 | publishedBody: "", 13 | publishedTitle: "", 14 | publishedSubtitle: "", 15 | publishedPrompt: "", 16 | publishedFirstQuestion: "", 17 | publishedEmailCapture: "", 18 | createdAt: new Date(), 19 | updatedAt: new Date(), 20 | publishedAt: null, 21 | slug: null, 22 | pageViews: 0, 23 | id: "", 24 | userId: "", 25 | }; 26 | -------------------------------------------------------------------------------- /app/api/openai/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from "ai"; 2 | import { Configuration, OpenAIApi } from "openai-edge"; 3 | 4 | // Create an OpenAI API client (that's edge friendly!) 5 | const config = new Configuration({ 6 | apiKey: process.env.OPENAI_API_KEY, 7 | }); 8 | const openai = new OpenAIApi(config); 9 | 10 | // IMPORTANT! Set the runtime to edge 11 | export const runtime = "edge"; 12 | 13 | export async function POST(request: Request) { 14 | const { messages } = await request.json(); 15 | 16 | const response = await openai.createChatCompletion({ 17 | model: "gpt-3.5-turbo", 18 | stream: true, 19 | messages, 20 | }); 21 | 22 | const stream = OpenAIStream(response); 23 | 24 | return new StreamingTextResponse(stream); 25 | } 26 | -------------------------------------------------------------------------------- /app/(dashboard)/leads/[leadMagnetId]/components/LeadsContainer.tsx: -------------------------------------------------------------------------------- 1 | import { Lead, LeadMagnet } from "@prisma/client"; 2 | import React from "react"; 3 | import LeadsTable from "./LeadsTable"; 4 | 5 | interface LeadsContainerProps { 6 | leadMagnet: LeadMagnet; 7 | leads: Lead[]; 8 | } 9 | 10 | function LeadsContainer({ leadMagnet, leads }: LeadsContainerProps) { 11 | return ( 12 |
13 |
14 |

{leadMagnet.publishedTitle}

15 | 16 | {leads.length} Leads 17 | 18 |
19 | 20 | 21 |
22 | ); 23 | } 24 | 25 | export default LeadsContainer; 26 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnet-editor/[[...leadMagnetId]]/components/LeadMagnetContentPreview.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface LeadMagnetContentPreviewProps { 4 | title: string; 5 | subtitle?: string; 6 | body: string; 7 | } 8 | 9 | function LeadMagnetContentPreview({ 10 | title, 11 | subtitle, 12 | body, 13 | }: LeadMagnetContentPreviewProps) { 14 | return ( 15 |
16 |

17 | {title} 18 |

19 | {subtitle && ( 20 |

{subtitle}

21 | )} 22 |
23 |
24 | ); 25 | } 26 | 27 | export default LeadMagnetContentPreview; 28 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | } 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnet-editor/[[...leadMagnetId]]/components/LeadMagnetProfilePreview.tsx: -------------------------------------------------------------------------------- 1 | import { Profile } from "@prisma/client"; 2 | import Image from "next/image"; 3 | import React from "react"; 4 | 5 | interface LeadMagnetProfileEditorProps { 6 | profile: Profile; 7 | } 8 | 9 | function LeadMagnetProfilePreview({ profile }: LeadMagnetProfileEditorProps) { 10 | return ( 11 |
12 | {profile.profileImageUrl && ( 13 | Profile Picture 20 | )} 21 |

22 | {profile.title} 23 |

24 |

{profile.description}

25 |
26 | ); 27 | } 28 | 29 | export default LeadMagnetProfilePreview; 30 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnet-editor/[[...leadMagnetId]]/components/LeadMagnetEditorContainer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { LeadMagnetEditorContextProvider } from "@/context/LeadMagnetEditorContex"; 4 | import { LeadMagnet } from "@prisma/client"; 5 | import React from "react"; 6 | import LeadMagnetEditor from "./LeadMagnetEditor"; 7 | import { useSession } from "@clerk/nextjs"; 8 | import LoadingScreen from "@/components/LoadingScreen"; 9 | import { ProfileEditorContextProvider } from "@/context/ProfileEditorContext"; 10 | 11 | interface LeadMagnetEditorContainerProps { 12 | leadMagnet: LeadMagnet; 13 | } 14 | 15 | function LeadMagnetEditorContainer({ 16 | leadMagnet, 17 | }: LeadMagnetEditorContainerProps) { 18 | const { isLoaded } = useSession(); 19 | 20 | if (!isLoaded) return ; 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | export default LeadMagnetEditorContainer; 32 | -------------------------------------------------------------------------------- /app/(landing)/components/LandingPageNavbar.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { SignInButton, UserButton, currentUser } from "@clerk/nextjs"; 3 | import { User } from "@clerk/nextjs/api"; 4 | import Link from "next/link"; 5 | import React from "react"; 6 | 7 | async function LandingPageNavbar() { 8 | const user: User | null = await currentUser(); 9 | 10 | console.log("USER", user); 11 | 12 | return ( 13 | 35 | ); 36 | } 37 | 38 | export default LandingPageNavbar; 39 | -------------------------------------------------------------------------------- /app/api/lead/route.ts: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { NextResponse } from "next/server"; 3 | import { object, string } from "zod"; 4 | 5 | const createLeadRequestSchema = object({ 6 | name: string(), 7 | email: string(), 8 | leadMagnetId: string(), 9 | }); 10 | 11 | export async function POST(request: Request) { 12 | const requestBody = await request.json(); 13 | const parsedRequest = createLeadRequestSchema.safeParse(requestBody); 14 | 15 | if (!parsedRequest.success) { 16 | return NextResponse.json({ message: parsedRequest.error }, { status: 400 }); 17 | } 18 | 19 | const leadMagnet = await prismadb.leadMagnet.findUnique({ 20 | where: { 21 | id: parsedRequest.data.leadMagnetId, 22 | }, 23 | }); 24 | 25 | if (!leadMagnet) { 26 | return NextResponse.json( 27 | { message: "Lead magnet not found" }, 28 | { status: 404 } 29 | ); 30 | } 31 | 32 | await prismadb.lead.create({ 33 | data: { 34 | email: parsedRequest.data.email, 35 | name: parsedRequest.data.name, 36 | leadMagnetId: parsedRequest.data.leadMagnetId, 37 | userId: leadMagnet.userId, 38 | }, 39 | }); 40 | 41 | return NextResponse.json({ message: "Lead created" }, { status: 200 }); 42 | } 43 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnet-editor/[[...leadMagnetId]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { LeadMagnet } from "@prisma/client"; 3 | import React from "react"; 4 | import { DEFAULT_LEAD_MAGNET } from "./lead-magnet-constants"; 5 | import LeadMagnetNotFound from "@/components/LeadMagnetNotFound"; 6 | import LeadMagnetEditorContainer from "./components/LeadMagnetEditorContainer"; 7 | 8 | interface LeadMagnetEditorParams { 9 | params: { 10 | leadMagnetId: string[]; 11 | }; 12 | } 13 | 14 | async function LeadMagnetEditorPage({ params }: LeadMagnetEditorParams) { 15 | const leadMagnetId = 16 | params.leadMagnetId?.length > 0 ? params.leadMagnetId[0] : null; 17 | 18 | console.log("leadMagnetId", leadMagnetId); 19 | 20 | let leadMagnet: LeadMagnet | null = null; 21 | 22 | if (!leadMagnetId) { 23 | leadMagnet = DEFAULT_LEAD_MAGNET; 24 | } else { 25 | leadMagnet = await prismadb.leadMagnet.findUnique({ 26 | where: { 27 | id: leadMagnetId, 28 | }, 29 | }); 30 | } 31 | 32 | if (!leadMagnet) { 33 | return ; 34 | } 35 | 36 | return ; 37 | } 38 | 39 | export default LeadMagnetEditorPage; 40 | -------------------------------------------------------------------------------- /app/(dashboard)/leads/[leadMagnetId]/components/LeadsTable.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | TableBody, 4 | TableCell, 5 | TableHead, 6 | TableHeader, 7 | TableRow, 8 | } from "@/components/ui/table"; 9 | import { Lead } from "@prisma/client"; 10 | import React from "react"; 11 | import dayjs from "dayjs"; 12 | 13 | function LeadsTable({ leads }: { leads: Lead[] }) { 14 | return ( 15 | <> 16 | 17 | 18 | 19 | Name 20 | Email 21 | Signup Date 22 | 23 | 24 | 25 | {leads.map((lead) => ( 26 | 27 | {lead.name} 28 | {lead.email} 29 | 30 | {dayjs(lead.createdAt).format("MM-DD-YYYY")} 31 | 32 | 33 | ))} 34 | 35 |
36 | {leads.length === 0 && ( 37 |
No Leads Found
38 | )} 39 | 40 | ); 41 | } 42 | 43 | export default LeadsTable; 44 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/(dashboard)/leads/[leadMagnetId]/page.tsx: -------------------------------------------------------------------------------- 1 | import LeadMagnetNotFound from "@/components/LeadMagnetNotFound"; 2 | import { prismadb } from "@/lib/prismadb"; 3 | import React from "react"; 4 | import LeadsContainer from "./components/LeadsContainer"; 5 | 6 | const getLeadMagnet = (leadMagnetId: string) => { 7 | return prismadb.leadMagnet.findUnique({ 8 | where: { 9 | id: leadMagnetId, 10 | }, 11 | }); 12 | }; 13 | 14 | const getLeads = (leadMagnetId: string) => { 15 | return prismadb.lead.findMany({ 16 | where: { 17 | leadMagnetId, 18 | }, 19 | orderBy: { 20 | createdAt: "desc", 21 | }, 22 | }); 23 | }; 24 | 25 | interface LeadPageProps { 26 | params: { 27 | leadMagnetId: string; 28 | }; 29 | } 30 | 31 | async function LeadsPage({ params }: LeadPageProps) { 32 | const leadMagnetId = params.leadMagnetId; 33 | 34 | if (!leadMagnetId) return ; 35 | 36 | const fetchLeadMagnet = getLeadMagnet(leadMagnetId); 37 | const fetchLeads = getLeads(leadMagnetId); 38 | 39 | const [leadMagnet, leads] = await Promise.all([fetchLeadMagnet, fetchLeads]); 40 | 41 | if (!leadMagnet) return ; 42 | 43 | return ; 44 | } 45 | 46 | export default LeadsPage; 47 | -------------------------------------------------------------------------------- /components/DashboardNavbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { UserButton } from "@clerk/nextjs"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | import React from "react"; 7 | 8 | const routes = [ 9 | { 10 | name: "Lead Magnets", 11 | path: "/lead-magnets", 12 | }, 13 | { 14 | name: "Account", 15 | path: "/account", 16 | }, 17 | ]; 18 | 19 | function DashboardNavBar() { 20 | const pathname = usePathname(); 21 | 22 | console.log("pathname", pathname); 23 | 24 | return ( 25 |
26 | {/* Logo Link */} 27 | 28 |

Lead Convert

29 | 30 | {/* Routes followed by the clerk user button */} 31 |
32 | {routes.map((route, idx) => ( 33 | 40 | {route.name} 41 | 42 | ))} 43 | 44 | 45 |
46 |
47 | ); 48 | } 49 | 50 | export default DashboardNavBar; 51 | -------------------------------------------------------------------------------- /app/(dashboard)/account/page.tsx: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { auth, currentUser } from "@clerk/nextjs"; 3 | import React from "react"; 4 | import { generateFromEmail } from "unique-username-generator"; 5 | import AccountContainer from "./components/AccountContainer"; 6 | 7 | async function AccountPage() { 8 | const fetchAccount = async (userId: string) => { 9 | let account = await prismadb.account.findUnique({ where: { userId } }); 10 | 11 | if (!account) { 12 | const user = await currentUser(); 13 | if (!user) throw new Error("User not found"); 14 | 15 | const baseEmail = user.emailAddresses[0].emailAddress; 16 | account = await prismadb.account.create({ 17 | data: { 18 | userId, 19 | email: baseEmail, 20 | username: generateFromEmail(baseEmail, 3), 21 | }, 22 | }); 23 | } 24 | 25 | return account; 26 | }; 27 | 28 | const fetchSubscription = (userId: string) => { 29 | return prismadb.subscription.findUnique({ 30 | where: { userId }, 31 | }); 32 | }; 33 | 34 | const { userId } = auth(); 35 | 36 | if (!userId) throw new Error("User not found"); 37 | 38 | const [account, subscription] = await Promise.all([ 39 | fetchAccount(userId), 40 | fetchSubscription(userId), 41 | ]); 42 | 43 | return ; 44 | } 45 | 46 | export default AccountPage; 47 | -------------------------------------------------------------------------------- /app/api/uploadthing/core.ts: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { currentUser } from "@clerk/nextjs"; 3 | import { createUploadthing, type FileRouter } from "uploadthing/next"; 4 | 5 | const f = createUploadthing(); 6 | 7 | // FileRouter for your app, can contain multiple FileRoutes 8 | export const uploadThingFileRouter = { 9 | // Define as many FileRoutes as you like, each with a unique routeSlug 10 | imageUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } }) 11 | // Set permissions and file types for this FileRoute 12 | .middleware(async () => { 13 | // This code runs on your server before upload 14 | const user = await currentUser(); 15 | 16 | // If you throw, the user will not be able to upload 17 | if (!user) throw new Error("Unauthorized"); 18 | 19 | // Whatever is returned here is accessible in onUploadComplete as `metadata` 20 | return { ...user }; 21 | }) 22 | .onUploadComplete(async ({ metadata, file }) => { 23 | const profile = await prismadb.profile.findUnique({ 24 | where: { userId: metadata.id }, 25 | }); 26 | 27 | if (!profile) throw new Error("No profile found"); 28 | 29 | prismadb.profile.update({ 30 | where: { userId: metadata.id }, 31 | data: { ...profile, profileImageUrl: file.url }, 32 | }); 33 | }), 34 | } satisfies FileRouter; 35 | 36 | export type uploadThingFileRouter = typeof uploadThingFileRouter; 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /components/LeadMagnetEmailCaptureModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction } from "react"; 2 | import LeadMagnetEmailCapturePreview from "./LeadMagnetEmailCapturePreview"; 3 | 4 | interface LeadMagnetEmailCaptureModalProps { 5 | leadMagnetId: string; 6 | emailCapturePrompt: string; 7 | setHasCapturedUserInfo: Dispatch>; 8 | setShowEmailCaptureModal: Dispatch>; 9 | } 10 | 11 | function LeadMagnetEmailCaptureModal({ 12 | setShowEmailCaptureModal, 13 | emailCapturePrompt, 14 | leadMagnetId, 15 | setHasCapturedUserInfo, 16 | }: LeadMagnetEmailCaptureModalProps) { 17 | return ( 18 |
19 |
20 |
21 | 27 | 33 |
34 |
35 |
36 | ); 37 | } 38 | 39 | export default LeadMagnetEmailCaptureModal; 40 | -------------------------------------------------------------------------------- /app/(public)/lm/[username]/[leadMagnetSlug]/components/LeadMagnetView.tsx: -------------------------------------------------------------------------------- 1 | import { LeadMagnet, Profile } from "@prisma/client"; 2 | import React from "react"; 3 | import Image from "next/image"; 4 | 5 | interface LeadMagnetViewProps { 6 | leadMagnet: LeadMagnet; 7 | profile: Profile; 8 | } 9 | 10 | function LeadMagnetView({ leadMagnet, profile }: LeadMagnetViewProps) { 11 | return ( 12 |
13 | {profile.profileImageUrl && ( 14 | profile picture 21 | )} 22 |

23 | {profile.title} 24 |

25 |

{profile.description}

26 |
27 |

28 | {leadMagnet.publishedTitle} 29 |

30 | {leadMagnet.publishedSubtitle && ( 31 |

32 | {leadMagnet.publishedSubtitle} 33 |

34 | )} 35 |
39 |
40 | ); 41 | } 42 | 43 | export default LeadMagnetView; 44 | -------------------------------------------------------------------------------- /app/api/lead-magnet/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const leadMagnetCreateRequest = z.object({ 4 | name: z.string({ required_error: "Name is required" }), 5 | status: z.string({ required_error: "Status is required" }), 6 | draftBody: z.string({ required_error: "Draft body is required" }), 7 | draftTitle: z.string({ required_error: "Draft title is required" }), 8 | draftSubtitle: z.string({ required_error: "Draft subtitle is required" }), 9 | draftPrompt: z.string({ required_error: "Draft prompt is required" }), 10 | draftFirstQuestion: z.string({ 11 | required_error: "Draft first question is required", 12 | }), 13 | publishedBody: z.string({ required_error: "Published body is required" }), 14 | publishedTitle: z.string({ required_error: "Published title is required" }), 15 | publishedSubtitle: z.string({ 16 | required_error: "Published subtitle is required", 17 | }), 18 | publishedPrompt: z.string({ required_error: "Published prompt is required" }), 19 | publishedFirstQuestion: z.string({ 20 | required_error: "Published first question is required", 21 | }), 22 | draftEmailCapture: z.string({ 23 | required_error: "Draft email capture is required", 24 | }), 25 | publishedEmailCapture: z.string({ 26 | required_error: "Published email capture is required", 27 | }), 28 | slug: z.string({ required_error: "Slug is required" }), 29 | }); 30 | 31 | export const leadMagnetUpdateRequest = leadMagnetCreateRequest.extend({ 32 | id: z.string({ required_error: "Id is required" }), 33 | userId: z.string({ required_error: "User Id is required" }), 34 | }); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lead-convert-yt", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "postinstall": "prisma generate" 11 | }, 12 | "prisma": { 13 | "seed": "ts-node prisma/seed.ts" 14 | }, 15 | "dependencies": { 16 | "@clerk/nextjs": "^4.25.4", 17 | "@prisma/client": "^5.4.2", 18 | "@radix-ui/react-slot": "^1.0.2", 19 | "@tiptap/pm": "^2.1.12", 20 | "@tiptap/react": "^2.1.12", 21 | "@tiptap/starter-kit": "^2.1.12", 22 | "@uploadthing/react": "^5.7.0", 23 | "ai": "2.2.21", 24 | "axios": "^1.5.1", 25 | "class-variance-authority": "^0.7.0", 26 | "clsx": "^2.0.0", 27 | "dayjs": "^1.11.10", 28 | "lucide-react": "^0.287.0", 29 | "next": "13.5.5", 30 | "openai": "4.13.0", 31 | "openai-edge": "^1.2.2", 32 | "react": "^18", 33 | "react-dom": "^18", 34 | "react-hot-toast": "^2.4.1", 35 | "react-icons": "^4.11.0", 36 | "react-spinners": "^0.13.8", 37 | "slugify": "^1.6.6", 38 | "stripe": "^14.2.0", 39 | "tailwind-merge": "^1.14.0", 40 | "tailwindcss-animate": "^1.0.7", 41 | "unique-username-generator": "^1.2.0", 42 | "uploadthing": "^5.7.2", 43 | "zod": "^3.22.4" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "^20", 47 | "@types/react": "^18", 48 | "@types/react-dom": "^18", 49 | "autoprefixer": "^10", 50 | "eslint": "^8", 51 | "eslint-config-next": "13.5.5", 52 | "postcss": "^8", 53 | "prisma": "^5.5.2", 54 | "tailwindcss": "^3", 55 | "ts-node": "^10.9.1", 56 | "typescript": "^5" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnet-editor/[[...leadMagnetId]]/components/LeadMagnetSettings.tsx: -------------------------------------------------------------------------------- 1 | import { useLeadMagnetEditorContext } from "@/context/LeadMagnetEditorContex"; 2 | import { slugifyLeadMagnet } from "@/lib/utils"; 3 | import React from "react"; 4 | 5 | function LeadMagnetSettings() { 6 | const { edittedLeadMagnet, setEdittedLeadMagnet } = 7 | useLeadMagnetEditorContext(); 8 | 9 | return ( 10 |
11 |
12 |

13 | Lead Magnet Settings 14 |

15 |
16 | 19 | { 26 | const newSlug = slugifyLeadMagnet(e.target.value); 27 | 28 | setEdittedLeadMagnet((prev) => ({ 29 | ...prev, 30 | slug: newSlug, 31 | })); 32 | }} 33 | placeholder="What is the title of your lead magnet?" 34 | /> 35 |

36 | Slug can only contain numbers, letters, and - 37 |

38 |
39 |
40 |
41 | ); 42 | } 43 | 44 | export default LeadMagnetSettings; 45 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | const PrismaClient = require("@prisma/client").PrismaClient; 2 | 3 | const prisma = new PrismaClient(); 4 | 5 | const seed = async () => { 6 | await prisma.leadMagnet.create({ 7 | data: { 8 | id: "123456789", 9 | draftBody: "This is a draft body", 10 | draftEmailCapture: "This is a draft email capture", 11 | draftFirstQuestion: "This is a draft first question", 12 | draftPrompt: "This is a draft prompt", 13 | draftSubtitle: "This is a draft subtitle", 14 | draftTitle: "This is a draft title", 15 | name: "This is a name", 16 | publishedBody: "This is a published body", 17 | publishedEmailCapture: "This is a published email capture", 18 | publishedFirstQuestion: "This is a published first question", 19 | publishedPrompt: "This is a published prompt", 20 | publishedSubtitle: "This is a published subtitle", 21 | publishedTitle: "This is a published title", 22 | slug: "lead-magnet-slug", 23 | status: "draft", 24 | userId: "user_2WruMGsskRrt6HDECpRhzNyH1vp", 25 | }, 26 | }); 27 | 28 | await prisma.lead.createMany({ 29 | data: [ 30 | { 31 | name: "Dummy User 1", 32 | email: "dummy1@gmail.com", 33 | leadMagnetId: "123456789", 34 | userId: "user_2WruMGsskRrt6HDECpRhzNyH1vp", 35 | }, 36 | { 37 | name: "Dummy User 2", 38 | email: "dummy2@gmail.com", 39 | leadMagnetId: "123456789", 40 | userId: "user_2WruMGsskRrt6HDECpRhzNyH1vp", 41 | }, 42 | ], 43 | }); 44 | }; 45 | 46 | // Run the seedDatabase function 47 | seed() 48 | .then(() => { 49 | console.log("Seeding completed successfully"); 50 | }) 51 | .catch((error) => { 52 | console.error("Error seeding the database:", error); 53 | }); 54 | -------------------------------------------------------------------------------- /app/api/lead-magnet/unpublish/route.ts: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { currentUser } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | import { z } from "zod"; 5 | 6 | const leadMagnetUnpublishRequest = z.object({ 7 | id: z.string({ required_error: "Id is required" }), 8 | }); 9 | 10 | export async function POST(request: Request) { 11 | const user = await currentUser(); 12 | 13 | if (!user || !user.id) { 14 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); 15 | } 16 | 17 | const userId = user.id; 18 | 19 | if (!userId) { 20 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); 21 | } 22 | 23 | const requestBody = await request.json(); 24 | const parsedPublishedRequest = 25 | leadMagnetUnpublishRequest.safeParse(requestBody); 26 | 27 | if (!parsedPublishedRequest.success) { 28 | return NextResponse.json( 29 | { message: parsedPublishedRequest.error }, 30 | { status: 400 } 31 | ); 32 | } 33 | 34 | const publishRequest = parsedPublishedRequest.data; 35 | 36 | const leadMagnet = await prismadb.leadMagnet.findUnique({ 37 | where: { 38 | id: publishRequest.id, 39 | }, 40 | }); 41 | 42 | if (!leadMagnet) { 43 | return NextResponse.json( 44 | { message: "Lead magnet not found" }, 45 | { status: 404 } 46 | ); 47 | } 48 | 49 | const unpublishedLeadMagnet = await prismadb.leadMagnet.update({ 50 | where: { 51 | id: publishRequest.id, 52 | }, 53 | data: { 54 | ...leadMagnet, 55 | status: "draft", 56 | updatedAt: new Date(), 57 | }, 58 | }); 59 | 60 | return NextResponse.json( 61 | { 62 | message: "Successfully unpublished new lead magnet!", 63 | data: unpublishedLeadMagnet, 64 | success: true, 65 | }, 66 | { status: 201 } 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnets/page.tsx: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { auth } from "@clerk/nextjs"; 3 | import React from "react"; 4 | import LeadMagnetsContainer from "./components/LeadMagnetsContainer"; 5 | 6 | const getLeadMagnets = async (userId: string) => { 7 | try { 8 | const leadMagnets = await prismadb.leadMagnet.findMany({ 9 | where: { userId }, 10 | }); 11 | 12 | return leadMagnets; 13 | } catch (error) { 14 | console.error(error); 15 | return []; 16 | } 17 | }; 18 | 19 | const getLeads = async (userId: string) => { 20 | try { 21 | const leads = await prismadb.lead.findMany({ 22 | where: { userId }, 23 | }); 24 | 25 | return leads; 26 | } catch (error) { 27 | console.error(error); 28 | return []; 29 | } 30 | }; 31 | 32 | const getSubscription = async (userId: string) => { 33 | try { 34 | const subscription = await prismadb.subscription.findUnique({ 35 | where: { userId }, 36 | }); 37 | 38 | return subscription; 39 | } catch (error) { 40 | console.error(error); 41 | return null; 42 | } 43 | }; 44 | 45 | async function LeadMagnetsPage() { 46 | const { userId } = auth(); 47 | 48 | console.log("userId", userId); 49 | 50 | if (!userId) return
No user found...
; 51 | 52 | const leadMagnetsRequest = getLeadMagnets(userId); 53 | const leadsRequest = getLeads(userId); 54 | const subscriptionRequest = getSubscription(userId); 55 | 56 | const [leadMagnets, leads, subscription] = await Promise.all([ 57 | leadMagnetsRequest, 58 | leadsRequest, 59 | subscriptionRequest, 60 | ]); 61 | 62 | console.log("leadMagnets", leadMagnets); 63 | console.log("leads", leads); 64 | 65 | return ( 66 | 71 | ); 72 | } 73 | 74 | export default LeadMagnetsPage; 75 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnet-editor/[[...leadMagnetId]]/components/LeadMagnetEditor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import LeadMagnetEditorNavbar from "./LeadMagnetEditorNavbar"; 4 | import LeadMagnetContentEditor from "./LeadMagnetContentEditor"; 5 | import LeadMagnetEditorSidebar from "./LeadMagnetEditorSidebar"; 6 | import LeadMagnetPromptEditor from "./LeadMagnetPromptEditor"; 7 | import LeadMagnetEmailEditor from "./LeadMagnetEmailEditor"; 8 | import LeadMagnetProfileEditor from "./LeadMagnetProfileEditor"; 9 | import LeadMagnetSettings from "./LeadMagnetSettings"; 10 | 11 | export type LeadMagnetSections = 12 | | "content" 13 | | "prompt" 14 | | "email" 15 | | "profile" 16 | | "settings"; 17 | 18 | function LeadMagnetEditor() { 19 | const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); 20 | const [selectedEditor, setSelectedEditor] = 21 | useState("content"); 22 | 23 | return ( 24 |
28 | 29 |
30 | 35 |
36 | {selectedEditor === "content" && } 37 | {selectedEditor === "prompt" && } 38 | {selectedEditor === "email" && } 39 | {selectedEditor === "profile" && } 40 | {selectedEditor === "settings" && } 41 |
42 |
43 |
44 | ); 45 | } 46 | 47 | export default LeadMagnetEditor; 48 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnet-editor/[[...leadMagnetId]]/components/LeadMagnetEmailEditor.tsx: -------------------------------------------------------------------------------- 1 | import LeadMagnetEmailCapturePreview from "@/components/LeadMagnetEmailCapturePreview"; 2 | import { useLeadMagnetEditorContext } from "@/context/LeadMagnetEditorContex"; 3 | import React from "react"; 4 | 5 | function LeadMagnetEmailEditor() { 6 | const { edittedLeadMagnet, setEdittedLeadMagnet } = 7 | useLeadMagnetEditorContext(); 8 | 9 | return ( 10 |
11 |
12 |

13 | Email Capture Editor 14 |

15 |
16 | 19 | 24 | setEdittedLeadMagnet((prev) => ({ 25 | ...prev, 26 | draftEmailCapture: e.target.value, 27 | })) 28 | } 29 | placeholder="How to ask users for their email to chat with the AI?" 30 | /> 31 |
32 |
33 |
34 |
35 | 39 |
40 |
41 |
42 | ); 43 | } 44 | 45 | export default LeadMagnetEmailEditor; 46 | -------------------------------------------------------------------------------- /app/api/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { stripe } from "@/utils/stripe"; 3 | import { currentUser } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | 6 | export async function GET() { 7 | try { 8 | const user = await currentUser(); 9 | 10 | if (!user) { 11 | return NextResponse.json({ message: "Unauthenticated" }, { status: 401 }); 12 | } 13 | 14 | const userSubscription = await prismadb.subscription.findUnique({ 15 | where: { userId: user.id }, 16 | }); 17 | 18 | if (userSubscription && userSubscription.stripeCustomerId) { 19 | const stripeSession = await stripe.billingPortal.sessions.create({ 20 | customer: userSubscription.stripeCustomerId, 21 | return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/account`, 22 | }); 23 | 24 | return NextResponse.json({ url: stripeSession.url }, { status: 200 }); 25 | } 26 | 27 | const stripeSession = await stripe.checkout.sessions.create({ 28 | success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/account`, 29 | cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/account`, 30 | payment_method_types: ["card"], 31 | mode: "subscription", 32 | billing_address_collection: "auto", 33 | customer_email: user.emailAddresses[0].emailAddress, 34 | line_items: [ 35 | { 36 | price_data: { 37 | currency: "USD", 38 | product_data: { 39 | name: "Lead Convert Pro", 40 | description: "Unlimited AI Lead Magnets", 41 | }, 42 | unit_amount: 1000, 43 | recurring: { 44 | interval: "month", 45 | }, 46 | }, 47 | quantity: 1, 48 | }, 49 | ], 50 | metadata: { 51 | userId: user.id, 52 | }, 53 | }); 54 | 55 | return NextResponse.json({ url: stripeSession.url }, { status: 200 }); 56 | } catch (e) { 57 | console.error("[STRIPE ERROR]", e); 58 | return new NextResponse("Internal Error", { status: 500 }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-purple-500 text-white hover:bg-purple-500/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border text-purple-500 border-purple-500 bg-white hover:bg-purple-500 hover:text-white", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-black underline-offset-4 underline hover:no-underline", 21 | ai: "bg-gradient-to-r from-red-500 to-purple-500 text-white hover:bg-ai/90", 22 | }, 23 | size: { 24 | default: "h-10 px-4 py-2", 25 | sm: "h-9 rounded-md px-3", 26 | lg: "h-11 rounded-md px-8", 27 | icon: "h-10 w-10", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | } 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /app/api/lead-magnet/publish/route.ts: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { slugifyLeadMagnet } from "@/lib/utils"; 3 | import { currentUser } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | import { z } from "zod"; 6 | 7 | const leadMagnetPublishRequest = z.object({ 8 | id: z.string({ required_error: "Id is required" }), 9 | }); 10 | 11 | export async function POST(request: Request) { 12 | const user = await currentUser(); 13 | 14 | if (!user || !user.id) { 15 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); 16 | } 17 | 18 | const userId = user.id; 19 | 20 | if (!userId) { 21 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); 22 | } 23 | 24 | const requestBody = await request.json(); 25 | const parsedPublishedRequest = 26 | leadMagnetPublishRequest.safeParse(requestBody); 27 | 28 | if (!parsedPublishedRequest.success) { 29 | return NextResponse.json( 30 | { message: parsedPublishedRequest.error }, 31 | { status: 400 } 32 | ); 33 | } 34 | 35 | const publishRequest = parsedPublishedRequest.data; 36 | 37 | const leadMagnet = await prismadb.leadMagnet.findUnique({ 38 | where: { 39 | id: publishRequest.id, 40 | }, 41 | }); 42 | 43 | if (!leadMagnet) { 44 | return NextResponse.json( 45 | { message: "Lead magnet not found" }, 46 | { status: 404 } 47 | ); 48 | } 49 | 50 | const publishedLeadMagnet = await prismadb.leadMagnet.update({ 51 | where: { 52 | id: publishRequest.id, 53 | }, 54 | data: { 55 | ...leadMagnet, 56 | publishedBody: leadMagnet.draftBody, 57 | publishedPrompt: leadMagnet.draftPrompt, 58 | publishedTitle: leadMagnet.draftTitle, 59 | publishedSubtitle: leadMagnet.draftSubtitle, 60 | publishedFirstQuestion: leadMagnet.draftFirstQuestion, 61 | publishedEmailCapture: leadMagnet.draftEmailCapture, 62 | updatedAt: new Date(), 63 | status: "published", 64 | publishedAt: new Date(), 65 | slug: leadMagnet.slug ?? slugifyLeadMagnet(leadMagnet.draftTitle), 66 | }, 67 | }); 68 | 69 | return NextResponse.json({ 70 | message: "Successfully published lead magnet!", 71 | data: publishedLeadMagnet, 72 | success: true, 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /app/api/profile/route.ts: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { currentUser } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | import { z } from "zod"; 5 | import { profileCreateRequest, profileUpdateRequest } from "./profile.schema"; 6 | 7 | export async function GET(request: Request) { 8 | const { searchParams } = new URL(request.url); 9 | const userId = searchParams.get("userId"); 10 | 11 | if (!userId) { 12 | return NextResponse.json( 13 | { message: "User ID is required", data: null }, 14 | { status: 400 } 15 | ); 16 | } 17 | 18 | let profile = await prismadb.profile.findFirst({ where: { userId } }); 19 | 20 | if (!profile) { 21 | profile = await prismadb.profile.create({ 22 | data: { userId, description: "", profileImageUrl: "", title: "" }, 23 | }); 24 | } 25 | 26 | return NextResponse.json({ message: "Success", data: profile }); 27 | } 28 | 29 | async function handleRequest( 30 | request: Request, 31 | schema: z.ZodType, 32 | isUpdate = false 33 | ) { 34 | const user = await currentUser(); 35 | 36 | if (!user || !user.id) { 37 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); 38 | } 39 | 40 | const userId = user.id; 41 | 42 | const requestBody = await request.json(); 43 | const parsed = schema.safeParse(requestBody); 44 | 45 | if (!parsed.success) { 46 | return NextResponse.json( 47 | { message: parsed.error, data: null }, 48 | { status: 400 } 49 | ); 50 | } 51 | 52 | if (isUpdate && parsed.data.userId !== userId) { 53 | return NextResponse.json({ message: "Unauthorized" }, { status: 403 }); 54 | } 55 | 56 | const data = { 57 | ...parsed.data, 58 | userId: userId, 59 | }; 60 | 61 | const updatedProfile = isUpdate 62 | ? await prismadb.profile.update({ where: { id: data.id }, data }) 63 | : await prismadb.profile.create({ data }); 64 | 65 | return NextResponse.json( 66 | { 67 | message: "Successfully handled lead magnet change!", 68 | data: updatedProfile, 69 | }, 70 | { status: isUpdate ? 200 : 201 } 71 | ); 72 | } 73 | 74 | export const POST = (request: Request) => 75 | handleRequest(request, profileCreateRequest); 76 | export const PUT = (request: Request) => 77 | handleRequest(request, profileUpdateRequest, true); 78 | -------------------------------------------------------------------------------- /app/api/webhooks/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { stripe } from "@/utils/stripe"; 3 | import { headers } from "next/headers"; 4 | import { NextResponse } from "next/server"; 5 | import Stripe from "stripe"; 6 | 7 | export async function POST(req: Request) { 8 | const body = await req.text(); 9 | const signature = headers().get("Stripe-Signature") as string; 10 | 11 | let event: Stripe.Event; 12 | 13 | if (!process.env.STRIPE_WEBHOOK_SECRET) 14 | throw new Error("Stripe webhook secret not set"); 15 | 16 | try { 17 | event = stripe.webhooks.constructEvent( 18 | body, 19 | signature, 20 | process.env.STRIPE_WEBHOOK_SECRET 21 | ); 22 | } catch (err) { 23 | console.log(`⚠️ Webhook signature verification failed.`, err); 24 | return NextResponse.json( 25 | { error: "Webhook signature verification failed." }, 26 | { status: 400 } 27 | ); 28 | } 29 | 30 | const session = event.data.object as Stripe.Checkout.Session; 31 | 32 | const subscription = await stripe.subscriptions.retrieve( 33 | session.subscription as string 34 | ); 35 | 36 | if (event.type === "checkout.session.completed") { 37 | if (!session?.metadata?.userId) { 38 | return new NextResponse("User id is required", { status: 400 }); 39 | } 40 | 41 | await prismadb.subscription.create({ 42 | data: { 43 | userId: session?.metadata?.userId, 44 | stripeSubscriptionId: subscription.id, 45 | stripeCustomerId: subscription.customer as string, 46 | stripeCurrentPeriodEnd: new Date( 47 | subscription.current_period_end * 1000 48 | ), 49 | }, 50 | }); 51 | } else if (event.type === "invoice.payment_succeeded") { 52 | const subscriptionFromDB = await prismadb.subscription.findFirst({ 53 | where: { 54 | stripeSubscriptionId: subscription.id, 55 | }, 56 | }); 57 | 58 | if (!subscriptionFromDB) { 59 | return new NextResponse("Subscription not found", { status: 404 }); 60 | } 61 | 62 | await prismadb.subscription.update({ 63 | where: { 64 | stripeSubscriptionId: subscription.id, 65 | }, 66 | data: { 67 | stripeCurrentPeriodEnd: new Date( 68 | subscription.current_period_end * 1000 69 | ), 70 | }, 71 | }); 72 | } 73 | 74 | return new NextResponse(null, { status: 200 }); 75 | } 76 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate")], 76 | } -------------------------------------------------------------------------------- /app/api/account/route.ts: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { currentUser } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | import { generateFromEmail } from "unique-username-generator"; 5 | import { object, string } from "zod"; 6 | 7 | export async function PUT(request: Request) { 8 | const user = await currentUser(); 9 | 10 | if (!user) { 11 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); 12 | } 13 | 14 | const requestBody = await request.json(); 15 | const usernameSchema = object({ 16 | username: string() 17 | .min(3, { message: "A username must be at least 3 characters long." }) 18 | .max(20, { message: "A username must be 20 characters or less." }), 19 | }); 20 | 21 | const parsed = usernameSchema.safeParse(requestBody); 22 | 23 | if (!parsed.success) { 24 | return NextResponse.json({ 25 | message: JSON.parse(parsed.error.message)[0], 26 | success: false, 27 | data: null, 28 | }); 29 | } 30 | 31 | const newUsername = parsed.data.username; 32 | 33 | const existingAccount = await prismadb.account.findFirst({ 34 | where: { 35 | username: newUsername, 36 | }, 37 | }); 38 | 39 | if (existingAccount) { 40 | return NextResponse.json({ 41 | message: "Username already exists", 42 | success: false, 43 | data: null, 44 | }); 45 | } 46 | 47 | const account = await prismadb.account.update({ 48 | where: { 49 | userId: user.id, 50 | }, 51 | data: { 52 | username: newUsername, 53 | }, 54 | }); 55 | 56 | return NextResponse.json({ 57 | message: "Username updated", 58 | success: true, 59 | data: account, 60 | }); 61 | } 62 | 63 | export async function GET() { 64 | const user = await currentUser(); 65 | 66 | if (!user) { 67 | return NextResponse.json({ message: "Unauthenticated" }, { status: 401 }); 68 | } 69 | 70 | let account = await prismadb.account.findFirst({ 71 | where: { 72 | userId: user.id, 73 | }, 74 | }); 75 | 76 | if (!account) { 77 | const baseEmail = user.emailAddresses[0].emailAddress; 78 | account = await prismadb.account.create({ 79 | data: { 80 | userId: user.id, 81 | email: baseEmail, 82 | username: generateFromEmail(baseEmail, 3), 83 | }, 84 | }); 85 | } 86 | 87 | return NextResponse.json( 88 | { 89 | message: "Account found", 90 | success: true, 91 | data: account, 92 | }, 93 | { status: 200 } 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnets/components/LeadMagnetTable.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Table, 4 | TableBody, 5 | TableCell, 6 | TableHead, 7 | TableHeader, 8 | TableRow, 9 | } from "@/components/ui/table"; 10 | import { Lead, LeadMagnet } from "@prisma/client"; 11 | import Link from "next/link"; 12 | import React from "react"; 13 | 14 | interface LeadMagnetTableProps { 15 | leadMagnets: LeadMagnet[]; 16 | leads: Lead[]; 17 | } 18 | 19 | function LeadMagnetTable({ leadMagnets, leads }: LeadMagnetTableProps) { 20 | const getLeadsForLeadMagnet = (leadMagnetId: string): number => { 21 | const leadsForLeadMagnet = leads.filter( 22 | (lead) => lead.leadMagnetId === leadMagnetId 23 | ); 24 | 25 | return leadsForLeadMagnet.length; 26 | }; 27 | 28 | const getConversionRate = ( 29 | leadMagnetId: string, 30 | pageViews: number 31 | ): number => { 32 | if (pageViews === 0) return 0; 33 | 34 | const conversionRate = Math.round( 35 | (getLeadsForLeadMagnet(leadMagnetId) / pageViews) * 100 36 | ); 37 | 38 | return conversionRate; 39 | }; 40 | 41 | return ( 42 | 43 | 44 | 45 | Name 46 | Page Visits 47 | Leads 48 | Conversion Rate 49 | 50 | 51 | 52 | {leadMagnets.map((leadMagnet) => ( 53 | 54 | 55 | 59 | {leadMagnet.name} 60 | 61 | 62 | {leadMagnet.pageViews} 63 | {getLeadsForLeadMagnet(leadMagnet.id)} 64 | 65 | {getConversionRate(leadMagnet.id, leadMagnet.pageViews)} % 66 | 67 | 68 | 69 | 72 | 73 | 74 | 75 | ))} 76 | 77 |
78 | ); 79 | } 80 | 81 | export default LeadMagnetTable; 82 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnet-editor/[[...leadMagnetId]]/components/LeadMagnetEditorSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LeadMagnetSections } from "./LeadMagnetEditor"; 3 | import { IconType } from "react-icons"; 4 | import { BsBook, BsPerson } from "react-icons/bs"; 5 | import { FiSettings } from "react-icons/fi"; 6 | import { 7 | TbLayoutSidebarLeftCollapse, 8 | TbLayoutSidebarRightCollapse, 9 | TbMail, 10 | TbPrompt, 11 | } from "react-icons/tb"; 12 | 13 | interface LeadMagnetEditorSidebarProps { 14 | isSidebarCollapsed: boolean; 15 | setSelectedEditor: React.Dispatch>; 16 | setIsSidebarCollapsed: React.Dispatch>; 17 | } 18 | 19 | const EDITOR_OPTIONS: { 20 | label: string; 21 | icon: IconType; 22 | value: LeadMagnetSections; 23 | }[] = [ 24 | { label: "Content Editor", icon: BsBook, value: "content" }, 25 | { label: "Prompt Editor", icon: TbPrompt, value: "prompt" }, 26 | { label: "Email Capture", icon: TbMail, value: "email" }, 27 | { label: "Profile Editor", icon: BsPerson, value: "profile" }, 28 | { label: "Settings", icon: FiSettings, value: "settings" }, 29 | ]; 30 | 31 | function LeadMagnetEditorSidebar({ 32 | isSidebarCollapsed, 33 | setIsSidebarCollapsed, 34 | setSelectedEditor, 35 | }: LeadMagnetEditorSidebarProps) { 36 | return ( 37 |
41 |
46 | {EDITOR_OPTIONS.map((option) => ( 47 | 59 | ))} 60 | 70 |
71 |
72 | ); 73 | } 74 | 75 | export default LeadMagnetEditorSidebar; 76 | -------------------------------------------------------------------------------- /app/(dashboard)/lead-magnet-editor/[[...leadMagnetId]]/components/LeadMagnetPromptEditor.tsx: -------------------------------------------------------------------------------- 1 | import LeadMagnetAIChatContainer from "@/components/LeadMagnetAIChatContainer"; 2 | import { useLeadMagnetEditorContext } from "@/context/LeadMagnetEditorContex"; 3 | import React from "react"; 4 | 5 | function LeadMagnetPromptEditor() { 6 | const { edittedLeadMagnet, setEdittedLeadMagnet } = 7 | useLeadMagnetEditorContext(); 8 | 9 | return ( 10 |
11 |
12 |

13 | AI Prompt Editor 14 |

15 |
16 | 19 |