Skip to content

Commit 1b0f3a7

Browse files
committed
add user actions, and implement industry insights generation
1 parent b3ddede commit 1b0f3a7

File tree

6 files changed

+243
-58
lines changed

6 files changed

+243
-58
lines changed

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "sensei",
2+
"name": "sensai",
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"@clerk/nextjs": "^6.12.0",
1313
"@clerk/themes": "^2.2.19",
14+
"@google/generative-ai": "^0.22.0",
1415
"@hookform/resolvers": "^4.1.2",
1516
"@prisma/client": "^6.4.1",
1617
"@radix-ui/react-accordion": "^1.2.3",
@@ -24,15 +25,20 @@
2425
"@radix-ui/react-slot": "^1.1.2",
2526
"@radix-ui/react-tabs": "^1.1.3",
2627
"@radix-ui/react-tooltip": "^1.1.8",
28+
"@tabler/icons-react": "^3.30.0",
2729
"class-variance-authority": "^0.7.1",
2830
"clsx": "^2.1.1",
31+
"date-fns": "^4.1.0",
32+
"framer-motion": "^12.4.7",
2933
"inngest": "^3.31.11",
3034
"lucide-react": "^0.476.0",
3135
"next": "15.1.7",
3236
"next-themes": "^0.4.4",
3337
"react": "^19.0.0",
3438
"react-dom": "^19.0.0",
3539
"react-hook-form": "^7.54.2",
40+
"react-spinners": "^0.15.0",
41+
"recharts": "^2.15.1",
3642
"sonner": "^2.0.1",
3743
"tailwind-merge": "^3.0.2",
3844
"tailwindcss-animate": "^1.0.7",

src/actions/user.action.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"use server";
2+
3+
import { db } from "@/lib/db";
4+
import { auth } from "@clerk/nextjs/server";
5+
// import { revalidatePath } from "next/cache";
6+
// import { generateAIInsights } from "./dashboard";
7+
8+
export async function updateUser(data: any) {
9+
const { userId } = await auth();
10+
if (!userId) throw new Error("Unauthorized");
11+
12+
const user = await db.user.findUnique({
13+
where: { clerkUserId: userId },
14+
});
15+
16+
if (!user) throw new Error("User not found");
17+
18+
try {
19+
// Start a transaction to handle both operations
20+
const result = await db.$transaction(
21+
async (tx) => {
22+
// First check if industry exists
23+
let industryInsight = await tx.industryInsight.findUnique({
24+
where: {
25+
industry: data.industry,
26+
},
27+
});
28+
29+
if(!industryInsight) {
30+
industryInsight = await tx.industryInsight.create({
31+
data: {
32+
industry: data.industry,
33+
salaryRanges: [],
34+
growthRate: 0,
35+
demandLevel: 'Medium',
36+
topSkills: [],
37+
marketOutlook: 'Neutral',
38+
keyTrends: [],
39+
recommendedSkills: [],
40+
nextUpdate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
41+
},
42+
});
43+
}
44+
45+
// If industry doesn't exist, create it with default values
46+
// if (!industryInsight) {
47+
// const insights = await generateAIInsights(data.industry);
48+
49+
// industryInsight = await db.industryInsight.create({
50+
// data: {
51+
// industry: data.industry,
52+
// ...insights,
53+
// nextUpdate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
54+
// },
55+
// });
56+
// }
57+
58+
// Now update the user
59+
const updatedUser = await tx.user.update({
60+
where: {
61+
id: user.id,
62+
},
63+
data: {
64+
industry: data.industry,
65+
experience: data.experience,
66+
bio: data.bio,
67+
skills: data.skills,
68+
},
69+
});
70+
71+
return { updatedUser, industryInsight };
72+
},
73+
{
74+
timeout: 10000, // default: 5000
75+
}
76+
);
77+
78+
// revalidatePath("/");
79+
return result.updatedUser;
80+
} catch (error: any) {
81+
console.error("Error updating user and industry:", error.message);
82+
throw new Error("Failed to update profile");
83+
}
84+
}
85+
86+
export async function getUserOnboardingStatus() {
87+
const { userId } = await auth();
88+
if (!userId) throw new Error("Unauthorized");
89+
90+
const user = await db.user.findUnique({
91+
where: { clerkUserId: userId },
92+
});
93+
94+
if (!user) throw new Error("User not found");
95+
96+
try {
97+
const user = await db.user.findUnique({
98+
where: {
99+
clerkUserId: userId,
100+
},
101+
select: {
102+
industry: true,
103+
},
104+
});
105+
106+
return {
107+
isOnboarded: !!user?.industry,
108+
};
109+
} catch (error) {
110+
console.error("Error checking onboarding status:", error);
111+
throw new Error("Failed to check onboarding status");
112+
}
113+
}

