Skip to content

Commit 689012e

Browse files
committed
Initial commit
0 parents  commit 689012e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+12095
-0
lines changed

.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["next/core-web-vitals", "next/typescript"]
3+
}

.gitignore

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# local env files
29+
.env*.local
30+
.env.local
31+
32+
# vercel
33+
.vercel
34+
35+
# typescript
36+
*.tsbuildinfo
37+
next-env.d.ts

README.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/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/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
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/app/building-your-application/deploying) for more details.

actions/edit-actions.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use server";
2+
3+
import getDbConnection from "@/lib/db";
4+
import { currentUser } from "@clerk/nextjs/server";
5+
import { revalidatePath } from "next/cache";
6+
import { redirect } from "next/navigation";
7+
8+
export async function updatePostAction(data: {
9+
postId: string;
10+
content: string;
11+
}) {
12+
const { postId, content } = data;
13+
14+
const user = await currentUser();
15+
if (!user) {
16+
redirect("/sign-in");
17+
}
18+
19+
try {
20+
const sql = await getDbConnection();
21+
22+
const [title, ...contentParts] = content?.split("\n\n") || [];
23+
const updatedTitle = title.split("#")[1].trim();
24+
25+
await sql`UPDATE posts SET content = ${content}, title = ${updatedTitle} where id = ${postId}`;
26+
} catch (error) {
27+
console.error("Error occurred in updating the post", postId);
28+
return {
29+
success: false,
30+
};
31+
}
32+
33+
revalidatePath(`/posts/${postId}`);
34+
return {
35+
success: true,
36+
};
37+
}

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+
}

0 commit comments

Comments
 (0)