Skip to content

Commit 25b3a4b

Browse files
committed
database and upload feat
1 parent 6c0c691 commit 25b3a4b

23 files changed

+2054
-14
lines changed

actions/upload-actions.ts

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"use server";
2+
import getDbConnection from "@/lib/db";
3+
import { revalidatePath } from "next/cache";
4+
import { redirect } from "next/navigation";
5+
import OpenAI from "openai";
6+
7+
const openai = new OpenAI({
8+
apiKey: process.env.OPENAI_API_KEY,
9+
});
10+
11+
export async function transcribeUploadedFile(
12+
resp: {
13+
serverData: { userId: string; file: any };
14+
}[]
15+
) {
16+
if (!resp) {
17+
return {
18+
success: false,
19+
message: "File upload failed",
20+
data: null,
21+
};
22+
}
23+
24+
const {
25+
serverData: {
26+
userId,
27+
file: { url: fileUrl, name: fileName },
28+
},
29+
} = resp[0];
30+
31+
if (!fileUrl || !fileName) {
32+
return {
33+
success: false,
34+
message: "File upload failed",
35+
data: null,
36+
};
37+
}
38+
39+
const response = await fetch(fileUrl);
40+
41+
try {
42+
const transcriptions = await openai.audio.transcriptions.create({
43+
model: "whisper-1",
44+
file: response,
45+
});
46+
47+
console.log({ transcriptions });
48+
return {
49+
success: true,
50+
message: "File uploaded successfully!",
51+
data: { transcriptions, userId },
52+
};
53+
} catch (error) {
54+
console.error("Error processing file", error);
55+
56+
if (error instanceof OpenAI.APIError && error.status === 413) {
57+
return {
58+
success: false,
59+
message: "File size exceeds the max limit of 20MB",
60+
data: null,
61+
};
62+
}
63+
64+
return {
65+
success: false,
66+
message: error instanceof Error ? error.message : "Error processing file",
67+
data: null,
68+
};
69+
}
70+
}
71+
72+
async function saveBlogPost(userId: string, title: string, content: string) {
73+
try {
74+
const sql = await getDbConnection();
75+
const [insertedPost] = await sql`
76+
INSERT INTO posts (user_id, title, content)
77+
VALUES (${userId}, ${title}, ${content})
78+
RETURNING id
79+
`;
80+
return insertedPost.id;
81+
} catch (error) {
82+
console.error("Error saving blog post", error);
83+
throw error;
84+
}
85+
}
86+
87+
async function getUserBlogPosts(userId: string) {
88+
try {
89+
const sql = await getDbConnection();
90+
const posts = await sql`
91+
SELECT content FROM posts
92+
WHERE user_id = ${userId}
93+
ORDER BY created_at DESC
94+
LIMIT 3
95+
`;
96+
return posts.map((post) => post.content).join("\n\n");
97+
} catch (error) {
98+
console.error("Error getting user blog posts", error);
99+
throw error;
100+
}
101+
}
102+
103+
async function generateBlogPost({
104+
transcriptions,
105+
userPosts,
106+
}: {
107+
transcriptions: string;
108+
userPosts: string;
109+
}) {
110+
const completion = await openai.chat.completions.create({
111+
messages: [
112+
{
113+
role: "system",
114+
content:
115+
"You are a skilled content writer that converts audio transcriptions into well-structured, engaging blog posts in Markdown format. Create a comprehensive blog post with a catchy title, introduction, main body with multiple sections, and a conclusion. Analyze the user's writing style from their previous posts and emulate their tone and style in the new post. Keep the tone casual and professional.",
116+
},
117+
{
118+
role: "user",
119+
content: `Here are some of my previous blog posts for reference:
120+
121+
${userPosts}
122+
123+
Please convert the following transcription into a well-structured blog post using Markdown formatting. Follow this structure:
124+
125+
1. Start with a SEO friendly catchy title on the first line.
126+
2. Add two newlines after the title.
127+
3. Write an engaging introduction paragraph.
128+
4. Create multiple sections for the main content, using appropriate headings (##, ###).
129+
5. Include relevant subheadings within sections if needed.
130+
6. Use bullet points or numbered lists where appropriate.
131+
7. Add a conclusion paragraph at the end.
132+
8. Ensure the content is informative, well-organized, and easy to read.
133+
9. Emulate my writing style, tone, and any recurring patterns you notice from my previous posts.
134+
135+
Here's the transcription to convert: ${transcriptions}`,
136+
},
137+
],
138+
model: "gpt-4o-mini",
139+
temperature: 0.7,
140+
max_tokens: 1000,
141+
});
142+
143+
return completion.choices[0].message.content;
144+
}
145+
export async function generateBlogPostAction({
146+
transcriptions,
147+
userId,
148+
}: {
149+
transcriptions: { text: string };
150+
userId: string;
151+
}) {
152+
const userPosts = await getUserBlogPosts(userId);
153+
154+
let postId = null;
155+
156+
if (transcriptions) {
157+
const blogPost = await generateBlogPost({
158+
transcriptions: transcriptions.text,
159+
userPosts,
160+
});
161+
162+
if (!blogPost) {
163+
return {
164+
success: false,
165+
message: "Blog post generation failed, please try again...",
166+
};
167+
}
168+
169+
const [title, ...contentParts] = blogPost?.split("\n\n") || [];
170+
171+
//database connection
172+
173+
if (blogPost) {
174+
postId = await saveBlogPost(userId, title, blogPost);
175+
}
176+
}
177+
178+
//navigate
179+
revalidatePath(`/posts/${postId}`);
180+
redirect(`/posts/${postId}`);
181+
}

