Skip to content

Commit b79494f

Browse files
authored
chore: changes /question/uid -> /question/slug (#397)
2 parents 6ab195d + 131ab30 commit b79494f

File tree

41 files changed

+519
-176
lines changed

Some content is hidden

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

41 files changed

+519
-176
lines changed

src/actions/ai/roadmap/generate.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { fetchRoadmapQuestions } from '@/utils/data/roadmap/questions/fetch-road
77
import { generateRoadmapResponse } from './utils/generate-roadmap';
88
import { revalidateTag } from 'next/cache';
99
import { QuestionDifficulty } from '@/types/Questions';
10+
import { getUser } from '@/actions/user/authed/get-user';
1011

1112
interface RoadmapQuestion {
1213
uid: string;
@@ -29,21 +30,29 @@ interface RoadmapQuestion {
2930

3031
export const roadmapGenerate = async (opts: {
3132
roadmapUid: string;
32-
// passed in the pass to generate data for ai + fetch questions
33-
userUid: string;
3433
generateMore?: boolean;
3534
}) => {
3635
const { roadmapUid } = opts;
3736
opts.generateMore = opts.generateMore ?? false;
3837

38+
// get the user
39+
const user = await getUser();
40+
if (!user || user?.userLevel === 'FREE') {
41+
throw new Error('User not found');
42+
}
43+
3944
// Retrieve and format the necessary data for AI
40-
const formattedData = await generateDataForAi(opts);
45+
const formattedData = await generateDataForAi({
46+
roadmapUid,
47+
userUid: user?.uid,
48+
generateMore: opts.generateMore,
49+
});
4150

4251
let existingQuestions = [];
4352
if (formattedData === 'generated' || formattedData === 'invalid') {
4453
existingQuestions = await fetchRoadmapQuestions({
4554
roadmapUid,
46-
userUid: opts.userUid,
55+
userUid: user?.uid,
4756
});
4857

4958
if (existingQuestions.length === 0) {

src/actions/demo/get-total-submissions.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
'use server';
22
import { prisma } from '@/lib/prisma';
33

4-
export const getTotalSubmissions = async (opts: { questionUid: string }) => {
5-
const { questionUid } = opts;
4+
export const getTotalSubmissions = async (opts: { questionSlug: string }) => {
5+
const { questionSlug } = opts;
66

77
// get the total submissions for the question
88
const totalSubmissions = await prisma.demoAnswers.count({
99
where: {
10-
questionUid,
10+
question: {
11+
slug: questionSlug,
12+
},
1113
},
1214
});
1315

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use server';
2+
import { prisma } from '@/lib/prisma';
3+
import { Questions } from '@prisma/client';
4+
import Anthropic from '@anthropic-ai/sdk';
5+
6+
export const addUrlToQuestion = async () => {
7+
// fetch all questions that need slugs
8+
const questions = await prisma.questions.findMany({
9+
where: {
10+
customQuestion: false,
11+
},
12+
include: {
13+
tags: {
14+
include: {
15+
tag: true,
16+
},
17+
},
18+
},
19+
});
20+
21+
for (const question of questions) {
22+
await addSlugToQuestion(question);
23+
}
24+
};
25+
26+
const addSlugToQuestion = async (question: Questions) => {
27+
const MAX_RETRIES = 5;
28+
let retries = 0;
29+
30+
while (retries < MAX_RETRIES) {
31+
try {
32+
const slug = await generateSlug({
33+
...question,
34+
tags:
35+
(question as any).tags?.map((tag: any) => ({
36+
id: tag.tag.uid,
37+
name: tag.tag.name,
38+
})) || [],
39+
});
40+
41+
// Attempt to update the question with the generated slug
42+
await prisma.questions.update({
43+
where: { uid: question.uid },
44+
data: { slug },
45+
});
46+
47+
console.log(`Slug "${slug}" added to question "${question.uid}".`);
48+
return; // Exit the loop if successful
49+
} catch (error: any) {
50+
if (error.code === 'P2002') {
51+
// Prisma unique constraint violation
52+
console.warn(
53+
`Slug conflict for question "${question.uid}". Retrying...`
54+
);
55+
} else {
56+
console.error(
57+
`Error updating question "${question.uid}": ${error.message}`
58+
);
59+
throw error;
60+
}
61+
}
62+
retries++;
63+
}
64+
65+
console.error(
66+
`Failed to add slug to question "${question.uid}" after ${MAX_RETRIES} attempts.`
67+
);
68+
};
69+
70+
const generateSlug = async (question: any): Promise<string> => {
71+
const apiKey = process.env.NEXT_PRIVATE_CLAUDE_API_KEY;
72+
73+
const anthropic = new Anthropic({ apiKey });
74+
75+
try {
76+
const response = await anthropic.messages.create({
77+
model: 'claude-3-5-sonnet-latest',
78+
max_tokens: 8192,
79+
temperature: 0,
80+
messages: [
81+
{
82+
role: 'user',
83+
content:
84+
'You will be given a question, the related tags/topics and a code snippet. Create a relevant slug for the question. The slug must be short, unique, SEO-friendly, lowercase, and use dashes instead of spaces. You must only return the slug, no other text.',
85+
},
86+
{
87+
role: 'user',
88+
content:
89+
'You must only return the slug, no other text. Slug MUST be seo friendly and readable to users',
90+
},
91+
{
92+
role: 'user',
93+
// @ts-ignore
94+
content: `Question: ${question.question}\nTags: ${question.tags.map((tag) => tag.name).join(', ')}\nCode Snippet: ${question.codeSnippet}`,
95+
},
96+
],
97+
});
98+
99+
// @ts-ignore
100+
const slug = response.content[0].text;
101+
if (!slug) {
102+
throw new Error('Empty slug generated by Claude.');
103+
}
104+
return slug;
105+
} catch (error: any) {
106+
console.error(`Error generating slug: ${error.message}`);
107+
throw error;
108+
}
109+
};

src/app/(app)/(questions)/layout.tsx renamed to src/app/(app)/(questions)/question/[slug]/layout.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import { getQuestion } from '@/utils/data/questions/get';
1212
export default async function QuestionUidLayout({
1313
children,
1414
params,
15-
}: Readonly<{ children: React.ReactNode; params: { uid: string } }>) {
16-
const { uid } = params;
15+
}: Readonly<{ children: React.ReactNode; params: { slug: string } }>) {
16+
const { slug } = params;
1717

18-
const question = await getQuestion(uid);
18+
const question = await getQuestion('slug', slug);
1919

2020
return (
2121
<>
@@ -32,8 +32,8 @@ export default async function QuestionUidLayout({
3232
</div>
3333
<div className="flex items-center gap-x-3">
3434
<CurrentStreak />
35-
<RandomQuestion currentQuestionUid={uid} />
36-
<FeedbackButton reference={question?.uid} />
35+
<RandomQuestion identifier="slug" currentQuestionSlug={slug} />
36+
<FeedbackButton reference={question?.slug || undefined} />
3737
</div>
3838
</div>
3939
<Separator className="bg-black-50" />
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { getQuestion } from '@/utils/data/questions/get';
2+
import { Separator } from '@/components/ui/separator';
3+
import NoDailyQuestion from '@/components/global/no-daily-question';
4+
import QuestionDisplay from '@/components/app/questions/single/code-snippet';
5+
import { BarChartIcon as ChartColumn, Check, User } from 'lucide-react';
6+
import { getQuestionStats } from '@/utils/data/questions/get-question-stats';
7+
import { useUserServer } from '@/hooks/use-user-server';
8+
import QuestionCard from '@/components/app/questions/single/question-card';
9+
import { getRandomQuestion } from '@/utils/data/questions/get-random';
10+
import ExpandedCodeModal from '@/components/app/questions/expanded-code-modal';
11+
import RelatedQuestions from '@/components/app/questions/single/related-question-card';
12+
import ResizableLayout from '@/components/ui/resizable-layout';
13+
14+
export default async function TodaysQuestionPage({
15+
params,
16+
}: {
17+
params: { slug: string };
18+
}) {
19+
const { slug } = params;
20+
21+
const user = await useUserServer();
22+
23+
const [question, totalSubmissions, nextQuestion] = await Promise.all([
24+
getQuestion('slug', slug),
25+
getQuestionStats('slug', slug),
26+
getRandomQuestion({
27+
identifier: 'slug',
28+
currentQuestionSlug: slug,
29+
}),
30+
]);
31+
32+
if (!question) {
33+
return <NoDailyQuestion textAlign="center" />;
34+
}
35+
36+
const leftContent = (
37+
<div className="flex flex-col gap-y-4 lg:pl-6 p-3">
38+
<QuestionCard
39+
question={question}
40+
user={user}
41+
nextQuestion={nextQuestion}
42+
identifier="slug"
43+
/>
44+
</div>
45+
);
46+
47+
const rightContent = (
48+
<div className="flex flex-col gap-4 lg:pr-6 p-3">
49+
<div
50+
id="code-snippet"
51+
className="h-fit lg:h-[45rem] bg-black-75 border border-black-50 rounded-xl relative overflow-hidden"
52+
>
53+
<div className="p-4 text-sm flex w-full items-center justify-between bg-black-25">
54+
<p className="font-onest">index.js</p>{' '}
55+
{question.codeSnippet && (
56+
<ExpandedCodeModal code={question.codeSnippet} />
57+
)}
58+
</div>
59+
<Separator className="bg-black-50" />
60+
{question?.codeSnippet && (
61+
<QuestionDisplay
62+
content={question.codeSnippet}
63+
backgroundColor="#111111"
64+
/>
65+
)}
66+
</div>
67+
68+
{!question.customQuestion && (
69+
<div className="min-h-fit bg-black-75 border border-black-50 rounded-xl overflow-hidden">
70+
<RelatedQuestions slug={slug} tags={question.tags || []} />
71+
</div>
72+
)}
73+
74+
{!question.customQuestion && (
75+
<div className="bg-black-75 border border-black-50 rounded-xl overflow-hidden min-h-fit">
76+
<div className="flex items-center gap-x-1 p-4 bg-black-25">
77+
<ChartColumn className="size-4" />
78+
<div className="text-sm">Stats</div>
79+
</div>
80+
<Separator className="bg-black-50" />
81+
<div className="p-4 flex items-center">
82+
<div className="flex items-start gap-4 text-sm text-gray-400">
83+
<div className="flex items-center gap-2">
84+
<div className="flex items-center gap-0.5">
85+
<User className="size-4" />
86+
<p>Total submissions:</p>
87+
</div>
88+
<p>{totalSubmissions?.totalSubmissions}</p>
89+
</div>
90+
{totalSubmissions?.percentageCorrect > 0 && (
91+
<>
92+
|
93+
<div className="flex items-center gap-0.5">
94+
<Check className="size-4" />
95+
<p>Success rate:</p>
96+
<p>{totalSubmissions?.percentageCorrect}%</p>
97+
</div>
98+
</>
99+
)}
100+
</div>
101+
</div>
102+
</div>
103+
)}
104+
</div>
105+
);
106+
107+
return (
108+
<ResizableLayout
109+
leftContent={leftContent}
110+
rightContent={rightContent}
111+
initialLeftWidth={50}
112+
/>
113+
);
114+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Components
2+
import BackToDashboard from '@/components/ui/back-to-dashboard';
3+
import CurrentStreak from '@/components/ui/current-streak';
4+
import { Separator } from '@/components/ui/separator';
5+
import FeedbackButton from '@/components/ui/feedback-button';
6+
import SidebarLayoutTrigger from '@/components/global/navigation/sidebar-layout-trigger';
7+
8+
// Actions
9+
import { getQuestion } from '@/utils/data/questions/get';
10+
11+
export default async function Layout({
12+
children,
13+
params,
14+
}: Readonly<{ children: React.ReactNode; params: { slug: string } }>) {
15+
const { slug } = params;
16+
17+
const question = await getQuestion('slug', slug);
18+
19+
return (
20+
<>
21+
<div className="flex items-center justify-between py-2 px-6">
22+
<div className="flex items-center gap-x-5 py-2">
23+
<SidebarLayoutTrigger />
24+
{/** Previous question button */}
25+
<BackToDashboard href="/questions/custom" />
26+
{question?.dailyQuestion && question?.questionDate && (
27+
<div className="font-ubuntu flex gap-x-5 items-center">
28+
<p>Daily question</p>
29+
</div>
30+
)}
31+
</div>
32+
<div className="flex items-center gap-x-3">
33+
<CurrentStreak />
34+
<FeedbackButton reference={question?.slug || undefined} />
35+
</div>
36+
</div>
37+
<Separator className="bg-black-50" />
38+
{children}
39+
</>
40+
);
41+
}

src/app/(app)/(questions)/question/[uid]/page.tsx renamed to src/app/(app)/(questions)/question/custom/[uid]/page.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ export default async function TodaysQuestionPage({
2121
const user = await useUserServer();
2222

2323
const [question, totalSubmissions, nextQuestion] = await Promise.all([
24-
getQuestion(uid),
25-
getQuestionStats(uid),
24+
getQuestion('uid', uid),
25+
getQuestionStats('uid', uid),
2626
getRandomQuestion({
27-
currentQuestionUid: uid,
27+
identifier: 'uid',
28+
currentQuestionSlug: uid,
2829
}),
2930
]);
3031

@@ -38,6 +39,7 @@ export default async function TodaysQuestionPage({
3839
question={question}
3940
user={user}
4041
nextQuestion={nextQuestion}
42+
identifier="uid"
4143
/>
4244
</div>
4345
);
@@ -65,7 +67,10 @@ export default async function TodaysQuestionPage({
6567

6668
{!question.customQuestion && (
6769
<div className="min-h-fit bg-black-75 border border-black-50 rounded-xl overflow-hidden">
68-
<RelatedQuestions uid={uid} tags={question.tags || []} />
70+
<RelatedQuestions
71+
slug={question.slug || ''}
72+
tags={question.tags || []}
73+
/>
6974
</div>
7075
)}
7176

0 commit comments

Comments
 (0)