feat(patterns): tutorial system

This commit is contained in:
2025-08-07 00:41:51 +01:00
parent 3fd3021d6e
commit deb2f64ea7
15 changed files with 1386 additions and 45 deletions

View 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>
);
}

View 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";

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}