feat(patterns): tutorial system
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getPattern, getQuestions } from "@/lib/api";
|
||||
import { QuestionCard } from "@/components/questions/question-card";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getPatternTutorial } from "@/lib/api";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { CodeBlock } from "@/components/ui/code-block";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
CommonMistakesList,
|
||||
LearningProgression,
|
||||
PatternVariations,
|
||||
RecognitionSignals,
|
||||
RelatedPatterns,
|
||||
} from "@/components/patterns";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
@@ -12,7 +22,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
||||
const { slug } = await params;
|
||||
|
||||
try {
|
||||
const pattern = await getPattern(slug);
|
||||
const pattern = await getPatternTutorial(slug);
|
||||
const description =
|
||||
pattern.description ||
|
||||
`Learn the ${pattern.name} pattern with ${pattern.question_count} practice problems.`;
|
||||
@@ -37,52 +47,142 @@ export default async function PatternDetailPage({ params }: PageProps) {
|
||||
const { slug } = await params;
|
||||
|
||||
let pattern;
|
||||
let questions;
|
||||
try {
|
||||
[pattern, questions] = await Promise.all([
|
||||
getPattern(slug),
|
||||
getQuestions({ pattern: slug, limit: 50 }),
|
||||
]);
|
||||
pattern = await getPatternTutorial(slug);
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const difficultyLabels = ["Beginner", "Easy", "Intermediate", "Advanced", "Expert"];
|
||||
const difficultyLabel = pattern.difficulty_level
|
||||
? difficultyLabels[pattern.difficulty_level - 1] || "Intermediate"
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">{pattern.name}</h1>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h1 className="text-3xl font-bold">{pattern.name}</h1>
|
||||
{difficultyLabel && (
|
||||
<Badge variant="outline">{difficultyLabel}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[var(--muted-foreground)]">
|
||||
{pattern.question_count} questions using this pattern
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{pattern.description && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Description</CardTitle>
|
||||
<CardTitle>What is {pattern.name}?</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>{pattern.description}</CardContent>
|
||||
<CardContent>
|
||||
<Markdown>{pattern.description}</Markdown>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Metaphor - The relatable analogy */}
|
||||
{pattern.metaphor && (
|
||||
<Callout variant="insight" title="Think of it like this...">
|
||||
<Markdown>{pattern.metaphor}</Markdown>
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
{/* Core Concept - The "aha!" insight */}
|
||||
{pattern.core_concept && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Core Concept</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Markdown>{pattern.core_concept}</Markdown>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Visualization - ASCII diagram walkthrough */}
|
||||
{pattern.visualization && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Visual Walkthrough</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Markdown>{pattern.visualization}</Markdown>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Code Template */}
|
||||
{pattern.code_template && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Code Template</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-4">
|
||||
Use this skeleton as a starting point for problems using this pattern:
|
||||
</p>
|
||||
<CodeBlock
|
||||
code={pattern.code_template}
|
||||
language="python"
|
||||
label="Pattern code template"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Recognition Signals */}
|
||||
{pattern.recognition_signals && pattern.recognition_signals.length > 0 && (
|
||||
<RecognitionSignals signals={pattern.recognition_signals} />
|
||||
)}
|
||||
|
||||
{/* When to Use */}
|
||||
{pattern.when_to_use && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>When to Use</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="whitespace-pre-wrap">
|
||||
{pattern.when_to_use}
|
||||
<CardContent>
|
||||
<Markdown>{pattern.when_to_use}</Markdown>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Questions</h2>
|
||||
<div className="grid gap-4">
|
||||
{questions.items.map((question) => (
|
||||
<QuestionCard key={question.id} question={question} />
|
||||
))}
|
||||
</div>
|
||||
{/* Common Mistakes */}
|
||||
{pattern.common_mistakes && pattern.common_mistakes.length > 0 && (
|
||||
<CommonMistakesList mistakes={pattern.common_mistakes} />
|
||||
)}
|
||||
|
||||
{/* Pattern Variations */}
|
||||
{pattern.variations && pattern.variations.length > 0 && (
|
||||
<PatternVariations variations={pattern.variations} />
|
||||
)}
|
||||
|
||||
{/* Learning Progression */}
|
||||
{pattern.learning_progression && (
|
||||
<LearningProgression progression={pattern.learning_progression} />
|
||||
)}
|
||||
|
||||
{/* Related Patterns */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{pattern.prerequisite_patterns && pattern.prerequisite_patterns.length > 0 && (
|
||||
<RelatedPatterns
|
||||
title="Prerequisites"
|
||||
description="Learn these patterns first:"
|
||||
patterns={pattern.prerequisite_patterns}
|
||||
/>
|
||||
)}
|
||||
{pattern.related_patterns && pattern.related_patterns.length > 0 && (
|
||||
<RelatedPatterns
|
||||
title="Related Patterns"
|
||||
description="Explore similar techniques:"
|
||||
patterns={pattern.related_patterns}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
37
frontend/src/components/patterns/common-mistakes-list.tsx
Normal file
37
frontend/src/components/patterns/common-mistakes-list.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import type { CommonMistake } from "@/types";
|
||||
|
||||
interface CommonMistakesListProps {
|
||||
mistakes: CommonMistake[];
|
||||
}
|
||||
|
||||
export function CommonMistakesList({ mistakes }: CommonMistakesListProps) {
|
||||
if (!mistakes.length) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Common Mistakes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{mistakes.map((mistake) => (
|
||||
<Callout key={mistake.title} variant="warning" title={mistake.title}>
|
||||
<div className="space-y-2">
|
||||
<Markdown>{mistake.description}</Markdown>
|
||||
{mistake.fix && (
|
||||
<div className="mt-3 pt-3 border-t border-[var(--callout-warning-border)]">
|
||||
<p className="text-sm font-medium mb-1">Fix:</p>
|
||||
<Markdown>{mistake.fix}</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Callout>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
5
frontend/src/components/patterns/index.ts
Normal file
5
frontend/src/components/patterns/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { CommonMistakesList } from "./common-mistakes-list";
|
||||
export { LearningProgression } from "./learning-progression";
|
||||
export { PatternVariations } from "./pattern-variations";
|
||||
export { RecognitionSignals } from "./recognition-signals";
|
||||
export { RelatedPatterns } from "./related-patterns";
|
||||
111
frontend/src/components/patterns/learning-progression.tsx
Normal file
111
frontend/src/components/patterns/learning-progression.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import Link from "next/link";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { getDifficultyVariant, capitalize } from "@/lib/utils";
|
||||
import type { LearningProgression as LearningProgressionType } from "@/types";
|
||||
|
||||
interface LearningProgressionProps {
|
||||
progression: LearningProgressionType;
|
||||
}
|
||||
|
||||
export function LearningProgression({ progression }: LearningProgressionProps) {
|
||||
const hasQuestions =
|
||||
progression.warmup.length > 0 ||
|
||||
progression.core.length > 0 ||
|
||||
progression.challenge.length > 0;
|
||||
|
||||
if (!hasQuestions) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Learning Path</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{progression.warmup.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-semibold text-[var(--difficulty-easy)] mb-3 flex items-center gap-2">
|
||||
<span className="w-6 h-6 rounded-full bg-[var(--difficulty-easy-bg)] flex items-center justify-center text-sm">
|
||||
1
|
||||
</span>
|
||||
Warmup
|
||||
</h4>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||
Start here to build foundational understanding.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{progression.warmup.map((q) => (
|
||||
<QuestionLink key={q.id} question={q} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{progression.core.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-semibold text-[var(--difficulty-medium)] mb-3 flex items-center gap-2">
|
||||
<span className="w-6 h-6 rounded-full bg-[var(--difficulty-medium-bg)] flex items-center justify-center text-sm">
|
||||
2
|
||||
</span>
|
||||
Core Practice
|
||||
</h4>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||
Master the pattern with these representative problems.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{progression.core.map((q) => (
|
||||
<QuestionLink key={q.id} question={q} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{progression.challenge.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-semibold text-[var(--difficulty-hard)] mb-3 flex items-center gap-2">
|
||||
<span className="w-6 h-6 rounded-full bg-[var(--difficulty-hard-bg)] flex items-center justify-center text-sm">
|
||||
3
|
||||
</span>
|
||||
Challenge
|
||||
</h4>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||
Test your mastery with complex variations.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{progression.challenge.map((q) => (
|
||||
<QuestionLink key={q.id} question={q} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface QuestionLinkProps {
|
||||
question: LearningProgressionType["warmup"][number];
|
||||
}
|
||||
|
||||
function QuestionLink({ question }: QuestionLinkProps) {
|
||||
return (
|
||||
<Link
|
||||
href={`/questions/${question.slug}`}
|
||||
className="flex items-center justify-between p-3 rounded-lg border border-[var(--border)] bg-[var(--card)] hover:border-[var(--primary)] transition-colors"
|
||||
>
|
||||
<span className="font-medium">{question.title}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{question.leetcode_id && (
|
||||
<span className="text-xs text-[var(--muted-foreground)]">
|
||||
#{question.leetcode_id}
|
||||
</span>
|
||||
)}
|
||||
<Badge variant={getDifficultyVariant(question.difficulty)}>
|
||||
{capitalize(question.difficulty)}
|
||||
</Badge>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
38
frontend/src/components/patterns/pattern-variations.tsx
Normal file
38
frontend/src/components/patterns/pattern-variations.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import type { PatternVariation } from "@/types";
|
||||
|
||||
interface PatternVariationsProps {
|
||||
variations: PatternVariation[];
|
||||
}
|
||||
|
||||
export function PatternVariations({ variations }: PatternVariationsProps) {
|
||||
if (!variations.length) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Pattern Variations</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{variations.map((variation) => (
|
||||
<div
|
||||
key={variation.name}
|
||||
className="border-l-2 border-[var(--primary)] pl-4"
|
||||
>
|
||||
<h4 className="font-semibold mb-1">{variation.name}</h4>
|
||||
<Markdown>{variation.description}</Markdown>
|
||||
{variation.example && (
|
||||
<p className="text-sm text-[var(--muted-foreground)] mt-2">
|
||||
<span className="font-medium">Examples:</span>{" "}
|
||||
{variation.example}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
32
frontend/src/components/patterns/recognition-signals.tsx
Normal file
32
frontend/src/components/patterns/recognition-signals.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
|
||||
interface RecognitionSignalsProps {
|
||||
signals: string[];
|
||||
}
|
||||
|
||||
export function RecognitionSignals({ signals }: RecognitionSignalsProps) {
|
||||
if (!signals.length) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recognition Signals</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-4">
|
||||
Look for these keywords and patterns in problem descriptions:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{signals.map((signal) => (
|
||||
<Badge key={signal} variant="pattern">
|
||||
{signal}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
48
frontend/src/components/patterns/related-patterns.tsx
Normal file
48
frontend/src/components/patterns/related-patterns.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import Link from "next/link";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import type { RelatedPattern } from "@/types";
|
||||
|
||||
interface RelatedPatternsProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
patterns: RelatedPattern[];
|
||||
}
|
||||
|
||||
export function RelatedPatterns({
|
||||
title,
|
||||
description,
|
||||
patterns,
|
||||
}: RelatedPatternsProps) {
|
||||
if (!patterns.length) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{description && (
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-4">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
{patterns.map((pattern) => (
|
||||
<Link
|
||||
key={pattern.slug}
|
||||
href={`/patterns/${pattern.slug}`}
|
||||
className="block p-3 rounded-lg border border-[var(--border)] bg-[var(--card)] hover:border-[var(--primary)] transition-colors"
|
||||
>
|
||||
<h4 className="font-semibold mb-1">{pattern.name}</h4>
|
||||
{pattern.description && (
|
||||
<p className="text-sm text-[var(--muted-foreground)] line-clamp-2">
|
||||
{pattern.description}
|
||||
</p>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
CategoryListResponse,
|
||||
Pattern,
|
||||
PatternListResponse,
|
||||
PatternTutorial,
|
||||
QuestionDetail,
|
||||
QuestionListResponse,
|
||||
Stats,
|
||||
@@ -98,6 +99,10 @@ export async function getPattern(slug: string): Promise<Pattern> {
|
||||
return fetchApi<Pattern>(`/api/patterns/${slug}`);
|
||||
}
|
||||
|
||||
export async function getPatternTutorial(slug: string): Promise<PatternTutorial> {
|
||||
return fetchApi<PatternTutorial>(`/api/patterns/${slug}/tutorial`);
|
||||
}
|
||||
|
||||
export async function getStats(): Promise<Stats> {
|
||||
return fetchApi<Stats>("/api/stats");
|
||||
}
|
||||
|
||||
@@ -23,6 +23,54 @@ export interface Pattern extends PatternBrief {
|
||||
question_count: number;
|
||||
}
|
||||
|
||||
// Pattern Tutorial Types
|
||||
|
||||
export interface CommonMistake {
|
||||
title: string;
|
||||
description: string;
|
||||
fix: string | null;
|
||||
}
|
||||
|
||||
export interface PatternVariation {
|
||||
name: string;
|
||||
description: string;
|
||||
example: string | null;
|
||||
}
|
||||
|
||||
export interface RelatedPattern {
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface LearningQuestion {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
difficulty: Difficulty;
|
||||
leetcode_id: number | null;
|
||||
}
|
||||
|
||||
export interface LearningProgression {
|
||||
warmup: LearningQuestion[];
|
||||
core: LearningQuestion[];
|
||||
challenge: LearningQuestion[];
|
||||
}
|
||||
|
||||
export interface PatternTutorial extends Pattern {
|
||||
metaphor: string | null;
|
||||
core_concept: string | null;
|
||||
visualization: string | null;
|
||||
code_template: string | null;
|
||||
recognition_signals: string[] | null;
|
||||
common_mistakes: CommonMistake[] | null;
|
||||
variations: PatternVariation[] | null;
|
||||
related_patterns: RelatedPattern[] | null;
|
||||
prerequisite_patterns: RelatedPattern[] | null;
|
||||
difficulty_level: number | null;
|
||||
learning_progression: LearningProgression | null;
|
||||
}
|
||||
|
||||
export interface QuestionListItem {
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
Reference in New Issue
Block a user