├── .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 | ThreadCraft AI Logo 3 |

4 | 5 | # ThreadCraft AI: Social Media Content Generator 6 | 7 | ThreadCraft AI is a powerful Next.js application that leverages AI to generate engaging content for various social media platforms. This project uses cutting-edge technologies to provide users with an intuitive interface for creating Twitter threads, Instagram captions, and LinkedIn posts. 8 | 9 | ## Features 10 | 11 | - AI-powered content generation for Twitter, Instagram, and LinkedIn 12 | - User authentication and account management with Clerk 13 | - Points-based system for content generation 14 | - Content history and regeneration 15 | - Responsive design for desktop and mobile devices 16 | - Preview functionality for generated content 17 | - Integration with Google's Generative AI (Gemini) 18 | 19 | ## Tech Stack 20 | 21 | - [Next.js](https://nextjs.org/) - React framework for building the frontend and API routes 22 | - [TypeScript](https://www.typescriptlang.org/) - Typed superset of JavaScript 23 | - [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework 24 | - [Clerk](https://clerk.com/) - Authentication and user management 25 | - [Google Generative AI](https://ai.google.dev/) - AI model for content generation 26 | - [Drizzle ORM](https://orm.drizzle.team/) - TypeScript ORM for database management 27 | - [Neon Database](https://neon.tech/) - Serverless Postgres database 28 | - [Stripe](https://stripe.com/) - Payment processing for subscriptions 29 | - [Lucide React](https://lucide.dev/) - Icon library 30 | 31 | ## Getting Started 32 | 33 | 1. Clone the repository: 34 | 35 | ```bash 36 | git clone https://github.com/your-username/threadcraft-ai.git 37 | cd threadcraft-ai 38 | ``` 39 | 40 | 2. Install dependencies: 41 | 42 | ```bash 43 | npm install 44 | ``` 45 | 46 | 3. Set up environment variables: 47 | Create a `.env.local` file in the root directory and add the following variables: 48 | 49 | ``` 50 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key 51 | CLERK_SECRET_KEY=your_clerk_secret_key 52 | NEXT_PUBLIC_GEMINI_API_KEY=your_gemini_api_key 53 | DATABASE_URL=your_neon_database_url 54 | STRIPE_SECRET_KEY=your_stripe_secret_key 55 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key 56 | ``` 57 | 58 | 4. Run the development server: 59 | 60 | ```bash 61 | npm run dev 62 | ``` 63 | 64 | 5. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 65 | 66 | ## Deployment 67 | 68 | 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. 69 | 70 | Check out the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 71 | 72 | ## Contributing 73 | 74 | Contributions are welcome! Please feel free to submit a Pull Request. 75 | 76 | ## License 77 | 78 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 79 | -------------------------------------------------------------------------------- /app/api/create-checkout-session/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import Stripe from "stripe"; 3 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 4 | apiVersion: "2024-06-20", 5 | }); 6 | export async function POST(req: Request) { 7 | try { 8 | const { priceId, userId } = await req.json(); 9 | if (!priceId || !userId) { 10 | return NextResponse.json( 11 | { error: "Missing priceId or userId" }, 12 | { status: 400 } 13 | ); 14 | } 15 | const session = await stripe.checkout.sessions.create({ 16 | mode: "subscription", 17 | payment_method_types: ["card"], 18 | line_items: [ 19 | { 20 | price: priceId, 21 | quantity: 1, 22 | }, 23 | ], 24 | success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/generate?session_id={CHECKOUT_SESSION_ID}`, 25 | cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`, 26 | client_reference_id: userId, 27 | }); 28 | return NextResponse.json({ sessionId: session.id }); 29 | } catch (error: any) { 30 | console.error("Error creating checkout session:", error); 31 | return NextResponse.json( 32 | { error: "Error creating checkout session", details: error.message }, 33 | { status: 500 } 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/api/send-welcome-email/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { MailtrapClient } from "mailtrap"; 3 | 4 | const TOKEN = process.env.MAILTRAP_API_TOKEN!; 5 | 6 | const client = new MailtrapClient({ token: TOKEN }); 7 | 8 | export async function POST(req: Request) { 9 | try { 10 | const { email, name } = await req.json(); 11 | 12 | if (!email || !name) { 13 | return NextResponse.json( 14 | { error: "Email and name are required" }, 15 | { status: 400 } 16 | ); 17 | } 18 | 19 | const sender = { name: "ThreadCraft AI", email: "hello@demomailtrap.com" }; 20 | const recipients = [{ email }]; 21 | 22 | await client.send({ 23 | from: sender, 24 | to: recipients, 25 | subject: "Welcome to ThreadCraft AI!", 26 | html: ` 27 |

Welcome to ThreadCraft AI, ${name}!

28 |

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 |
41 | 42 |
43 |

44 | Documentation 45 |