app/(logged-in)/dashboard/page.tsx

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import BgGradient from "@/components/common/bg-gradient";
2+
import { Badge } from "@/components/ui/badge";
3+
import UpgradeYourPlan from "@/components/upload/upgrade-your-plan";
4+
// import UploadForm from "@/components/upload/upload-form";
5+
import getDbConnection from "@/lib/db";
6+
import {
7+
doesUserExist,
8+
getPlanType,
9+
hasCancelledSubscription,
10+
updateUser,
11+
} from "@/lib/user-helpers";
12+
import { currentUser } from "@clerk/nextjs/server";
13+
import { redirect } from "next/navigation";
14+
15+
export default async function Dashboard() {
16+
const clerkUser = await currentUser();
17+
18+
if (!clerkUser) {
19+
return redirect("/sign-in");
20+
}
21+
22+
const email = clerkUser?.emailAddresses?.[0].emailAddress ?? "";
23+
24+
const sql = await getDbConnection();
25+
26+
//updatethe user id
27+
let userId = null;
28+
let priceId = null;
29+
30+
const hasUserCancelled = await hasCancelledSubscription(sql, email);
31+
const user = await doesUserExist(sql, email);
32+
33+
if (user) {
34+
//update the user_id in users table
35+
userId = clerkUser?.id;
36+
if (userId) {
37+
await updateUser(sql, userId, email);
38+
}
39+
40+
priceId = user[0].price_id;
41+
}
42+
43+
const { id: planTypeId = "starter", name: planTypeName } =
44+
getPlanType(priceId);
45+
46+
const isBasicPlan = planTypeId === "basic";
47+
const isProPlan = planTypeId === "pro";
48+
49+
// check number of posts per plan
50+
const posts = await sql`SELECT * FROM posts WHERE user_id = ${userId}`;
51+
52+
const isValidBasicPlan = isBasicPlan && posts.length < 3;
53+
54+
return (
55+
<BgGradient>
56+
<div className="mx-auto max-w-7xl px-6 py-24 sm:py-32 lg:px-8">
57+
<div className="flex flex-col items-center justify-center gap-6 text-center">
58+
<Badge className="bg-gradient-to-r from-purple-700 to-pink-800 text-white px-4 py-1 text-lg font-semibold capitalize">
59+
{planTypeName} Plan
60+
</Badge>
61+
62+
<h2 className="capitalize text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
63+
Start creating amazing content
64+
</h2>
65+
66+
<p className="mt-2 text-lg leading-8 text-gray-600 max-w-2xl text-center">
67+
Upload your audio or video file and let our AI do the magic!
68+
</p>
69+
70+
{(isBasicPlan || isProPlan) && (
71+
<p className="mt-2 text-lg leading-8 text-gray-600 max-w-2xl text-center">
72+
You get{" "}
73+
<span className="font-bold text-amber-600 bg-amber-100 px-2 py-1 rounded-md">
74+
{isBasicPlan ? "3" : "Unlimited"} blog posts
75+
</span>{" "}
76+
as part of the{" "}
77+
<span className="font-bold capitalize">{planTypeName}</span> Plan.
78+
</p>
79+
)}
80+
81+
{isValidBasicPlan || isProPlan ? (
82+
<BgGradient>
83+
{/* <UploadForm /> */}
84+
</BgGradient>
85+
) : (
86+
<UpgradeYourPlan />
87+
)}
88+
</div>
89+
</div>
90+
</BgGradient>
91+
);
92+
}