src/app/api/inngest/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { serve } from "inngest/next";
22
import { inngest } from "@/lib/inngest/client";
3-
import { helloWorld } from "@/lib/inngest/functions";
3+
import { generateIndustryInsights } from "@/lib/inngest/functions";
44

55
// Create an API that serves zero functions
66
export const { GET, POST, PUT } = serve({
77
client: inngest,
88
functions: [
99
/* your functions will be passed here later! */
10-
helloWorld
10+
generateIndustryInsights
1111
],
1212
});

src/components/Navbar.tsx

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,69 @@ import { UserButton } from '@clerk/nextjs'
55
import { Book, Headphones, Search } from 'lucide-react'
66
import Link from 'next/link'
77
import Image from 'next/image'
8+
import { auth } from "@clerk/nextjs/server";
89

9-
type Props = {}
10-
11-
const Navbar = (props: Props) => {
10+
const Navbar = async () => {
11+
const { userId } = await auth();
1212
return (
13-
<div className='flex container w-full mx-auto bg-black/70 p-2 pb-6 border-b-4 border-gray-800'>
14-
<div className='flex justify-start'>
15-
<Link href='/'>
16-
<Image src="/logo.png" alt="logo" width={200} height={50} />
17-
</Link>
18-
</div>
13+
<div className='flex w-full container mx-auto my-0 gap-10 items-center bg-black/50 py-4 border-b-4 border-primary rounded-xl'>
14+
<div className='flex justify-start'>
15+
<Link href='/'>
16+
<Image src="/logo.png" alt="logo" width={200} height={50} />
17+
</Link>
18+
</div>
1919

20-
<div>
20+
<div className='flex gap-8 text-white justify-center'>
21+
<Link href={'/dashboard'}>Dashboard</Link>
22+
<Link href={'/onboarding'}>Onboarding</Link>
23+
<Link href={'/interview'}>Interview</Link>
24+
<Link href={'/cover-letter'}>Cover Letter</Link>
25+
<Link href={'/reports'}>Reports</Link>
26+
</div>
2127

28+
<div className='flex float-right gap-8 mx-auto p-4 text-2xl text-white'>
29+
{/* <span className="flex items-center gap-2 font-bold">
30+
<p className="text-sm font-light text-gray-300">Credits</p>
31+
<span>0</span>
32+
</span> */}
33+
<span className="flex items-center rounded-md bg-muted px-4">
34+
<Search />
35+
<Input
36+
placeholder="Quick Search"
37+
className="border-none bg-transparent"
38+
/>
39+
</span>
40+
<TooltipProvider>
41+
<Tooltip delayDuration={800}>
42+
<TooltipTrigger>
43+
<Link href='/contact-us'>
44+
<Headphones />
45+
</Link>
46+
</TooltipTrigger>
47+
<TooltipContent>
48+
<p>Contact Support</p>
49+
</TooltipContent>
50+
</Tooltip>
51+
</TooltipProvider>
52+
<TooltipProvider>
53+
<Tooltip delayDuration={800}>
54+
<TooltipTrigger>
55+
<Book />
56+
</TooltipTrigger>
57+
<TooltipContent>
58+
<p>Guide</p>
59+
</TooltipContent>
60+
</Tooltip>
61+
</TooltipProvider>
62+
{userId ?
63+
<UserButton />
64+
: (
65+
<Link href='/sign-in'>
66+
Sign In
67+
</Link>
68+
)}
69+
</div>
2270
</div>
23-
24-
<div className='flex float-end gap-8 mx-auto p-4 text-2xl text-white'>
25-
{/* <span className="flex items-center gap-2 font-bold">
26-
<p className="text-sm font-light text-gray-300">Credits</p>
27-
<span>0</span>
28-
</span> */}
29-
<span className="flex items-center rounded-md bg-muted px-4">
30-
<Search />
31-
<Input
32-
placeholder="Quick Search"
33-
className="border-none bg-transparent"
34-
/>
35-
</span>
36-
<TooltipProvider>
37-
<Tooltip delayDuration={0}>
38-
<TooltipTrigger>
39-
<Link href='/contact-us'>
40-
<Headphones />
41-
</Link>
42-
</TooltipTrigger>
43-
<TooltipContent>
44-
<p>Contact Support</p>
45-
</TooltipContent>
46-
</Tooltip>
47-
</TooltipProvider>
48-
<TooltipProvider>
49-
<Tooltip delayDuration={0}>
50-
<TooltipTrigger>
51-
<Book />
52-
</TooltipTrigger>
53-
<TooltipContent>
54-
<p>Guide</p>
55-
</TooltipContent>
56-
</Tooltip>
57-
</TooltipProvider>
58-
<UserButton />
59-
</div>
60-
</div>
6171
)
6272
}
6373

src/lib/inngest/functions.ts

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,66 @@
1+
import { db } from "@/lib/db";
12
import { inngest } from "./client";
3+
import { GoogleGenerativeAI } from "@google/generative-ai";
24

3-
export const helloWorld = inngest.createFunction(
4-
{ id: "hello-world" },
5-
{ event: "test/hello.world" },
6-
async ({ event, step }) => {
7-
await step.sleep("wait-a-moment", "1s");
8-
return { message: `Hello ${event.data.email}!` };
9-
},
5+
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
6+
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash-lite" });
7+
8+
export const generateIndustryInsights = inngest.createFunction(
9+
{ id:'AB',name: "Generate Industry Insights" },
10+
{ cron: "0 0 * * 0" }, // Run every Sunday at midnight
11+
async ({ step }) => {
12+
const industries = await step.run("Fetch industries", async () => {
13+
return await db.industryInsight.findMany({
14+
select: { industry: true },
15+
});
16+
});
17+
18+
for (const { industry } of industries) {
19+
const prompt = `
20+
Analyze the current state of the ${industry} industry and provide insights in ONLY the following JSON format without any additional notes or explanations:
21+
{
22+
"salaryRanges": [
23+
{ "role": "string", "min": number, "max": number, "median": number, "location": "string" }
24+
],
25+
"growthRate": number,
26+
"demandLevel": "High" | "Medium" | "Low",
27+
"topSkills": ["skill1", "skill2"],
28+
"marketOutlook": "Positive" | "Neutral" | "Negative",
29+
"keyTrends": ["trend1", "trend2"],
30+
"recommendedSkills": ["skill1", "skill2"]
31+
}
32+
33+
IMPORTANT: Return ONLY the JSON. No additional text, notes, or markdown formatting.
34+
Include at least 5 common roles for salary ranges.
35+
Growth rate should be a percentage.
36+
Include at least 5 skills and trends.
37+
`;
38+
39+
const res = await step.ai.wrap(
40+
"gemini",
41+
async (p) => {
42+
return await model.generateContent(p);
43+
},
44+
prompt
45+
);
46+
47+
console.log(res);
48+
49+
const text = res.response.candidates![0].content.parts[0].text || "";
50+
const cleanedText = text.replace(/```(?:json)?\n?/g, "").trim();
51+
52+
const insights = JSON.parse(cleanedText);
53+
54+
await step.run(`Update ${industry} insights`, async () => {
55+
await db.industryInsight.update({
56+
where: { industry },
57+
data: {
58+
...insights,
59+
lastUpdated: new Date(),
60+
nextUpdate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
61+
},
62+
});
63+
});
64+
}
65+
}
1066
);
File renamed without changes.

0 commit comments

Comments
 (0)