Skip to content

Commit bf851fe

Browse files
authored
feat: lesson and quiz cross-links to related coding problems (#156)
1 parent 6fae284 commit bf851fe

4 files changed

Lines changed: 526 additions & 0 deletions

File tree

frontend/pages/lesson/[slug].vue

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useMarkdown } from '@/composables/useMarkdown'
33
import { useAuth } from '@/composables/useAuth'
44
import { useAnalytics } from '@/composables/useAnalytics'
55
import { getCategoryDisplayName } from '@/utils/categoryMeta'
6+
import type { CodingProblemOut } from '@/types/problem'
67
78
const route = useRoute()
89
const slug = route.params.slug as string
@@ -53,6 +54,9 @@ const isCompleted = ref(false)
5354
const completing = ref(false)
5455
const completionSuccess = ref(false)
5556
const linkedQuiz = ref<{ slug: string; title: string } | null>(null)
57+
const relatedProblems = ref<CodingProblemOut[]>([])
58+
59+
const RELATED_PROBLEMS_LIMIT = 6
5660
5761
const fetchCategoryLessons = async () => {
5862
if (!lesson.value?.category) return
@@ -82,10 +86,23 @@ const fetchLinkedQuiz = async () => {
8286
}
8387
}
8488
89+
const fetchRelatedProblems = async () => {
90+
if (!lesson.value?.category) return
91+
try {
92+
const problems = await apiFetch<CodingProblemOut[]>(
93+
`/problems?category=${encodeURIComponent(lesson.value.category)}`
94+
)
95+
relatedProblems.value = problems
96+
} catch {
97+
// non-fatal
98+
}
99+
}
100+
85101
onMounted(async () => {
86102
track('lesson_view', { category: lesson.value?.category, slug })
87103
await fetchCategoryLessons()
88104
await fetchLinkedQuiz()
105+
await fetchRelatedProblems()
89106
})
90107
91108
const prevLesson = computed<CategoryLessonInfo | null>(() => {
@@ -106,6 +123,20 @@ const categoryDisplayName = computed(() =>
106123
lesson.value?.category ? getCategoryDisplayName(lesson.value.category) : ''
107124
)
108125
126+
const visibleProblems = computed(() =>
127+
relatedProblems.value.slice(0, RELATED_PROBLEMS_LIMIT)
128+
)
129+
130+
const hasMoreProblems = computed(() =>
131+
relatedProblems.value.length > RELATED_PROBLEMS_LIMIT
132+
)
133+
134+
const difficultyClasses: Record<string, string> = {
135+
easy: 'bg-green-100 text-green-700',
136+
medium: 'bg-yellow-100 text-yellow-700',
137+
hard: 'bg-red-100 text-red-700',
138+
}
139+
109140
async function markComplete() {
110141
if (!lesson.value || completing.value || isCompleted.value) return
111142
completing.value = true
@@ -165,6 +196,38 @@ const renderedContent = computed(() =>
165196
:initial-rating="lesson.user_rating ?? null"
166197
/>
167198

199+
<!-- Related Problems -->
200+
<div v-if="relatedProblems.length > 0" class="border-t border-gray-200 pt-8 mb-8">
201+
<h2 class="text-lg font-semibold text-gray-900 mb-4">Related Problems</h2>
202+
<ul class="space-y-2">
203+
<li
204+
v-for="problem in visibleProblems"
205+
:key="problem.id"
206+
class="flex items-center gap-3"
207+
>
208+
<NuxtLink
209+
:to="`/problems/${problem.id}`"
210+
class="text-purple-700 hover:text-purple-900 font-medium hover:underline"
211+
>
212+
{{ problem.title }}
213+
</NuxtLink>
214+
<span
215+
class="inline-block px-2 py-0.5 text-xs font-semibold rounded capitalize"
216+
:class="difficultyClasses[problem.difficulty] ?? 'bg-gray-100 text-gray-600'"
217+
>
218+
{{ problem.difficulty }}
219+
</span>
220+
</li>
221+
</ul>
222+
<NuxtLink
223+
v-if="hasMoreProblems"
224+
:to="`/problems?category=${lesson.category}`"
225+
class="inline-block mt-3 text-sm text-purple-700 hover:text-purple-900 hover:underline"
226+
>
227+
See all {{ relatedProblems.length }} problems &rarr;
228+
</NuxtLink>
229+
</div>
230+
168231
<!-- Completion section -->
169232
<div class="border-t border-gray-200 pt-8">
170233
<!-- Quiz exists: quiz is the completion mechanism -->

frontend/pages/quiz/[slug].vue

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import { useAnalytics } from '@/composables/useAnalytics'
33
import { getCategoryDisplayName } from '@/utils/categoryMeta'
4+
import type { CodingProblemOut } from '@/types/problem'
45
56
const route = useRoute()
67
const slug = route.params.slug as string
@@ -66,6 +67,9 @@ const submitting = ref(false)
6667
const retryQuestions = ref<QuizQuestion[]>([])
6768
const inRetryRound = ref(false)
6869
const firstPassAnswers = ref<Record<number, number>>({})
70+
const relatedProblems = ref<CodingProblemOut[]>([])
71+
72+
const QUIZ_PROBLEMS_LIMIT = 3
6973
7074
const categoryDisplayName = computed(() =>
7175
quiz.value?.category ? getCategoryDisplayName(quiz.value.category) : ''
@@ -99,6 +103,22 @@ async function fetchNextLesson() {
99103
}
100104
}
101105
106+
async function fetchRelatedProblems() {
107+
if (!quiz.value?.category) return
108+
try {
109+
const problems = await apiFetch<CodingProblemOut[]>(
110+
`/problems?category=${encodeURIComponent(quiz.value.category)}`
111+
)
112+
relatedProblems.value = problems
113+
} catch {
114+
// non-fatal
115+
}
116+
}
117+
118+
const suggestedProblems = computed(() =>
119+
relatedProblems.value.slice(0, QUIZ_PROBLEMS_LIMIT)
120+
)
121+
102122
const activeQuestions = computed(() =>
103123
inRetryRound.value ? retryQuestions.value : (quiz.value?.questions ?? [])
104124
)
@@ -228,6 +248,7 @@ async function submitQuiz() {
228248
submitting.value = false
229249
quizComplete.value = true
230250
await fetchNextLesson()
251+
await fetchRelatedProblems()
231252
}
232253
}
233254
@@ -317,6 +338,22 @@ function resultForQuestion(questionId: number): QuizAnswerResult | undefined {
317338
</div>
318339
</div>
319340

341+
<!-- Ready to code? -->
342+
<div v-if="suggestedProblems.length > 0" class="max-w-sm mx-auto rounded-xl border border-purple-100 bg-purple-50 px-6 py-5 mb-6 text-left">
343+
<p class="text-sm font-semibold text-purple-800 mb-1">Ready to code?</p>
344+
<p class="text-sm text-purple-700 mb-3">Try these problems to put your knowledge into practice:</p>
345+
<ul class="space-y-1.5">
346+
<li v-for="problem in suggestedProblems" :key="problem.id">
347+
<NuxtLink
348+
:to="`/problems/${problem.id}`"
349+
class="text-sm text-purple-700 hover:text-purple-900 font-medium hover:underline"
350+
>
351+
{{ problem.title }}
352+
</NuxtLink>
353+
</li>
354+
</ul>
355+
</div>
356+
320357
<!-- Signup CTA for anonymous users -->
321358
<div v-if="!isLoggedIn" class="max-w-sm mx-auto rounded-xl border border-purple-100 bg-purple-50 px-6 py-5 mb-6 text-left">
322359
<p class="text-sm font-semibold text-purple-800 mb-1">Save your progress</p>

0 commit comments

Comments
 (0)