app/api/payments/routes.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
handleCheckoutSessionCompleted,
3+
handleSubscriptionDeleted,
4+
} from "@/lib/payment-helpers";
5+
import { NextRequest, NextResponse } from "next/server";
6+
import Stripe from "stripe";
7+
8+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
9+
10+
export async function POST(req: NextRequest) {
11+
//webhook functionality
12+
const payload = await req.text();
13+
14+
const sig = req.headers.get("stripe-signature");
15+
16+
let event;
17+
18+
try {
19+
event = stripe.webhooks.constructEvent(
20+
payload,
21+
sig!,
22+
process.env.STRIPE_WEBHOOK_SECRET!
23+
);
24+
25+
// Handle the event
26+
switch (event.type) {
27+
// case "payment_intent.succeeded"
28+
29+
case "checkout.session.completed": {
30+
const session = await stripe.checkout.sessions.retrieve(
31+
event.data.object.id,
32+
{
33+
expand: ["line_items"],
34+
}
35+
);
36+
console.log({ session });
37+
38+
//connect to the db create or update user
39+
await handleCheckoutSessionCompleted({ session, stripe });
40+
break;
41+
}
42+
case "customer.subscription.deleted": {
43+
// connect to db
44+
const subscriptionId = event.data.object.id;
45+
46+
await handleSubscriptionDeleted({ subscriptionId, stripe });
47+
break;
48+
}
49+
default:
50+
console.log(`Unhandled event type ${event.type}`);
51+
}
52+
return NextResponse.json({
53+
status: "success",
54+
});
55+
} catch (err) {
56+
return NextResponse.json({ status: "Failed", err });
57+
}
58+
}

app/api/uploadthing/core.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { currentUser } from "@clerk/nextjs/server";
2+
import { createUploadthing, type FileRouter } from "uploadthing/next";
3+
import { UploadThingError } from "uploadthing/server";
4+
5+
const f = createUploadthing();
6+
7+
export const ourFileRouter = {
8+
videoOrAudioUploader: f({ video: { maxFileSize: "32MB" } })
9+
.middleware(async ({ req }) => {
10+
const user = await currentUser();
11+
12+
console.log({ user });
13+
14+
if (!user) throw new UploadThingError("Unauthorized");
15+
16+
return { userId: user.id };
17+
})
18+
.onUploadComplete(async ({ metadata, file }) => {
19+
// This code RUNS ON YOUR SERVER after upload
20+
console.log("Upload complete for userId:", metadata.userId);
21+
22+
console.log("file url", file.url);
23+
24+
// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
25+
return { userId: metadata.userId, file };
26+
}),
27+
} satisfies FileRouter;
28+
29+
export type OurFileRouter = typeof ourFileRouter;

app/api/uploadthing/route.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createRouteHandler } from "uploadthing/next";
2+
3+
import { ourFileRouter } from "./core";
4+
5+
// Export routes for Next App Router
6+
export const { GET, POST } = createRouteHandler({
7+
router: ourFileRouter,
8+
9+
// Apply an (optional) custom config:
10+
// config: { ... },
11+
});

0 commit comments

Comments
 (0)