44 | Documentation 45 |
46 |53 | {section.title} 54 |
55 |56 | {section.description} 57 |
58 | 64 |├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── api │ ├── create-checkout-session │ │ └── route.ts │ ├── send-welcome-email │ │ └── route.ts │ └── webhooks │ │ ├── clerk │ │ └── route.ts │ │ └── stripe │ │ └── route.ts ├── docs │ └── page.tsx ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── generate │ └── page.tsx ├── globals.css ├── layout.tsx ├── page.tsx ├── pricing │ └── page.tsx ├── sign-in │ └── [[...sign-in]] │ │ └── page.tsx └── sign-up │ └── [[...sign-up]] │ └── page.tsx ├── components.json ├── components ├── Navbar.tsx ├── social-mocks │ ├── InstagramMock.tsx │ ├── LinkedInMock.tsx │ └── TwitterMock.tsx ├── theme-provider.tsx └── ui │ ├── avatar.tsx │ ├── button.tsx │ ├── card.tsx │ ├── input.tsx │ ├── select.tsx │ └── textarea.tsx ├── drizzle.config.js ├── index.mjs ├── lib └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public └── thumbnail.jpg ├── tailwind.config.ts ├── threadcraft.drawio ├── tsconfig.json ├── utils ├── db │ ├── actions.ts │ ├── dbConfig.jsx │ └── schema.ts └── mailtrap.ts └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | .script.md 9 | script.md 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | script.md 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
We're excited to have you on board. Get started by...
29 | `, 30 | category: "Welcome Email", 31 | }); 32 | 33 | return NextResponse.json({ message: "Welcome email sent successfully" }); 34 | } catch (error) { 35 | console.error("Error sending welcome email:", error); 36 | return NextResponse.json( 37 | { error: "Failed to send welcome email" }, 38 | { status: 500 } 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/api/webhooks/clerk/route.ts: -------------------------------------------------------------------------------- 1 | import { Webhook } from "svix"; 2 | import { headers } from "next/headers"; 3 | import { WebhookEvent } from "@clerk/nextjs/server"; 4 | import { createOrUpdateUser } from "@/utils/db/actions"; 5 | import { NextResponse } from "next/server"; 6 | 7 | export async function POST(req: Request) { 8 | const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET; 9 | 10 | if (!WEBHOOK_SECRET) { 11 | throw new Error( 12 | "Please add CLERK_WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local" 13 | ); 14 | } 15 | 16 | const headerPayload = headers(); 17 | const svix_id = headerPayload.get("svix-id"); 18 | const svix_timestamp = headerPayload.get("svix-timestamp"); 19 | const svix_signature = headerPayload.get("svix-signature"); 20 | 21 | if (!svix_id || !svix_timestamp || !svix_signature) { 22 | return new Response("Error occurred -- no svix headers", { 23 | status: 400, 24 | }); 25 | } 26 | 27 | const payload = await req.json(); 28 | const body = JSON.stringify(payload); 29 | 30 | const wh = new Webhook(WEBHOOK_SECRET); 31 | 32 | let evt: WebhookEvent; 33 | 34 | try { 35 | evt = wh.verify(body, { 36 | "svix-id": svix_id, 37 | "svix-timestamp": svix_timestamp, 38 | "svix-signature": svix_signature, 39 | }) as WebhookEvent; 40 | } catch (err) { 41 | console.error("Error verifying webhook:", err); 42 | return new Response("Error occurred", { 43 | status: 400, 44 | }); 45 | } 46 | 47 | const eventType = evt.type; 48 | if (eventType === "user.created" || eventType === "user.updated") { 49 | const { id, email_addresses, first_name, last_name } = evt.data; 50 | const email = email_addresses[0]?.email_address; 51 | const name = `${first_name} ${last_name}`; 52 | 53 | if (email) { 54 | try { 55 | await createOrUpdateUser(id, email, name); 56 | 57 | console.log(`User ${id} created/updated successfully`); 58 | } catch (error) { 59 | console.error("Error creating/updating user:", error); 60 | return new Response("Error processing user data", { status: 500 }); 61 | } 62 | } 63 | } 64 | 65 | // console.log(`Webhook with an ID of ${evt.data.id} and type of ${eventType}`); 66 | // console.log("Webhook body:", body); 67 | 68 | return NextResponse.json( 69 | { message: "Webhook processed successfully" }, 70 | { status: 200 } 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/api/webhooks/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import Stripe from "stripe"; 3 | import { headers } from "next/headers"; 4 | import { 5 | createOrUpdateSubscription, 6 | updateUserPoints, 7 | } from "@/utils/db/actions"; 8 | 9 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 10 | apiVersion: "2024-06-20", 11 | }); 12 | 13 | export async function POST(req: Request) { 14 | const body = await req.text(); 15 | const signature = headers().get("Stripe-Signature") as string; 16 | 17 | if (!signature) { 18 | console.error("No Stripe signature found"); 19 | return NextResponse.json({ error: "No Stripe signature" }, { status: 400 }); 20 | } 21 | 22 | let event: Stripe.Event; 23 | 24 | try { 25 | event = stripe.webhooks.constructEvent( 26 | body, 27 | signature, 28 | process.env.STRIPE_WEBHOOK_SECRET! 29 | ); 30 | } catch (err: any) { 31 | console.error(`Webhook signature verification failed: ${err.message}`); 32 | return NextResponse.json( 33 | { error: `Webhook Error: ${err.message}` }, 34 | { status: 400 } 35 | ); 36 | } 37 | 38 | console.log(`Received event type: ${event.type}`); 39 | 40 | if (event.type === "checkout.session.completed") { 41 | const session = event.data.object as Stripe.Checkout.Session; 42 | const userId = session.client_reference_id; 43 | const subscriptionId = session.subscription as string; 44 | 45 | if (!userId || !subscriptionId) { 46 | console.error("Missing userId or subscriptionId in session", { session }); 47 | return NextResponse.json( 48 | { error: "Invalid session data" }, 49 | { status: 400 } 50 | ); 51 | } 52 | 53 | try { 54 | console.log(`Retrieving subscription: ${subscriptionId}`); 55 | const subscription = await stripe.subscriptions.retrieve(subscriptionId); 56 | console.log("Retrieved subscription:", subscription); 57 | 58 | if (!subscription.items.data.length) { 59 | console.error("No items found in subscription", { subscription }); 60 | return NextResponse.json( 61 | { error: "Invalid subscription data" }, 62 | { status: 400 } 63 | ); 64 | } 65 | 66 | const priceId = subscription.items.data[0].price.id; 67 | console.log(`Price ID: ${priceId}`); 68 | 69 | let plan: string; 70 | let pointsToAdd: number; 71 | 72 | // Map price IDs to plan names and points 73 | switch (priceId) { 74 | case "price_1PyFKGBibz3ZDixDAaJ3HO74": 75 | plan = "Basic"; 76 | pointsToAdd = 100; 77 | break; 78 | case "price_1PyFN0Bibz3ZDixDqm9eYL8W": 79 | plan = "Pro"; 80 | pointsToAdd = 500; 81 | break; 82 | default: 83 | console.error("Unknown price ID", { priceId }); 84 | return NextResponse.json( 85 | { error: "Unknown price ID" }, 86 | { status: 400 } 87 | ); 88 | } 89 | 90 | console.log(`Creating/updating subscription for user ${userId}`); 91 | const updatedSubscription = await createOrUpdateSubscription( 92 | userId, 93 | subscriptionId, 94 | plan, 95 | "active", 96 | new Date(subscription.current_period_start * 1000), 97 | new Date(subscription.current_period_end * 1000) 98 | ); 99 | 100 | if (!updatedSubscription) { 101 | console.error("Failed to create or update subscription"); 102 | return NextResponse.json( 103 | { error: "Failed to create or update subscription" }, 104 | { status: 500 } 105 | ); 106 | } 107 | 108 | console.log(`Updating points for user ${userId}: +${pointsToAdd}`); 109 | await updateUserPoints(userId, pointsToAdd); 110 | 111 | console.log(`Successfully processed subscription for user ${userId}`); 112 | } catch (error: any) { 113 | console.error("Error processing subscription:", error); 114 | return NextResponse.json( 115 | { error: "Error processing subscription", details: error.message }, 116 | { status: 500 } 117 | ); 118 | } 119 | } 120 | 121 | return NextResponse.json({ received: true }); 122 | } 123 | -------------------------------------------------------------------------------- /app/docs/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Navbar } from "@/components/Navbar"; 4 | 5 | const docsSections = [ 6 | { 7 | title: "Getting Started", 8 | description: 9 | "Learn how to set up your account and create your first AI-generated content.", 10 | link: "/docs/getting-started", 11 | }, 12 | { 13 | title: "Twitter Threads", 14 | description: 15 | "Discover how to create engaging Twitter threads using our AI technology.", 16 | link: "/docs/twitter-threads", 17 | }, 18 | { 19 | title: "Instagram Captions", 20 | description: 21 | "Learn the best practices for generating Instagram captions that boost engagement.", 22 | link: "/docs/instagram-captions", 23 | }, 24 | { 25 | title: "LinkedIn Posts", 26 | description: 27 | "Explore techniques for crafting professional LinkedIn content with AI assistance.", 28 | link: "/docs/linkedin-posts", 29 | }, 30 | { 31 | title: "API Reference", 32 | description: 33 | "Detailed documentation for integrating our AI content generation into your applications.", 34 | link: "/docs/api-reference", 35 | }, 36 | ]; 37 | 38 | export default function DocsPage() { 39 | return ( 40 |56 | {section.description} 57 |
58 | 64 |253 | To start generating amazing content, please sign in or create an 254 | account. 255 |
256 |262 | By signing in, you agree to our Terms of Service and Privacy Policy. 263 |
264 |308 | {item.prompt} 309 |
310 |Available Points
327 |328 | {userPoints !== null ? userPoints : "Loading..."} 329 |
330 |44 | Create engaging content for Twitter, Instagram, and LinkedIn with 45 | cutting-edge AI technology. 46 |
47 |{feature.description}
101 |No credit card required
203 |102 | ${plan.price} 103 | 104 | /month 105 | 106 |
107 |Your Name
14 |{content}
27 |Your Name
15 |Your Title • 1st
16 |{content}
19 |Your Name
15 |@yourhandle
16 |{tweet}
21 |We're excited to have you on board. Get started by...
31 | `, 32 | }); 33 | }; 34 | --------------------------------------------------------------------------------