46 |
47 | {docsSections.map((section, index) => ( 48 |
52 |

53 | {section.title} 54 |

55 |

56 | {section.description} 57 |

58 | 64 |
65 | ))} 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendsalbert/threadcraftai/0e14ff674d13330af1af87995190dcb03a6e71df/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendsalbert/threadcraftai/0e14ff674d13330af1af87995190dcb03a6e71df/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendsalbert/threadcraftai/0e14ff674d13330af1af87995190dcb03a6e71df/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/generate/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, useEffect } from "react"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Input } from "@/components/ui/input"; 5 | import { Textarea } from "@/components/ui/textarea"; 6 | import { 7 | Select, 8 | SelectContent, 9 | SelectItem, 10 | SelectTrigger, 11 | SelectValue, 12 | } from "@/components/ui/select"; 13 | import { 14 | Loader2, 15 | Upload, 16 | Copy, 17 | Twitter, 18 | Instagram, 19 | Linkedin, 20 | Clock, 21 | Zap, 22 | } from "lucide-react"; 23 | import { GoogleGenerativeAI, Part } from "@google/generative-ai"; 24 | import ReactMarkdown from "react-markdown"; 25 | import { Navbar } from "@/components/Navbar"; 26 | import { SignInButton, useUser } from "@clerk/nextjs"; 27 | import { useRouter } from "next/navigation"; 28 | import { 29 | getUserPoints, 30 | saveGeneratedContent, 31 | updateUserPoints, 32 | getGeneratedContentHistory, 33 | createOrUpdateUser, 34 | } from "@/utils/db/actions"; 35 | import { TwitterMock } from "@/components/social-mocks/TwitterMock"; 36 | import { InstagramMock } from "@/components/social-mocks/InstagramMock"; 37 | import { LinkedInMock } from "@/components/social-mocks/LinkedInMock"; 38 | import Link from "next/link"; 39 | 40 | const apiKey = process.env.NEXT_PUBLIC_GEMINI_API_KEY; 41 | const genAI = apiKey ? new GoogleGenerativeAI(apiKey) : null; 42 | 43 | const contentTypes = [ 44 | { value: "twitter", label: "Twitter Thread" }, 45 | { value: "instagram", label: "Instagram Caption" }, 46 | { value: "linkedin", label: "LinkedIn Post" }, 47 | ]; 48 | 49 | const MAX_TWEET_LENGTH = 280; 50 | const POINTS_PER_GENERATION = 5; 51 | 52 | interface HistoryItem { 53 | id: number; 54 | contentType: string; 55 | prompt: string; 56 | content: string; 57 | createdAt: Date; 58 | } 59 | 60 | export default function GenerateContent() { 61 | const { isLoaded, isSignedIn, user } = useUser(); 62 | const router = useRouter(); 63 | 64 | const [contentType, setContentType] = useState(contentTypes[0].value); 65 | const [prompt, setPrompt] = useState(""); 66 | const [generatedContent, setGeneratedContent] = useState([]); 67 | const [isLoading, setIsLoading] = useState(false); 68 | const [image, setImage] = useState(null); 69 | const [userPoints, setUserPoints] = useState(null); 70 | const [history, setHistory] = useState([]); 71 | const [selectedHistoryItem, setSelectedHistoryItem] = 72 | useState(null); 73 | 74 | useEffect(() => { 75 | if (!apiKey) { 76 | console.error("Gemini API key is not set"); 77 | } 78 | }, []); 79 | 80 | useEffect(() => { 81 | if (isLoaded && !isSignedIn) { 82 | router.push("/"); 83 | } else if (isSignedIn && user) { 84 | console.log("User loaded:", user); 85 | fetchUserPoints(); 86 | fetchContentHistory(); 87 | } 88 | }, [isLoaded, isSignedIn, user, router]); 89 | 90 | const fetchUserPoints = async () => { 91 | if (user?.id) { 92 | console.log("Fetching points for user:", user.id); 93 | const points = await getUserPoints(user.id); 94 | console.log("Fetched points:", points); 95 | setUserPoints(points); 96 | if (points === 0) { 97 | console.log("User has 0 points. Attempting to create/update user."); 98 | const updatedUser = await createOrUpdateUser( 99 | user.id, 100 | user.emailAddresses[0].emailAddress, 101 | user.fullName || "" 102 | ); 103 | console.log("Updated user:", updatedUser); 104 | if (updatedUser) { 105 | setUserPoints(updatedUser.points); 106 | } 107 | } 108 | } 109 | }; 110 | 111 | const fetchContentHistory = async () => { 112 | if (user?.id) { 113 | const contentHistory = await getGeneratedContentHistory(user.id); 114 | setHistory(contentHistory); 115 | } 116 | }; 117 | 118 | const handleGenerate = async () => { 119 | if ( 120 | !genAI || 121 | !user?.id || 122 | userPoints === null || 123 | userPoints < POINTS_PER_GENERATION 124 | ) { 125 | alert("Not enough points or API key not set."); 126 | return; 127 | } 128 | 129 | setIsLoading(true); 130 | try { 131 | const model = genAI.getGenerativeModel({ model: "gemini-1.5-pro" }); 132 | 133 | let promptText = `Generate ${contentType} content about "${prompt}".`; 134 | if (contentType === "twitter") { 135 | promptText += 136 | " Provide a thread of 5 tweets, each under 280 characters."; 137 | } 138 | 139 | let imagePart: Part | null = null; 140 | if (contentType === "instagram" && image) { 141 | const reader = new FileReader(); 142 | const imageData = await new Promise((resolve) => { 143 | reader.onload = (e) => { 144 | if (e.target && typeof e.target.result === "string") { 145 | resolve(e.target.result); 146 | } else { 147 | resolve(""); 148 | } 149 | }; 150 | reader.readAsDataURL(image); 151 | }); 152 | 153 | const base64Data = imageData.split(",")[1]; 154 | if (base64Data) { 155 | imagePart = { 156 | inlineData: { 157 | data: base64Data, 158 | mimeType: image.type, 159 | }, 160 | }; 161 | } 162 | promptText += 163 | " Describe the image and incorporate it into the caption."; 164 | } 165 | 166 | const parts: (string | Part)[] = [promptText]; 167 | if (imagePart) parts.push(imagePart); 168 | 169 | const result = await model.generateContent(parts); 170 | const generatedText = result.response.text(); 171 | 172 | let content: string[]; 173 | if (contentType === "twitter") { 174 | content = generatedText 175 | .split("\n\n") 176 | .filter((tweet) => tweet.trim() !== ""); 177 | } else { 178 | content = [generatedText]; 179 | } 180 | 181 | setGeneratedContent(content); 182 | 183 | // Update points 184 | const updatedUser = await updateUserPoints( 185 | user.id, 186 | -POINTS_PER_GENERATION 187 | ); 188 | if (updatedUser) { 189 | setUserPoints(updatedUser.points); 190 | } 191 | 192 | // Save generated content 193 | const savedContent = await saveGeneratedContent( 194 | user.id, 195 | content.join("\n\n"), 196 | prompt, 197 | contentType 198 | ); 199 | 200 | if (savedContent) { 201 | setHistory((prevHistory) => [savedContent, ...prevHistory]); 202 | } 203 | } catch (error) { 204 | console.error("Error generating content:", error); 205 | setGeneratedContent(["An error occurred while generating content."]); 206 | } finally { 207 | setIsLoading(false); 208 | } 209 | }; 210 | 211 | const handleHistoryItemClick = (item: HistoryItem) => { 212 | setSelectedHistoryItem(item); 213 | setContentType(item.contentType); 214 | setPrompt(item.prompt); 215 | setGeneratedContent( 216 | item.contentType === "twitter" 217 | ? item.content.split("\n\n") 218 | : [item.content] 219 | ); 220 | }; 221 | 222 | const copyToClipboard = (text: string) => { 223 | navigator.clipboard.writeText(text); 224 | }; 225 | 226 | const renderContentMock = () => { 227 | if (generatedContent.length === 0) return null; 228 | 229 | switch (contentType) { 230 | case "twitter": 231 | return ; 232 | case "instagram": 233 | return ; 234 | case "linkedin": 235 | return ; 236 | default: 237 | return null; 238 | } 239 | }; 240 | 241 | if (!isLoaded) { 242 | return
Loading...
; 243 | } 244 | 245 | if (!isSignedIn) { 246 | return ( 247 |
248 |
249 |

250 | Welcome to ThreadCraft AI 251 |

252 |

253 | To start generating amazing content, please sign in or create an 254 | account. 255 |

256 | 257 | 260 | 261 |

262 | By signing in, you agree to our Terms of Service and Privacy Policy. 263 |

264 |
265 |
266 | ); 267 | } 268 | 269 | const handleImageUpload = (event: React.ChangeEvent) => { 270 | if (event.target.files && event.target.files[0]) { 271 | setImage(event.target.files[0]); 272 | } 273 | }; 274 | 275 | return ( 276 |
277 | 278 |
279 |
280 | {/* Left sidebar - History */} 281 |
282 |
283 |

History

284 | 285 |
286 |
287 | {history.map((item) => ( 288 |
handleHistoryItemClick(item)} 292 | > 293 |
294 | {item.contentType === "twitter" && ( 295 | 296 | )} 297 | {item.contentType === "instagram" && ( 298 | 299 | )} 300 | {item.contentType === "linkedin" && ( 301 | 302 | )} 303 | 304 | {item.contentType} 305 | 306 |
307 |

308 | {item.prompt} 309 |

310 |
311 | 312 | {new Date(item.createdAt).toLocaleString()} 313 |
314 |
315 | ))} 316 |
317 |
318 | 319 | {/* Main content area */} 320 |
321 | {/* Points display */} 322 |
323 |
324 | 325 |
326 |

Available Points

327 |

328 | {userPoints !== null ? userPoints : "Loading..."} 329 |

330 |
331 |
332 | 335 |
336 | 337 | {/* Content generation form */} 338 |
339 |
340 | 343 | 369 |
370 | 371 |
372 | 378 |