Files
edbridge-scholars/src/pages/student/lessons/EBRWCommasLesson.tsx
2026-03-12 02:39:34 +06:00

430 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useRef, useState, useEffect } from "react";
import { ArrowDown, Check, BookOpen, AlertTriangle, Zap } from "lucide-react";
import { PracticeFromDataset } from "../../../components/lessons/LessonShell";
import {
BOUNDARIES_EASY,
BOUNDARIES_MEDIUM,
} from "../../../data/rw/boundaries";
import ClauseBreakdownWidget, {
type ClauseExample,
} from "../../../components/lessons/ClauseBreakdownWidget";
import DecisionTreeWidget, {
type TreeScenario,
type TreeNode,
} from "../../../components/lessons/DecisionTreeWidget";
interface LessonProps {
onFinish?: () => void;
}
// ── Clause Breakdown data ──────────────────────────────────────────────────
const CLAUSE_EXAMPLES: ClauseExample[] = [
{
title: "Rule 1 — FANBOYS Comma",
segments: [
{
text: "The study was carefully designed",
type: "ic",
label: "Independent Clause",
},
{ text: ",", type: "punct" },
{ text: " but", type: "conjunction", label: "FANBOYS Conjunction" },
{
text: " the results were inconclusive",
type: "ic",
label: "Independent Clause",
},
{ text: ".", type: "punct" },
],
},
{
title: "Rule 2 — Introductory Element",
segments: [
{
text: "After reviewing the data",
type: "modifier",
label: "Introductory Phrase",
},
{ text: ",", type: "punct" },
{
text: " the researchers revised their hypothesis",
type: "ic",
label: "Main Clause",
},
{ text: ".", type: "punct" },
],
},
{
title: "Rule 3 — Nonessential Phrase",
segments: [
{ text: "The experiment", type: "subject", label: "Subject" },
{ text: ",", type: "punct" },
{
text: " conducted in 2021",
type: "modifier",
label: "Nonessential Phrase",
},
{ text: ",", type: "punct" },
{ text: " yielded unexpected results", type: "verb", label: "Predicate" },
{ text: ".", type: "punct" },
],
},
];
// ── Decision Tree data ─────────────────────────────────────────────────────
const NO_FANBOYS_SUBTREE: TreeNode = {
id: "no-fanboys",
question:
"Can BOTH sides stand alone as complete sentences (independent clauses)?",
hint: "Check each side for its own subject + verb. A phrase or fragment cannot stand alone.",
yesLabel: "Yes — both sides are complete sentences",
noLabel: "No — one side is a phrase/fragment",
yes: {
id: "comma-splice",
result:
"⚠ Comma Splice! A comma alone cannot join two independent clauses. Fix: add a FANBOYS conjunction after the comma, or replace the comma with a semicolon.",
resultType: "warning",
ruleRef: "Fix: [IC]; [IC] or [IC], [FANBOYS] [IC]",
},
no: {
id: "no-fanboys-no-two-ic",
question:
"Is there an introductory element (phrase or clause) at the START of the sentence?",
hint: 'Introductory elements include: participial phrases ("Running quickly"), prepositional phrases ("After the meeting"), or adverb clauses ("Because she studied").',
yesLabel: "Yes — opens with an introductory phrase/clause",
noLabel: "No — sentence starts with the subject",
yes: {
id: "intro-element",
result:
"✓ Use a comma AFTER the introductory element to separate it from the main clause.",
resultType: "correct",
ruleRef: "[Introductory element], [Main clause]",
},
no: {
id: "no-intro",
question:
"Is there a nonessential phrase in the MIDDLE that can be removed without changing the core meaning?",
hint: "Removal test: delete the phrase — does the sentence still make complete sense? If yes, it's nonessential.",
yesLabel: "Yes — removable nonessential phrase",
noLabel: "No — no removable phrase",
yes: {
id: "nonessential",
result:
"✓ Use a comma on EACH SIDE of the nonessential phrase (two commas total).",
resultType: "correct",
ruleRef: "[Subject], [nonessential phrase], [predicate]",
},
no: {
id: "no-comma",
result:
"✓ No comma needed here. Commas are only used for FANBOYS, introductory elements, nonessential info, or lists.",
resultType: "info",
ruleRef: "No comma — essential or uninterrupted structure",
},
},
},
};
const COMMA_TREE: TreeNode = {
id: "root",
question:
"Is there a FANBOYS conjunction (for, and, nor, but, or, yet, so) in this sentence?",
hint: "Scan for these 7 words: For · And · Nor · But · Or · Yet · So",
yesLabel: "Yes — there's a FANBOYS word",
noLabel: "No FANBOYS conjunction",
yes: {
id: "has-fanboys",
question:
"Can BOTH sides of the conjunction stand alone as complete sentences?",
hint: "Cover each side with your hand. Can it stand alone with its own subject and verb?",
yesLabel: "Yes — both sides are complete sentences",
noLabel: "No — one side is a phrase or fragment",
yes: {
id: "fanboys-both-ic",
result: "✓ Use a comma BEFORE the FANBOYS conjunction.",
resultType: "correct",
ruleRef: "[Independent Clause], [FANBOYS] [Independent Clause]",
},
no: {
id: "fanboys-not-both-ic",
result:
"✗ No comma needed before the conjunction. It is not joining two independent clauses.",
resultType: "warning",
ruleRef: "[IC] [FANBOYS] [phrase] — no comma",
},
},
no: NO_FANBOYS_SUBTREE,
};
const TREE_SCENARIOS: TreeScenario[] = [
{
label: "Sentence 1",
sentence:
"The study was carefully designed, the results were inconclusive.",
tree: COMMA_TREE,
},
{
label: "Sentence 2",
sentence:
"After reviewing the data the researchers revised their hypothesis.",
tree: COMMA_TREE,
},
{
label: "Sentence 3",
sentence: "The experiment conducted in 2021 yielded unexpected results.",
tree: COMMA_TREE,
},
];
// ── Lesson component ───────────────────────────────────────────────────────
const EBRWCommasLesson: React.FC<LessonProps> = ({ onFinish }) => {
const [activeSection, setActiveSection] = useState(0);
const sectionsRef = useRef<(HTMLElement | null)[]>([]);
useEffect(() => {
const observers: IntersectionObserver[] = [];
sectionsRef.current.forEach((el, idx) => {
if (!el) return;
const obs = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setActiveSection(idx);
},
{ threshold: 0.3 },
);
obs.observe(el);
observers.push(obs);
});
return () => observers.forEach((o) => o.disconnect());
}, []);
const scrollToSection = (index: number) => {
setActiveSection(index);
sectionsRef.current[index]?.scrollIntoView({ behavior: "smooth" });
};
const SectionMarker = ({
index,
title,
icon: Icon,
}: {
index: number;
title: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
return (
<button
onClick={() => scrollToSection(index)}
className={`flex items-center gap-3 p-3 w-full rounded-lg text-left transition-all ${isActive ? "bg-purple-50" : "hover:bg-slate-50"}`}
>
<div
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0
${isActive ? "bg-purple-600 text-white" : isPast ? "bg-purple-400 text-white" : "bg-slate-200 text-slate-500"}`}
>
{isPast ? (
<Check className="w-4 h-4" />
) : (
<Icon className="w-4 h-4" />
)}
</div>
<p
className={`text-sm font-bold ${isActive ? "text-purple-900" : "text-slate-600"}`}
>
{title}
</p>
</button>
);
};
return (
<div className="flex flex-col lg:flex-row min-h-screen">
<aside className="w-full lg:w-64 lg:fixed lg:top-14 lg:bottom-0 lg:overflow-y-auto p-4 border-r border-slate-200 bg-slate-50 z-0 hidden lg:block">
<nav className="space-y-2 pt-6">
<SectionMarker index={0} title="Clause Anatomy" icon={BookOpen} />
<SectionMarker
index={1}
title="Decision Tree Lab"
icon={AlertTriangle}
/>
<SectionMarker index={2} title="Practice Questions" icon={Zap} />
</nav>
</aside>
<div className="flex-1 lg:ml-64 p-6 md:p-12 max-w-4xl mx-auto">
{/* Section 0 — Concept + Clause Breakdown */}
<section
ref={(el) => {
sectionsRef.current[0] = el;
}}
className="min-h-screen flex flex-col justify-center mb-24 pt-20 lg:pt-0"
>
<div className="inline-flex items-center gap-2 bg-purple-100 text-purple-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-4 w-fit">
Standard English Conventions
</div>
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
Punctuation: Commas
</h2>
<p className="text-lg text-slate-500 mb-8">
See how sentences are built then learn exactly where commas go.
</p>
{/* Rule summary */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
{[
{
num: 1,
rule: "FANBOYS",
desc: "Comma before for/and/nor/but/or/yet/so when joining two complete sentences.",
},
{
num: 2,
rule: "Introductory Element",
desc: "Comma after any word, phrase, or clause that opens the sentence.",
},
{
num: 3,
rule: "Nonessential Phrase",
desc: "Two commas around any removable mid-sentence insertion.",
},
{
num: 4,
rule: "Lists of 3+",
desc: "Commas between items in a series; SAT prefers the Oxford comma.",
},
].map((r) => (
<div
key={r.num}
className="bg-purple-50 border border-purple-200 rounded-xl p-4"
>
<div className="flex items-center gap-2 mb-1">
<span className="w-6 h-6 rounded-full bg-purple-600 text-white flex items-center justify-center text-xs font-bold shrink-0">
{r.num}
</span>
<span className="font-bold text-purple-900 text-sm">
{r.rule}
</span>
</div>
<p className="text-xs text-slate-600 ml-8">{r.desc}</p>
</div>
))}
</div>
{/* Clause Breakdown */}
<h3 className="text-xl font-bold text-slate-800 mb-3">
Sentence Anatomy
</h3>
<p className="text-sm text-slate-500 mb-4">
Hover over any colored span to see its label. Use the tabs to switch
between examples.
</p>
<ClauseBreakdownWidget
examples={CLAUSE_EXAMPLES}
accentColor="purple"
/>
<div className="mt-6 bg-purple-900 text-white rounded-2xl p-5">
<p className="font-bold mb-1">Golden Rule</p>
<p className="text-sm text-purple-100">
A comma alone can <em>never</em> join two independent clauses
that's a comma splice. Every comma needs a job: FANBOYS, intro
element, nonessential info, or list.
</p>
</div>
<button
onClick={() => scrollToSection(1)}
className="mt-12 group flex items-center text-purple-600 font-bold hover:text-purple-800 transition-colors"
>
Next: Decision Tree Lab{" "}
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
</button>
</section>
{/* Section 1 — Decision Tree */}
<section
ref={(el) => {
sectionsRef.current[1] = el;
}}
className="min-h-screen flex flex-col justify-center mb-24"
>
<h2 className="text-4xl font-extrabold text-slate-900 mb-2">
Decision Tree Lab
</h2>
<p className="text-lg text-slate-500 mb-8">
Work through the grammar logic one question at a time. Click your
answer at each step.
</p>
{/* Trap callouts */}
<div className="space-y-3 mb-8">
{[
{
label: "Comma Splice",
desc: "Two full sentences joined by a comma alone. The most tested comma error.",
},
{
label: "Missing Second Comma",
desc: "Nonessential phrases need a comma on EACH side — never just one.",
},
{
label: "SubjectVerb Comma",
desc: "Never put a single comma between a subject and its verb.",
},
].map((t) => (
<div
key={t.label}
className="flex gap-3 bg-red-50 border border-red-200 rounded-xl px-4 py-3"
>
<AlertTriangle className="w-4 h-4 text-red-500 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-bold text-red-800">{t.label}</p>
<p className="text-xs text-slate-600 mt-0.5">{t.desc}</p>
</div>
</div>
))}
</div>
<DecisionTreeWidget scenarios={TREE_SCENARIOS} accentColor="purple" />
<button
onClick={() => scrollToSection(2)}
className="mt-12 group flex items-center text-purple-600 font-bold hover:text-purple-800 transition-colors"
>
Next: Practice Questions{" "}
<ArrowDown className="ml-2 w-5 h-5 group-hover:translate-y-1 transition-transform" />
</button>
</section>
{/* Section 2 — Quiz */}
<section
ref={(el) => {
sectionsRef.current[2] = el;
}}
className="min-h-screen flex flex-col justify-center mb-24"
>
<h2 className="text-4xl font-extrabold text-slate-900 mb-6">
Practice Questions
</h2>
{BOUNDARIES_EASY.slice(0, 2).map((q) => (
<PracticeFromDataset key={q.id} question={q} color="purple" />
))}
{BOUNDARIES_MEDIUM.slice(0, 1).map((q) => (
<PracticeFromDataset key={q.id} question={q} color="purple" />
))}
<div className="mt-8 text-center">
<button
onClick={onFinish}
className="px-6 py-3 bg-purple-900 text-white font-bold rounded-full hover:bg-purple-700 transition-colors"
>
Finish Lesson
</button>
</div>
</section>
</div>
</div>
);
};
export default EBRWCommasLesson;